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
nsXULPopupListener(mozilla::dom::Element * aElement,bool aIsContext)57 nsXULPopupListener::nsXULPopupListener(mozilla::dom::Element* aElement,
58 bool aIsContext)
59 : mElement(aElement), mPopupContent(nullptr), mIsContext(aIsContext)
60 {
61 }
62
~nsXULPopupListener(void)63 nsXULPopupListener::~nsXULPopupListener(void)
64 {
65 ClosePopup();
66 }
67
68 NS_IMPL_CYCLE_COLLECTION(nsXULPopupListener, mElement, mPopupContent)
69 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPopupListener)
70 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPopupListener)
71
72 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsXULPopupListener)
73 // If the owner, mElement, can be skipped, so can we.
74 if (tmp->mElement) {
75 return mozilla::dom::FragmentOrElement::CanSkip(tmp->mElement, true);
76 }
77 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
78
79 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXULPopupListener)
80 if (tmp->mElement) {
81 return mozilla::dom::FragmentOrElement::CanSkipInCC(tmp->mElement);
82 }
83 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
84
85 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXULPopupListener)
86 if (tmp->mElement) {
87 return mozilla::dom::FragmentOrElement::CanSkipThis(tmp->mElement);
88 }
89 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
90
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener)91 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener)
92 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
93 NS_INTERFACE_MAP_ENTRY(nsISupports)
94 NS_INTERFACE_MAP_END
95
96 ////////////////////////////////////////////////////////////////
97 // nsIDOMEventListener
98
99 nsresult
100 nsXULPopupListener::HandleEvent(nsIDOMEvent* aEvent)
101 {
102 nsAutoString eventType;
103 aEvent->GetType(eventType);
104
105 if(!((eventType.EqualsLiteral("mousedown") && !mIsContext) ||
106 (eventType.EqualsLiteral("contextmenu") && mIsContext)))
107 return NS_OK;
108
109 int16_t button;
110
111 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
112 if (!mouseEvent) {
113 //non-ui event passed in. bad things.
114 return NS_OK;
115 }
116
117 // Get the node that was clicked on.
118 EventTarget* target = mouseEvent->AsEvent()->InternalDOMEvent()->GetTarget();
119 nsCOMPtr<nsIDOMNode> targetNode = do_QueryInterface(target);
120
121 if (!targetNode && mIsContext) {
122 // Not a DOM node, see if it's the DOM window (bug 380818).
123 nsCOMPtr<nsPIDOMWindowInner> domWin = do_QueryInterface(target);
124 if (!domWin) {
125 return NS_ERROR_DOM_WRONG_TYPE_ERR;
126 }
127 // Try to use the root node as target node.
128 nsCOMPtr<nsIDocument> doc = domWin->GetDoc();
129
130 if (doc)
131 targetNode = do_QueryInterface(doc->GetRootElement());
132 if (!targetNode) {
133 return NS_ERROR_FAILURE;
134 }
135 }
136
137 nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
138 if (!targetContent) {
139 return NS_OK;
140 }
141 if (EventStateManager::IsRemoteTarget(targetContent)) {
142 return NS_OK;
143 }
144
145 bool preventDefault;
146 mouseEvent->AsEvent()->GetDefaultPrevented(&preventDefault);
147 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 Preferences::GetBool("dom.event.contextmenu.enabled", true);
152 if (!eventEnabled) {
153 // If the target node is for plug-in, we should not open XUL context
154 // menu on windowless plug-ins.
155 nsCOMPtr<nsIObjectLoadingContent> olc = do_QueryInterface(targetNode);
156 uint32_t type;
157 if (olc && NS_SUCCEEDED(olc->GetDisplayedType(&type)) &&
158 type == nsIObjectLoadingContent::TYPE_PLUGIN) {
159 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 nsCOMPtr<nsINode> node = do_QueryInterface(targetNode);
166 if (node) {
167 nsCOMPtr<nsIPrincipal> system;
168 nsContentUtils::GetSecurityManager()->
169 GetSystemPrincipal(getter_AddRefs(system));
170 if (node->NodePrincipal() != system) {
171 // This isn't chrome. Cancel the preventDefault() and
172 // let the event go forth.
173 preventDefault = false;
174 }
175 }
176 }
177 }
178
179 if (preventDefault) {
180 // someone called preventDefault. bail.
181 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 if (!mIsContext) {
190 if (targetContent &&
191 targetContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem))
192 return NS_OK;
193 }
194
195 if (mIsContext) {
196 #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
197 uint16_t inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN;
198 mouseEvent->GetMozInputSource(&inputSource);
199 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 FireFocusOnTargetContent(targetNode, isTouch);
203 #endif
204 }
205 else {
206 // Only open popups when the left mouse button is down.
207 mouseEvent->GetButton(&button);
208 if (button != 0)
209 return NS_OK;
210 }
211
212 // Open the popup. LaunchPopup will call StopPropagation and PreventDefault
213 // in the right situations.
214 LaunchPopup(aEvent, targetContent);
215
216 return NS_OK;
217 }
218
219 #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
220 nsresult
FireFocusOnTargetContent(nsIDOMNode * aTargetNode,bool aIsTouch)221 nsXULPopupListener::FireFocusOnTargetContent(nsIDOMNode* aTargetNode, bool aIsTouch)
222 {
223 nsresult rv;
224 nsCOMPtr<nsIDOMDocument> domDoc;
225 rv = aTargetNode->GetOwnerDocument(getter_AddRefs(domDoc));
226 if(NS_SUCCEEDED(rv) && domDoc)
227 {
228 nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
229
230 // Get nsIDOMElement for targetNode
231 nsIPresShell *shell = doc->GetShell();
232 if (!shell)
233 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 RefPtr<nsPresContext> context = shell->GetPresContext();
238
239 nsCOMPtr<nsIContent> content = do_QueryInterface(aTargetNode);
240 nsIFrame* targetFrame = content->GetPrimaryFrame();
241 if (!targetFrame) return NS_ERROR_FAILURE;
242
243 const nsStyleUserInterface* ui = targetFrame->StyleUserInterface();
244 bool suppressBlur = (ui->mUserFocus == StyleUserFocus::Ignore);
245
246 nsCOMPtr<nsIDOMElement> element;
247 nsCOMPtr<nsIContent> newFocus = do_QueryInterface(content);
248
249 nsIFrame* currFrame = targetFrame;
250 // Look for the nearest enclosing focusable frame.
251 while (currFrame) {
252 int32_t tabIndexUnused;
253 if (currFrame->IsFocusable(&tabIndexUnused, true)) {
254 newFocus = currFrame->GetContent();
255 nsCOMPtr<nsIDOMElement> domElement(do_QueryInterface(newFocus));
256 if (domElement) {
257 element = domElement;
258 break;
259 }
260 }
261 currFrame = currFrame->GetParent();
262 }
263
264 nsIFocusManager* fm = nsFocusManager::GetFocusManager();
265 if (fm) {
266 if (element) {
267 uint32_t focusFlags = nsIFocusManager::FLAG_BYMOUSE |
268 nsIFocusManager::FLAG_NOSCROLL;
269 if (aIsTouch) {
270 focusFlags |= nsIFocusManager::FLAG_BYTOUCH;
271 }
272 fm->SetFocus(element, focusFlags);
273 } else if (!suppressBlur) {
274 nsPIDOMWindowOuter *window = doc->GetWindow();
275 fm->ClearFocus(window);
276 }
277 }
278
279 EventStateManager* esm = context->EventStateManager();
280 nsCOMPtr<nsIContent> focusableContent = do_QueryInterface(element);
281 esm->SetContentState(focusableContent, NS_EVENT_STATE_ACTIVE);
282 }
283 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
ClosePopup()294 nsXULPopupListener::ClosePopup()
295 {
296 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 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
301 if (pm)
302 pm->HidePopup(mPopupContent, false, true, true, false);
303 mPopupContent = nullptr; // release the popup
304 }
305 } // ClosePopup
306
307 static already_AddRefed<nsIContent>
GetImmediateChild(nsIContent * aContent,nsIAtom * aTag)308 GetImmediateChild(nsIContent* aContent, nsIAtom *aTag)
309 {
310 for (nsIContent* child = aContent->GetFirstChild();
311 child;
312 child = child->GetNextSibling()) {
313 if (child->IsXULElement(aTag)) {
314 nsCOMPtr<nsIContent> ret = child;
315 return ret.forget();
316 }
317 }
318
319 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
LaunchPopup(nsIDOMEvent * aEvent,nsIContent * aTargetContent)338 nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
339 {
340 nsresult rv = NS_OK;
341
342 nsAutoString identifier;
343 nsIAtom* type = mIsContext ? nsGkAtoms::context : nsGkAtoms::popup;
344 bool hasPopupAttr = mElement->GetAttr(kNameSpaceID_None, type, identifier);
345
346 if (identifier.IsEmpty()) {
347 hasPopupAttr = mElement->GetAttr(kNameSpaceID_None,
348 mIsContext ? nsGkAtoms::contextmenu : nsGkAtoms::menu,
349 identifier) || hasPopupAttr;
350 }
351
352 if (hasPopupAttr) {
353 aEvent->StopPropagation();
354 aEvent->PreventDefault();
355 }
356
357 if (identifier.IsEmpty())
358 return rv;
359
360 // Try to find the popup content and the document.
361 nsCOMPtr<nsIDocument> document = mElement->GetComposedDoc();
362 if (!document) {
363 NS_WARNING("No document!");
364 return NS_ERROR_FAILURE;
365 }
366
367 // Handle the _child case for popups and context menus
368 nsCOMPtr<nsIContent> popup;
369 if (identifier.EqualsLiteral("_child")) {
370 popup = GetImmediateChild(mElement, nsGkAtoms::menupopup);
371 if (!popup) {
372 nsCOMPtr<nsIDOMDocumentXBL> nsDoc(do_QueryInterface(document));
373 nsCOMPtr<nsIDOMNodeList> list;
374 nsCOMPtr<nsIDOMElement> el = do_QueryInterface(mElement);
375 nsDoc->GetAnonymousNodes(el, getter_AddRefs(list));
376 if (list) {
377 uint32_t ctr,listLength;
378 nsCOMPtr<nsIDOMNode> node;
379 list->GetLength(&listLength);
380 for (ctr = 0; ctr < listLength; ctr++) {
381 list->Item(ctr, getter_AddRefs(node));
382 nsCOMPtr<nsIContent> childContent(do_QueryInterface(node));
383
384 if (childContent->NodeInfo()->Equals(nsGkAtoms::menupopup,
385 kNameSpaceID_XUL)) {
386 popup.swap(childContent);
387 break;
388 }
389 }
390 }
391 }
392 } else if (!mElement->IsInUncomposedDoc() ||
393 !(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 NS_WARNING("GetElementById had some kind of spasm.");
400 return rv;
401 }
402
403 // return if no popup was found or the popup is the element itself.
404 if (!popup || popup == mElement)
405 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 nsIContent* parent = popup->GetParent();
410 if (parent) {
411 nsMenuFrame* menu = do_QueryFrame(parent->GetPrimaryFrame());
412 if (menu)
413 return NS_OK;
414 }
415
416 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
417 if (!pm)
418 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 mPopupContent = popup;
425 if (!mIsContext &&
426 (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::position) ||
427 (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupanchor) &&
428 mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupalign)))) {
429 pm->ShowPopup(mPopupContent, mElement, EmptyString(), 0, 0,
430 false, true, false, aEvent);
431 }
432 else {
433 int32_t xPos = 0, yPos = 0;
434 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
435 mouseEvent->GetScreenX(&xPos);
436 mouseEvent->GetScreenY(&yPos);
437
438 pm->ShowPopupAtScreen(mPopupContent, xPos, yPos, mIsContext, aEvent);
439 }
440
441 return NS_OK;
442 }
443