1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* ***** BEGIN LICENSE BLOCK *****
3 : * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 : *
5 : * The contents of this file are subject to the Mozilla Public License Version
6 : * 1.1 (the "License"); you may not use this file except in compliance with
7 : * the License. You may obtain a copy of the License at
8 : * http://www.mozilla.org/MPL/
9 : *
10 : * Software distributed under the License is distributed on an "AS IS" basis,
11 : * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 : * for the specific language governing rights and limitations under the
13 : * License.
14 : *
15 : * The Original Code is mozilla.org code.
16 : *
17 : * The Initial Developer of the Original Code is
18 : * Netscape Communications Corporation.
19 : * Portions created by the Initial Developer are Copyright (C) 1998
20 : * the Initial Developer. All Rights Reserved.
21 : *
22 : * Contributor(s):
23 : * Original Author: David W. Hyatt (hyatt@netscape.com)
24 : * Dean Tessman <dean_tessman@hotmail.com>
25 : * Pierre Phaneuf <pp@ludusdesign.com>
26 : * Robert O'Callahan <roc+moz@cs.cmu.edu>
27 : *
28 : * Alternatively, the contents of this file may be used under the terms of
29 : * either of the GNU General Public License Version 2 or later (the "GPL"),
30 : * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
31 : * in which case the provisions of the GPL or the LGPL are applicable instead
32 : * of those above. If you wish to allow use of your version of this file only
33 : * under the terms of either the GPL or the LGPL, and not to allow others to
34 : * use your version of this file under the terms of the MPL, indicate your
35 : * decision by deleting the provisions above and replace them with the notice
36 : * and other provisions required by the GPL or the LGPL. If you do not delete
37 : * the provisions above, a recipient may use your version of this file under
38 : * the terms of any one of the MPL, the GPL or the LGPL.
39 : *
40 : * ***** END LICENSE BLOCK ***** */
41 :
42 : /*
43 : This file provides the implementation for xul popup listener which
44 : tracks xul popups and context menus
45 : */
46 :
47 : #include "nsXULPopupListener.h"
48 : #include "nsCOMPtr.h"
49 : #include "nsGkAtoms.h"
50 : #include "nsIDOMElement.h"
51 : #include "nsIDOMXULElement.h"
52 : #include "nsIDOMNodeList.h"
53 : #include "nsIDOMDocument.h"
54 : #include "nsIDOMDocumentXBL.h"
55 : #include "nsContentCID.h"
56 : #include "nsContentUtils.h"
57 : #include "nsXULPopupManager.h"
58 : #include "nsEventStateManager.h"
59 : #include "nsIScriptContext.h"
60 : #include "nsIDOMWindow.h"
61 : #include "nsIDOMXULDocument.h"
62 : #include "nsIDocument.h"
63 : #include "nsIDOMEventTarget.h"
64 : #include "nsIDOMNSEvent.h"
65 : #include "nsServiceManagerUtils.h"
66 : #include "nsIPrincipal.h"
67 : #include "nsIScriptSecurityManager.h"
68 : #include "nsLayoutUtils.h"
69 : #include "nsFrameManager.h"
70 : #include "nsHTMLReflowState.h"
71 : #include "nsIObjectLoadingContent.h"
72 : #include "mozilla/Preferences.h"
73 : #include "mozilla/dom/Element.h"
74 :
75 : // for event firing in context menus
76 : #include "nsPresContext.h"
77 : #include "nsIPresShell.h"
78 : #include "nsFocusManager.h"
79 : #include "nsPIDOMWindow.h"
80 : #include "nsIViewManager.h"
81 : #include "nsDOMError.h"
82 :
83 : using namespace mozilla;
84 :
85 : // on win32 and os/2, context menus come up on mouse up. On other platforms,
86 : // they appear on mouse down. Certain bits of code care about this difference.
87 : #if defined(XP_WIN) || defined(XP_OS2)
88 : #define NS_CONTEXT_MENU_IS_MOUSEUP 1
89 : #endif
90 :
91 0 : nsXULPopupListener::nsXULPopupListener(nsIDOMElement *aElement, bool aIsContext)
92 0 : : mElement(aElement), mPopupContent(nsnull), mIsContext(aIsContext)
93 : {
94 0 : }
95 :
96 0 : nsXULPopupListener::~nsXULPopupListener(void)
97 : {
98 0 : ClosePopup();
99 0 : }
100 :
101 1464 : NS_IMPL_CYCLE_COLLECTION_2(nsXULPopupListener, mElement, mPopupContent)
102 0 : NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPopupListener)
103 0 : NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPopupListener)
104 :
105 0 : NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener)
106 0 : NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
107 0 : NS_INTERFACE_MAP_ENTRY(nsISupports)
108 0 : NS_INTERFACE_MAP_END
109 :
110 : ////////////////////////////////////////////////////////////////
111 : // nsIDOMEventListener
112 :
113 : nsresult
114 0 : nsXULPopupListener::HandleEvent(nsIDOMEvent* aEvent)
115 : {
116 0 : nsAutoString eventType;
117 0 : aEvent->GetType(eventType);
118 :
119 0 : if(!((eventType.EqualsLiteral("mousedown") && !mIsContext) ||
120 0 : (eventType.EqualsLiteral("contextmenu") && mIsContext)))
121 0 : return NS_OK;
122 :
123 : PRUint16 button;
124 :
125 0 : nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
126 0 : if (!mouseEvent) {
127 : //non-ui event passed in. bad things.
128 0 : return NS_OK;
129 : }
130 :
131 : // check if someone has attempted to prevent this action.
132 0 : nsCOMPtr<nsIDOMNSEvent> domNSEvent = do_QueryInterface(mouseEvent);
133 0 : if (!domNSEvent) {
134 0 : return NS_OK;
135 : }
136 :
137 : // Get the node that was clicked on.
138 0 : nsCOMPtr<nsIDOMEventTarget> target;
139 0 : mouseEvent->GetTarget(getter_AddRefs(target));
140 0 : nsCOMPtr<nsIDOMNode> targetNode = do_QueryInterface(target);
141 :
142 0 : if (!targetNode && mIsContext) {
143 : // Not a DOM node, see if it's the DOM window (bug 380818).
144 0 : nsCOMPtr<nsIDOMWindow> domWin = do_QueryInterface(target);
145 0 : if (!domWin) {
146 0 : return NS_ERROR_DOM_WRONG_TYPE_ERR;
147 : }
148 : // Try to use the root node as target node.
149 0 : nsCOMPtr<nsIDOMDocument> domDoc;
150 0 : domWin->GetDocument(getter_AddRefs(domDoc));
151 :
152 0 : nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
153 0 : if (doc)
154 0 : targetNode = do_QueryInterface(doc->GetRootElement());
155 0 : if (!targetNode) {
156 0 : return NS_ERROR_FAILURE;
157 : }
158 : }
159 :
160 : bool preventDefault;
161 0 : domNSEvent->GetPreventDefault(&preventDefault);
162 0 : if (preventDefault && targetNode && mIsContext) {
163 : // Someone called preventDefault on a context menu.
164 : // Let's make sure they are allowed to do so.
165 : bool eventEnabled =
166 0 : Preferences::GetBool("dom.event.contextmenu.enabled", true);
167 0 : if (!eventEnabled) {
168 : // If the target node is for plug-in, we should not open XUL context
169 : // menu on windowless plug-ins.
170 0 : nsCOMPtr<nsIObjectLoadingContent> olc = do_QueryInterface(targetNode);
171 : PRUint32 type;
172 0 : if (olc && NS_SUCCEEDED(olc->GetDisplayedType(&type)) &&
173 : type == nsIObjectLoadingContent::TYPE_PLUGIN) {
174 0 : return NS_OK;
175 : }
176 :
177 : // The user wants his contextmenus. Let's make sure that this is a website
178 : // and not chrome since there could be places in chrome which don't want
179 : // contextmenus.
180 0 : nsCOMPtr<nsINode> node = do_QueryInterface(targetNode);
181 0 : if (node) {
182 0 : nsCOMPtr<nsIPrincipal> system;
183 0 : nsContentUtils::GetSecurityManager()->
184 0 : GetSystemPrincipal(getter_AddRefs(system));
185 0 : if (node->NodePrincipal() != system) {
186 : // This isn't chrome. Cancel the preventDefault() and
187 : // let the event go forth.
188 0 : preventDefault = false;
189 : }
190 : }
191 : }
192 : }
193 :
194 0 : if (preventDefault) {
195 : // someone called preventDefault. bail.
196 0 : return NS_OK;
197 : }
198 :
199 : // prevent popups on menu and menuitems as they handle their own popups
200 : // This was added for bug 96920.
201 : // If a menu item child was clicked on that leads to a popup needing
202 : // to show, we know (guaranteed) that we're dealing with a menu or
203 : // submenu of an already-showing popup. We don't need to do anything at all.
204 0 : nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
205 0 : if (!mIsContext) {
206 0 : nsIAtom *tag = targetContent ? targetContent->Tag() : nsnull;
207 0 : if (tag == nsGkAtoms::menu || tag == nsGkAtoms::menuitem)
208 0 : return NS_OK;
209 : }
210 :
211 0 : nsCOMPtr<nsIDOMNSEvent> nsevent = do_QueryInterface(aEvent);
212 :
213 0 : if (mIsContext) {
214 : #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
215 : // If the context menu launches on mousedown,
216 : // we have to fire focus on the content we clicked on
217 0 : FireFocusOnTargetContent(targetNode);
218 : #endif
219 : }
220 : else {
221 : // Only open popups when the left mouse button is down.
222 0 : mouseEvent->GetButton(&button);
223 0 : if (button != 0)
224 0 : return NS_OK;
225 : }
226 :
227 : // Open the popup and cancel the default handling of the event.
228 0 : LaunchPopup(aEvent, targetContent);
229 0 : aEvent->StopPropagation();
230 0 : aEvent->PreventDefault();
231 :
232 0 : return NS_OK;
233 : }
234 :
235 : #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
236 : nsresult
237 0 : nsXULPopupListener::FireFocusOnTargetContent(nsIDOMNode* aTargetNode)
238 : {
239 : nsresult rv;
240 0 : nsCOMPtr<nsIDOMDocument> domDoc;
241 0 : rv = aTargetNode->GetOwnerDocument(getter_AddRefs(domDoc));
242 0 : if(NS_SUCCEEDED(rv) && domDoc)
243 : {
244 0 : nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
245 :
246 : // Get nsIDOMElement for targetNode
247 0 : nsIPresShell *shell = doc->GetShell();
248 0 : if (!shell)
249 0 : return NS_ERROR_FAILURE;
250 :
251 : // strong reference to keep this from going away between events
252 : // XXXbz between what events? We don't use this local at all!
253 0 : nsRefPtr<nsPresContext> context = shell->GetPresContext();
254 :
255 0 : nsCOMPtr<nsIContent> content = do_QueryInterface(aTargetNode);
256 0 : nsIFrame* targetFrame = content->GetPrimaryFrame();
257 0 : if (!targetFrame) return NS_ERROR_FAILURE;
258 :
259 0 : const nsStyleUserInterface* ui = targetFrame->GetStyleUserInterface();
260 0 : bool suppressBlur = (ui->mUserFocus == NS_STYLE_USER_FOCUS_IGNORE);
261 :
262 0 : nsCOMPtr<nsIDOMElement> element;
263 0 : nsCOMPtr<nsIContent> newFocus = do_QueryInterface(content);
264 :
265 0 : nsIFrame* currFrame = targetFrame;
266 : // Look for the nearest enclosing focusable frame.
267 0 : while (currFrame) {
268 : PRInt32 tabIndexUnused;
269 0 : if (currFrame->IsFocusable(&tabIndexUnused, true)) {
270 0 : newFocus = currFrame->GetContent();
271 0 : nsCOMPtr<nsIDOMElement> domElement(do_QueryInterface(newFocus));
272 0 : if (domElement) {
273 0 : element = domElement;
274 : break;
275 : }
276 : }
277 0 : currFrame = currFrame->GetParent();
278 : }
279 :
280 0 : nsIFocusManager* fm = nsFocusManager::GetFocusManager();
281 0 : if (fm) {
282 0 : if (element) {
283 : fm->SetFocus(element, nsIFocusManager::FLAG_BYMOUSE |
284 0 : nsIFocusManager::FLAG_NOSCROLL);
285 0 : } else if (!suppressBlur) {
286 0 : nsPIDOMWindow *window = doc->GetWindow();
287 0 : fm->ClearFocus(window);
288 : }
289 : }
290 :
291 0 : nsEventStateManager *esm = context->EventStateManager();
292 0 : nsCOMPtr<nsIContent> focusableContent = do_QueryInterface(element);
293 0 : esm->SetContentState(focusableContent, NS_EVENT_STATE_ACTIVE);
294 : }
295 0 : return rv;
296 : }
297 : #endif
298 :
299 : // ClosePopup
300 : //
301 : // Do everything needed to shut down the popup.
302 : //
303 : // NOTE: This routine is safe to call even if the popup is already closed.
304 : //
305 : void
306 0 : nsXULPopupListener::ClosePopup()
307 : {
308 0 : if (mPopupContent) {
309 : // this is called when the listener is going away, so make sure that the
310 : // popup is hidden. Use asynchronous hiding just to be safe so we don't
311 : // fire events during destruction.
312 0 : nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
313 0 : if (pm)
314 0 : pm->HidePopup(mPopupContent, false, true, true);
315 0 : mPopupContent = nsnull; // release the popup
316 : }
317 0 : } // ClosePopup
318 :
319 : static already_AddRefed<nsIContent>
320 0 : GetImmediateChild(nsIContent* aContent, nsIAtom *aTag)
321 : {
322 0 : for (nsIContent* child = aContent->GetFirstChild();
323 : child;
324 0 : child = child->GetNextSibling()) {
325 0 : if (child->Tag() == aTag) {
326 0 : NS_ADDREF(child);
327 0 : return child;
328 : }
329 : }
330 :
331 0 : return nsnull;
332 : }
333 :
334 : //
335 : // LaunchPopup
336 : //
337 : // Given the element on which the event was triggered and the mouse locations in
338 : // Client and widget coordinates, popup a new window showing the appropriate
339 : // content.
340 : //
341 : // aTargetContent is the target of the mouse event aEvent that triggered the
342 : // popup. mElement is the element that the popup menu is attached to.
343 : // aTargetContent may be equal to mElement or it may be a descendant.
344 : //
345 : // This looks for an attribute on |mElement| of the appropriate popup type
346 : // (popup, context) and uses that attribute's value as an ID for
347 : // the popup content in the document.
348 : //
349 : nsresult
350 0 : nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
351 : {
352 0 : nsresult rv = NS_OK;
353 :
354 0 : nsAutoString type(NS_LITERAL_STRING("popup"));
355 0 : if (mIsContext)
356 0 : type.AssignLiteral("context");
357 :
358 0 : nsAutoString identifier;
359 0 : mElement->GetAttribute(type, identifier);
360 :
361 0 : if (identifier.IsEmpty()) {
362 0 : if (type.EqualsLiteral("popup"))
363 0 : mElement->GetAttribute(NS_LITERAL_STRING("menu"), identifier);
364 0 : else if (type.EqualsLiteral("context"))
365 0 : mElement->GetAttribute(NS_LITERAL_STRING("contextmenu"), identifier);
366 0 : if (identifier.IsEmpty())
367 0 : return rv;
368 : }
369 :
370 : // Try to find the popup content and the document.
371 0 : nsCOMPtr<nsIContent> content = do_QueryInterface(mElement);
372 0 : nsCOMPtr<nsIDocument> document = content->GetDocument();
373 :
374 : // Turn the document into a DOM document so we can use getElementById
375 0 : nsCOMPtr<nsIDOMDocument> domDocument = do_QueryInterface(document);
376 0 : if (!domDocument) {
377 0 : NS_ERROR("Popup attached to an element that isn't in XUL!");
378 0 : return NS_ERROR_FAILURE;
379 : }
380 :
381 : // Handle the _child case for popups and context menus
382 0 : nsCOMPtr<nsIDOMElement> popupElement;
383 :
384 0 : if (identifier.EqualsLiteral("_child")) {
385 0 : nsCOMPtr<nsIContent> popup = GetImmediateChild(content, nsGkAtoms::menupopup);
386 0 : if (popup)
387 0 : popupElement = do_QueryInterface(popup);
388 : else {
389 0 : nsCOMPtr<nsIDOMDocumentXBL> nsDoc(do_QueryInterface(domDocument));
390 0 : nsCOMPtr<nsIDOMNodeList> list;
391 0 : nsDoc->GetAnonymousNodes(mElement, getter_AddRefs(list));
392 0 : if (list) {
393 : PRUint32 ctr,listLength;
394 0 : nsCOMPtr<nsIDOMNode> node;
395 0 : list->GetLength(&listLength);
396 0 : for (ctr = 0; ctr < listLength; ctr++) {
397 0 : list->Item(ctr, getter_AddRefs(node));
398 0 : nsCOMPtr<nsIContent> childContent(do_QueryInterface(node));
399 :
400 0 : if (childContent->NodeInfo()->Equals(nsGkAtoms::menupopup,
401 0 : kNameSpaceID_XUL)) {
402 0 : popupElement = do_QueryInterface(childContent);
403 : break;
404 : }
405 : }
406 : }
407 : }
408 : }
409 0 : else if (NS_FAILED(rv = domDocument->GetElementById(identifier,
410 : getter_AddRefs(popupElement)))) {
411 : // Use getElementById to obtain the popup content and gracefully fail if
412 : // we didn't find any popup content in the document.
413 0 : NS_ERROR("GetElementById had some kind of spasm.");
414 0 : return rv;
415 : }
416 :
417 : // return if no popup was found or the popup is the element itself.
418 0 : if ( !popupElement || popupElement == mElement)
419 0 : return NS_OK;
420 :
421 : // Submenus can't be used as context menus or popups, bug 288763.
422 : // Similar code also in nsXULTooltipListener::GetTooltipFor.
423 0 : nsCOMPtr<nsIContent> popup = do_QueryInterface(popupElement);
424 0 : nsIContent* parent = popup->GetParent();
425 0 : if (parent) {
426 0 : nsIFrame* frame = parent->GetPrimaryFrame();
427 0 : if (frame && frame->GetType() == nsGkAtoms::menuFrame)
428 0 : return NS_OK;
429 : }
430 :
431 0 : nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
432 0 : if (!pm)
433 0 : return NS_OK;
434 :
435 : // For left-clicks, if the popup has an position attribute, or both the
436 : // popupanchor and popupalign attributes are used, anchor the popup to the
437 : // element, otherwise just open it at the screen position where the mouse
438 : // was clicked. Context menus always open at the mouse position.
439 0 : mPopupContent = popup;
440 0 : if (!mIsContext &&
441 0 : (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::position) ||
442 0 : (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupanchor) &&
443 0 : mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupalign)))) {
444 0 : pm->ShowPopup(mPopupContent, content, EmptyString(), 0, 0,
445 0 : false, true, false, aEvent);
446 : }
447 : else {
448 0 : PRInt32 xPos = 0, yPos = 0;
449 0 : nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
450 0 : mouseEvent->GetScreenX(&xPos);
451 0 : mouseEvent->GetScreenY(&yPos);
452 :
453 0 : pm->ShowPopupAtScreen(mPopupContent, xPos, yPos, mIsContext, aEvent);
454 : }
455 :
456 0 : return NS_OK;
457 4392 : }
|