mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2025-01-10 12:40:10 +00:00
Merge branch 'amolenaar/macos-default-actions' into 'main'
macos: Provide Edit menu with actions for undo/copy/paste Closes #6829 See merge request GNOME/gtk!7906
This commit is contained in:
commit
b837847128
@ -32,6 +32,37 @@
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_Edit</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Undo</attribute>
|
||||
<attribute name="action">text.undo</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Redo</attribute>
|
||||
<attribute name="action">text.redo</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Cut</attribute>
|
||||
<attribute name="action">clipboard.cut</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Copy</attribute>
|
||||
<attribute name="action">clipboard.copy</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Paste</attribute>
|
||||
<attribute name="action">clipboard.paste</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Select All</attribute>
|
||||
<attribute name="action">selection.select-all</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_Preferences</attribute>
|
||||
<section>
|
||||
|
@ -56,3 +56,20 @@ backend built into GDK. For Wayland, the symbol is `GDK_WINDOWING_MACOS`.
|
||||
The run time check is performed by looking at the type of the
|
||||
[class@Gdk.Display] object. For Wayland, the display objects will be of type
|
||||
`GdkMacosDisplay`.
|
||||
|
||||
## Menus
|
||||
|
||||
By default a GTK app shows an app menu and an edit menu.
|
||||
To make menu actions work well with native windows, such as a file dialog,
|
||||
the most common edit commands are treated special:
|
||||
|
||||
* `text.undo`
|
||||
* `text.redo`
|
||||
* `clipboard.cut`
|
||||
* `clipboard.copy`
|
||||
* `clipboard.paste`
|
||||
* `selection.select-all`
|
||||
|
||||
Those actions map to their respective macOS counterparts.
|
||||
The actions are enabled in GTK if the action is available on the focused widget
|
||||
and is enabled.
|
||||
|
@ -44,6 +44,26 @@
|
||||
|
||||
@implementation GdkMacosWindow
|
||||
|
||||
static Class _contentViewClass = nil;
|
||||
|
||||
+(void)setContentViewClass:(Class)newViewClass
|
||||
{
|
||||
GDK_DEBUG (MISC, "Setting new content view class to %s", [[newViewClass description] UTF8String]);
|
||||
|
||||
if (newViewClass == nil || [newViewClass isSubclassOfClass:[GdkMacosView class]])
|
||||
_contentViewClass = newViewClass;
|
||||
else
|
||||
g_critical ("Assigned content view class %s is not a subclass of GdkMacosView", [[newViewClass description] UTF8String]);
|
||||
}
|
||||
|
||||
+(Class)contentViewClass
|
||||
{
|
||||
if (_contentViewClass != nil)
|
||||
return _contentViewClass;
|
||||
|
||||
return [GdkMacosView class];
|
||||
}
|
||||
|
||||
-(BOOL)windowShouldClose:(id)sender
|
||||
{
|
||||
GdkDisplay *display;
|
||||
@ -221,7 +241,7 @@
|
||||
[self setReleasedWhenClosed:YES];
|
||||
[self setPreservesContentDuringLiveResize:NO];
|
||||
|
||||
view = [[GdkMacosView alloc] initWithFrame:contentRect];
|
||||
view = [[[GdkMacosWindow contentViewClass] alloc] initWithFrame:contentRect];
|
||||
[self setContentView:view];
|
||||
[view release];
|
||||
|
||||
|
@ -52,6 +52,8 @@
|
||||
BOOL inFullscreenTransition;
|
||||
}
|
||||
|
||||
+(void)setContentViewClass:(Class)newViewClass;
|
||||
|
||||
-(void)beginManualMove;
|
||||
-(void)beginManualResize:(GdkSurfaceEdge)edge;
|
||||
-(void)hide;
|
||||
|
@ -21,15 +21,15 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "gtkapplicationprivate.h"
|
||||
#include "gtkmenutrackerprivate.h"
|
||||
#include "gtkicontheme.h"
|
||||
#include "gtkquartz.h"
|
||||
#include "gtkprivate.h"
|
||||
#include "gtkwidgetprivate.h"
|
||||
|
||||
#include <gdk/macos/gdkmacos.h>
|
||||
#include <gdk/macos/gdkmacoskeymap-private.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "gtkapplication-quartz-private.h"
|
||||
|
||||
#define ICON_SIZE 16
|
||||
|
||||
@ -55,27 +55,6 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSMenuItem : NSMenuItem
|
||||
{
|
||||
GtkMenuTrackerItem *trackerItem;
|
||||
gulong trackerItemChangedHandler;
|
||||
GCancellable *cancellable;
|
||||
BOOL isSpecial;
|
||||
}
|
||||
|
||||
- (id)initWithTrackerItem:(GtkMenuTrackerItem *)aTrackerItem;
|
||||
|
||||
- (void)didChangeLabel;
|
||||
- (void)didChangeIcon;
|
||||
- (void)didChangeVisible;
|
||||
- (void)didChangeToggled;
|
||||
- (void)didChangeAccel;
|
||||
|
||||
- (void)didSelectItem:(id)sender;
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem;
|
||||
|
||||
@end
|
||||
|
||||
static void
|
||||
tracker_item_changed (GObject *object,
|
||||
GParamSpec *pspec,
|
||||
@ -158,11 +137,6 @@ icon_loaded (GObject *object,
|
||||
|
||||
@implementation GNSMenuItem
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
|
||||
{
|
||||
return gtk_menu_tracker_item_get_sensitive (trackerItem) ? YES : NO;
|
||||
}
|
||||
|
||||
- (id)initWithTrackerItem:(GtkMenuTrackerItem *)aTrackerItem
|
||||
{
|
||||
self = [super initWithTitle:@""
|
||||
@ -171,6 +145,7 @@ icon_loaded (GObject *object,
|
||||
|
||||
if (self != nil)
|
||||
{
|
||||
const char *action_name = gtk_menu_tracker_item_get_action_name (aTrackerItem);
|
||||
const char *special = gtk_menu_tracker_item_get_special (aTrackerItem);
|
||||
|
||||
if (special && g_str_equal (special, "hide-this"))
|
||||
@ -194,6 +169,18 @@ icon_loaded (GObject *object,
|
||||
[NSApp setServicesMenu:[self submenu]];
|
||||
[self setTarget:self];
|
||||
}
|
||||
else if (action_name && g_str_equal (action_name, "text.undo"))
|
||||
[self setAction:@selector(undo:)];
|
||||
else if (action_name && g_str_equal (action_name, "text.redo"))
|
||||
[self setAction:@selector(redo:)];
|
||||
else if (action_name && g_str_equal (action_name, "clipboard.cut"))
|
||||
[self setAction:@selector(cut:)];
|
||||
else if (action_name && g_str_equal (action_name, "clipboard.copy"))
|
||||
[self setAction:@selector(copy:)];
|
||||
else if (action_name && g_str_equal (action_name, "clipboard.paste"))
|
||||
[self setAction:@selector(paste:)];
|
||||
else if (action_name && g_str_equal (action_name, "selection.select-all"))
|
||||
[self setAction:@selector(selectAll:)];
|
||||
else
|
||||
[self setTarget:self];
|
||||
|
||||
@ -365,7 +352,47 @@ icon_loaded (GObject *object,
|
||||
|
||||
- (void)didSelectItem:(id)sender
|
||||
{
|
||||
gtk_menu_tracker_item_activated (trackerItem);
|
||||
/* Mimic macOS' behavior of traversing the reponder chain. */
|
||||
GtkWidget *focus_widget = [self findFocusWidget];
|
||||
const char *action_name = gtk_menu_tracker_item_get_action_name (trackerItem);
|
||||
|
||||
if (focus_widget != NULL && action_name != NULL)
|
||||
gtk_widget_activate_action (focus_widget, action_name, NULL);
|
||||
else
|
||||
gtk_menu_tracker_item_activated (trackerItem);
|
||||
}
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
|
||||
{
|
||||
/* Mimic macOS' behavior of traversing the reponder chain. */
|
||||
GtkWidget *focus_widget = [self findFocusWidget];
|
||||
if (focus_widget != NULL && gtk_widget_get_sensitive (focus_widget))
|
||||
{
|
||||
const char *action_name = gtk_menu_tracker_item_get_action_name (trackerItem);
|
||||
gboolean enabled = FALSE;
|
||||
GtkActionMuxer *muxer = _gtk_widget_get_action_muxer (focus_widget, FALSE);
|
||||
|
||||
if (action_name == NULL || muxer == NULL)
|
||||
return gtk_menu_tracker_item_get_sensitive (trackerItem) ? YES : NO;
|
||||
|
||||
if (gtk_action_muxer_query_action (muxer, action_name, &enabled, NULL, NULL, NULL, NULL))
|
||||
return enabled ? YES : NO;
|
||||
}
|
||||
return gtk_menu_tracker_item_get_sensitive (trackerItem) ? YES : NO;
|
||||
}
|
||||
|
||||
-(GtkWidget *)findFocusWidget
|
||||
{
|
||||
GApplication *app = g_application_get_default ();
|
||||
GtkWindow *window;
|
||||
|
||||
if (!GTK_IS_APPLICATION (app))
|
||||
return NULL;
|
||||
|
||||
window = gtk_application_get_active_window (GTK_APPLICATION (app));
|
||||
if (window != NULL)
|
||||
return gtk_window_get_focus (window);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@end
|
||||
|
43
gtk/gtkapplication-quartz-private.h
Normal file
43
gtk/gtkapplication-quartz-private.h
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright © 2010 Codethink Limited
|
||||
* Copyright © 2013 Canonical Limited
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the licence, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Author: Ryan Lortie <desrt@desrt.ca>
|
||||
*/
|
||||
|
||||
#include "gtkmenutrackerprivate.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface GNSMenuItem : NSMenuItem
|
||||
{
|
||||
GtkMenuTrackerItem *trackerItem;
|
||||
gulong trackerItemChangedHandler;
|
||||
GCancellable *cancellable;
|
||||
BOOL isSpecial;
|
||||
}
|
||||
|
||||
- (id)initWithTrackerItem:(GtkMenuTrackerItem *)aTrackerItem;
|
||||
|
||||
- (void)didChangeLabel;
|
||||
- (void)didChangeIcon;
|
||||
- (void)didChangeVisible;
|
||||
- (void)didChangeToggled;
|
||||
- (void)didChangeAccel;
|
||||
|
||||
- (void)didSelectItem:(id)sender;
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem;
|
||||
|
||||
@end
|
@ -22,7 +22,10 @@
|
||||
|
||||
#include "gtkapplicationprivate.h"
|
||||
#include "gtkbuilder.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include "gtknative.h"
|
||||
#import <gdk/macos/GdkMacosView.h>
|
||||
#import <gdk/macos/GdkMacosWindow.h>
|
||||
#import "gtkapplication-quartz-private.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
@ -126,7 +129,101 @@ G_DEFINE_TYPE (GtkApplicationImplQuartz, gtk_application_impl_quartz, GTK_TYPE_A
|
||||
}
|
||||
@end
|
||||
|
||||
/* these exist only for accel handling */
|
||||
@interface GtkMacosContentView : GdkMacosView<NSMenuItemValidation>
|
||||
|
||||
/* In some cases GTK pops up a native window, such as when opening or
|
||||
* saving a file. We map common actions such as undo, copy, paste, etc.
|
||||
* to selectors, so these actions can be activated in a native window.
|
||||
* As a concequence, we also need to implement them on our own view,
|
||||
* and activate the action to the focused widget.
|
||||
*/
|
||||
|
||||
- (void) undo:(id)sender;
|
||||
- (void) redo:(id)sender;
|
||||
|
||||
- (void) cut:(id)sender;
|
||||
- (void) copy:(id)sender;
|
||||
- (void) paste:(id)sender;
|
||||
|
||||
- (void) selectAll:(id)sender;
|
||||
@end
|
||||
|
||||
@implementation GtkMacosContentView
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
|
||||
{
|
||||
if ([menuItem isKindOfClass:[GNSMenuItem class]])
|
||||
return [((GNSMenuItem *) menuItem) validateMenuItem:menuItem];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)undo:(id)sender
|
||||
{
|
||||
[self maybeActivateAction:"text.undo" sender:sender];
|
||||
}
|
||||
|
||||
- (void)redo:(id)sender
|
||||
{
|
||||
[self maybeActivateAction:"text.redo" sender:sender];
|
||||
}
|
||||
|
||||
- (void)cut:(id)sender
|
||||
{
|
||||
[self maybeActivateAction:"clipboard.cut" sender:sender];
|
||||
}
|
||||
|
||||
- (void)copy:(id)sender
|
||||
{
|
||||
[self maybeActivateAction:"clipboard.copy" sender:sender];
|
||||
}
|
||||
|
||||
- (void)paste:(id)sender
|
||||
{
|
||||
[self maybeActivateAction:"clipboard.paste" sender:sender];
|
||||
}
|
||||
|
||||
- (void)selectAll:(id)sender
|
||||
{
|
||||
[super selectAll:sender];
|
||||
[self maybeActivateAction:"selection.select-all" sender:sender];
|
||||
}
|
||||
|
||||
-(void)maybeActivateAction:(const char*)actionName sender:(id)sender
|
||||
{
|
||||
if ([sender isKindOfClass:[GNSMenuItem class]])
|
||||
[((GNSMenuItem *) sender) didSelectItem:sender];
|
||||
else
|
||||
g_warning ("%s: sender %s is not a GNSMenuItem", actionName, [[sender description] UTF8String]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static void
|
||||
gtk_application_impl_quartz_set_default_accels (GtkApplicationImpl *impl)
|
||||
{
|
||||
const char *pref_accel[] = {"<Meta>comma", NULL};
|
||||
const char *hide_others_accel[] = {"<Meta><Alt>h", NULL};
|
||||
const char *hide_accel[] = {"<Meta>h", NULL};
|
||||
const char *quit_accel[] = {"<Meta>q", NULL};
|
||||
const char *undo_accel[] = {"<Meta>z", NULL};
|
||||
const char *redo_accel[] = {"<Meta><Shift>z", NULL};
|
||||
const char *cut_accel[] = {"<Meta>x", NULL};
|
||||
const char *copy_accel[] = {"<Meta>c", NULL};
|
||||
const char *paste_accel[] = {"<Meta>v", NULL};
|
||||
const char *select_all_accel[] = {"<Meta>a", NULL};
|
||||
|
||||
gtk_application_set_accels_for_action (impl->application, "app.preferences", pref_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "gtkinternal.hide-others", hide_others_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "gtkinternal.hide", hide_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "app.quit", quit_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "text.undo", undo_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "text.redo", redo_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "clipboard.cut", cut_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "clipboard.copy", copy_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "clipboard.paste", paste_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "selection.select-all", select_all_accel);
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_application_impl_quartz_hide (GSimpleAction *action,
|
||||
GVariant *parameter,
|
||||
@ -186,10 +283,7 @@ gtk_application_impl_quartz_startup (GtkApplicationImpl *impl,
|
||||
{
|
||||
GtkApplicationImplQuartz *quartz = (GtkApplicationImplQuartz *) impl;
|
||||
GSimpleActionGroup *gtkinternal;
|
||||
const char *pref_accel[] = {"<Meta>comma", NULL};
|
||||
const char *hide_others_accel[] = {"<Meta><Alt>h", NULL};
|
||||
const char *hide_accel[] = {"<Meta>h", NULL};
|
||||
const char *quit_accel[] = {"<Meta>q", NULL};
|
||||
GMenuModel *menubar;
|
||||
|
||||
if (register_session)
|
||||
{
|
||||
@ -200,11 +294,7 @@ gtk_application_impl_quartz_startup (GtkApplicationImpl *impl,
|
||||
quartz->muxer = gtk_action_muxer_new (NULL);
|
||||
gtk_action_muxer_set_parent (quartz->muxer, gtk_application_get_action_muxer (impl->application));
|
||||
|
||||
/* Add the default accels */
|
||||
gtk_application_set_accels_for_action (impl->application, "app.preferences", pref_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "gtkinternal.hide-others", hide_others_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "gtkinternal.hide", hide_accel);
|
||||
gtk_application_set_accels_for_action (impl->application, "app.quit", quit_accel);
|
||||
gtk_application_impl_quartz_set_default_accels (impl);
|
||||
|
||||
/* and put code behind the 'special' accels */
|
||||
gtkinternal = g_simple_action_group_new ();
|
||||
@ -231,7 +321,19 @@ gtk_application_impl_quartz_startup (GtkApplicationImpl *impl,
|
||||
gtk_application_impl_quartz_set_app_menu (impl, quartz->standard_app_menu);
|
||||
|
||||
/* This may or may not add an item to 'combined' */
|
||||
gtk_application_impl_set_menubar (impl, gtk_application_get_menubar (impl->application));
|
||||
menubar = gtk_application_get_menubar (impl->application);
|
||||
if (menubar == NULL)
|
||||
{
|
||||
GtkBuilder *builder;
|
||||
|
||||
/* Provide a fallback menu, so keyboard shortcuts work in native windows too.
|
||||
*/
|
||||
builder = gtk_builder_new_from_resource ("/org/gtk/libgtk/ui/gtkapplication-quartz.ui");
|
||||
menubar = G_MENU_MODEL (g_object_ref (gtk_builder_get_object (builder, "default-menu")));
|
||||
g_object_unref (builder);
|
||||
}
|
||||
|
||||
gtk_application_impl_set_menubar (impl, menubar);
|
||||
|
||||
/* OK. Now put it in the menu. */
|
||||
gtk_application_impl_quartz_setup_menu (G_MENU_MODEL (quartz->combined), quartz->muxer);
|
||||
@ -394,4 +496,6 @@ gtk_application_impl_quartz_class_init (GtkApplicationImplClass *class)
|
||||
class->uninhibit = gtk_application_impl_quartz_uninhibit;
|
||||
|
||||
gobject_class->finalize = gtk_application_impl_quartz_finalize;
|
||||
|
||||
[GdkMacosWindow setContentViewClass:[GtkMacosContentView class]];
|
||||
}
|
||||
|
@ -737,6 +737,16 @@ gtk_menu_tracker_item_get_accel (GtkMenuTrackerItem *self)
|
||||
return gtk_action_muxer_get_primary_accel (GTK_ACTION_MUXER (self->observable), self->action_and_target);
|
||||
}
|
||||
|
||||
const char *
|
||||
gtk_menu_tracker_item_get_action_name (GtkMenuTrackerItem *self)
|
||||
{
|
||||
|
||||
if (!self->action_and_target)
|
||||
return NULL;
|
||||
|
||||
return strrchr (self->action_and_target, '|') + 1;
|
||||
}
|
||||
|
||||
const char *
|
||||
gtk_menu_tracker_item_get_special (GtkMenuTrackerItem *self)
|
||||
{
|
||||
|
@ -48,6 +48,8 @@ GtkMenuTrackerItem * _gtk_menu_tracker_item_new (GtkActi
|
||||
const char *action_namespace,
|
||||
gboolean is_separator);
|
||||
|
||||
const char * gtk_menu_tracker_item_get_action_name (GtkMenuTrackerItem *self);
|
||||
|
||||
const char * gtk_menu_tracker_item_get_special (GtkMenuTrackerItem *self);
|
||||
|
||||
const char * gtk_menu_tracker_item_get_custom (GtkMenuTrackerItem *self);
|
||||
|
@ -45,4 +45,41 @@
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="default-menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_Edit</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Undo</attribute>
|
||||
<attribute name="action">text.undo</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Redo</attribute>
|
||||
<attribute name="action">text.redo</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Cut</attribute>
|
||||
<attribute name="action">clipboard.cut</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Copy</attribute>
|
||||
<attribute name="action">clipboard.copy</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Paste</attribute>
|
||||
<attribute name="action">clipboard.paste</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Delete</attribute>
|
||||
<attribute name="action">selection.delete</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Select All</attribute>
|
||||
<attribute name="action">selection.select-all</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
</interface>
|
||||
|
Loading…
Reference in New Issue
Block a user