Line data Source code
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/. */
5 :
6 : /*
7 : This file provides the implementation for xul popup listener which
8 : tracks xul popups and context menus
9 : */
10 :
11 : #include "nsXULPopupListener.h"
12 : #include "nsCOMPtr.h"
13 : #include "nsGkAtoms.h"
14 : #include "nsIDOMElement.h"
15 : #include "nsIDOMXULElement.h"
16 : #include "nsIDOMNodeList.h"
17 : #include "nsIDOMDocument.h"
18 : #include "nsIDOMDocumentXBL.h"
19 : #include "nsContentCID.h"
20 : #include "nsContentUtils.h"
21 : #include "nsXULPopupManager.h"
22 : #include "nsIScriptContext.h"
23 : #include "nsIDOMWindow.h"
24 : #include "nsIDOMXULDocument.h"
25 : #include "nsIDocument.h"
26 : #include "nsServiceManagerUtils.h"
27 : #include "nsIPrincipal.h"
28 : #include "nsIScriptSecurityManager.h"
29 : #include "nsLayoutUtils.h"
30 : #include "mozilla/ReflowInput.h"
31 : #include "nsIObjectLoadingContent.h"
32 : #include "mozilla/EventStateManager.h"
33 : #include "mozilla/EventStates.h"
34 : #include "mozilla/Preferences.h"
35 : #include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
36 : #include "mozilla/dom/EventTarget.h"
37 : #include "mozilla/dom/FragmentOrElement.h"
38 :
39 : // for event firing in context menus
40 : #include "nsPresContext.h"
41 : #include "nsIPresShell.h"
42 : #include "nsFocusManager.h"
43 : #include "nsPIDOMWindow.h"
44 : #include "nsViewManager.h"
45 : #include "nsError.h"
46 : #include "nsMenuFrame.h"
47 :
48 : using namespace mozilla;
49 : using namespace mozilla::dom;
50 :
51 : // on win32 and os/2, context menus come up on mouse up. On other platforms,
52 : // they appear on mouse down. Certain bits of code care about this difference.
53 : #if defined(XP_WIN)
54 : #define NS_CONTEXT_MENU_IS_MOUSEUP 1
55 : #endif
56 :
57 25 : nsXULPopupListener::nsXULPopupListener(mozilla::dom::Element* aElement,
58 25 : bool aIsContext)
59 25 : : mElement(aElement), mPopupContent(nullptr), mIsContext(aIsContext)
60 : {
61 25 : }
62 :
63 0 : nsXULPopupListener::~nsXULPopupListener(void)
64 : {
65 0 : ClosePopup();
66 0 : }
67 :
68 0 : NS_IMPL_CYCLE_COLLECTION(nsXULPopupListener, mElement, mPopupContent)
69 75 : NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPopupListener)
70 50 : NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPopupListener)
71 :
72 0 : NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsXULPopupListener)
73 : // If the owner, mElement, can be skipped, so can we.
74 0 : if (tmp->mElement) {
75 0 : return mozilla::dom::FragmentOrElement::CanSkip(tmp->mElement, true);
76 : }
77 0 : NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
78 :
79 0 : NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXULPopupListener)
80 0 : if (tmp->mElement) {
81 0 : return mozilla::dom::FragmentOrElement::CanSkipInCC(tmp->mElement);
82 : }
83 0 : NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
84 :
85 0 : NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXULPopupListener)
86 0 : if (tmp->mElement) {
87 0 : return mozilla::dom::FragmentOrElement::CanSkipThis(tmp->mElement);
88 : }
89 0 : NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
90 :
91 75 : NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener)
92 50 : NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
93 25 : NS_INTERFACE_MAP_ENTRY(nsISupports)
94 25 : NS_INTERFACE_MAP_END
95 :
96 : ////////////////////////////////////////////////////////////////
97 : // nsIDOMEventListener
98 :
99 : nsresult
100 0 : nsXULPopupListener::HandleEvent(nsIDOMEvent* aEvent)
101 : {
102 0 : nsAutoString eventType;
103 0 : aEvent->GetType(eventType);
104 :
105 0 : if(!((eventType.EqualsLiteral("mousedown") && !mIsContext) ||
106 0 : (eventType.EqualsLiteral("contextmenu") && mIsContext)))
107 0 : return NS_OK;
108 :
109 : int16_t button;
110 :
111 0 : nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
112 0 : if (!mouseEvent) {
113 : //non-ui event passed in. bad things.
114 0 : return NS_OK;
115 : }
116 :
117 : // Get the node that was clicked on.
118 0 : EventTarget* target = mouseEvent->AsEvent()->InternalDOMEvent()->GetTarget();
119 0 : nsCOMPtr<nsIDOMNode> targetNode = do_QueryInterface(target);
120 :
121 0 : if (!targetNode && mIsContext) {
122 : // Not a DOM node, see if it's the DOM window (bug 380818).
123 0 : nsCOMPtr<nsPIDOMWindowInner> domWin = do_QueryInterface(target);
124 0 : if (!domWin) {
125 0 : return NS_ERROR_DOM_WRONG_TYPE_ERR;
126 : }
127 : // Try to use the root node as target node.
128 0 : nsCOMPtr<nsIDocument> doc = domWin->GetDoc();
129 :
130 0 : if (doc)
131 0 : targetNode = do_QueryInterface(doc->GetRootElement());
132 0 : if (!targetNode) {
133 0 : return NS_ERROR_FAILURE;
134 : }
135 : }
136 :
137 0 : nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
138 0 : if (!targetContent) {
139 0 : return NS_OK;
140 : }
141 0 : if (EventStateManager::IsRemoteTarget(targetContent)) {
142 0 : return NS_OK;
143 : }
144 :
145 : bool preventDefault;
146 0 : mouseEvent->AsEvent()->GetDefaultPrevented(&preventDefault);
147 0 : if (preventDefault && targetNode && mIsContext) {
148 : // Someone called preventDefault on a context menu.
149 : // Let's make sure they are allowed to do so.
150 : bool eventEnabled =
151 0 : Preferences::GetBool("dom.event.contextmenu.enabled", true);
152 0 : if (!eventEnabled) {
153 : // If the target node is for plug-in, we should not open XUL context
154 : // menu on windowless plug-ins.
155 0 : nsCOMPtr<nsIObjectLoadingContent> olc = do_QueryInterface(targetNode);
156 : uint32_t type;
157 0 : if (olc && NS_SUCCEEDED(olc->GetDisplayedType(&type)) &&
158 0 : type == nsIObjectLoadingContent::TYPE_PLUGIN) {
159 0 : return NS_OK;
160 : }
161 :
162 : // The user wants his contextmenus. Let's make sure that this is a website
163 : // and not chrome since there could be places in chrome which don't want
164 : // contextmenus.
165 0 : nsCOMPtr<nsINode> node = do_QueryInterface(targetNode);
166 0 : if (node) {
167 0 : nsCOMPtr<nsIPrincipal> system;
168 0 : nsContentUtils::GetSecurityManager()->
169 0 : GetSystemPrincipal(getter_AddRefs(system));
170 0 : if (node->NodePrincipal() != system) {
171 : // This isn't chrome. Cancel the preventDefault() and
172 : // let the event go forth.
173 0 : preventDefault = false;
174 : }
175 : }
176 : }
177 : }
178 :
179 0 : if (preventDefault) {
180 : // someone called preventDefault. bail.
181 0 : return NS_OK;
182 : }
183 :
184 : // prevent popups on menu and menuitems as they handle their own popups
185 : // This was added for bug 96920.
186 : // If a menu item child was clicked on that leads to a popup needing
187 : // to show, we know (guaranteed) that we're dealing with a menu or
188 : // submenu of an already-showing popup. We don't need to do anything at all.
189 0 : if (!mIsContext) {
190 0 : if (targetContent &&
191 0 : targetContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem))
192 0 : return NS_OK;
193 : }
194 :
195 0 : if (mIsContext) {
196 : #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
197 0 : uint16_t inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN;
198 0 : mouseEvent->GetMozInputSource(&inputSource);
199 0 : bool isTouch = inputSource == nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
200 : // If the context menu launches on mousedown,
201 : // we have to fire focus on the content we clicked on
202 0 : FireFocusOnTargetContent(targetNode, isTouch);
203 : #endif
204 : }
205 : else {
206 : // Only open popups when the left mouse button is down.
207 0 : mouseEvent->GetButton(&button);
208 0 : if (button != 0)
209 0 : return NS_OK;
210 : }
211 :
212 : // Open the popup. LaunchPopup will call StopPropagation and PreventDefault
213 : // in the right situations.
214 0 : LaunchPopup(aEvent, targetContent);
215 :
216 0 : return NS_OK;
217 : }
218 :
219 : #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
220 : nsresult
221 0 : nsXULPopupListener::FireFocusOnTargetContent(nsIDOMNode* aTargetNode, bool aIsTouch)
222 : {
223 : nsresult rv;
224 0 : nsCOMPtr<nsIDOMDocument> domDoc;
225 0 : rv = aTargetNode->GetOwnerDocument(getter_AddRefs(domDoc));
226 0 : if(NS_SUCCEEDED(rv) && domDoc)
227 : {
228 0 : nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
229 :
230 : // Get nsIDOMElement for targetNode
231 0 : nsIPresShell *shell = doc->GetShell();
232 0 : if (!shell)
233 0 : return NS_ERROR_FAILURE;
234 :
235 : // strong reference to keep this from going away between events
236 : // XXXbz between what events? We don't use this local at all!
237 0 : RefPtr<nsPresContext> context = shell->GetPresContext();
238 :
239 0 : nsCOMPtr<nsIContent> content = do_QueryInterface(aTargetNode);
240 0 : nsIFrame* targetFrame = content->GetPrimaryFrame();
241 0 : if (!targetFrame) return NS_ERROR_FAILURE;
242 :
243 0 : const nsStyleUserInterface* ui = targetFrame->StyleUserInterface();
244 0 : bool suppressBlur = (ui->mUserFocus == StyleUserFocus::Ignore);
245 :
246 0 : nsCOMPtr<nsIDOMElement> element;
247 0 : nsCOMPtr<nsIContent> newFocus = do_QueryInterface(content);
248 :
249 0 : nsIFrame* currFrame = targetFrame;
250 : // Look for the nearest enclosing focusable frame.
251 0 : while (currFrame) {
252 : int32_t tabIndexUnused;
253 0 : if (currFrame->IsFocusable(&tabIndexUnused, true)) {
254 0 : newFocus = currFrame->GetContent();
255 0 : nsCOMPtr<nsIDOMElement> domElement(do_QueryInterface(newFocus));
256 0 : if (domElement) {
257 0 : element = domElement;
258 0 : break;
259 : }
260 : }
261 0 : currFrame = currFrame->GetParent();
262 : }
263 :
264 0 : nsIFocusManager* fm = nsFocusManager::GetFocusManager();
265 0 : if (fm) {
266 0 : if (element) {
267 : uint32_t focusFlags = nsIFocusManager::FLAG_BYMOUSE |
268 0 : nsIFocusManager::FLAG_NOSCROLL;
269 0 : if (aIsTouch) {
270 0 : focusFlags |= nsIFocusManager::FLAG_BYTOUCH;
271 : }
272 0 : fm->SetFocus(element, focusFlags);
273 0 : } else if (!suppressBlur) {
274 0 : nsPIDOMWindowOuter *window = doc->GetWindow();
275 0 : fm->ClearFocus(window);
276 : }
277 : }
278 :
279 0 : EventStateManager* esm = context->EventStateManager();
280 0 : nsCOMPtr<nsIContent> focusableContent = do_QueryInterface(element);
281 0 : esm->SetContentState(focusableContent, NS_EVENT_STATE_ACTIVE);
282 : }
283 0 : return rv;
284 : }
285 : #endif
286 :
287 : // ClosePopup
288 : //
289 : // Do everything needed to shut down the popup.
290 : //
291 : // NOTE: This routine is safe to call even if the popup is already closed.
292 : //
293 : void
294 0 : nsXULPopupListener::ClosePopup()
295 : {
296 0 : if (mPopupContent) {
297 : // this is called when the listener is going away, so make sure that the
298 : // popup is hidden. Use asynchronous hiding just to be safe so we don't
299 : // fire events during destruction.
300 0 : nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
301 0 : if (pm)
302 0 : pm->HidePopup(mPopupContent, false, true, true, false);
303 0 : mPopupContent = nullptr; // release the popup
304 : }
305 0 : } // ClosePopup
306 :
307 : static already_AddRefed<nsIContent>
308 0 : GetImmediateChild(nsIContent* aContent, nsIAtom *aTag)
309 : {
310 0 : for (nsIContent* child = aContent->GetFirstChild();
311 0 : child;
312 0 : child = child->GetNextSibling()) {
313 0 : if (child->IsXULElement(aTag)) {
314 0 : nsCOMPtr<nsIContent> ret = child;
315 0 : return ret.forget();
316 : }
317 : }
318 :
319 0 : return nullptr;
320 : }
321 :
322 : //
323 : // LaunchPopup
324 : //
325 : // Given the element on which the event was triggered and the mouse locations in
326 : // Client and widget coordinates, popup a new window showing the appropriate
327 : // content.
328 : //
329 : // aTargetContent is the target of the mouse event aEvent that triggered the
330 : // popup. mElement is the element that the popup menu is attached to.
331 : // aTargetContent may be equal to mElement or it may be a descendant.
332 : //
333 : // This looks for an attribute on |mElement| of the appropriate popup type
334 : // (popup, context) and uses that attribute's value as an ID for
335 : // the popup content in the document.
336 : //
337 : nsresult
338 0 : nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
339 : {
340 0 : nsresult rv = NS_OK;
341 :
342 0 : nsAutoString identifier;
343 0 : nsIAtom* type = mIsContext ? nsGkAtoms::context : nsGkAtoms::popup;
344 0 : bool hasPopupAttr = mElement->GetAttr(kNameSpaceID_None, type, identifier);
345 :
346 0 : if (identifier.IsEmpty()) {
347 0 : hasPopupAttr = mElement->GetAttr(kNameSpaceID_None,
348 0 : mIsContext ? nsGkAtoms::contextmenu : nsGkAtoms::menu,
349 0 : identifier) || hasPopupAttr;
350 : }
351 :
352 0 : if (hasPopupAttr) {
353 0 : aEvent->StopPropagation();
354 0 : aEvent->PreventDefault();
355 : }
356 :
357 0 : if (identifier.IsEmpty())
358 0 : return rv;
359 :
360 : // Try to find the popup content and the document.
361 0 : nsCOMPtr<nsIDocument> document = mElement->GetComposedDoc();
362 0 : if (!document) {
363 0 : NS_WARNING("No document!");
364 0 : return NS_ERROR_FAILURE;
365 : }
366 :
367 : // Handle the _child case for popups and context menus
368 0 : nsCOMPtr<nsIContent> popup;
369 0 : if (identifier.EqualsLiteral("_child")) {
370 0 : popup = GetImmediateChild(mElement, nsGkAtoms::menupopup);
371 0 : if (!popup) {
372 0 : nsCOMPtr<nsIDOMDocumentXBL> nsDoc(do_QueryInterface(document));
373 0 : nsCOMPtr<nsIDOMNodeList> list;
374 0 : nsCOMPtr<nsIDOMElement> el = do_QueryInterface(mElement);
375 0 : nsDoc->GetAnonymousNodes(el, getter_AddRefs(list));
376 0 : if (list) {
377 : uint32_t ctr,listLength;
378 0 : nsCOMPtr<nsIDOMNode> node;
379 0 : list->GetLength(&listLength);
380 0 : for (ctr = 0; ctr < listLength; ctr++) {
381 0 : list->Item(ctr, getter_AddRefs(node));
382 0 : nsCOMPtr<nsIContent> childContent(do_QueryInterface(node));
383 :
384 0 : if (childContent->NodeInfo()->Equals(nsGkAtoms::menupopup,
385 : kNameSpaceID_XUL)) {
386 0 : popup.swap(childContent);
387 0 : break;
388 : }
389 : }
390 : }
391 : }
392 0 : } else if (!mElement->IsInUncomposedDoc() ||
393 0 : !(popup = document->GetElementById(identifier))) {
394 : // XXXsmaug Should we try to use ShadowRoot::GetElementById in case
395 : // mElement is in shadow DOM?
396 : //
397 : // Use getElementById to obtain the popup content and gracefully fail if
398 : // we didn't find any popup content in the document.
399 0 : NS_WARNING("GetElementById had some kind of spasm.");
400 0 : return rv;
401 : }
402 :
403 : // return if no popup was found or the popup is the element itself.
404 0 : if (!popup || popup == mElement)
405 0 : return NS_OK;
406 :
407 : // Submenus can't be used as context menus or popups, bug 288763.
408 : // Similar code also in nsXULTooltipListener::GetTooltipFor.
409 0 : nsIContent* parent = popup->GetParent();
410 0 : if (parent) {
411 0 : nsMenuFrame* menu = do_QueryFrame(parent->GetPrimaryFrame());
412 0 : if (menu)
413 0 : return NS_OK;
414 : }
415 :
416 0 : nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
417 0 : if (!pm)
418 0 : return NS_OK;
419 :
420 : // For left-clicks, if the popup has an position attribute, or both the
421 : // popupanchor and popupalign attributes are used, anchor the popup to the
422 : // element, otherwise just open it at the screen position where the mouse
423 : // was clicked. Context menus always open at the mouse position.
424 0 : mPopupContent = popup;
425 0 : if (!mIsContext &&
426 0 : (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::position) ||
427 0 : (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupanchor) &&
428 0 : mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupalign)))) {
429 0 : pm->ShowPopup(mPopupContent, mElement, EmptyString(), 0, 0,
430 0 : false, true, false, aEvent);
431 : }
432 : else {
433 0 : int32_t xPos = 0, yPos = 0;
434 0 : nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
435 0 : mouseEvent->GetScreenX(&xPos);
436 0 : mouseEvent->GetScreenY(&yPos);
437 :
438 0 : pm->ShowPopupAtScreen(mPopupContent, xPos, yPos, mIsContext, aEvent);
439 : }
440 :
441 0 : return NS_OK;
442 : }
|