Bug 1700051: part 30) Narrow scope of `newOffset`. r=smaug
[gecko.git] / editor / libeditor / TypeInState.cpp
blobf5680764c9c549327c0eb198738cd11d1999b85d
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "TypeInState.h"
8 #include <stddef.h>
10 #include "HTMLEditUtils.h"
11 #include "mozilla/EditAction.h"
12 #include "mozilla/EditorBase.h"
13 #include "mozilla/HTMLEditor.h"
14 #include "mozilla/mozalloc.h"
15 #include "mozilla/dom/AncestorIterator.h"
16 #include "mozilla/dom/MouseEvent.h"
17 #include "mozilla/dom/Selection.h"
18 #include "nsAString.h"
19 #include "nsDebug.h"
20 #include "nsError.h"
21 #include "nsGkAtoms.h"
22 #include "nsINode.h"
23 #include "nsISupportsBase.h"
24 #include "nsISupportsImpl.h"
25 #include "nsReadableUtils.h"
26 #include "nsStringFwd.h"
28 // Workaround for windows headers
29 #ifdef SetProp
30 # undef SetProp
31 #endif
33 class nsAtom;
35 namespace mozilla {
37 using namespace dom;
39 /********************************************************************
40 * mozilla::TypeInState
41 *******************************************************************/
43 NS_IMPL_CYCLE_COLLECTION_CLASS(TypeInState)
45 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TypeInState)
46 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint)
47 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
49 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TypeInState)
50 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint)
51 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
53 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(TypeInState, AddRef)
54 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(TypeInState, Release)
56 TypeInState::TypeInState()
57 : mRelativeFontSize(0),
58 mLastSelectionCommand(Command::DoNothing),
59 mMouseDownFiredInLinkElement(false),
60 mMouseUpFiredInLinkElement(false) {
61 Reset();
64 TypeInState::~TypeInState() {
65 // Call Reset() to release any data that may be in
66 // mClearedArray and mSetArray.
68 Reset();
71 nsresult TypeInState::UpdateSelState(Selection& aSelection) {
72 if (!aSelection.IsCollapsed()) {
73 return NS_OK;
76 mLastSelectionPoint = EditorBase::GetStartPoint(aSelection);
77 if (!mLastSelectionPoint.IsSet()) {
78 return NS_ERROR_FAILURE;
80 // We need to store only offset because referring child may be removed by
81 // we'll check the point later.
82 AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
83 return NS_OK;
86 void TypeInState::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) {
87 MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown ||
88 aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp);
89 bool& eventFiredInLinkElement =
90 aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown
91 ? mMouseDownFiredInLinkElement
92 : mMouseUpFiredInLinkElement;
93 eventFiredInLinkElement = false;
94 if (aMouseDownOrUpEvent.DefaultPrevented()) {
95 return;
97 // If mouse button is down or up in a link element, we shouldn't unlink
98 // it when we get a notification of selection change.
99 EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget();
100 if (NS_WARN_IF(!target)) {
101 return;
103 nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
104 if (NS_WARN_IF(!targetContent)) {
105 return;
107 eventFiredInLinkElement =
108 HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent);
111 void TypeInState::PreHandleSelectionChangeCommand(Command aCommand) {
112 mLastSelectionCommand = aCommand;
115 void TypeInState::PostHandleSelectionChangeCommand(
116 const HTMLEditor& aHTMLEditor, Command aCommand) {
117 if (mLastSelectionCommand != aCommand) {
118 return;
121 // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
122 // it means that it didn't cause selection change.
123 if (!aHTMLEditor.SelectionRef().IsCollapsed() ||
124 !aHTMLEditor.SelectionRef().RangeCount()) {
125 return;
128 EditorRawDOMPoint caretPoint(
129 EditorBase::GetStartPoint(aHTMLEditor.SelectionRef()));
130 if (NS_WARN_IF(!caretPoint.IsSet())) {
131 return;
134 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
135 return;
138 // If all styles are cleared or link style is explicitly set, we
139 // shouldn't reset them without caret move.
140 if (AreAllStylesCleared() || IsLinkStyleSet()) {
141 return;
143 // And if non-link styles are cleared or some styles are set, we
144 // shouldn't reset them too, but we may need to change the link
145 // style.
146 if (AreSomeStylesSet() ||
147 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
148 ClearLinkPropAndDiscardItsSpecifiedStyle();
149 return;
152 Reset();
153 ClearLinkPropAndDiscardItsSpecifiedStyle();
156 void TypeInState::OnSelectionChange(const HTMLEditor& aHTMLEditor,
157 int16_t aReason) {
158 // XXX: Selection currently generates bogus selection changed notifications
159 // XXX: (bug 140303). It can notify us when the selection hasn't actually
160 // XXX: changed, and it notifies us more than once for the same change.
161 // XXX:
162 // XXX: The following code attempts to work around the bogus notifications,
163 // XXX: and should probably be removed once bug 140303 is fixed.
164 // XXX:
165 // XXX: This code temporarily fixes the problem where clicking the mouse in
166 // XXX: the same location clears the type-in-state.
168 const bool causedByFrameSelectionMoveCaret =
169 (aReason & (nsISelectionListener::KEYPRESS_REASON |
170 nsISelectionListener::COLLAPSETOSTART_REASON |
171 nsISelectionListener::COLLAPSETOEND_REASON)) &&
172 !(aReason & nsISelectionListener::JS_REASON);
174 Command lastSelectionCommand = mLastSelectionCommand;
175 if (causedByFrameSelectionMoveCaret) {
176 mLastSelectionCommand = Command::DoNothing;
179 bool mouseEventFiredInLinkElement = false;
180 if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
181 nsISelectionListener::MOUSEUP_REASON)) {
182 MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
183 nsISelectionListener::MOUSEUP_REASON)) !=
184 (nsISelectionListener::MOUSEDOWN_REASON |
185 nsISelectionListener::MOUSEUP_REASON));
186 bool& eventFiredInLinkElement =
187 aReason & nsISelectionListener::MOUSEDOWN_REASON
188 ? mMouseDownFiredInLinkElement
189 : mMouseUpFiredInLinkElement;
190 mouseEventFiredInLinkElement = eventFiredInLinkElement;
191 eventFiredInLinkElement = false;
194 bool unlink = false;
195 bool resetAllStyles = true;
196 if (aHTMLEditor.SelectionRef().IsCollapsed() &&
197 aHTMLEditor.SelectionRef().RangeCount()) {
198 EditorRawDOMPoint selectionStartPoint(
199 EditorBase::GetStartPoint(aHTMLEditor.SelectionRef()));
200 if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
201 return;
204 if (mLastSelectionPoint == selectionStartPoint) {
205 // If all styles are cleared or link style is explicitly set, we
206 // shouldn't reset them without caret move.
207 if (AreAllStylesCleared() || IsLinkStyleSet()) {
208 return;
210 // And if non-link styles are cleared or some styles are set, we
211 // shouldn't reset them too, but we may need to change the link
212 // style.
213 if (AreSomeStylesSet() ||
214 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
215 resetAllStyles = false;
219 RefPtr<Element> linkElement;
220 if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint,
221 getter_AddRefs(linkElement))) {
222 // If caret comes from outside of <a href> element, we should clear "link"
223 // style after reset.
224 if (causedByFrameSelectionMoveCaret) {
225 MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON |
226 nsISelectionListener::MOUSEUP_REASON)));
227 // If caret is moves in a link per character, we should keep inserting
228 // new text to the link because user may want to keep extending the link
229 // text. Otherwise, e.g., using `End` or `Home` key. we should insert
230 // new text outside the link because it should be possible to user
231 // choose it, and this is similar to the other browsers.
232 switch (lastSelectionCommand) {
233 case Command::CharNext:
234 case Command::CharPrevious:
235 case Command::MoveLeft:
236 case Command::MoveLeft2:
237 case Command::MoveRight:
238 case Command::MoveRight2:
239 // If selection becomes collapsed, we should unlink new text.
240 if (!mLastSelectionPoint.IsSet()) {
241 unlink = true;
242 break;
244 // Special case, if selection isn't moved, it means that caret is
245 // positioned at start or end of an editing host. In this case,
246 // we can unlink it even with arrow key press.
247 // TODO: This does not work as expected for `ArrowLeft` key press
248 // at start of an editing host.
249 if (mLastSelectionPoint == selectionStartPoint) {
250 unlink = true;
251 break;
253 // Otherwise, if selection is moved in a link element, we should
254 // keep inserting new text into the link. Note that this is our
255 // traditional behavior, but different from the other browsers.
256 // If this breaks some web apps, we should change our behavior,
257 // but let's wait a report because our traditional behavior allows
258 // user to type text into start/end of a link only when user
259 // moves caret inside the link with arrow keys.
260 unlink =
261 !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
262 linkElement);
263 break;
264 default:
265 // If selection is moved without arrow keys, e.g., `Home` and
266 // `End`, we should not insert new text into the link element.
267 // This is important for web-compat especially when the link is
268 // the last content in the block.
269 unlink = true;
270 break;
272 } else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
273 nsISelectionListener::MOUSEUP_REASON)) {
274 // If the corresponding mouse event is fired in a link element,
275 // we should keep treating inputting content as content in the link,
276 // but otherwise, i.e., clicked outside the link, we should stop
277 // treating inputting content as content in the link.
278 unlink = !mouseEventFiredInLinkElement;
279 } else if (aReason & nsISelectionListener::JS_REASON) {
280 // If this is caused by a call of Selection API or something similar
281 // API, we should not contain new inserting content to the link.
282 unlink = true;
283 } else {
284 switch (aHTMLEditor.GetEditAction()) {
285 case EditAction::eDeleteBackward:
286 case EditAction::eDeleteForward:
287 case EditAction::eDeleteSelection:
288 case EditAction::eDeleteToBeginningOfSoftLine:
289 case EditAction::eDeleteToEndOfSoftLine:
290 case EditAction::eDeleteWordBackward:
291 case EditAction::eDeleteWordForward:
292 // This selection change is caused by the editor and the edit
293 // action is deleting content at edge of a link, we shouldn't
294 // keep the link style for new inserted content.
295 unlink = true;
296 break;
297 default:
298 break;
301 } else if (mLastSelectionPoint == selectionStartPoint) {
302 return;
305 mLastSelectionPoint = selectionStartPoint;
306 // We need to store only offset because referring child may be removed by
307 // we'll check the point later.
308 AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
309 } else {
310 if (aHTMLEditor.SelectionRef().RangeCount()) {
311 // If selection starts from a link, we shouldn't preserve the link style
312 // unless the range is entirely in the link.
313 EditorRawDOMRange firstRange(*aHTMLEditor.SelectionRef().GetRangeAt(0));
314 if (firstRange.StartRef().IsInContentNode() &&
315 HTMLEditUtils::IsContentInclusiveDescendantOfLink(
316 *firstRange.StartRef().ContainerAsContent())) {
317 unlink = !HTMLEditUtils::IsRangeEntirelyInLink(firstRange);
320 mLastSelectionPoint.Clear();
323 if (resetAllStyles) {
324 Reset();
325 if (unlink) {
326 ClearLinkPropAndDiscardItsSpecifiedStyle();
328 return;
331 if (unlink == IsExplicitlyLinkStyleCleared()) {
332 return;
335 // Even if we shouldn't touch existing style, we need to set/clear only link
336 // style in some cases.
337 if (unlink) {
338 ClearLinkPropAndDiscardItsSpecifiedStyle();
339 } else if (!unlink) {
340 RemovePropFromClearedList(nsGkAtoms::a, nullptr);
344 void TypeInState::Reset() {
345 for (size_t i = 0, n = mClearedArray.Length(); i < n; i++) {
346 delete mClearedArray[i];
348 mClearedArray.Clear();
349 for (size_t i = 0, n = mSetArray.Length(); i < n; i++) {
350 delete mSetArray[i];
352 mSetArray.Clear();
355 void TypeInState::SetProp(nsAtom* aProp, nsAtom* aAttr,
356 const nsAString& aValue) {
357 // special case for big/small, these nest
358 if (nsGkAtoms::big == aProp) {
359 mRelativeFontSize++;
360 return;
362 if (nsGkAtoms::small == aProp) {
363 mRelativeFontSize--;
364 return;
367 int32_t index;
368 if (IsPropSet(aProp, aAttr, nullptr, index)) {
369 // if it's already set, update the value
370 mSetArray[index]->value = aValue;
371 return;
374 // Make a new propitem and add it to the list of set properties.
375 mSetArray.AppendElement(new PropItem(aProp, aAttr, aValue));
377 // remove it from the list of cleared properties, if we have a match
378 RemovePropFromClearedList(aProp, aAttr);
381 void TypeInState::ClearAllProps() {
382 // null prop means "all" props
383 ClearProp(nullptr, nullptr);
386 void TypeInState::ClearProp(nsAtom* aProp, nsAtom* aAttr) {
387 // if it's already cleared we are done
388 if (IsPropCleared(aProp, aAttr)) {
389 return;
392 // make a new propitem
393 PropItem* item = new PropItem(aProp, aAttr, u""_ns);
395 // remove it from the list of set properties, if we have a match
396 RemovePropFromSetList(aProp, aAttr);
398 // add it to the list of cleared properties
399 mClearedArray.AppendElement(item);
402 void TypeInState::ClearLinkPropAndDiscardItsSpecifiedStyle() {
403 ClearProp(nsGkAtoms::a, nullptr);
404 int32_t index = -1;
405 MOZ_ALWAYS_TRUE(IsPropCleared(nsGkAtoms::a, nullptr, index));
406 if (index >= 0) {
407 mClearedArray[index]->specifiedStyle = SpecifiedStyle::Discard;
412 * TakeClearProperty() hands back next property item on the clear list.
413 * Caller assumes ownership of PropItem and must delete it.
415 UniquePtr<PropItem> TypeInState::TakeClearProperty() {
416 return mClearedArray.Length()
417 ? UniquePtr<PropItem>{mClearedArray.PopLastElement()}
418 : nullptr;
422 * TakeSetProperty() hands back next poroperty item on the set list.
423 * Caller assumes ownership of PropItem and must delete it.
425 UniquePtr<PropItem> TypeInState::TakeSetProperty() {
426 return mSetArray.Length() ? UniquePtr<PropItem>{mSetArray.PopLastElement()}
427 : nullptr;
431 * TakeRelativeFontSize() hands back relative font value, which is then
432 * cleared out.
434 int32_t TypeInState::TakeRelativeFontSize() {
435 int32_t relSize = mRelativeFontSize;
436 mRelativeFontSize = 0;
437 return relSize;
440 void TypeInState::GetTypingState(bool& isSet, bool& theSetting, nsAtom* aProp,
441 nsAtom* aAttr, nsString* aValue) {
442 if (IsPropSet(aProp, aAttr, aValue)) {
443 isSet = true;
444 theSetting = true;
445 } else if (IsPropCleared(aProp, aAttr)) {
446 isSet = true;
447 theSetting = false;
448 } else {
449 isSet = false;
453 void TypeInState::RemovePropFromSetList(nsAtom* aProp, nsAtom* aAttr) {
454 int32_t index;
455 if (!aProp) {
456 // clear _all_ props
457 for (size_t i = 0, n = mSetArray.Length(); i < n; i++) {
458 delete mSetArray[i];
460 mSetArray.Clear();
461 mRelativeFontSize = 0;
462 } else if (FindPropInList(aProp, aAttr, nullptr, mSetArray, index)) {
463 delete mSetArray[index];
464 mSetArray.RemoveElementAt(index);
468 void TypeInState::RemovePropFromClearedList(nsAtom* aProp, nsAtom* aAttr) {
469 int32_t index;
470 if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, index)) {
471 delete mClearedArray[index];
472 mClearedArray.RemoveElementAt(index);
476 bool TypeInState::IsPropSet(nsAtom* aProp, nsAtom* aAttr, nsAString* outValue) {
477 int32_t i;
478 return IsPropSet(aProp, aAttr, outValue, i);
481 bool TypeInState::IsPropSet(nsAtom* aProp, nsAtom* aAttr, nsAString* outValue,
482 int32_t& outIndex) {
483 if (aAttr == nsGkAtoms::_empty) {
484 aAttr = nullptr;
486 // linear search. list should be short.
487 size_t count = mSetArray.Length();
488 for (size_t i = 0; i < count; i++) {
489 PropItem* item = mSetArray[i];
490 if (item->tag == aProp && item->attr == aAttr) {
491 if (outValue) {
492 *outValue = item->value;
494 outIndex = i;
495 return true;
498 return false;
501 bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr) {
502 int32_t i;
503 return IsPropCleared(aProp, aAttr, i);
506 bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr,
507 int32_t& outIndex) {
508 if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, outIndex)) {
509 return true;
511 if (AreAllStylesCleared()) {
512 // special case for all props cleared
513 outIndex = -1;
514 return true;
516 return false;
519 bool TypeInState::FindPropInList(nsAtom* aProp, nsAtom* aAttr,
520 nsAString* outValue,
521 const nsTArray<PropItem*>& aList,
522 int32_t& outIndex) {
523 if (aAttr == nsGkAtoms::_empty) {
524 aAttr = nullptr;
526 // linear search. list should be short.
527 size_t count = aList.Length();
528 for (size_t i = 0; i < count; i++) {
529 PropItem* item = aList[i];
530 if (item->tag == aProp && item->attr == aAttr) {
531 if (outValue) {
532 *outValue = item->value;
534 outIndex = i;
535 return true;
538 return false;
541 } // namespace mozilla