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