1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/widget/cocoa/nsMenuItemX.mm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,369 @@ 1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +#include "nsMenuItemX.h" 1.10 +#include "nsMenuBarX.h" 1.11 +#include "nsMenuX.h" 1.12 +#include "nsMenuItemIconX.h" 1.13 +#include "nsMenuUtilsX.h" 1.14 +#include "nsCocoaUtils.h" 1.15 + 1.16 +#include "nsObjCExceptions.h" 1.17 + 1.18 +#include "nsCOMPtr.h" 1.19 +#include "nsGkAtoms.h" 1.20 + 1.21 +#include "mozilla/dom/Element.h" 1.22 +#include "nsIWidget.h" 1.23 +#include "nsIDocument.h" 1.24 +#include "nsIDOMDocument.h" 1.25 +#include "nsIDOMElement.h" 1.26 +#include "nsIDOMEvent.h" 1.27 + 1.28 +nsMenuItemX::nsMenuItemX() 1.29 +{ 1.30 + mType = eRegularMenuItemType; 1.31 + mNativeMenuItem = nil; 1.32 + mMenuParent = nullptr; 1.33 + mMenuGroupOwner = nullptr; 1.34 + mIsChecked = false; 1.35 + 1.36 + MOZ_COUNT_CTOR(nsMenuItemX); 1.37 +} 1.38 + 1.39 +nsMenuItemX::~nsMenuItemX() 1.40 +{ 1.41 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.42 + 1.43 + // Prevent the icon object from outliving us. 1.44 + if (mIcon) 1.45 + mIcon->Destroy(); 1.46 + 1.47 + // autorelease the native menu item so that anything else happening to this 1.48 + // object happens before the native menu item actually dies 1.49 + [mNativeMenuItem autorelease]; 1.50 + 1.51 + if (mContent) 1.52 + mMenuGroupOwner->UnregisterForContentChanges(mContent); 1.53 + if (mCommandContent) 1.54 + mMenuGroupOwner->UnregisterForContentChanges(mCommandContent); 1.55 + 1.56 + MOZ_COUNT_DTOR(nsMenuItemX); 1.57 + 1.58 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.59 +} 1.60 + 1.61 +nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType, 1.62 + nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode) 1.63 +{ 1.64 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.65 + 1.66 + mType = aItemType; 1.67 + mMenuParent = aParent; 1.68 + mContent = aNode; 1.69 + 1.70 + mMenuGroupOwner = aMenuGroupOwner; 1.71 + NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!"); 1.72 + 1.73 + mMenuGroupOwner->RegisterForContentChanges(mContent, this); 1.74 + 1.75 + nsIDocument *doc = mContent->GetCurrentDoc(); 1.76 + 1.77 + // if we have a command associated with this menu item, register for changes 1.78 + // to the command DOM node 1.79 + if (doc) { 1.80 + nsAutoString ourCommand; 1.81 + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand); 1.82 + 1.83 + if (!ourCommand.IsEmpty()) { 1.84 + nsIContent *commandElement = doc->GetElementById(ourCommand); 1.85 + 1.86 + if (commandElement) { 1.87 + mCommandContent = commandElement; 1.88 + // register to observe the command DOM element 1.89 + mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this); 1.90 + } 1.91 + } 1.92 + } 1.93 + 1.94 + // decide enabled state based on command content if it exists, otherwise do it based 1.95 + // on our own content 1.96 + bool isEnabled; 1.97 + if (mCommandContent) 1.98 + isEnabled = !mCommandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); 1.99 + else 1.100 + isEnabled = !mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); 1.101 + 1.102 + // set up the native menu item 1.103 + if (mType == eSeparatorMenuItemType) { 1.104 + mNativeMenuItem = [[NSMenuItem separatorItem] retain]; 1.105 + } 1.106 + else { 1.107 + NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel); 1.108 + mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""]; 1.109 + 1.110 + [mNativeMenuItem setEnabled:(BOOL)isEnabled]; 1.111 + 1.112 + SetChecked(mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, 1.113 + nsGkAtoms::_true, eCaseMatters)); 1.114 + SetKeyEquiv(); 1.115 + } 1.116 + 1.117 + mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem); 1.118 + if (!mIcon) 1.119 + return NS_ERROR_OUT_OF_MEMORY; 1.120 + 1.121 + return NS_OK; 1.122 + 1.123 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.124 +} 1.125 + 1.126 +nsresult nsMenuItemX::SetChecked(bool aIsChecked) 1.127 +{ 1.128 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; 1.129 + 1.130 + mIsChecked = aIsChecked; 1.131 + 1.132 + // update the content model. This will also handle unchecking our siblings 1.133 + // if we are a radiomenu 1.134 + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, 1.135 + mIsChecked ? NS_LITERAL_STRING("true") : NS_LITERAL_STRING("false"), true); 1.136 + 1.137 + // update native menu item 1.138 + if (mIsChecked) 1.139 + [mNativeMenuItem setState:NSOnState]; 1.140 + else 1.141 + [mNativeMenuItem setState:NSOffState]; 1.142 + 1.143 + return NS_OK; 1.144 + 1.145 + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; 1.146 +} 1.147 + 1.148 +EMenuItemType nsMenuItemX::GetMenuItemType() 1.149 +{ 1.150 + return mType; 1.151 +} 1.152 + 1.153 +// Executes the "cached" javaScript command. 1.154 +// Returns NS_OK if the command was executed properly, otherwise an error code. 1.155 +void nsMenuItemX::DoCommand() 1.156 +{ 1.157 + // flip "checked" state if we're a checkbox menu, or an un-checked radio menu 1.158 + if (mType == eCheckboxMenuItemType || 1.159 + (mType == eRadioMenuItemType && !mIsChecked)) { 1.160 + if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, 1.161 + nsGkAtoms::_false, eCaseMatters)) 1.162 + SetChecked(!mIsChecked); 1.163 + /* the AttributeChanged code will update all the internal state */ 1.164 + } 1.165 + 1.166 + nsMenuUtilsX::DispatchCommandTo(mContent); 1.167 +} 1.168 + 1.169 +nsresult nsMenuItemX::DispatchDOMEvent(const nsString &eventName, bool *preventDefaultCalled) 1.170 +{ 1.171 + if (!mContent) 1.172 + return NS_ERROR_FAILURE; 1.173 + 1.174 + // get owner document for content 1.175 + nsCOMPtr<nsIDocument> parentDoc = mContent->OwnerDoc(); 1.176 + 1.177 + // get interface for creating DOM events from content owner document 1.178 + nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(parentDoc); 1.179 + if (!domDoc) { 1.180 + NS_WARNING("Failed to QI parent nsIDocument to nsIDOMDocument"); 1.181 + return NS_ERROR_FAILURE; 1.182 + } 1.183 + 1.184 + // create DOM event 1.185 + nsCOMPtr<nsIDOMEvent> event; 1.186 + nsresult rv = domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event)); 1.187 + if (NS_FAILED(rv)) { 1.188 + NS_WARNING("Failed to create nsIDOMEvent"); 1.189 + return rv; 1.190 + } 1.191 + event->InitEvent(eventName, true, true); 1.192 + 1.193 + // mark DOM event as trusted 1.194 + event->SetTrusted(true); 1.195 + 1.196 + // send DOM event 1.197 + rv = mContent->DispatchEvent(event, preventDefaultCalled); 1.198 + if (NS_FAILED(rv)) { 1.199 + NS_WARNING("Failed to send DOM event via EventTarget"); 1.200 + return rv; 1.201 + } 1.202 + 1.203 + return NS_OK; 1.204 +} 1.205 + 1.206 +// Walk the sibling list looking for nodes with the same name and 1.207 +// uncheck them all. 1.208 +void nsMenuItemX::UncheckRadioSiblings(nsIContent* inCheckedContent) 1.209 +{ 1.210 + nsAutoString myGroupName; 1.211 + inCheckedContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName); 1.212 + if (!myGroupName.Length()) // no groupname, nothing to do 1.213 + return; 1.214 + 1.215 + nsCOMPtr<nsIContent> parent = inCheckedContent->GetParent(); 1.216 + if (!parent) 1.217 + return; 1.218 + 1.219 + // loop over siblings 1.220 + uint32_t count = parent->GetChildCount(); 1.221 + for (uint32_t i = 0; i < count; i++) { 1.222 + nsIContent *sibling = parent->GetChildAt(i); 1.223 + if (sibling) { 1.224 + if (sibling != inCheckedContent) { // skip this node 1.225 + // if the current sibling is in the same group, clear it 1.226 + if (sibling->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, 1.227 + myGroupName, eCaseMatters)) 1.228 + sibling->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("false"), true); 1.229 + } 1.230 + } 1.231 + } 1.232 +} 1.233 + 1.234 +void nsMenuItemX::SetKeyEquiv() 1.235 +{ 1.236 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.237 + 1.238 + // Set key shortcut and modifiers 1.239 + nsAutoString keyValue; 1.240 + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue); 1.241 + if (!keyValue.IsEmpty() && mContent->GetCurrentDoc()) { 1.242 + nsIContent *keyContent = mContent->GetCurrentDoc()->GetElementById(keyValue); 1.243 + if (keyContent) { 1.244 + nsAutoString keyChar; 1.245 + bool hasKey = keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar); 1.246 + 1.247 + if (!hasKey || keyChar.IsEmpty()) { 1.248 + nsAutoString keyCodeName; 1.249 + keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeName); 1.250 + uint32_t charCode = 1.251 + nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName); 1.252 + if (charCode) { 1.253 + keyChar.Assign(charCode); 1.254 + } 1.255 + else { 1.256 + keyChar.Assign(NS_LITERAL_STRING(" ")); 1.257 + } 1.258 + } 1.259 + 1.260 + nsAutoString modifiersStr; 1.261 + keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr); 1.262 + uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr); 1.263 + 1.264 + unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers); 1.265 + [mNativeMenuItem setKeyEquivalentModifierMask:macModifiers]; 1.266 + 1.267 + NSString *keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get() 1.268 + length:keyChar.Length()] lowercaseString]; 1.269 + if ([keyEquivalent isEqualToString:@" "]) 1.270 + [mNativeMenuItem setKeyEquivalent:@""]; 1.271 + else 1.272 + [mNativeMenuItem setKeyEquivalent:keyEquivalent]; 1.273 + 1.274 + return; 1.275 + } 1.276 + } 1.277 + 1.278 + // if the key was removed, clear the key 1.279 + [mNativeMenuItem setKeyEquivalent:@""]; 1.280 + 1.281 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.282 +} 1.283 + 1.284 +// 1.285 +// nsChangeObserver 1.286 +// 1.287 + 1.288 +void 1.289 +nsMenuItemX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *aAttribute) 1.290 +{ 1.291 + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1.292 + 1.293 + if (!aContent) 1.294 + return; 1.295 + 1.296 + if (aContent == mContent) { // our own content node changed 1.297 + if (aAttribute == nsGkAtoms::checked) { 1.298 + // if we're a radio menu, uncheck our sibling radio items. No need to 1.299 + // do any of this if we're just a normal check menu. 1.300 + if (mType == eRadioMenuItemType) { 1.301 + if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, 1.302 + nsGkAtoms::_true, eCaseMatters)) 1.303 + UncheckRadioSiblings(mContent); 1.304 + } 1.305 + mMenuParent->SetRebuild(true); 1.306 + } 1.307 + else if (aAttribute == nsGkAtoms::hidden || 1.308 + aAttribute == nsGkAtoms::collapsed || 1.309 + aAttribute == nsGkAtoms::label) { 1.310 + mMenuParent->SetRebuild(true); 1.311 + } 1.312 + else if (aAttribute == nsGkAtoms::key) { 1.313 + SetKeyEquiv(); 1.314 + } 1.315 + else if (aAttribute == nsGkAtoms::image) { 1.316 + SetupIcon(); 1.317 + } 1.318 + else if (aAttribute == nsGkAtoms::disabled) { 1.319 + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters)) 1.320 + [mNativeMenuItem setEnabled:NO]; 1.321 + else 1.322 + [mNativeMenuItem setEnabled:YES]; 1.323 + } 1.324 + } 1.325 + else if (aContent == mCommandContent) { 1.326 + // the only thing that really matters when the menu isn't showing is the 1.327 + // enabled state since it enables/disables keyboard commands 1.328 + if (aAttribute == nsGkAtoms::disabled) { 1.329 + // first we sync our menu item DOM node with the command DOM node 1.330 + nsAutoString commandDisabled; 1.331 + nsAutoString menuDisabled; 1.332 + aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled); 1.333 + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, menuDisabled); 1.334 + if (!commandDisabled.Equals(menuDisabled)) { 1.335 + // The menu's disabled state needs to be updated to match the command. 1.336 + if (commandDisabled.IsEmpty()) 1.337 + mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); 1.338 + else 1.339 + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, true); 1.340 + } 1.341 + // now we sync our native menu item with the command DOM node 1.342 + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters)) 1.343 + [mNativeMenuItem setEnabled:NO]; 1.344 + else 1.345 + [mNativeMenuItem setEnabled:YES]; 1.346 + } 1.347 + } 1.348 + 1.349 + NS_OBJC_END_TRY_ABORT_BLOCK; 1.350 +} 1.351 + 1.352 +void nsMenuItemX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, int32_t aIndexInContainer) 1.353 +{ 1.354 + if (aChild == mCommandContent) { 1.355 + mMenuGroupOwner->UnregisterForContentChanges(mCommandContent); 1.356 + mCommandContent = nullptr; 1.357 + } 1.358 + 1.359 + mMenuParent->SetRebuild(true); 1.360 +} 1.361 + 1.362 +void nsMenuItemX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer, 1.363 + nsIContent *aChild) 1.364 +{ 1.365 + mMenuParent->SetRebuild(true); 1.366 +} 1.367 + 1.368 +void nsMenuItemX::SetupIcon() 1.369 +{ 1.370 + if (mIcon) 1.371 + mIcon->SetupIcon(); 1.372 +}