nsXULPopupListener.cpp (9657B)
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 13 #include "XULButtonElement.h" 14 #include "mozilla/BasePrincipal.h" 15 #include "mozilla/EventStateManager.h" 16 #include "mozilla/Preferences.h" 17 #include "mozilla/ReflowInput.h" 18 #include "mozilla/dom/Document.h" 19 #include "mozilla/dom/DocumentInlines.h" 20 #include "mozilla/dom/Element.h" 21 #include "mozilla/dom/Event.h" // for Event 22 #include "mozilla/dom/EventTarget.h" 23 #include "mozilla/dom/FragmentOrElement.h" 24 #include "mozilla/dom/MouseEvent.h" 25 #include "mozilla/dom/MouseEventBinding.h" 26 #include "nsCOMPtr.h" 27 #include "nsContentUtils.h" 28 #include "nsGkAtoms.h" 29 #include "nsIObjectLoadingContent.h" 30 #include "nsIScriptContext.h" 31 #include "nsLayoutUtils.h" 32 #include "nsServiceManagerUtils.h" 33 #include "nsXULPopupManager.h" 34 35 // for event firing in context menus 36 #include "nsError.h" 37 #include "nsFocusManager.h" 38 #include "nsPIDOMWindow.h" 39 #include "nsPresContext.h" 40 41 using namespace mozilla; 42 using namespace mozilla::dom; 43 44 // on win32 and os/2, context menus come up on mouse up. On other platforms, 45 // they appear on mouse down. Certain bits of code care about this difference. 46 #if defined(XP_WIN) 47 # define NS_CONTEXT_MENU_IS_MOUSEUP 1 48 #endif 49 50 nsXULPopupListener::nsXULPopupListener(mozilla::dom::Element* aElement, 51 bool aIsContext) 52 : mElement(aElement), mPopupContent(nullptr), mIsContext(aIsContext) {} 53 54 nsXULPopupListener::~nsXULPopupListener(void) { ClosePopup(); } 55 56 NS_IMPL_CYCLE_COLLECTION(nsXULPopupListener, mElement, mPopupContent) 57 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPopupListener) 58 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPopupListener) 59 60 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsXULPopupListener) 61 // If the owner, mElement, can be skipped, so can we. 62 if (tmp->mElement) { 63 return mozilla::dom::FragmentOrElement::CanSkip(tmp->mElement, true); 64 } 65 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END 66 67 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXULPopupListener) 68 if (tmp->mElement) { 69 return mozilla::dom::FragmentOrElement::CanSkipInCC(tmp->mElement); 70 } 71 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END 72 73 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXULPopupListener) 74 if (tmp->mElement) { 75 return mozilla::dom::FragmentOrElement::CanSkipThis(tmp->mElement); 76 } 77 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END 78 79 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener) 80 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) 81 NS_INTERFACE_MAP_ENTRY(nsISupports) 82 NS_INTERFACE_MAP_END 83 84 //////////////////////////////////////////////////////////////// 85 // nsIDOMEventListener 86 87 nsresult nsXULPopupListener::HandleEvent(Event* aEvent) { 88 nsAutoString eventType; 89 aEvent->GetType(eventType); 90 91 if (!((eventType.EqualsLiteral("mousedown") && !mIsContext) || 92 (eventType.EqualsLiteral("contextmenu") && mIsContext))) 93 return NS_OK; 94 95 MouseEvent* mouseEvent = aEvent->AsMouseEvent(); 96 if (!mouseEvent) { 97 // non-ui event passed in. bad things. 98 return NS_OK; 99 } 100 101 // Get the node that was clicked on. 102 nsCOMPtr<nsIContent> targetContent = 103 nsIContent::FromEventTargetOrNull(mouseEvent->GetTarget()); 104 if (!targetContent) { 105 return NS_OK; 106 } 107 108 if (nsIContent* content = 109 nsIContent::FromEventTargetOrNull(mouseEvent->GetOriginalTarget())) { 110 if (EventStateManager::IsTopLevelRemoteTarget(content)) { 111 return NS_OK; 112 } 113 } 114 115 bool preventDefault = mouseEvent->DefaultPrevented(); 116 if (preventDefault && mIsContext) { 117 // Someone called preventDefault on a context menu. 118 // Let's make sure they are allowed to do so. 119 bool eventEnabled = 120 Preferences::GetBool("dom.event.contextmenu.enabled", true); 121 if (!eventEnabled) { 122 // The user wants his contextmenus. Let's make sure that this is a 123 // website and not chrome since there could be places in chrome which 124 // don't want contextmenus. 125 if (!targetContent->NodePrincipal()->IsSystemPrincipal()) { 126 // This isn't chrome. Cancel the preventDefault() and 127 // let the event go forth. 128 preventDefault = false; 129 } 130 } 131 } 132 133 if (preventDefault) { 134 // someone called preventDefault. bail. 135 return NS_OK; 136 } 137 138 // prevent popups on menu and menuitems as they handle their own popups 139 // This was added for bug 96920. 140 // If a menu item child was clicked on that leads to a popup needing 141 // to show, we know (guaranteed) that we're dealing with a menu or 142 // submenu of an already-showing popup. We don't need to do anything at all. 143 if (!mIsContext && 144 targetContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem)) { 145 return NS_OK; 146 } 147 148 if (!mIsContext && mouseEvent->Button() != 0) { 149 // Only open popups when the left mouse button is down. 150 return NS_OK; 151 } 152 153 // Open the popup. LaunchPopup will call StopPropagation and PreventDefault 154 // in the right situations. 155 LaunchPopup(mouseEvent); 156 157 return NS_OK; 158 } 159 160 // ClosePopup 161 // 162 // Do everything needed to shut down the popup. 163 // 164 // NOTE: This routine is safe to call even if the popup is already closed. 165 // 166 void nsXULPopupListener::ClosePopup() { 167 if (mPopupContent) { 168 // this is called when the listener is going away, so make sure that the 169 // popup is hidden. Use asynchronous hiding just to be safe so we don't 170 // fire events during destruction. 171 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); 172 if (pm) 173 pm->HidePopup(mPopupContent, 174 {HidePopupOption::DeselectMenu, HidePopupOption::Async}); 175 mPopupContent = nullptr; // release the popup 176 } 177 } // ClosePopup 178 179 static already_AddRefed<Element> GetImmediateChild(nsIContent* aContent, 180 nsAtom* aTag) { 181 for (nsIContent* child = aContent->GetFirstChild(); child; 182 child = child->GetNextSibling()) { 183 if (child->IsXULElement(aTag)) { 184 RefPtr<Element> ret = child->AsElement(); 185 return ret.forget(); 186 } 187 } 188 189 return nullptr; 190 } 191 192 // 193 // LaunchPopup 194 // 195 // Given the element on which the event was triggered and the mouse locations in 196 // Client and widget coordinates, popup a new window showing the appropriate 197 // content. 198 // 199 // aTargetContent is the target of the mouse event aEvent that triggered the 200 // popup. mElement is the element that the popup menu is attached to. 201 // aTargetContent may be equal to mElement or it may be a descendant. 202 // 203 // This looks for an attribute on |mElement| of the appropriate popup type 204 // (popup, context) and uses that attribute's value as an ID for 205 // the popup content in the document. 206 // 207 nsresult nsXULPopupListener::LaunchPopup(MouseEvent* aEvent) { 208 nsresult rv = NS_OK; 209 210 nsAutoString identifier; 211 nsAtom* type = mIsContext ? nsGkAtoms::context : nsGkAtoms::popup; 212 bool hasPopupAttr = mElement->GetAttr(type, identifier); 213 214 if (identifier.IsEmpty()) { 215 hasPopupAttr = 216 mElement->GetAttr(mIsContext ? nsGkAtoms::contextmenu : nsGkAtoms::menu, 217 identifier) || 218 hasPopupAttr; 219 } 220 221 if (hasPopupAttr) { 222 aEvent->StopPropagation(); 223 aEvent->PreventDefault(); 224 } 225 226 if (identifier.IsEmpty()) return rv; 227 228 // Try to find the popup content and the document. 229 nsCOMPtr<Document> document = mElement->GetComposedDoc(); 230 if (!document) { 231 NS_WARNING("No document!"); 232 return NS_ERROR_FAILURE; 233 } 234 235 // Handle the _child case for popups and context menus 236 RefPtr<Element> popup; 237 if (identifier.EqualsLiteral("_child")) { 238 popup = GetImmediateChild(mElement, nsGkAtoms::menupopup); 239 } else if (!mElement->IsInUncomposedDoc() || 240 !(popup = document->GetElementById(identifier))) { 241 // XXXsmaug Should we try to use ShadowRoot::GetElementById in case 242 // mElement is in shadow DOM? 243 // 244 // Use getElementById to obtain the popup content and gracefully fail if 245 // we didn't find any popup content in the document. 246 NS_WARNING("GetElementById had some kind of spasm."); 247 return rv; 248 } 249 250 // return if no popup was found or the popup is the element itself. 251 if (!popup || popup == mElement) { 252 return NS_OK; 253 } 254 255 // Submenus can't be used as context menus or popups, bug 288763. 256 // Similar code also in nsXULTooltipListener::GetTooltipFor. 257 if (auto* button = XULButtonElement::FromNodeOrNull(popup->GetParent())) { 258 if (button->IsMenu()) { 259 return NS_OK; 260 } 261 } 262 263 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); 264 if (!pm) return NS_OK; 265 266 // For left-clicks, if the popup has an position attribute, or both the 267 // popupanchor and popupalign attributes are used, anchor the popup to the 268 // element, otherwise just open it at the screen position where the mouse 269 // was clicked. Context menus always open at the mouse position. 270 mPopupContent = popup; 271 if (!mIsContext && (mPopupContent->HasAttr(nsGkAtoms::position) || 272 (mPopupContent->HasAttr(nsGkAtoms::popupanchor) && 273 mPopupContent->HasAttr(nsGkAtoms::popupalign)))) { 274 pm->ShowPopup(mPopupContent, mElement, u""_ns, 0, 0, false, true, false, 275 aEvent); 276 } else { 277 const CSSIntPoint pos = 278 RoundedToInt(aEvent->ScreenPoint(CallerType::System)); 279 pm->ShowPopupAtScreen(mPopupContent, pos.x, pos.y, mIsContext, aEvent); 280 } 281 282 return NS_OK; 283 }