]> Pileus Git - ~andy/gtk/blob - gtk/gtkmodelmenu-quartz.c
Action helper support in Mac OS menus.
[~andy/gtk] / gtk / gtkmodelmenu-quartz.c
1 /*
2  * Copyright © 2011 William Hua, Ryan Lortie
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the licence, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Author: William Hua <william@attente.ca>
18  *         Ryan Lortie <desrt@desrt.ca>
19  */
20
21 #include "gtkmodelmenu-quartz.h"
22
23 #include <gdk/gdkkeysyms.h>
24 #include "gtkaccelmapprivate.h"
25 #include "gtkactionhelper.h"
26
27 #import <Cocoa/Cocoa.h>
28
29 /*
30  * Code for key code conversion
31  *
32  * Copyright (C) 2009 Paul Davis
33  */
34 static unichar
35 gtk_quartz_model_menu_get_unichar (gint key)
36 {
37   if (key >= GDK_KEY_A && key <= GDK_KEY_Z)
38     return key + (GDK_KEY_a - GDK_KEY_A);
39
40   if (key >= GDK_KEY_space && key <= GDK_KEY_asciitilde)
41     return key;
42
43   switch (key)
44     {
45       case GDK_KEY_BackSpace:
46         return NSBackspaceCharacter;
47       case GDK_KEY_Delete:
48         return NSDeleteFunctionKey;
49       case GDK_KEY_Pause:
50         return NSPauseFunctionKey;
51       case GDK_KEY_Scroll_Lock:
52         return NSScrollLockFunctionKey;
53       case GDK_KEY_Sys_Req:
54         return NSSysReqFunctionKey;
55       case GDK_KEY_Home:
56         return NSHomeFunctionKey;
57       case GDK_KEY_Left:
58       case GDK_KEY_leftarrow:
59         return NSLeftArrowFunctionKey;
60       case GDK_KEY_Up:
61       case GDK_KEY_uparrow:
62         return NSUpArrowFunctionKey;
63       case GDK_KEY_Right:
64       case GDK_KEY_rightarrow:
65         return NSRightArrowFunctionKey;
66       case GDK_KEY_Down:
67       case GDK_KEY_downarrow:
68         return NSDownArrowFunctionKey;
69       case GDK_KEY_Page_Up:
70         return NSPageUpFunctionKey;
71       case GDK_KEY_Page_Down:
72         return NSPageDownFunctionKey;
73       case GDK_KEY_End:
74         return NSEndFunctionKey;
75       case GDK_KEY_Begin:
76         return NSBeginFunctionKey;
77       case GDK_KEY_Select:
78         return NSSelectFunctionKey;
79       case GDK_KEY_Print:
80         return NSPrintFunctionKey;
81       case GDK_KEY_Execute:
82         return NSExecuteFunctionKey;
83       case GDK_KEY_Insert:
84         return NSInsertFunctionKey;
85       case GDK_KEY_Undo:
86         return NSUndoFunctionKey;
87       case GDK_KEY_Redo:
88         return NSRedoFunctionKey;
89       case GDK_KEY_Menu:
90         return NSMenuFunctionKey;
91       case GDK_KEY_Find:
92         return NSFindFunctionKey;
93       case GDK_KEY_Help:
94         return NSHelpFunctionKey;
95       case GDK_KEY_Break:
96         return NSBreakFunctionKey;
97       case GDK_KEY_Mode_switch:
98         return NSModeSwitchFunctionKey;
99       case GDK_KEY_F1:
100         return NSF1FunctionKey;
101       case GDK_KEY_F2:
102         return NSF2FunctionKey;
103       case GDK_KEY_F3:
104         return NSF3FunctionKey;
105       case GDK_KEY_F4:
106         return NSF4FunctionKey;
107       case GDK_KEY_F5:
108         return NSF5FunctionKey;
109       case GDK_KEY_F6:
110         return NSF6FunctionKey;
111       case GDK_KEY_F7:
112         return NSF7FunctionKey;
113       case GDK_KEY_F8:
114         return NSF8FunctionKey;
115       case GDK_KEY_F9:
116         return NSF9FunctionKey;
117       case GDK_KEY_F10:
118         return NSF10FunctionKey;
119       case GDK_KEY_F11:
120         return NSF11FunctionKey;
121       case GDK_KEY_F12:
122         return NSF12FunctionKey;
123       case GDK_KEY_F13:
124         return NSF13FunctionKey;
125       case GDK_KEY_F14:
126         return NSF14FunctionKey;
127       case GDK_KEY_F15:
128         return NSF15FunctionKey;
129       case GDK_KEY_F16:
130         return NSF16FunctionKey;
131       case GDK_KEY_F17:
132         return NSF17FunctionKey;
133       case GDK_KEY_F18:
134         return NSF18FunctionKey;
135       case GDK_KEY_F19:
136         return NSF19FunctionKey;
137       case GDK_KEY_F20:
138         return NSF20FunctionKey;
139       case GDK_KEY_F21:
140         return NSF21FunctionKey;
141       case GDK_KEY_F22:
142         return NSF22FunctionKey;
143       case GDK_KEY_F23:
144         return NSF23FunctionKey;
145       case GDK_KEY_F24:
146         return NSF24FunctionKey;
147       case GDK_KEY_F25:
148         return NSF25FunctionKey;
149       case GDK_KEY_F26:
150         return NSF26FunctionKey;
151       case GDK_KEY_F27:
152         return NSF27FunctionKey;
153       case GDK_KEY_F28:
154         return NSF28FunctionKey;
155       case GDK_KEY_F29:
156         return NSF29FunctionKey;
157       case GDK_KEY_F30:
158         return NSF30FunctionKey;
159       case GDK_KEY_F31:
160         return NSF31FunctionKey;
161       case GDK_KEY_F32:
162         return NSF32FunctionKey;
163       case GDK_KEY_F33:
164         return NSF33FunctionKey;
165       case GDK_KEY_F34:
166         return NSF34FunctionKey;
167       case GDK_KEY_F35:
168         return NSF35FunctionKey;
169       default:
170         break;
171     }
172
173   return '\0';
174 }
175
176
177
178 @interface GNSMenu : NSMenu
179 {
180   GtkApplication *application;
181   GMenuModel     *model;
182   guint           update_idle;
183   GSList         *connected;
184   gboolean        with_separators;
185 }
186
187 - (id)initWithTitle:(NSString *)title model:(GMenuModel *)aModel application:(GtkApplication *)application hasSeparators:(BOOL)hasSeparators;
188
189 - (void)model:(GMenuModel *)model didChangeAtPosition:(NSInteger)position removed:(NSInteger)removed added:(NSInteger)added;
190
191 - (gboolean)handleChanges;
192
193 @end
194
195
196
197 @interface GNSMenuItem : NSMenuItem
198 {
199   GtkActionHelper *helper;
200 }
201
202 - (id)initWithModel:(GMenuModel *)model index:(NSInteger)index application:(GtkApplication *)application;
203
204 - (void)didSelectItem:(id)sender;
205
206 - (void)helperChanged;
207
208 @end
209
210
211
212 static gboolean
213 gtk_quartz_model_menu_handle_changes (gpointer user_data)
214 {
215   GNSMenu *menu = user_data;
216
217   return [menu handleChanges];
218 }
219
220 static void
221 gtk_quartz_model_menu_items_changed (GMenuModel *model,
222                                gint        position,
223                                gint        removed,
224                                gint        added,
225                                gpointer    user_data)
226 {
227   GNSMenu *menu = user_data;
228
229   [menu model:model didChangeAtPosition:position removed:removed added:added];
230 }
231
232 void
233 gtk_quartz_set_main_menu (GMenuModel     *model,
234                           GtkApplication *application)
235 {
236   [NSApp setMainMenu:[[[GNSMenu alloc] initWithTitle:@"Main Menu" model:model application:application hasSeparators:NO] autorelease]];
237 }
238
239 void
240 gtk_quartz_clear_main_menu (void)
241 {
242   // ensure that we drop all GNSMenuItem (to ensure 'application' has no extra references)
243   [NSApp setMainMenu:[[[NSMenu alloc] init] autorelease]];
244 }
245
246 @interface GNSMenu ()
247
248 - (void)appendFromModel:(GMenuModel *)aModel withSeparators:(BOOL)withSeparators;
249
250 @end
251
252
253
254 @implementation GNSMenu
255
256 - (void)model:(GMenuModel *)model didChangeAtPosition:(NSInteger)position removed:(NSInteger)removed added:(NSInteger)added
257 {
258   if (update_idle == 0)
259     update_idle = gdk_threads_add_idle (gtk_quartz_model_menu_handle_changes, self);
260 }
261
262 - (void)appendItemFromModel:(GMenuModel *)aModel atIndex:(gint)index withHeading:(gchar **)heading
263 {
264   GMenuModel *section;
265
266   if ((section = g_menu_model_get_item_link (aModel, index, G_MENU_LINK_SECTION)))
267     {
268       g_menu_model_get_item_attribute (aModel, index, G_MENU_ATTRIBUTE_LABEL, "s", heading);
269       [self appendFromModel:section withSeparators:NO];
270       g_object_unref (section);
271     }
272   else
273     [self addItem:[[[GNSMenuItem alloc] initWithModel:aModel index:index application:application] autorelease]];
274 }
275
276 - (void)appendFromModel:(GMenuModel *)aModel withSeparators:(BOOL)withSeparators
277 {
278   gint n, i;
279
280   g_signal_connect (aModel, "items-changed", G_CALLBACK (gtk_quartz_model_menu_items_changed), self);
281   connected = g_slist_prepend (connected, g_object_ref (aModel));
282
283   n = g_menu_model_get_n_items (aModel);
284
285   for (i = 0; i < n; i++)
286     {
287       NSInteger ourPosition = [self numberOfItems];
288       gchar *heading = NULL;
289
290       [self appendItemFromModel:aModel atIndex:i withHeading:&heading];
291
292       if (withSeparators && ourPosition < [self numberOfItems])
293         {
294           NSMenuItem *separator = nil;
295
296           if (heading)
297             {
298               separator = [[[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:heading] action:NULL keyEquivalent:@""] autorelease];
299
300               [separator setEnabled:NO];
301             }
302           else if (ourPosition > 0)
303             separator = [NSMenuItem separatorItem];
304
305           if (separator != nil)
306             [self insertItem:separator atIndex:ourPosition];
307         }
308
309       g_free (heading);
310     }
311 }
312
313 - (void)populate
314 {
315   [self removeAllItems];
316
317   [self appendFromModel:model withSeparators:with_separators];
318 }
319
320 - (gboolean)handleChanges
321 {
322   while (connected)
323     {
324       g_signal_handlers_disconnect_by_func (connected->data, gtk_quartz_model_menu_items_changed, self);
325       g_object_unref (connected->data);
326
327       connected = g_slist_delete_link (connected, connected);
328     }
329
330   [self populate];
331
332   update_idle = 0;
333
334   return G_SOURCE_REMOVE;
335 }
336
337 - (id)initWithTitle:(NSString *)title model:(GMenuModel *)aModel application:(GtkApplication *)anApplication hasSeparators:(BOOL)hasSeparators
338 {
339   if((self = [super initWithTitle:title]) != nil)
340     {
341       [self setAutoenablesItems:NO];
342
343       model = g_object_ref (aModel);
344       application = g_object_ref (anApplication);
345       with_separators = hasSeparators;
346
347       [self populate];
348     }
349
350   return self;
351 }
352
353 - (void)dealloc
354 {
355   while (connected)
356     {
357       g_signal_handlers_disconnect_by_func (connected->data, gtk_quartz_model_menu_items_changed, self);
358       g_object_unref (connected->data);
359
360       connected = g_slist_delete_link (connected, connected);
361     }
362
363   g_object_unref (application);
364   g_object_unref (model);
365
366   [super dealloc];
367 }
368
369 @end
370
371
372
373 static void
374 gtk_quartz_action_helper_changed (GObject    *object,
375                                   GParamSpec *pspec,
376                                   gpointer    user_data)
377 {
378   GNSMenuItem *item = user_data;
379
380   [item helperChanged];
381 }
382
383 @implementation GNSMenuItem
384
385 - (id)initWithModel:(GMenuModel *)model index:(NSInteger)index application:(GtkApplication *)application
386 {
387   gchar *title = NULL;
388
389   if (g_menu_model_get_item_attribute (model, index, G_MENU_ATTRIBUTE_LABEL, "s", &title))
390     {
391       gchar *from, *to;
392
393       to = from = title;
394
395       while (*from)
396         {
397           if (*from == '_' && from[1])
398             from++;
399
400           *to++ = *from++;
401         }
402
403       *to = '\0';
404     }
405
406   if ((self = [super initWithTitle:[NSString stringWithUTF8String:title ? : ""] action:@selector(didSelectItem:) keyEquivalent:@""]) != nil)
407     {
408       GMenuModel *submenu;
409       gchar      *action;
410       GVariant   *target;
411
412       action = NULL;
413       g_menu_model_get_item_attribute (model, index, G_MENU_ATTRIBUTE_ACTION, "s", &action);
414       target = g_menu_model_get_item_attribute_value (model, index, G_MENU_ATTRIBUTE_TARGET, NULL);
415
416       if ((submenu = g_menu_model_get_item_link (model, index, G_MENU_LINK_SUBMENU)))
417         {
418           [self setSubmenu:[[[GNSMenu alloc] initWithTitle:[NSString stringWithUTF8String:title] model:submenu application:application hasSeparators:YES] autorelease]];
419           g_object_unref (submenu);
420         }
421
422       else if (action != NULL)
423         {
424           GtkAccelKey key;
425           gchar *path;
426
427           helper = gtk_action_helper_new_with_application (application);
428           gtk_action_helper_set_action_name         (helper, action);
429           gtk_action_helper_set_action_target_value (helper, target);
430
431           g_signal_connect (helper, "notify", G_CALLBACK (gtk_quartz_action_helper_changed), self);
432
433           [self helperChanged];
434
435           path = _gtk_accel_path_for_action (action, target);
436           if (gtk_accel_map_lookup_entry (path, &key))
437             {
438               unichar character = gtk_quartz_model_menu_get_unichar (key.accel_key);
439
440               if (character)
441                 {
442                   NSUInteger modifiers = 0;
443
444                   if (key.accel_mods & GDK_SHIFT_MASK)
445                     modifiers |= NSShiftKeyMask;
446
447                   if (key.accel_mods & GDK_MOD1_MASK)
448                     modifiers |= NSAlternateKeyMask;
449
450                   if (key.accel_mods & GDK_CONTROL_MASK)
451                     modifiers |= NSControlKeyMask;
452
453                   if (key.accel_mods & GDK_META_MASK)
454                     modifiers |= NSCommandKeyMask;
455
456                   [self setKeyEquivalent:[NSString stringWithCharacters:&character length:1]];
457                   [self setKeyEquivalentModifierMask:modifiers];
458                 }
459             }
460
461           g_free (path);
462
463           [self setTarget:self];
464         }
465     }
466
467   g_free (title);
468
469   return self;
470 }
471
472 - (void)dealloc
473 {
474   if (helper != NULL)
475     g_object_unref (helper);
476
477   [super dealloc];
478 }
479
480 - (void)didSelectItem:(id)sender
481 {
482   gtk_action_helper_activate (helper);
483 }
484
485 - (void)helperChanged
486 {
487   [self setEnabled:gtk_action_helper_get_enabled (helper)];
488   [self setState:gtk_action_helper_get_active (helper)];
489
490   switch (gtk_action_helper_get_role (helper))
491     {
492       case GTK_ACTION_HELPER_ROLE_NORMAL:
493         [self setOnStateImage:nil];
494         break;
495       case GTK_ACTION_HELPER_ROLE_TOGGLE:
496         [self setOnStateImage:[NSImage imageNamed:@"NSMenuCheckmark"]];
497         break;
498       case GTK_ACTION_HELPER_ROLE_RADIO:
499         [self setOnStateImage:[NSImage imageNamed:@"NSMenuRadio"]];
500         break;
501       default:
502         g_assert_not_reached ();
503     }
504 }
505
506 @end