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