|
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 } |