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 "nsMenuUtilsX.h"
7#include <unordered_set>
8
9#include "mozilla/EventForwards.h"
10#include "mozilla/dom/Document.h"
11#include "mozilla/dom/DocumentInlines.h"
12#include "mozilla/dom/Event.h"
13#include "mozilla/dom/XULCommandEvent.h"
14#include "nsMenuBarX.h"
15#include "nsMenuX.h"
16#include "nsMenuItemX.h"
17#include "NativeMenuMac.h"
18#include "nsObjCExceptions.h"
19#include "nsCocoaUtils.h"
20#include "nsCocoaWindow.h"
21#include "nsGkAtoms.h"
22#include "nsGlobalWindowInner.h"
23#include "nsPIDOMWindow.h"
24#include "nsQueryObject.h"
25
26using namespace mozilla;
27
28void nsMenuUtilsX::DispatchCommandTo(nsIContent* aTargetContent,
29                                     NSEventModifierFlags aModifierFlags, int16_t aButton) {
30  MOZ_ASSERT(aTargetContent, "null ptr");
31
32  dom::Document* doc = aTargetContent->OwnerDoc();
33  if (doc) {
34    RefPtr<dom::XULCommandEvent> event =
35        new dom::XULCommandEvent(doc, doc->GetPresContext(), nullptr);
36
37    bool ctrlKey = aModifierFlags & NSEventModifierFlagControl;
38    bool altKey = aModifierFlags & NSEventModifierFlagOption;
39    bool shiftKey = aModifierFlags & NSEventModifierFlagShift;
40    bool cmdKey = aModifierFlags & NSEventModifierFlagCommand;
41
42    IgnoredErrorResult rv;
43    event->InitCommandEvent(u"command"_ns, true, true,
44                            nsGlobalWindowInner::Cast(doc->GetInnerWindow()), 0, ctrlKey, altKey,
45                            shiftKey, cmdKey, aButton, nullptr, 0, rv);
46    if (!rv.Failed()) {
47      event->SetTrusted(true);
48      aTargetContent->DispatchEvent(*event);
49    }
50  }
51}
52
53NSString* nsMenuUtilsX::GetTruncatedCocoaLabel(const nsString& itemLabel) {
54  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
55
56  // We want to truncate long strings to some reasonable pixel length but there is no
57  // good API for doing that which works for all OS versions and architectures. For now
58  // we'll do nothing for consistency and depend on good user interface design to limit
59  // string lengths.
60  return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(itemLabel.get())
61                                 length:itemLabel.Length()];
62
63  NS_OBJC_END_TRY_ABORT_BLOCK;
64}
65
66uint8_t nsMenuUtilsX::GeckoModifiersForNodeAttribute(const nsString& modifiersAttribute) {
67  uint8_t modifiers = knsMenuItemNoModifier;
68  char* str = ToNewCString(modifiersAttribute);
69  char* newStr;
70  char* token = strtok_r(str, ", \t", &newStr);
71  while (token != nullptr) {
72    if (strcmp(token, "shift") == 0) {
73      modifiers |= knsMenuItemShiftModifier;
74    } else if (strcmp(token, "alt") == 0) {
75      modifiers |= knsMenuItemAltModifier;
76    } else if (strcmp(token, "control") == 0) {
77      modifiers |= knsMenuItemControlModifier;
78    } else if ((strcmp(token, "accel") == 0) || (strcmp(token, "meta") == 0)) {
79      modifiers |= knsMenuItemCommandModifier;
80    }
81    token = strtok_r(newStr, ", \t", &newStr);
82  }
83  free(str);
84
85  return modifiers;
86}
87
88unsigned int nsMenuUtilsX::MacModifiersForGeckoModifiers(uint8_t geckoModifiers) {
89  unsigned int macModifiers = 0;
90
91  if (geckoModifiers & knsMenuItemShiftModifier) {
92    macModifiers |= NSEventModifierFlagShift;
93  }
94  if (geckoModifiers & knsMenuItemAltModifier) {
95    macModifiers |= NSEventModifierFlagOption;
96  }
97  if (geckoModifiers & knsMenuItemControlModifier) {
98    macModifiers |= NSEventModifierFlagControl;
99  }
100  if (geckoModifiers & knsMenuItemCommandModifier) {
101    macModifiers |= NSEventModifierFlagCommand;
102  }
103
104  return macModifiers;
105}
106
107nsMenuBarX* nsMenuUtilsX::GetHiddenWindowMenuBar() {
108  nsIWidget* hiddenWindowWidgetNoCOMPtr = nsCocoaUtils::GetHiddenWindowWidget();
109  if (hiddenWindowWidgetNoCOMPtr) {
110    return static_cast<nsCocoaWindow*>(hiddenWindowWidgetNoCOMPtr)->GetMenuBar();
111  }
112  return nullptr;
113}
114
115// It would be nice if we could localize these edit menu names.
116NSMenuItem* nsMenuUtilsX::GetStandardEditMenuItem() {
117  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
118
119  // In principle we should be able to allocate this once and then always
120  // return the same object.  But weird interactions happen between native
121  // app-modal dialogs and Gecko-modal dialogs that open above them.  So what
122  // we return here isn't always released before it needs to be added to
123  // another menu.  See bmo bug 468393.
124  NSMenuItem* standardEditMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Edit"
125                                                                 action:nil
126                                                          keyEquivalent:@""] autorelease];
127  NSMenu* standardEditMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
128  standardEditMenuItem.submenu = standardEditMenu;
129  [standardEditMenu release];
130
131  // Add Undo
132  NSMenuItem* undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo"
133                                                    action:@selector(undo:)
134                                             keyEquivalent:@"z"];
135  [standardEditMenu addItem:undoItem];
136  [undoItem release];
137
138  // Add Redo
139  NSMenuItem* redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo"
140                                                    action:@selector(redo:)
141                                             keyEquivalent:@"Z"];
142  [standardEditMenu addItem:redoItem];
143  [redoItem release];
144
145  // Add separator
146  [standardEditMenu addItem:[NSMenuItem separatorItem]];
147
148  // Add Cut
149  NSMenuItem* cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut"
150                                                   action:@selector(cut:)
151                                            keyEquivalent:@"x"];
152  [standardEditMenu addItem:cutItem];
153  [cutItem release];
154
155  // Add Copy
156  NSMenuItem* copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
157                                                    action:@selector(copy:)
158                                             keyEquivalent:@"c"];
159  [standardEditMenu addItem:copyItem];
160  [copyItem release];
161
162  // Add Paste
163  NSMenuItem* pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste"
164                                                     action:@selector(paste:)
165                                              keyEquivalent:@"v"];
166  [standardEditMenu addItem:pasteItem];
167  [pasteItem release];
168
169  // Add Delete
170  NSMenuItem* deleteItem = [[NSMenuItem alloc] initWithTitle:@"Delete"
171                                                      action:@selector(delete:)
172                                               keyEquivalent:@""];
173  [standardEditMenu addItem:deleteItem];
174  [deleteItem release];
175
176  // Add Select All
177  NSMenuItem* selectAllItem = [[NSMenuItem alloc] initWithTitle:@"Select All"
178                                                         action:@selector(selectAll:)
179                                                  keyEquivalent:@"a"];
180  [standardEditMenu addItem:selectAllItem];
181  [selectAllItem release];
182
183  return standardEditMenuItem;
184
185  NS_OBJC_END_TRY_ABORT_BLOCK;
186}
187
188bool nsMenuUtilsX::NodeIsHiddenOrCollapsed(nsIContent* aContent) {
189  return aContent->IsElement() &&
190         (aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, nsGkAtoms::_true,
191                                             eCaseMatters) ||
192          aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed,
193                                             nsGkAtoms::_true, eCaseMatters));
194}
195
196NSMenuItem* nsMenuUtilsX::NativeMenuItemWithLocation(NSMenu* aRootMenu, NSString* aLocationString,
197                                                     bool aIsMenuBar) {
198  NSArray<NSString*>* indexes = [aLocationString componentsSeparatedByString:@"|"];
199  unsigned int pathLength = indexes.count;
200  if (pathLength == 0) {
201    return nil;
202  }
203
204  NSMenu* currentSubmenu = aRootMenu;
205  for (unsigned int depth = 0; depth < pathLength; depth++) {
206    NSInteger targetIndex = [indexes objectAtIndex:depth].integerValue;
207    if (aIsMenuBar && depth == 0) {
208      // We remove the application menu from consideration for the top-level menu.
209      targetIndex++;
210    }
211    int itemCount = currentSubmenu.numberOfItems;
212    if (targetIndex >= itemCount) {
213      return nil;
214    }
215    NSMenuItem* menuItem = [currentSubmenu itemAtIndex:targetIndex];
216    // if this is the last index just return the menu item
217    if (depth == pathLength - 1) {
218      return menuItem;
219    }
220    // if this is not the last index find the submenu and keep going
221    if (menuItem.hasSubmenu) {
222      currentSubmenu = menuItem.submenu;
223    } else {
224      return nil;
225    }
226  }
227
228  return nil;
229}
230
231static void CheckNativeMenuConsistencyImpl(NSMenu* aMenu, std::unordered_set<void*>& aSeenObjects);
232
233static void CheckNativeMenuItemConsistencyImpl(NSMenuItem* aMenuItem,
234                                               std::unordered_set<void*>& aSeenObjects) {
235  bool inserted = aSeenObjects.insert(aMenuItem).second;
236  MOZ_RELEASE_ASSERT(inserted, "Duplicate NSMenuItem object in native menu structure");
237  if (aMenuItem.hasSubmenu) {
238    CheckNativeMenuConsistencyImpl(aMenuItem.submenu, aSeenObjects);
239  }
240}
241
242static void CheckNativeMenuConsistencyImpl(NSMenu* aMenu, std::unordered_set<void*>& aSeenObjects) {
243  bool inserted = aSeenObjects.insert(aMenu).second;
244  MOZ_RELEASE_ASSERT(inserted, "Duplicate NSMenu object in native menu structure");
245  for (NSMenuItem* item in aMenu.itemArray) {
246    CheckNativeMenuItemConsistencyImpl(item, aSeenObjects);
247  }
248}
249
250void nsMenuUtilsX::CheckNativeMenuConsistency(NSMenu* aMenu) {
251  std::unordered_set<void*> seenObjects;
252  CheckNativeMenuConsistencyImpl(aMenu, seenObjects);
253}
254
255void nsMenuUtilsX::CheckNativeMenuConsistency(NSMenuItem* aMenuItem) {
256  std::unordered_set<void*> seenObjects;
257  CheckNativeMenuItemConsistencyImpl(aMenuItem, seenObjects);
258}
259
260static void DumpNativeNSMenuItemImpl(NSMenuItem* aItem, uint32_t aIndent,
261                                     const Maybe<int>& aIndexInParentMenu);
262
263static void DumpNativeNSMenuImpl(NSMenu* aMenu, uint32_t aIndent) {
264  printf("%*sNSMenu [%p] %-16s\n", aIndent * 2, "", aMenu,
265         (aMenu.title.length == 0 ? "(no title)" : aMenu.title.UTF8String));
266  int index = 0;
267  for (NSMenuItem* item in aMenu.itemArray) {
268    DumpNativeNSMenuItemImpl(item, aIndent + 1, Some(index));
269    index++;
270  }
271}
272
273static void DumpNativeNSMenuItemImpl(NSMenuItem* aItem, uint32_t aIndent,
274                                     const Maybe<int>& aIndexInParentMenu) {
275  printf("%*s", aIndent * 2, "");
276  if (aIndexInParentMenu) {
277    printf("[%d] ", *aIndexInParentMenu);
278  }
279  printf("NSMenuItem [%p] %-16s%s\n", aItem,
280         aItem.isSeparatorItem ? "----"
281                               : (aItem.title.length == 0 ? "(no title)" : aItem.title.UTF8String),
282         aItem.hasSubmenu ? " [hasSubmenu]" : "");
283  if (aItem.hasSubmenu) {
284    DumpNativeNSMenuImpl(aItem.submenu, aIndent + 1);
285  }
286}
287
288void nsMenuUtilsX::DumpNativeMenu(NSMenu* aMenu) { DumpNativeNSMenuImpl(aMenu, 0); }
289
290void nsMenuUtilsX::DumpNativeMenuItem(NSMenuItem* aMenuItem) {
291  DumpNativeNSMenuItemImpl(aMenuItem, 0, Nothing());
292}
293