diff --git a/app/display/gimpimagewindow.c b/app/display/gimpimagewindow.c
index b44cce9be7..14ab14d5f4 100644
--- a/app/display/gimpimagewindow.c
+++ b/app/display/gimpimagewindow.c
@@ -50,6 +50,7 @@
#include "widgets/gimpdockcolumns.h"
#include "widgets/gimpdockcontainer.h"
#include "widgets/gimphelp-ids.h"
+#include "widgets/gimpmenu.h"
#include "widgets/gimpmenufactory.h"
#include "widgets/gimpsessioninfo.h"
#include "widgets/gimpsessioninfo-aux.h"
@@ -116,6 +117,7 @@ struct _GimpImageWindowPrivate
GimpDisplayShell *active_shell;
GtkWidget *main_vbox;
+ GtkWidget *new_menubar;
GtkWidget *menubar;
GtkWidget *hbox;
GtkWidget *left_hpane;
@@ -347,6 +349,9 @@ gimp_image_window_constructed (GObject *object)
GimpImageWindowPrivate *private = GIMP_IMAGE_WINDOW_GET_PRIVATE (window);
GimpMenuFactory *menu_factory;
GimpGuiConfig *config;
+ GtkBuilder *builder;
+ GMenuModel *model;
+ gchar *filename;
G_OBJECT_CLASS (parent_class)->constructed (object);
@@ -384,33 +389,71 @@ gimp_image_window_constructed (GObject *object)
gtk_widget_show (private->main_vbox);
/* Create the menubar */
-#ifndef GDK_WINDOWING_QUARTZ
+ filename = g_build_filename (gimp_data_directory (), "menus",
+ "image-menu.ui", NULL);
+
+ builder = gtk_builder_new_from_file (filename);
+ model = G_MENU_MODEL (gtk_builder_get_object (builder, "/image-menubar"));
+
+#ifdef GDK_WINDOWING_QUARTZ
+ /* macOS has its native menubar system, and this should support it. It means
+ * that we won't have tooltips on macOS menu though.
+ * TODO: Since the .ui file has no title/labels, I should edit the model to
+ * extract titles from actions.
+ */
+ gtk_application_set_menubar (private->gimp->app, G_MENU_MODEL (model));
+#else
+ /* TODO: this is the old menubar system. It should go away soon. */
private->menubar = gimp_ui_manager_get_widget (private->menubar_manager,
"/image-menubar");
+
+ gtk_box_pack_start (GTK_BOX (private->main_vbox),
+ private->menubar, FALSE, FALSE, 0);
+
+ /* make sure we can activate accels even if the menubar is invisible
+ * (see https://bugzilla.gnome.org/show_bug.cgi?id=137151)
+ */
+ g_signal_connect (private->menubar, "can-activate-accel",
+ G_CALLBACK (gtk_true),
+ NULL);
+
+ /* active display callback */
+ g_signal_connect (private->menubar, "button-press-event",
+ G_CALLBACK (gimp_image_window_shell_events),
+ window);
+ g_signal_connect (private->menubar, "button-release-event",
+ G_CALLBACK (gimp_image_window_shell_events),
+ window);
+ g_signal_connect (private->menubar, "key-press-event",
+ G_CALLBACK (gimp_image_window_shell_events),
+ window);
+
+ private->new_menubar = gimp_menu_new (model, private->gimp);
+
+ gtk_box_pack_start (GTK_BOX (private->main_vbox),
+ private->new_menubar, FALSE, FALSE, 0);
+
+ /* make sure we can activate accels even if the menubar is invisible
+ * (see https://bugzilla.gnome.org/show_bug.cgi?id=137151)
+ */
+ g_signal_connect (private->new_menubar, "can-activate-accel",
+ G_CALLBACK (gtk_true),
+ NULL);
+
+ /* active display callback */
+ g_signal_connect (private->new_menubar, "button-press-event",
+ G_CALLBACK (gimp_image_window_shell_events),
+ window);
+ g_signal_connect (private->new_menubar, "button-release-event",
+ G_CALLBACK (gimp_image_window_shell_events),
+ window);
+ g_signal_connect (private->new_menubar, "key-press-event",
+ G_CALLBACK (gimp_image_window_shell_events),
+ window);
#endif /* !GDK_WINDOWING_QUARTZ */
- if (private->menubar)
- {
- gtk_box_pack_start (GTK_BOX (private->main_vbox),
- private->menubar, FALSE, FALSE, 0);
- /* make sure we can activate accels even if the menubar is invisible
- * (see https://bugzilla.gnome.org/show_bug.cgi?id=137151)
- */
- g_signal_connect (private->menubar, "can-activate-accel",
- G_CALLBACK (gtk_true),
- NULL);
-
- /* active display callback */
- g_signal_connect (private->menubar, "button-press-event",
- G_CALLBACK (gimp_image_window_shell_events),
- window);
- g_signal_connect (private->menubar, "button-release-event",
- G_CALLBACK (gimp_image_window_shell_events),
- window);
- g_signal_connect (private->menubar, "key-press-event",
- G_CALLBACK (gimp_image_window_shell_events),
- window);
- }
+ g_object_unref (builder);
+ g_free (filename);
/* Create the hbox that contains docks and images */
private->hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
@@ -1347,6 +1390,8 @@ gimp_image_window_set_show_menubar (GimpImageWindow *window,
if (private->menubar)
gtk_widget_set_visible (private->menubar, show);
+ if (private->new_menubar)
+ gtk_widget_set_visible (private->new_menubar, show);
}
gboolean
diff --git a/app/widgets/Makefile.am b/app/widgets/Makefile.am
index 9b7d9a758b..8c759d3762 100644
--- a/app/widgets/Makefile.am
+++ b/app/widgets/Makefile.am
@@ -272,6 +272,8 @@ libappwidgets_a_sources = \
gimplayermodecombobox.h \
gimplayertreeview.c \
gimplayertreeview.h \
+ gimpmenu.c \
+ gimpmenu.h \
gimpmenudock.c \
gimpmenudock.h \
gimpmenufactory.c \
diff --git a/app/widgets/gimpmenu.c b/app/widgets/gimpmenu.c
new file mode 100644
index 0000000000..2528084945
--- /dev/null
+++ b/app/widgets/gimpmenu.c
@@ -0,0 +1,308 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpmenu.c
+ * Copyright (C) 2022 Jehan
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "libgimpbase/gimpbase.h"
+
+#include "widgets-types.h"
+
+#include "core/gimp.h"
+
+#include "gimpaction.h"
+#include "gimpmenu.h"
+#include "gimpradioaction.h"
+#include "gimptoggleaction.h"
+
+
+/**
+ * GimpMenu:
+ *
+ * Our own menu widget.
+ *
+ * We cannot use the simpler gtk_menu_new_from_model() because it lacks
+ * tooltip support and unfortunately GTK does not plan to implement this:
+ * https://gitlab.gnome.org/GNOME/gtk/-/issues/785
+ * This is why we need to implement our own GimpMenu subclass.
+ */
+
+enum
+{
+ PROP_0,
+ PROP_GIMP,
+ PROP_MODEL
+};
+
+
+struct _GimpMenuPrivate
+{
+ Gimp *gimp;
+ GMenuModel *model;
+};
+
+
+/* local function prototypes */
+
+static void gimp_menu_dispose (GObject *object);
+static void gimp_menu_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+static void gimp_menu_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+
+static void gimp_menu_update (GimpMenu *menu,
+ GtkContainer *container,
+ GMenuModel *model);
+static void gimp_menu_radio_item_toggled (GtkWidget *item,
+ GAction *action);
+
+
+G_DEFINE_TYPE_WITH_PRIVATE (GimpMenu, gimp_menu, GTK_TYPE_MENU_BAR)
+
+#define parent_class gimp_menu_parent_class
+
+
+/* private functions */
+
+static void
+gimp_menu_class_init (GimpMenuClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gimp_menu_dispose;
+ object_class->get_property = gimp_menu_get_property;
+ object_class->set_property = gimp_menu_set_property;
+
+ g_object_class_install_property (object_class, PROP_GIMP,
+ g_param_spec_object ("gimp",
+ NULL, NULL,
+ GIMP_TYPE_GIMP,
+ GIMP_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class, PROP_MODEL,
+ g_param_spec_object ("model",
+ NULL, NULL,
+ G_TYPE_MENU_MODEL,
+ GIMP_PARAM_READWRITE));
+}
+
+static void
+gimp_menu_init (GimpMenu *menu)
+{
+ menu->priv = gimp_menu_get_instance_private (menu);
+}
+
+static void
+gimp_menu_dispose (GObject *object)
+{
+ GimpMenu *menu = GIMP_MENU (object);
+
+ g_clear_object (&menu->priv->model);
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gimp_menu_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GimpMenu *menu = GIMP_MENU (object);
+
+ switch (property_id)
+ {
+ case PROP_MODEL:
+ menu->priv->model = g_value_dup_object (value);
+ gimp_menu_update (menu, NULL, NULL);
+ break;
+ case PROP_GIMP:
+ menu->priv->gimp = g_value_get_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gimp_menu_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GimpMenu *menu = GIMP_MENU (object);
+
+ switch (property_id)
+ {
+ case PROP_MODEL:
+ g_value_set_object (value, menu->priv->model);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+/* Public functions */
+
+GtkWidget *
+gimp_menu_new (GMenuModel *model,
+ Gimp *gimp)
+{
+ g_return_val_if_fail (GIMP_IS_GIMP (gimp) && G_IS_MENU_MODEL (model), NULL);
+
+ return g_object_new (GIMP_TYPE_MENU,
+ "model", model,
+ "gimp", gimp,
+ NULL);
+}
+
+
+/* Private functions */
+
+static void
+gimp_menu_update (GimpMenu *menu,
+ GtkContainer *container,
+ GMenuModel *model)
+{
+ static GtkRadioMenuItem *group = NULL;
+ gint n_items;
+
+ if (container == NULL)
+ {
+ container = GTK_CONTAINER (menu);
+ model = menu->priv->model;
+ }
+
+ n_items = g_menu_model_get_n_items (model);
+ for (gint i = 0; i < n_items; i++)
+ {
+ GMenuModel *subsection;
+ GMenuModel *submenu;
+ GtkWidget *item;
+ gchar *label = NULL;
+ gchar *action_name = NULL;
+
+ subsection = g_menu_model_get_item_link (model, i, G_MENU_LINK_SECTION);
+ submenu = g_menu_model_get_item_link (model, i, G_MENU_LINK_SUBMENU);
+ g_menu_model_get_item_attribute (model, i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
+ g_menu_model_get_item_attribute (model, i, G_MENU_ATTRIBUTE_ACTION, "s", &action_name);
+
+ if (subsection != NULL)
+ {
+ group = NULL;
+
+ item = gtk_separator_menu_item_new ();
+ gtk_container_add (container, item);
+ gtk_widget_show (item);
+
+ gimp_menu_update (menu, container, subsection);
+
+ item = gtk_separator_menu_item_new ();
+ gtk_container_add (container, item);
+ gtk_widget_show (item);
+ }
+ else if (submenu != NULL)
+ {
+ GtkWidget *subcontainer;
+
+ group = NULL;
+
+ item = gtk_menu_item_new_with_mnemonic (label);
+ gtk_container_add (container, item);
+ gtk_widget_show (item);
+
+ subcontainer = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), subcontainer);
+ gtk_widget_show (subcontainer);
+
+ gimp_menu_update (menu, GTK_CONTAINER (subcontainer), submenu);
+ }
+ else
+ {
+ GAction *action;
+ const gchar *action_label;
+
+ g_return_if_fail (g_str_has_prefix (action_name, "app."));
+ action = g_action_map_lookup_action (G_ACTION_MAP (menu->priv->gimp->app), action_name + 4);
+ g_return_if_fail (GIMP_IS_ACTION (action));
+
+ action_label = gimp_action_get_label (GIMP_ACTION (action));
+ g_return_if_fail (action_label != NULL);
+
+ if (GIMP_IS_TOGGLE_ACTION (action))
+ {
+ item = gtk_check_menu_item_new_with_mnemonic (action_label);
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item),
+ gimp_toggle_action_get_active (GIMP_TOGGLE_ACTION (action)));
+ group = NULL;
+ }
+ else if (GIMP_IS_RADIO_ACTION (action))
+ {
+ item = gtk_radio_menu_item_new_with_mnemonic_from_widget (group, action_label);
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item),
+ /* TODO: see comment in gimp_menu_radio_item_toggled(). */
+ gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action)));
+ group = GTK_RADIO_MENU_ITEM (item);
+ g_signal_connect (item, "toggled",
+ G_CALLBACK (gimp_menu_radio_item_toggled),
+ action);
+ }
+ else
+ {
+ item = gtk_menu_item_new_with_mnemonic (action_label);
+ group = NULL;
+ }
+
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (item), action_name);
+
+ if (gimp_action_get_tooltip (GIMP_ACTION (action)))
+ gtk_widget_set_tooltip_text (item,
+ gimp_action_get_tooltip (GIMP_ACTION (action)));
+
+ gtk_container_add (container, item);
+ gtk_widget_show (item);
+ }
+ g_free (label);
+ g_free (action_name);
+ }
+}
+
+static void
+gimp_menu_radio_item_toggled (GtkWidget *item,
+ GAction *action)
+{
+ gboolean active = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item));
+
+ /* TODO: when we remove GtkAction dependency, GimpRadioAction should become a
+ * child of GimpToggleAction, and therefore, we'll be able to use
+ * gimp_toggle_action_set_active().
+ */
+ gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), active);
+}
diff --git a/app/widgets/gimpmenu.h b/app/widgets/gimpmenu.h
new file mode 100644
index 0000000000..fdd246a84e
--- /dev/null
+++ b/app/widgets/gimpmenu.h
@@ -0,0 +1,55 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpmenu.h
+ * Copyright (C) 2022 Jehan
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef __GIMP_MENU_H__
+#define __GIMP_MENU_H__
+
+
+#define GIMP_TYPE_MENU (gimp_menu_get_type ())
+#define GIMP_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_MENU, GimpMenu))
+#define GIMP_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_MENU, GimpMenuClass))
+#define GIMP_IS_MENU(obj) (G_TYPE_CHECK_INSTANCE_TYPE (obj, GIMP_TYPE_MENU))
+#define GIMP_IS_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_MENU))
+#define GIMP_MENU_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_MENU, GimpMenuClass))
+
+
+typedef struct _GimpMenuPrivate GimpMenuPrivate;
+typedef struct _GimpMenuClass GimpMenuClass;
+
+struct _GimpMenu
+{
+ GtkMenuBar parent_instance;
+
+ GimpMenuPrivate *priv;
+};
+
+struct _GimpMenuClass
+{
+ GtkMenuBarClass parent_class;
+};
+
+
+GType gimp_menu_get_type (void) G_GNUC_CONST;
+
+GtkWidget * gimp_menu_new (GMenuModel *model,
+ Gimp *gimp);
+
+
+#endif /* __GIMP_MENU_H__ */
diff --git a/app/widgets/meson.build b/app/widgets/meson.build
index b8071376d2..1717069d65 100644
--- a/app/widgets/meson.build
+++ b/app/widgets/meson.build
@@ -137,6 +137,7 @@ libappwidgets_sources = [
'gimplayermodebox.c',
'gimplayermodecombobox.c',
'gimplayertreeview.c',
+ 'gimpmenu.c',
'gimpmenudock.c',
'gimpmenufactory.c',
'gimpmessagebox.c',
diff --git a/app/widgets/widgets-types.h b/app/widgets/widgets-types.h
index 18e28185da..5f4ede2d67 100644
--- a/app/widgets/widgets-types.h
+++ b/app/widgets/widgets-types.h
@@ -210,6 +210,7 @@ typedef struct _GimpLanguageStore GimpLanguageStore;
typedef struct _GimpLayerModeBox GimpLayerModeBox;
typedef struct _GimpLayerModeComboBox GimpLayerModeComboBox;
typedef struct _GimpMessageBox GimpMessageBox;
+typedef struct _GimpMenu GimpMenu;
typedef struct _GimpMeter GimpMeter;
typedef struct _GimpModifiersEditor GimpModifiersEditor;
typedef struct _GimpOverlayBox GimpOverlayBox;
diff --git a/menus/image-menu.ui b/menus/image-menu.ui
new file mode 100644
index 0000000000..d585856338
--- /dev/null
+++ b/menus/image-menu.ui
@@ -0,0 +1,872 @@
+
+
+
+
+
diff --git a/menus/meson.build b/menus/meson.build
index e7147a2d33..80b839dea0 100644
--- a/menus/meson.build
+++ b/menus/meson.build
@@ -39,6 +39,14 @@ install_data(menus_files,
install_dir: menus_dir,
)
+ui_menus_files = files(
+ 'image-menu.ui',
+)
+
+install_data(ui_menus_files,
+ install_dir: menus_dir,
+)
+
unstable_menus_args = stable ? [] : [ '--stringparam', 'unstable-menus', 'yes' ]
menus_built_files = []
@@ -74,4 +82,20 @@ if xmllint.found()
build_by_default: true,
install: false
)
+
+ # XXX: no DTD validation as GtkBuilder UI format does not have a DTD (as far as
+ # we could find).
+ custom_target('validate_ui_menus',
+ command: [
+ xmllint,
+ '--output', '@OUTPUT@',
+ '--path', meson.current_source_dir(),
+ ui_menus_files, menus_built_files
+ ],
+ # The output file is only useful as a flag file, so that the command
+ # knows if it has been run already.
+ output: [ 'validate_ui_menus-output.xml' ],
+ build_by_default: true,
+ install: false
+ )
endif