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:
Matthias Clasen 2025-01-03 22:23:06 +00:00
commit b837847128
10 changed files with 335 additions and 42 deletions

View File

@ -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>

View File

@ -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.

View File

@ -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];

View File

@ -52,6 +52,8 @@
BOOL inFullscreenTransition;
}
+(void)setContentViewClass:(Class)newViewClass;
-(void)beginManualMove;
-(void)beginManualResize:(GdkSurfaceEdge)edge;
-(void)hide;

View File

@ -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

View 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

View File

@ -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]];
}

View File

@ -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)
{

View File

@ -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);

View File

@ -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>