widget/cocoa/nsMenuItemX.mm

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:41993de825d7
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 #include "nsMenuItemX.h"
7 #include "nsMenuBarX.h"
8 #include "nsMenuX.h"
9 #include "nsMenuItemIconX.h"
10 #include "nsMenuUtilsX.h"
11 #include "nsCocoaUtils.h"
12
13 #include "nsObjCExceptions.h"
14
15 #include "nsCOMPtr.h"
16 #include "nsGkAtoms.h"
17
18 #include "mozilla/dom/Element.h"
19 #include "nsIWidget.h"
20 #include "nsIDocument.h"
21 #include "nsIDOMDocument.h"
22 #include "nsIDOMElement.h"
23 #include "nsIDOMEvent.h"
24
25 nsMenuItemX::nsMenuItemX()
26 {
27 mType = eRegularMenuItemType;
28 mNativeMenuItem = nil;
29 mMenuParent = nullptr;
30 mMenuGroupOwner = nullptr;
31 mIsChecked = false;
32
33 MOZ_COUNT_CTOR(nsMenuItemX);
34 }
35
36 nsMenuItemX::~nsMenuItemX()
37 {
38 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
39
40 // Prevent the icon object from outliving us.
41 if (mIcon)
42 mIcon->Destroy();
43
44 // autorelease the native menu item so that anything else happening to this
45 // object happens before the native menu item actually dies
46 [mNativeMenuItem autorelease];
47
48 if (mContent)
49 mMenuGroupOwner->UnregisterForContentChanges(mContent);
50 if (mCommandContent)
51 mMenuGroupOwner->UnregisterForContentChanges(mCommandContent);
52
53 MOZ_COUNT_DTOR(nsMenuItemX);
54
55 NS_OBJC_END_TRY_ABORT_BLOCK;
56 }
57
58 nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType,
59 nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode)
60 {
61 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
62
63 mType = aItemType;
64 mMenuParent = aParent;
65 mContent = aNode;
66
67 mMenuGroupOwner = aMenuGroupOwner;
68 NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!");
69
70 mMenuGroupOwner->RegisterForContentChanges(mContent, this);
71
72 nsIDocument *doc = mContent->GetCurrentDoc();
73
74 // if we have a command associated with this menu item, register for changes
75 // to the command DOM node
76 if (doc) {
77 nsAutoString ourCommand;
78 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::command, ourCommand);
79
80 if (!ourCommand.IsEmpty()) {
81 nsIContent *commandElement = doc->GetElementById(ourCommand);
82
83 if (commandElement) {
84 mCommandContent = commandElement;
85 // register to observe the command DOM element
86 mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this);
87 }
88 }
89 }
90
91 // decide enabled state based on command content if it exists, otherwise do it based
92 // on our own content
93 bool isEnabled;
94 if (mCommandContent)
95 isEnabled = !mCommandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
96 else
97 isEnabled = !mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
98
99 // set up the native menu item
100 if (mType == eSeparatorMenuItemType) {
101 mNativeMenuItem = [[NSMenuItem separatorItem] retain];
102 }
103 else {
104 NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
105 mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""];
106
107 [mNativeMenuItem setEnabled:(BOOL)isEnabled];
108
109 SetChecked(mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
110 nsGkAtoms::_true, eCaseMatters));
111 SetKeyEquiv();
112 }
113
114 mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem);
115 if (!mIcon)
116 return NS_ERROR_OUT_OF_MEMORY;
117
118 return NS_OK;
119
120 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
121 }
122
123 nsresult nsMenuItemX::SetChecked(bool aIsChecked)
124 {
125 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
126
127 mIsChecked = aIsChecked;
128
129 // update the content model. This will also handle unchecking our siblings
130 // if we are a radiomenu
131 mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
132 mIsChecked ? NS_LITERAL_STRING("true") : NS_LITERAL_STRING("false"), true);
133
134 // update native menu item
135 if (mIsChecked)
136 [mNativeMenuItem setState:NSOnState];
137 else
138 [mNativeMenuItem setState:NSOffState];
139
140 return NS_OK;
141
142 NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
143 }
144
145 EMenuItemType nsMenuItemX::GetMenuItemType()
146 {
147 return mType;
148 }
149
150 // Executes the "cached" javaScript command.
151 // Returns NS_OK if the command was executed properly, otherwise an error code.
152 void nsMenuItemX::DoCommand()
153 {
154 // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
155 if (mType == eCheckboxMenuItemType ||
156 (mType == eRadioMenuItemType && !mIsChecked)) {
157 if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
158 nsGkAtoms::_false, eCaseMatters))
159 SetChecked(!mIsChecked);
160 /* the AttributeChanged code will update all the internal state */
161 }
162
163 nsMenuUtilsX::DispatchCommandTo(mContent);
164 }
165
166 nsresult nsMenuItemX::DispatchDOMEvent(const nsString &eventName, bool *preventDefaultCalled)
167 {
168 if (!mContent)
169 return NS_ERROR_FAILURE;
170
171 // get owner document for content
172 nsCOMPtr<nsIDocument> parentDoc = mContent->OwnerDoc();
173
174 // get interface for creating DOM events from content owner document
175 nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(parentDoc);
176 if (!domDoc) {
177 NS_WARNING("Failed to QI parent nsIDocument to nsIDOMDocument");
178 return NS_ERROR_FAILURE;
179 }
180
181 // create DOM event
182 nsCOMPtr<nsIDOMEvent> event;
183 nsresult rv = domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event));
184 if (NS_FAILED(rv)) {
185 NS_WARNING("Failed to create nsIDOMEvent");
186 return rv;
187 }
188 event->InitEvent(eventName, true, true);
189
190 // mark DOM event as trusted
191 event->SetTrusted(true);
192
193 // send DOM event
194 rv = mContent->DispatchEvent(event, preventDefaultCalled);
195 if (NS_FAILED(rv)) {
196 NS_WARNING("Failed to send DOM event via EventTarget");
197 return rv;
198 }
199
200 return NS_OK;
201 }
202
203 // Walk the sibling list looking for nodes with the same name and
204 // uncheck them all.
205 void nsMenuItemX::UncheckRadioSiblings(nsIContent* inCheckedContent)
206 {
207 nsAutoString myGroupName;
208 inCheckedContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, myGroupName);
209 if (!myGroupName.Length()) // no groupname, nothing to do
210 return;
211
212 nsCOMPtr<nsIContent> parent = inCheckedContent->GetParent();
213 if (!parent)
214 return;
215
216 // loop over siblings
217 uint32_t count = parent->GetChildCount();
218 for (uint32_t i = 0; i < count; i++) {
219 nsIContent *sibling = parent->GetChildAt(i);
220 if (sibling) {
221 if (sibling != inCheckedContent) { // skip this node
222 // if the current sibling is in the same group, clear it
223 if (sibling->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
224 myGroupName, eCaseMatters))
225 sibling->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("false"), true);
226 }
227 }
228 }
229 }
230
231 void nsMenuItemX::SetKeyEquiv()
232 {
233 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
234
235 // Set key shortcut and modifiers
236 nsAutoString keyValue;
237 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue);
238 if (!keyValue.IsEmpty() && mContent->GetCurrentDoc()) {
239 nsIContent *keyContent = mContent->GetCurrentDoc()->GetElementById(keyValue);
240 if (keyContent) {
241 nsAutoString keyChar;
242 bool hasKey = keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar);
243
244 if (!hasKey || keyChar.IsEmpty()) {
245 nsAutoString keyCodeName;
246 keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeName);
247 uint32_t charCode =
248 nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
249 if (charCode) {
250 keyChar.Assign(charCode);
251 }
252 else {
253 keyChar.Assign(NS_LITERAL_STRING(" "));
254 }
255 }
256
257 nsAutoString modifiersStr;
258 keyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
259 uint8_t modifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
260
261 unsigned int macModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
262 [mNativeMenuItem setKeyEquivalentModifierMask:macModifiers];
263
264 NSString *keyEquivalent = [[NSString stringWithCharacters:(unichar*)keyChar.get()
265 length:keyChar.Length()] lowercaseString];
266 if ([keyEquivalent isEqualToString:@" "])
267 [mNativeMenuItem setKeyEquivalent:@""];
268 else
269 [mNativeMenuItem setKeyEquivalent:keyEquivalent];
270
271 return;
272 }
273 }
274
275 // if the key was removed, clear the key
276 [mNativeMenuItem setKeyEquivalent:@""];
277
278 NS_OBJC_END_TRY_ABORT_BLOCK;
279 }
280
281 //
282 // nsChangeObserver
283 //
284
285 void
286 nsMenuItemX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, nsIAtom *aAttribute)
287 {
288 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
289
290 if (!aContent)
291 return;
292
293 if (aContent == mContent) { // our own content node changed
294 if (aAttribute == nsGkAtoms::checked) {
295 // if we're a radio menu, uncheck our sibling radio items. No need to
296 // do any of this if we're just a normal check menu.
297 if (mType == eRadioMenuItemType) {
298 if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
299 nsGkAtoms::_true, eCaseMatters))
300 UncheckRadioSiblings(mContent);
301 }
302 mMenuParent->SetRebuild(true);
303 }
304 else if (aAttribute == nsGkAtoms::hidden ||
305 aAttribute == nsGkAtoms::collapsed ||
306 aAttribute == nsGkAtoms::label) {
307 mMenuParent->SetRebuild(true);
308 }
309 else if (aAttribute == nsGkAtoms::key) {
310 SetKeyEquiv();
311 }
312 else if (aAttribute == nsGkAtoms::image) {
313 SetupIcon();
314 }
315 else if (aAttribute == nsGkAtoms::disabled) {
316 if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters))
317 [mNativeMenuItem setEnabled:NO];
318 else
319 [mNativeMenuItem setEnabled:YES];
320 }
321 }
322 else if (aContent == mCommandContent) {
323 // the only thing that really matters when the menu isn't showing is the
324 // enabled state since it enables/disables keyboard commands
325 if (aAttribute == nsGkAtoms::disabled) {
326 // first we sync our menu item DOM node with the command DOM node
327 nsAutoString commandDisabled;
328 nsAutoString menuDisabled;
329 aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled);
330 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled, menuDisabled);
331 if (!commandDisabled.Equals(menuDisabled)) {
332 // The menu's disabled state needs to be updated to match the command.
333 if (commandDisabled.IsEmpty())
334 mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
335 else
336 mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, commandDisabled, true);
337 }
338 // now we sync our native menu item with the command DOM node
339 if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters))
340 [mNativeMenuItem setEnabled:NO];
341 else
342 [mNativeMenuItem setEnabled:YES];
343 }
344 }
345
346 NS_OBJC_END_TRY_ABORT_BLOCK;
347 }
348
349 void nsMenuItemX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, int32_t aIndexInContainer)
350 {
351 if (aChild == mCommandContent) {
352 mMenuGroupOwner->UnregisterForContentChanges(mCommandContent);
353 mCommandContent = nullptr;
354 }
355
356 mMenuParent->SetRebuild(true);
357 }
358
359 void nsMenuItemX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer,
360 nsIContent *aChild)
361 {
362 mMenuParent->SetRebuild(true);
363 }
364
365 void nsMenuItemX::SetupIcon()
366 {
367 if (mIcon)
368 mIcon->SetupIcon();
369 }

mercurial