File: nsmenuitem_additions.mm

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (313 lines) | stat: -rw-r--r-- 12,871 bytes parent folder | download | duplicates (9)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// Copyright 2009 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ui/base/cocoa/nsmenuitem_additions.h"
#include "base/apple/foundation_util.h"

#include <Carbon/Carbon.h>

#include "base/apple/bridging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/check.h"
#include "ui/events/keycodes/keyboard_code_conversion_mac.h"

namespace ui::cocoa {

namespace {
bool g_is_input_source_command_qwerty = false;
bool g_is_input_source_dvorak_right_or_left = false;
bool g_is_input_source_command_hebrew = false;
bool g_is_input_source_command_arabic = false;
}  // namespace

void SetIsInputSourceCommandQwertyForTesting(bool is_command_qwerty) {
  g_is_input_source_command_qwerty = is_command_qwerty;
}

void SetIsInputSourceDvorakRightOrLeftForTesting(bool is_dvorak_right_or_left) {
  g_is_input_source_dvorak_right_or_left = is_dvorak_right_or_left;
}

void SetIsInputSourceCommandHebrewForTesting(bool is_command_hebrew) {
  g_is_input_source_command_hebrew = is_command_hebrew;
}

void SetIsInputSourceCommandArabicForTesting(bool is_command_arabic) {
  g_is_input_source_command_arabic = is_command_arabic;
}

bool IsKeyboardLayoutCommandQwerty(NSString* layout_id) {
  return [layout_id isEqualToString:@"com.apple.keylayout.DVORAK-QWERTYCMD"] ||
         [layout_id isEqualToString:@"com.apple.keylayout.Dhivehi-QWERTY"] ||
         [layout_id isEqualToString:@"com.apple.keylayout.Inuktitut-QWERTY"] ||
         [layout_id isEqualToString:@"com.apple.keylayout.Cherokee-QWERTY"];
}

bool IsKeyboardLayoutDvorakRightOrLeft(NSString* layout_id) {
  return [layout_id isEqualToString:@"com.apple.keylayout.Dvorak-Right"] ||
         [layout_id isEqualToString:@"com.apple.keylayout.Dvorak-Left"];
}

bool IsKeyboardLayoutCommandHebrew(NSString* layout_id) {
  // com.apple.keylayout.Hebrew, com.apple.keylayout.Hebrew-PC,
  // com.apple.keylayout.Hebrew-QWERTY.
  return [layout_id hasPrefix:@"com.apple.keylayout.Hebrew"];
}

bool IsKeyboardLayoutCommandArabic(NSString* layout_id) {
  return [layout_id hasPrefix:@"com.apple.keylayout.ArabicPC"] ||
         [layout_id hasPrefix:@"com.apple.keylayout.Arabic-AZERTY"];
}

NSUInteger ModifierMaskForKeyEvent(NSEvent* event) {
  NSUInteger eventModifierMask =
      NSEventModifierFlagCommand | NSEventModifierFlagControl |
      NSEventModifierFlagOption | NSEventModifierFlagShift;

  // If `event` isn't a function key press or it's not a character key press
  // (e.g. it's a flags change), we can simply return the mask.
  if ((event.modifierFlags & NSEventModifierFlagFunction) == 0 ||
      event.type != NSEventTypeKeyDown) {
    return eventModifierMask;
  }

  NSString* eventString = event.charactersIgnoringModifiers;
  if (eventString.length == 0) {
    return eventModifierMask;
  }

  // "Up arrow", home, and other "function" key events include
  // NSEventModifierFlagFunction in their flags even though the user isn't
  // holding down the keyboard's function / world key. Add
  // NSEventModifierFlagFunction to the returned modifier mask only if the
  // event isn't for a function key.
  unichar firstCharacter = [eventString characterAtIndex:0];
  if (firstCharacter < NSUpArrowFunctionKey ||
      firstCharacter > NSModeSwitchFunctionKey)
    eventModifierMask |= NSEventModifierFlagFunction;

  return eventModifierMask;
}

}  // namespace ui::cocoa

@interface KeyboardInputSourceListener : NSObject
@end

@implementation KeyboardInputSourceListener

- (instancetype)init {
  if (self = [super init]) {
    [NSNotificationCenter.defaultCenter
        addObserver:self
           selector:@selector(inputSourceDidChange:)
               name:NSTextInputContextKeyboardSelectionDidChangeNotification
             object:nil];
    [self updateInputSource];
  }
  return self;
}

- (void)dealloc {
  [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (void)updateInputSource {
  base::apple::ScopedCFTypeRef<TISInputSourceRef> inputSource(
      TISCopyCurrentKeyboardInputSource());
  NSString* layoutId = base::apple::CFToNSPtrCast(
      base::apple::CFCast<CFStringRef>(TISGetInputSourceProperty(
          inputSource.get(), kTISPropertyInputSourceID)));
  ui::cocoa::g_is_input_source_command_qwerty =
      ui::cocoa::IsKeyboardLayoutCommandQwerty(layoutId);
  ui::cocoa::g_is_input_source_dvorak_right_or_left =
      ui::cocoa::IsKeyboardLayoutDvorakRightOrLeft(layoutId);
  ui::cocoa::g_is_input_source_command_hebrew =
      ui::cocoa::IsKeyboardLayoutCommandHebrew(layoutId);
  ui::cocoa::g_is_input_source_command_arabic =
      ui::cocoa::IsKeyboardLayoutCommandArabic(layoutId);
}

- (void)inputSourceDidChange:(NSNotification*)notification {
  [self updateInputSource];
}

@end

@implementation NSMenuItem (ChromeAdditions)

- (BOOL)cr_firesForKeyEquivalentEvent:(NSEvent*)event {
  if (![self isEnabled])
    return NO;

  DCHECK(event.type == NSEventTypeKeyDown);
  // In System Preferences->Keyboard->Keyboard Shortcuts, it is possible to add
  // arbitrary keyboard shortcuts to applications. It is not documented how this
  // works in detail, but |NSMenuItem| has a method |userKeyEquivalent| that
  // sounds related.
  // However, it looks like |userKeyEquivalent| is equal to |keyEquivalent| when
  // a user shortcut is set in system preferences, i.e. Cocoa automatically
  // sets/overwrites |keyEquivalent| as well. Hence, this method can ignore
  // |userKeyEquivalent| and check |keyEquivalent| only.

  // Menu item key equivalents are nearly all stored without modifiers. The
  // exception is shift, which is included in the key and not in the modifiers
  // for printable characters (but not for stuff like arrow keys etc).
  NSString* eventString = event.charactersIgnoringModifiers;
  NSUInteger eventModifiers =
      event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask;

  // cmd-opt-a gives some weird char as characters and "a" as
  // charactersWithoutModifiers with an US layout, but an "a" as characters and
  // a weird char as "charactersWithoutModifiers" with a cyrillic layout. Oh,
  // Cocoa! Instead of getting the current layout from Text Input Services,
  // and then requesting the kTISPropertyUnicodeKeyLayoutData and looking in
  // there, let's go with a pragmatic hack.
  bool useEventCharacters = eventString.length == 0;
  NSString* eventCharacters = event.characters;
  if (eventString.length > 0 && eventCharacters.length > 0) {
    if ([eventString characterAtIndex:0] > 0x7f &&
        [eventCharacters characterAtIndex:0] <= 0x7f) {
      useEventCharacters = true;
    } else if (ui::cocoa::g_is_input_source_command_hebrew &&
               [eventString isEqualToString:@"/"] &&
               [eventCharacters isEqualToString:@"q"]) {
      // Our pragmatic hack works very well except for the "q" key in Hebrew
      // layouts. In this case, the first char of eventString ("/") is
      // not < 0x7f, so the hack doesn't choose eventCharacters (which is
      // "q"). This causes Cmd-q to not take the normal processing path which
      // includes a warning to hold "Cmd q" to quit (if that option is set).
      // Instead, the Cmd-q likely travels to the renderer and upon its return
      // triggers -[NSApplication terminate:], the selector associated with
      // Chrome -> Quit. We handle this special case here.
      useEventCharacters = true;
    } else if (ui::cocoa::g_is_input_source_command_arabic &&
               [eventString isEqualToString:@"{"] &&
               [eventCharacters isEqualToString:@"V"]) {
      // Similar problem of our hack not working for the "V" key in certain
      // Arabic layouts. In this case, the first char of eventString ("{") is
      // not < 0x7f, so the hack doesn't choose eventCharacters (which is
      // "V"). This causes ⇧⌘V not to match Paste and Match Style.
      useEventCharacters = true;
    }
  }
  if (useEventCharacters) {
    eventString = eventCharacters;

    // If the user is pressing the Shift key, force the shortcut string to
    // uppercase. Otherwise, if only Caps Lock is down, ensure the shortcut
    // string is lowercase.
    if (eventModifiers & NSEventModifierFlagShift) {
      eventString = eventString.uppercaseString;
    } else if (eventModifiers & NSEventModifierFlagCapsLock) {
      eventString = eventString.lowercaseString;
    }
  }

  if (eventString.length == 0 || self.keyEquivalent.length == 0) {
    return NO;
  }

  // Turns out esc never fires unless cmd or ctrl is down.
  if (event.keyCode == kVK_Escape &&
      (eventModifiers &
       (NSEventModifierFlagControl | NSEventModifierFlagCommand)) == 0) {
    return NO;
  }

  // From the |NSMenuItem setKeyEquivalent:| documentation:
  //
  // If you want to specify the Backspace key as the key equivalent for a menu
  // item, use a single character string with NSBackspaceCharacter (defined in
  // NSText.h as 0x08) and for the Forward Delete key, use NSDeleteCharacter
  // (defined in NSText.h as 0x7F). Note that these are not the same characters
  // you get from an NSEvent key-down event when pressing those keys.
  if ([self.keyEquivalent characterAtIndex:0] == NSBackspaceCharacter &&
      [eventString characterAtIndex:0] == NSDeleteCharacter) {
    unichar chr = NSBackspaceCharacter;
    eventString = [NSString stringWithCharacters:&chr length:1];

    // Make sure "shift" is not removed from modifiers below.
    eventModifiers |= NSEventModifierFlagFunction;
  }
  if ([self.keyEquivalent characterAtIndex:0] == NSDeleteCharacter &&
      [eventString characterAtIndex:0] == NSDeleteFunctionKey) {
    unichar chr = NSDeleteCharacter;
    eventString = [NSString stringWithCharacters:&chr length:1];

    // Make sure "shift" is not removed from modifiers below.
    eventModifiers |= NSEventModifierFlagFunction;
  }

  // We intentionally leak this object.
  [[maybe_unused]] static KeyboardInputSourceListener* listener =
      [[KeyboardInputSourceListener alloc] init];

  // We typically want to compare [NSMenuItem keyEquivalent] against [NSEvent
  // charactersIgnoringModifiers]. There are special command-qwerty layouts
  // (such as DVORAK-QWERTY) which use QWERTY-style shortcuts when the Command
  // key is held down. In this case, we want to use the keycode of the event
  // rather than looking at the characters.
  if (ui::cocoa::g_is_input_source_command_qwerty) {
    ui::KeyboardCode windows_keycode =
        ui::KeyboardCodeFromKeyCode(event.keyCode);
    unichar shifted_character, character;
    ui::MacKeyCodeForWindowsKeyCode(windows_keycode, event.modifierFlags,
                                    &shifted_character, &character);
    eventString = [NSString stringWithFormat:@"%C", shifted_character];
  }

  // On all keyboards except Dvorak-Right/Left, treat cmd + <number key> as the
  // equivalent numerical key. This is technically incorrect, since the actual
  // character produced may not be a number key, but this causes Chrome to match
  // platform behavior. For example, on the Czech keyboard, we want to interpret
  // cmd + '+' as cmd + '1', even though the '1' character normally requires
  // cmd + shift + '+'.
  if (!ui::cocoa::g_is_input_source_dvorak_right_or_left &&
      eventModifiers == NSEventModifierFlagCommand) {
    ui::KeyboardCode windows_keycode =
        ui::KeyboardCodeFromKeyCode(event.keyCode);
    if (windows_keycode >= ui::VKEY_0 && windows_keycode <= ui::VKEY_9) {
      eventString =
          [NSString stringWithFormat:@"%d", windows_keycode - ui::VKEY_0];
    }
  }

  // [ctr + shift + tab] generates the "End of Medium" keyEquivalent rather than
  // "Horizontal Tab". We still use "Horizontal Tab" in the main menu to match
  // the behavior of Safari and Terminal. Thus, we need to explicitly check for
  // this case.
  if ((eventModifiers & NSEventModifierFlagShift) &&
      [eventString isEqualToString:@"\x19"]) {
    eventString = @"\x9";
  } else {
    // Clear shift key for printable characters, excluding tab.
    if ((eventModifiers &
         (NSEventModifierFlagNumericPad | NSEventModifierFlagFunction)) == 0 &&
        [self.keyEquivalent characterAtIndex:0] != '\r' &&
        [self.keyEquivalent characterAtIndex:0] != '\x9') {
      eventModifiers &= ~NSEventModifierFlagShift;
    }
  }

  // Clear all non-interesting modifiers
  eventModifiers &= ui::cocoa::ModifierMaskForKeyEvent(event);

  return [eventString isEqualToString:self.keyEquivalent] &&
         eventModifiers == self.keyEquivalentModifierMask;
}

- (void)cr_setKeyEquivalent:(NSString*)aString
               modifierMask:(NSEventModifierFlags)mask {
  DCHECK(aString);
  self.keyEquivalent = aString;
  self.keyEquivalentModifierMask = mask;
}

- (void)cr_clearKeyEquivalent {
  self.keyEquivalent = @"";
  self.keyEquivalentModifierMask = 0;
}

@end