1/* 2 PPGNUstepGlue_MenuKeyEquivalents.m 3 4 Copyright 2014-2018 Josh Freeman 5 http://www.twilightedge.com 6 7 This file is part of PikoPixel for GNUstep. 8 PikoPixel is a graphical application for drawing & editing pixel-art images. 9 10 PikoPixel is free software: you can redistribute it and/or modify it under 11 the terms of the GNU Affero General Public License as published by the 12 Free Software Foundation, either version 3 of the License, or (at your 13 option) any later version approved for PikoPixel by its copyright holder (or 14 an authorized proxy). 15 16 PikoPixel is distributed in the hope that it will be useful, but WITHOUT ANY 17 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 19 details. 20 21 You should have received a copy of the GNU Affero General Public License 22 along with this program. If not, see <http://www.gnu.org/licenses/>. 23*/ 24 25#ifdef GNUSTEP 26 27#import <Cocoa/Cocoa.h> 28#import "NSObject_PPUtilities.h" 29#import "PPAppBootUtilities.h" 30 31 32#define kModifierNamesArraySize 16 33 34#define macroModifierNamesIndexForModifierMask(mask) ((mask >> 17) & 0x0F) 35#define macroModifierMaskForModifierNamesIndex(index) (index << 17) 36 37 38static NSString *gModifierNamesArray[kModifierNamesArraySize]; 39 40 41static void DequeueKeyAutorepeatEventsForKeyChars(NSString *keyChars); 42 43static void SetupModifierNamesArray(void); 44 45static NSDictionary *KeyToDisplayKeyDict(void); 46 47 48@implementation NSObject (PPGNUstepGlue_MenuKeyEquivalents) 49 50+ (void) ppGSGlue_MenuKeyEquivalents_InstallPatches 51{ 52 macroSwizzleInstanceMethod(NSMenu, performKeyEquivalent:, ppGSPatch_PerformKeyEquivalent:); 53 54 macroSwizzleInstanceMethod(NSMenuItemCell, _keyEquivalentString, 55 ppGSPatch_KeyEquivalentString); 56} 57 58+ (void) ppGSGlue_MenuKeyEquivalents_Install 59{ 60 SetupModifierNamesArray(); 61 62 [self ppGSGlue_MenuKeyEquivalents_InstallPatches]; 63} 64 65+ (void) load 66{ 67 macroPerformNSObjectSelectorAfterAppLoads(ppGSGlue_MenuKeyEquivalents_Install); 68} 69 70@end 71 72@implementation NSMenu (PPGNUstepGlue_MenuKeyEquivalents) 73 74- (BOOL) ppGSPatch_PerformKeyEquivalent: (NSEvent *) theEvent 75{ 76 static bool isRootCall = YES; 77 BOOL didPerformKeyEquivalent; 78 79 if (isRootCall) 80 { 81 NSString *eventChars = [theEvent charactersIgnoringModifiers]; 82 83 // Delete key won't trigger the "Delete" menu item because the item's key equivalent is 84 // the backspace, so substitute the delete key event with a backspace key event 85 86 if ([eventChars length] 87 && ([eventChars characterAtIndex: 0] == NSDeleteCharacter)) 88 { 89 NSEvent *backspaceKeyEvent = [NSEvent keyEventWithType: [theEvent type] 90 location: [theEvent locationInWindow] 91 modifierFlags: [theEvent modifierFlags] 92 timestamp: [theEvent timestamp] 93 windowNumber: [theEvent windowNumber] 94 context: [theEvent context] 95 characters: @"\b" 96 charactersIgnoringModifiers: @"\b" 97 isARepeat: [theEvent isARepeat] 98 keyCode: [theEvent keyCode]]; 99 100 if (backspaceKeyEvent) 101 { 102 theEvent = backspaceKeyEvent; 103 } 104 } 105 106 isRootCall = NO; 107 didPerformKeyEquivalent = [self ppGSPatch_PerformKeyEquivalent: theEvent]; 108 isRootCall = YES; 109 110 if (didPerformKeyEquivalent) 111 { 112 // after performing a menu item's action by pressing its key-equivalent, clear the 113 // event queue of key-autorepeat events - this is so that when holding down a menu 114 // item's key-equivalent, the automatic repetition of the item's action stops 115 // quickly when the key is released - otherwise, a backlog of key-autorepeat events 116 // can accumulate in the event queue (especially when the item's action is a slow 117 // operation) and the app will temporarily become unresponsive after the key is 118 // released, as the actions triggered by the remaining enqueued key events are 119 // performed 120 121 DequeueKeyAutorepeatEventsForKeyChars(eventChars); 122 } 123 } 124 else 125 { 126 didPerformKeyEquivalent = [self ppGSPatch_PerformKeyEquivalent: theEvent]; 127 } 128 129 return didPerformKeyEquivalent; 130} 131 132@end 133 134@implementation NSMenuItemCell (PPGNUstepGlue_MenuKeyEquivalents) 135 136- (NSString *) ppGSPatch_KeyEquivalentString 137{ 138 static NSDictionary *keyToDisplayKeyDict = nil; 139 static NSCharacterSet *uppercaseLetterCharacterSet = nil; 140 NSMenuItem *menuItem; 141 NSString *key, *displayKey; 142 NSUInteger modifierKeyMask; 143 144 menuItem = [self menuItem]; 145 146 key = [menuItem keyEquivalent]; 147 148 if (!key || ![key length]) 149 { 150 return nil; 151 } 152 153 modifierKeyMask = [menuItem keyEquivalentModifierMask]; 154 155 if (!keyToDisplayKeyDict) 156 { 157 keyToDisplayKeyDict = [KeyToDisplayKeyDict() retain]; 158 } 159 160 if (!uppercaseLetterCharacterSet) 161 { 162 uppercaseLetterCharacterSet = [[NSCharacterSet uppercaseLetterCharacterSet] retain]; 163 } 164 165 displayKey = [keyToDisplayKeyDict objectForKey: key]; 166 167 if (displayKey) 168 { 169 key = displayKey; 170 } 171 else if ([key rangeOfCharacterFromSet: uppercaseLetterCharacterSet].length) 172 { 173 modifierKeyMask |= NSShiftKeyMask; 174 } 175 176 return [gModifierNamesArray[macroModifierNamesIndexForModifierMask(modifierKeyMask)] 177 stringByAppendingString: key]; 178} 179 180@end 181 182// DequeueKeyAutorepeatEventsForKeyChars(): 183// GNUstep currently doesn't set the repeat flag for key events (-[NSEvent isARepeat] always 184// returns NO), so need to manually determine whether key events are autorepeats (key is 185// held down) so that autorepeat events can be removed and normal key events (key is pressed & 186// released) are left alone: 187// - Group consecutive keyDown events (no keyUp events between them) into a single event 188// - Ignore keyUp events that are immediately followed by a keyDown event with an identical 189// timestamp (GNUstep can sometimes post keyUp events while a key is autorepeating, but in that 190// case, it will also post a second event (keyDown) with no elapsed time since the keyUp) 191 192#define kMinTimeBetweenNonAutorepeatKeyUpAndKeyDownEvents 0.005 193 194static void DequeueKeyAutorepeatEventsForKeyChars(NSString *keyChars) 195{ 196 static NSMutableArray *dequeuedEvents = nil; 197 NSUInteger keyEventsMask = NSKeyDownMask | NSKeyUpMask | NSFlagsChanged; 198 NSEvent *dequeuedEvent, *lastDequeuedEvent; 199 int dequeuedEventIndex; 200 201 if (![keyChars length]) 202 { 203 return; 204 } 205 206 if (!dequeuedEvents) 207 { 208 dequeuedEvents = [[NSMutableArray array] retain]; 209 210 if (!dequeuedEvents) 211 return; 212 } 213 214 [dequeuedEvents removeAllObjects]; 215 216 dequeuedEvent = [NSApp nextEventMatchingMask: keyEventsMask 217 untilDate: nil 218 inMode: NSEventTrackingRunLoopMode 219 dequeue: YES]; 220 221 while (dequeuedEvent) 222 { 223 if (![[dequeuedEvent charactersIgnoringModifiers] isEqualToString: keyChars] 224 || ([dequeuedEvent type] == NSFlagsChanged)) 225 { 226 [NSApp postEvent: dequeuedEvent atStart: YES]; 227 228 dequeuedEvent = nil; 229 } 230 else 231 { 232 [dequeuedEvents addObject: dequeuedEvent]; 233 234 dequeuedEvent = [NSApp nextEventMatchingMask: keyEventsMask 235 untilDate: nil 236 inMode: NSEventTrackingRunLoopMode 237 dequeue: YES]; 238 } 239 } 240 241 dequeuedEventIndex = [dequeuedEvents count] - 1; 242 243 if (dequeuedEventIndex >= 0) 244 { 245 NSEventType lastRequeuedEventType, dequeuedEventType; 246 247 dequeuedEvent = [dequeuedEvents objectAtIndex: dequeuedEventIndex]; 248 249 [NSApp postEvent: dequeuedEvent atStart: YES]; 250 251 lastRequeuedEventType = [dequeuedEvent type]; 252 253 lastDequeuedEvent = dequeuedEvent; 254 dequeuedEventIndex--; 255 256 while (dequeuedEventIndex >= 0) 257 { 258 dequeuedEvent = [dequeuedEvents objectAtIndex: dequeuedEventIndex]; 259 260 dequeuedEventType = [dequeuedEvent type]; 261 262 if ((lastRequeuedEventType != NSKeyDown) 263 || ((dequeuedEventType == NSKeyUp) 264 && (([lastDequeuedEvent timestamp] - [dequeuedEvent timestamp]) 265 > kMinTimeBetweenNonAutorepeatKeyUpAndKeyDownEvents))) 266 { 267 [NSApp postEvent: dequeuedEvent atStart: YES]; 268 269 lastRequeuedEventType = dequeuedEventType; 270 } 271 272 lastDequeuedEvent = dequeuedEvent; 273 dequeuedEventIndex--; 274 } 275 } 276} 277 278static void SetupModifierNamesArray(void) 279{ 280 NSUInteger i, modifierKeyMask; 281 282 for (i=0; i<kModifierNamesArraySize; i++) 283 { 284 modifierKeyMask = macroModifierMaskForModifierNamesIndex(i); 285 286 gModifierNamesArray[i] = 287 [[NSString stringWithFormat: @" %@%@%@%@", 288 (modifierKeyMask & NSControlKeyMask) ? @"Ctrl+" : @"", 289 (modifierKeyMask & NSAlternateKeyMask) ? @"Alt+" : @"", 290 (modifierKeyMask & NSCommandKeyMask) ? @"Super+" : @"", 291 (modifierKeyMask & NSShiftKeyMask) ? @"Shift+" : @""] 292 retain]; 293 } 294} 295 296static NSDictionary *KeyToDisplayKeyDict(void) 297{ 298 NSMutableDictionary *keyToDisplayKeyDict; 299 unichar lowercaseChar, uppercaseChar; 300 301 keyToDisplayKeyDict = [NSMutableDictionary dictionaryWithObjectsAndKeys: 302 303 // Tab 304 @"\u21E5", 305 @"\t", 306 307 // Return 308 @"\u21A9", 309 @"\r", 310 311 // ESC 312 @"\u238B", 313 @"\e", 314 315 // Space 316 @"\u23B5", 317 @" ", 318 319 // Backspace 320 @"\u232B", 321 @"\b", 322 323 // Left arrow 324 @"\u2190", 325 @"\uF702", 326 327 // Up arrow 328 @"\u2191", 329 @"\uF700", 330 331 // Right arrow 332 @"\u2192", 333 @"\uF703", 334 335 // Down arrow 336 @"\u2193", 337 @"\uF701", 338 339 nil]; 340 341 // Lowercase to uppercase alphabet chars 342 343 for (lowercaseChar = 'a'; lowercaseChar <= 'z'; lowercaseChar++) 344 { 345 uppercaseChar = 'A' + lowercaseChar - 'a'; 346 347 [keyToDisplayKeyDict 348 setObject: [NSString stringWithCharacters: &uppercaseChar length: 1] 349 forKey: [NSString stringWithCharacters: &lowercaseChar length: 1]]; 350 } 351 352 return [NSDictionary dictionaryWithDictionary: keyToDisplayKeyDict]; 353} 354 355#endif // GNUSTEP 356 357