1 /* -*- Mode: C++; tab-width: 4; 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/. */
5 
6 #include "mozilla/a11y/SelectionManager.h"
7 
8 #include "DocAccessible-inl.h"
9 #include "HyperTextAccessible.h"
10 #include "HyperTextAccessible-inl.h"
11 #include "nsAccessibilityService.h"
12 #include "nsAccUtils.h"
13 #include "nsCoreUtils.h"
14 #include "nsEventShell.h"
15 #include "nsFrameSelection.h"
16 
17 #include "mozilla/PresShell.h"
18 #include "mozilla/dom/Document.h"
19 #include "mozilla/dom/Selection.h"
20 #include "mozilla/dom/Element.h"
21 
22 using namespace mozilla;
23 using namespace mozilla::a11y;
24 using mozilla::dom::Selection;
25 
26 struct mozilla::a11y::SelData final {
SelDatamozilla::a11y::SelData27   SelData(Selection* aSel, int32_t aReason) : mSel(aSel), mReason(aReason) {}
28 
29   RefPtr<Selection> mSel;
30   int16_t mReason;
31 
32   NS_INLINE_DECL_REFCOUNTING(SelData)
33 
34  private:
35   // Private destructor, to discourage deletion outside of Release():
~SelDatamozilla::a11y::SelData36   ~SelData() {}
37 };
38 
SelectionManager()39 SelectionManager::SelectionManager()
40     : mCaretOffset(-1), mAccWithCaret(nullptr) {}
41 
ClearControlSelectionListener()42 void SelectionManager::ClearControlSelectionListener() {
43   // Remove 'this' registered as selection listener for the normal selection.
44   if (mCurrCtrlNormalSel) {
45     mCurrCtrlNormalSel->RemoveSelectionListener(this);
46     mCurrCtrlNormalSel = nullptr;
47   }
48 
49   // Remove 'this' registered as selection listener for the spellcheck
50   // selection.
51   if (mCurrCtrlSpellSel) {
52     mCurrCtrlSpellSel->RemoveSelectionListener(this);
53     mCurrCtrlSpellSel = nullptr;
54   }
55 }
56 
SetControlSelectionListener(dom::Element * aFocusedElm)57 void SelectionManager::SetControlSelectionListener(dom::Element* aFocusedElm) {
58   // When focus moves such that the caret is part of a new frame selection
59   // this removes the old selection listener and attaches a new one for
60   // the current focus.
61   ClearControlSelectionListener();
62 
63   nsIFrame* controlFrame = aFocusedElm->GetPrimaryFrame();
64   if (!controlFrame) return;
65 
66   const nsFrameSelection* frameSel = controlFrame->GetConstFrameSelection();
67   NS_ASSERTION(frameSel, "No frame selection for focused element!");
68   if (!frameSel) return;
69 
70   // Register 'this' as selection listener for the normal selection.
71   Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal);
72   normalSel->AddSelectionListener(this);
73   mCurrCtrlNormalSel = normalSel;
74 
75   // Register 'this' as selection listener for the spell check selection.
76   Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck);
77   spellSel->AddSelectionListener(this);
78   mCurrCtrlSpellSel = spellSel;
79 }
80 
AddDocSelectionListener(PresShell * aPresShell)81 void SelectionManager::AddDocSelectionListener(PresShell* aPresShell) {
82   const nsFrameSelection* frameSel = aPresShell->ConstFrameSelection();
83 
84   // Register 'this' as selection listener for the normal selection.
85   Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal);
86   normalSel->AddSelectionListener(this);
87 
88   // Register 'this' as selection listener for the spell check selection.
89   Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck);
90   spellSel->AddSelectionListener(this);
91 }
92 
RemoveDocSelectionListener(PresShell * aPresShell)93 void SelectionManager::RemoveDocSelectionListener(PresShell* aPresShell) {
94   const nsFrameSelection* frameSel = aPresShell->ConstFrameSelection();
95 
96   // Remove 'this' registered as selection listener for the normal selection.
97   Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal);
98   normalSel->RemoveSelectionListener(this);
99 
100   // Remove 'this' registered as selection listener for the spellcheck
101   // selection.
102   Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck);
103   spellSel->RemoveSelectionListener(this);
104 
105   if (mCurrCtrlNormalSel) {
106     if (mCurrCtrlNormalSel->GetPresShell() == aPresShell) {
107       // Remove 'this' registered as selection listener for the normal selection
108       // if we are removing listeners for its PresShell.
109       mCurrCtrlNormalSel->RemoveSelectionListener(this);
110       mCurrCtrlNormalSel = nullptr;
111     }
112   }
113 
114   if (mCurrCtrlSpellSel) {
115     if (mCurrCtrlSpellSel->GetPresShell() == aPresShell) {
116       // Remove 'this' registered as selection listener for the spellcheck
117       // selection if we are removing listeners for its PresShell.
118       mCurrCtrlSpellSel->RemoveSelectionListener(this);
119       mCurrCtrlSpellSel = nullptr;
120     }
121   }
122 }
123 
ProcessTextSelChangeEvent(AccEvent * aEvent)124 void SelectionManager::ProcessTextSelChangeEvent(AccEvent* aEvent) {
125   // Fire selection change event if it's not pure caret-move selection change,
126   // i.e. the accessible has or had not collapsed selection.
127   AccTextSelChangeEvent* event = downcast_accEvent(aEvent);
128   if (!event->IsCaretMoveOnly()) nsEventShell::FireEvent(aEvent);
129 
130   // Fire caret move event if there's a caret in the selection.
131   nsINode* caretCntrNode = nsCoreUtils::GetDOMNodeFromDOMPoint(
132       event->mSel->GetFocusNode(), event->mSel->FocusOffset());
133   if (!caretCntrNode) return;
134 
135   HyperTextAccessible* caretCntr = nsAccUtils::GetTextContainer(caretCntrNode);
136   NS_ASSERTION(
137       caretCntr,
138       "No text container for focus while there's one for common ancestor?!");
139   if (!caretCntr) return;
140 
141   Selection* selection = caretCntr->DOMSelection();
142 
143   // XXX Sometimes we can't get a selection for caretCntr, in that case assume
144   // event->mSel is correct.
145   if (!selection) selection = event->mSel;
146 
147   mCaretOffset = caretCntr->DOMPointToOffset(selection->GetFocusNode(),
148                                              selection->FocusOffset());
149   mAccWithCaret = caretCntr;
150   if (mCaretOffset != -1) {
151     RefPtr<AccCaretMoveEvent> caretMoveEvent =
152         new AccCaretMoveEvent(caretCntr, mCaretOffset, selection->IsCollapsed(),
153                               aEvent->FromUserInput());
154     nsEventShell::FireEvent(caretMoveEvent);
155   }
156 }
157 
158 NS_IMETHODIMP
NotifySelectionChanged(dom::Document * aDocument,Selection * aSelection,int16_t aReason)159 SelectionManager::NotifySelectionChanged(dom::Document* aDocument,
160                                          Selection* aSelection,
161                                          int16_t aReason) {
162   if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) {
163     return NS_ERROR_INVALID_ARG;
164   }
165 
166   DocAccessible* document = GetAccService()->GetDocAccessible(aDocument);
167 
168 #ifdef A11Y_LOG
169   if (logging::IsEnabled(logging::eSelection)) {
170     logging::SelChange(aSelection, document, aReason);
171   }
172 #endif
173 
174   if (document) {
175     // Selection manager has longer lifetime than any document accessible,
176     // so that we are guaranteed that the notification is processed before
177     // the selection manager is destroyed.
178     RefPtr<SelData> selData = new SelData(aSelection, aReason);
179     document->HandleNotification<SelectionManager, SelData>(
180         this, &SelectionManager::ProcessSelectionChanged, selData);
181   }
182 
183   return NS_OK;
184 }
185 
ProcessSelectionChanged(SelData * aSelData)186 void SelectionManager::ProcessSelectionChanged(SelData* aSelData) {
187   Selection* selection = aSelData->mSel;
188   if (!selection->GetPresShell()) return;
189 
190   const nsRange* range = selection->GetAnchorFocusRange();
191   nsINode* cntrNode = nullptr;
192   if (range) {
193     cntrNode = range->GetClosestCommonInclusiveAncestor();
194   }
195 
196   if (!cntrNode) {
197     cntrNode = selection->GetFrameSelection()->GetAncestorLimiter();
198     if (!cntrNode) {
199       cntrNode = selection->GetPresShell()->GetDocument();
200       NS_ASSERTION(aSelData->mSel->GetPresShell()->ConstFrameSelection() ==
201                        selection->GetFrameSelection(),
202                    "Wrong selection container was used!");
203     }
204   }
205 
206   HyperTextAccessible* text = nsAccUtils::GetTextContainer(cntrNode);
207   if (!text) {
208     // FIXME bug 1126649
209     NS_ERROR("We must reach document accessible implementing text interface!");
210     return;
211   }
212 
213   if (selection->GetType() == SelectionType::eNormal) {
214     RefPtr<AccEvent> event =
215         new AccTextSelChangeEvent(text, selection, aSelData->mReason);
216     text->Document()->FireDelayedEvent(event);
217 
218   } else if (selection->GetType() == SelectionType::eSpellCheck) {
219     // XXX: fire an event for container accessible of the focus/anchor range
220     // of the spelcheck selection.
221     text->Document()->FireDelayedEvent(
222         nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED, text);
223   }
224 }
225 
226 SelectionManager::~SelectionManager() = default;
227