[libdazzle] menu-button: add menu button helper



commit 0bc4d14332f22c88358bab427d25096934373d73
Author: Christian Hergert <chergert redhat com>
Date:   Sat Jul 8 19:22:13 2017 -0700

    menu-button: add menu button helper
    
    This is meant to be a base menu button class that we can use
    for a few different situations in Builder. We want to be able
    to replace the perspectives icon, the layoutstack icon (with
    different theming), the run menu, and the gear menu with this.
    
    It should handle the various styling we need as well as custom
    GMenu information we support. It also needs to auto-update the
    accelerator when that changes from keytheme changes.
    
    This handles some of that, but continues to need iteration.

 data/themes/shared.css               |    1 +
 data/themes/shared/shared-menus.css  |   25 ++
 examples/app/example-window.c        |    8 -
 examples/app/example-window.ui       |   23 +-
 examples/app/gtk/menus.ui            |    4 +
 src/dazzle.gresources.xml            |    3 +
 src/dazzle.h                         |    3 +
 src/menus/dzl-menu-button-item.c     |  264 +++++++++++++++
 src/menus/dzl-menu-button-item.h     |   32 ++
 src/menus/dzl-menu-button-section.c  |  278 ++++++++++++++++
 src/menus/dzl-menu-button-section.h  |   35 ++
 src/menus/dzl-menu-button-section.ui |   20 ++
 src/menus/dzl-menu-button.c          |  588 ++++++++++++++++++++++++++++++++++
 src/menus/dzl-menu-button.h          |   52 +++
 src/menus/dzl-menu-button.ui         |   35 ++
 src/meson.build                      |    6 +
 16 files changed, 1361 insertions(+), 16 deletions(-)
---
diff --git a/data/themes/shared.css b/data/themes/shared.css
index 45f0fcb..91a2a32 100644
--- a/data/themes/shared.css
+++ b/data/themes/shared.css
@@ -1,5 +1,6 @@
 /* Theme agnostic or base-layer CSS styling */
 @import url("resource:///org/gnome/dazzle/themes/shared/shared-graphs.css");
+@import url("resource:///org/gnome/dazzle/themes/shared/shared-menus.css");
 @import url("resource:///org/gnome/dazzle/themes/shared/shared-panels.css");
 @import url("resource:///org/gnome/dazzle/themes/shared/shared-pathbar.css");
 @import url("resource:///org/gnome/dazzle/themes/shared/shared-pillbox.css");
diff --git a/data/themes/shared/shared-menus.css b/data/themes/shared/shared-menus.css
new file mode 100644
index 0000000..0817614
--- /dev/null
+++ b/data/themes/shared/shared-menus.css
@@ -0,0 +1,25 @@
+dzlmenubuttonsection {
+  margin: 18px 15px 15px 12px;
+}
+dzlmenubuttonsection > label.title {
+  font-weight: bold;
+  font-size: 0.8333em;
+  opacity: 0.5;
+  margin-bottom: 6px;
+}
+dzlmenubuttonsection box.vertical checkbutton.dzlmenubuttonitem,
+dzlmenubuttonsection box.vertical button.dzlmenubuttonitem {
+  border: none;
+  background: none;
+  box-shadow: none;
+  padding: 3px;
+}
+dzlmenubuttonsection box.vertical checkbutton.dzlmenubuttonitem:hover,
+dzlmenubuttonsection box.vertical button.dzlmenubuttonitem:hover {
+  background-color: alpha(@theme_fg_color, 0.05);
+}
+dzlmenubuttonsection box.vertical checkbutton.dzlmenubuttonitem > box > image:first-child,
+dzlmenubuttonsection box.vertical button.dzlmenubuttonitem > box > image:first-child {
+  margin: 3px 6px 3px 0px;
+  color: @theme_fg_color;
+}
diff --git a/examples/app/example-window.c b/examples/app/example-window.c
index aaec2e2..7c9a73e 100644
--- a/examples/app/example-window.c
+++ b/examples/app/example-window.c
@@ -12,7 +12,6 @@ struct _ExampleWindow
   GtkNotebook *notebook;
   DzlEmptyState *empty_state;
   GtkStack *stack;
-  GtkPopover *gear_popover;
 };
 
 G_DEFINE_TYPE (ExampleWindow, example_window, DZL_TYPE_APPLICATION_WINDOW)
@@ -153,7 +152,6 @@ example_window_class_init (ExampleWindowClass *klass)
   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/example/ui/example-window.ui");
   gtk_widget_class_bind_template_child (widget_class, ExampleWindow, dockbin);
   gtk_widget_class_bind_template_child (widget_class, ExampleWindow, empty_state);
-  gtk_widget_class_bind_template_child (widget_class, ExampleWindow, gear_popover);
   gtk_widget_class_bind_template_child (widget_class, ExampleWindow, header_bar);
   gtk_widget_class_bind_template_child (widget_class, ExampleWindow, notebook);
   gtk_widget_class_bind_template_child (widget_class, ExampleWindow, stack);
@@ -165,8 +163,6 @@ example_window_init (ExampleWindow *self)
   DzlShortcutController *controller;
   g_autoptr(GPropertyAction) left = NULL;
   g_autoptr(GPropertyAction) right = NULL;
-  DzlApplication *app;
-  GMenu *menu;
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
@@ -195,10 +191,6 @@ example_window_init (ExampleWindow *self)
 
   g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (left));
   g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (right));
-
-  app = DZL_APPLICATION (g_application_get_default ());
-  menu = dzl_application_get_menu_by_id (app, "gear-menu");
-  gtk_popover_bind_model (self->gear_popover, G_MENU_MODEL (menu), NULL);
 }
 
 GtkWidget *
diff --git a/examples/app/example-window.ui b/examples/app/example-window.ui
index 5e11289..74a33dd 100644
--- a/examples/app/example-window.ui
+++ b/examples/app/example-window.ui
@@ -21,6 +21,16 @@
           </packing>
         </child>
         <child>
+          <object class="DzlMenuButton">
+            <property name="icon-name">document-open-symbolic</property>
+            <property name="show-arrow">true</property>
+            <property name="show-icons">true</property>
+            <property name="show-accels">true</property>
+            <property name="menu-id">app-menu</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
           <object class="GtkBox">
             <property name="visible">true</property>
             <style>
@@ -56,15 +66,12 @@
           </packing>
         </child>
         <child>
-          <object class="GtkMenuButton" id="gear_menu">
+          <object class="DzlMenuButton" id="gear_menu">
+            <property name="menu-id">gear-menu</property>
             <property name="visible">true</property>
-            <property name="popover">gear_popover</property>
-            <child>
-              <object class="GtkImage">
-                <property name="icon-name">open-menu-symbolic</property>
-                <property name="visible">true</property>
-              </object>
-            </child>
+            <property name="icon-name">open-menu-symbolic</property>
+            <property name="show-arrow">false</property>
+            <property name="show-icons">false</property>
           </object>
           <packing>
             <property name="position">0</property>
diff --git a/examples/app/gtk/menus.ui b/examples/app/gtk/menus.ui
index 009d9a7..573de44 100644
--- a/examples/app/gtk/menus.ui
+++ b/examples/app/gtk/menus.ui
@@ -3,7 +3,10 @@
   <menu id="app-menu">
     <section id="app-menu-help-section">
       <attribute name="id">help-section</attribute>
+      <attribute name="label" translatable="yes">Help</attribute>
       <item>
+        <!-- Dazzle supports the special attribute "verb-icon-name" -->
+        <attribute name="verb-icon-name">preferences-desktop-keyboard-shortcuts-symbolic</attribute>
         <attribute name="label" translatable="yes">Keyboard _Shortcuts</attribute>
         <attribute name="action">app.shortcuts</attribute>
       </item>
@@ -21,6 +24,7 @@
     <section id="gear-menu-fullscreen">
       <item>
         <attribute name="label" translatable="yes">_Fullscreen</attribute>
+        <attribute name="verb-icon-name">view-fullscreen-symbolic</attribute>
         <attribute name="action">win.fullscreen</attribute>
       </item>
     </section>
diff --git a/src/dazzle.gresources.xml b/src/dazzle.gresources.xml
index 5c152cb..a5061d4 100644
--- a/src/dazzle.gresources.xml
+++ b/src/dazzle.gresources.xml
@@ -4,6 +4,8 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-counters-window.ui">widgets/dzl-counters-window.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-simple-popover.ui">widgets/dzl-simple-popover.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-empty-state.ui">widgets/dzl-empty-state.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-menu-button.ui">menus/dzl-menu-button.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-menu-button-section.ui">menus/dzl-menu-button-section.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-pill-box.ui">widgets/dzl-pill-box.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-file-chooser-button.ui">prefs/dzl-preferences-file-chooser-button.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-entry.ui">prefs/dzl-preferences-entry.ui</file>
@@ -51,6 +53,7 @@
     <!-- Default and Fallback Theme -->
     <file compressed="true" alias="shared.css">../data/themes/shared.css</file>
     <file compressed="true" alias="shared/shared-graphs.css">../data/themes/shared/shared-graphs.css</file>
+    <file compressed="true" alias="shared/shared-menus.css">../data/themes/shared/shared-menus.css</file>
     <file compressed="true" alias="shared/shared-panels.css">../data/themes/shared/shared-panels.css</file>
     <file compressed="true" alias="shared/shared-pathbar.css">../data/themes/shared/shared-pathbar.css</file>
     <file compressed="true" alias="shared/shared-pillbox.css">../data/themes/shared/shared-pillbox.css</file>
diff --git a/src/dazzle.h b/src/dazzle.h
index 5020e7a..0516cc1 100644
--- a/src/dazzle.h
+++ b/src/dazzle.h
@@ -54,6 +54,9 @@ G_BEGIN_DECLS
 #include "graphing/dzl-graph-renderer.h"
 #include "graphing/dzl-graph-view.h"
 #include "menus/dzl-joined-menu.h"
+#include "menus/dzl-menu-button.h"
+#include "menus/dzl-menu-button-item.h"
+#include "menus/dzl-menu-button-section.h"
 #include "menus/dzl-menu-manager.h"
 #include "panel/dzl-dock-bin-edge.h"
 #include "panel/dzl-dock-bin.h"
diff --git a/src/menus/dzl-menu-button-item.c b/src/menus/dzl-menu-button-item.c
new file mode 100644
index 0000000..fdefca1
--- /dev/null
+++ b/src/menus/dzl-menu-button-item.c
@@ -0,0 +1,264 @@
+/* dzl-menu-button-item.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "dzl-menu-button-item"
+
+#include "menus/dzl-menu-button-item.h"
+#include "shortcuts/dzl-shortcut-label.h"
+#include "util/dzl-gtk.h"
+
+struct _DzlMenuButtonItem
+{
+  GtkCheckButton    parent_instance;
+
+  const gchar      *action_name;
+
+  /* Template references */
+  GtkLabel         *text;
+  DzlShortcutLabel *accel;
+  GtkImage         *image;
+
+  guint             has_icon : 1;
+  guint             show_image : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_ACCEL,
+  PROP_ICON_NAME,
+  PROP_SHOW_ACCEL,
+  PROP_SHOW_IMAGE,
+  PROP_TEXT,
+  N_PROPS,
+};
+
+G_DEFINE_TYPE (DzlMenuButtonItem, dzl_menu_button_item, GTK_TYPE_CHECK_BUTTON)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+dzl_menu_button_item_clicked (DzlMenuButtonItem *self)
+{
+  GtkWidget *popover;
+
+  g_assert (DZL_IS_MENU_BUTTON_ITEM (self));
+
+  popover = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_POPOVER);
+  if (popover != NULL)
+    gtk_popover_popdown (GTK_POPOVER (popover));
+}
+
+static gboolean
+action_is_stateful (GtkWidget   *widget,
+                    const gchar *group,
+                    const gchar *name)
+{
+  GActionGroup *actions = gtk_widget_get_action_group (widget, group);
+  GtkWidget *parent;
+
+  if (actions != NULL)
+    {
+      if (g_action_group_has_action (actions, name) &&
+          g_action_group_get_action_state_type (actions, name) != NULL)
+        return TRUE;
+    }
+
+  if (GTK_IS_POPOVER (widget))
+    parent = gtk_popover_get_relative_to (GTK_POPOVER (widget));
+  else
+    parent = gtk_widget_get_parent (widget);
+
+  if (parent)
+    return action_is_stateful (parent, group, name);
+
+  return FALSE;
+}
+
+static void
+dzl_menu_button_item_notify_action_name (DzlMenuButtonItem *self,
+                                         GParamSpec        *pspec)
+{
+  const gchar *action_name;
+  g_auto(GStrv) parts = NULL;
+  gboolean draw = FALSE;
+
+  g_assert (DZL_IS_MENU_BUTTON_ITEM (self));
+
+  action_name = gtk_actionable_get_action_name (GTK_ACTIONABLE (self));
+
+  if (action_name)
+    parts = g_strsplit (action_name, ".", 2);
+
+  if (parts[0] && parts[1])
+    draw = action_is_stateful (GTK_WIDGET (self), parts[0], parts[1]);
+
+  g_object_set (self, "draw-indicator", draw, NULL);
+}
+
+static void
+dzl_menu_button_item_hierarchy_changed (GtkWidget *widget,
+                                        GtkWidget *old_toplevel)
+{
+  dzl_menu_button_item_notify_action_name (DZL_MENU_BUTTON_ITEM (widget), NULL);
+}
+
+static void
+dzl_menu_button_item_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  DzlMenuButtonItem *self = DZL_MENU_BUTTON_ITEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCEL:
+      dzl_shortcut_label_set_accelerator (self->accel, g_value_get_string (value));
+      break;
+
+    case PROP_ICON_NAME:
+      self->has_icon = !!g_value_get_string (value);
+      g_object_set_property (G_OBJECT (self->image), "icon-name", value);
+      gtk_widget_set_visible (GTK_WIDGET (self->image), self->has_icon && self->show_image);
+      break;
+
+    case PROP_SHOW_ACCEL:
+      gtk_widget_set_visible (GTK_WIDGET (self->accel), g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_IMAGE:
+      self->show_image = g_value_get_boolean (value);
+      gtk_widget_set_visible (GTK_WIDGET (self->image), self->has_icon && self->show_image);
+      break;
+
+    case PROP_TEXT:
+      gtk_label_set_label (self->text, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_menu_button_item_class_init (DzlMenuButtonItemClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->set_property = dzl_menu_button_item_set_property;
+
+  widget_class->hierarchy_changed = dzl_menu_button_item_hierarchy_changed;
+
+  properties [PROP_ACCEL] =
+    g_param_spec_string ("accel",
+                         "Accel",
+                         "The accelerator for the item",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon to display with the item",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_ACCEL] =
+    g_param_spec_boolean ("show-accel",
+                          "Show Accel",
+                          "If the accel label should be shown",
+                          FALSE,
+                          (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_IMAGE] =
+    g_param_spec_boolean ("show-image",
+                          "Show Image",
+                          "If the image should be shown",
+                          FALSE,
+                          (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TEXT] =
+    g_param_spec_string ("text",
+                         "Text",
+                         "The text for the menu item",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dzl_menu_button_item_init (DzlMenuButtonItem *self)
+{
+  GtkTextDirection dir;
+  GtkBox *box;
+
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "dzlmenubuttonitem");
+
+  g_signal_connect (self,
+                    "clicked",
+                    G_CALLBACK (dzl_menu_button_item_clicked),
+                    NULL);
+
+  g_signal_connect (self,
+                    "notify::action-name",
+                    G_CALLBACK (dzl_menu_button_item_notify_action_name),
+                    NULL);
+
+  /* flip the location of the checkbutton */
+  dir = gtk_widget_get_direction (GTK_WIDGET (self));
+  if (dir != GTK_TEXT_DIR_LTR)
+    dir = GTK_TEXT_DIR_LTR;
+  else
+    dir = GTK_TEXT_DIR_RTL;
+  gtk_widget_set_direction (GTK_WIDGET (self), dir);
+
+  g_object_set (self, "draw-indicator", FALSE, NULL);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (box));
+
+  self->image = g_object_new (GTK_TYPE_IMAGE,
+                              "hexpand", FALSE,
+                              NULL);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->image),
+                                     "pack-type", GTK_PACK_START,
+                                     "position", 0,
+                                     NULL);
+
+  self->text = g_object_new (GTK_TYPE_LABEL,
+                             "hexpand", TRUE,
+                             "use-underline", TRUE,
+                             "xalign", 0.0f,
+                             "visible", TRUE,
+                             NULL);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->text),
+                                     "pack-type", GTK_PACK_START,
+                                     "position", 1,
+                                     NULL);
+
+  self->accel = g_object_new (DZL_TYPE_SHORTCUT_LABEL,
+                              "hexpand", FALSE,
+                              NULL);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->accel),
+                                     "pack-type", GTK_PACK_END,
+                                     NULL);
+}
diff --git a/src/menus/dzl-menu-button-item.h b/src/menus/dzl-menu-button-item.h
new file mode 100644
index 0000000..2f3ad47
--- /dev/null
+++ b/src/menus/dzl-menu-button-item.h
@@ -0,0 +1,32 @@
+/* dzl-menu-button-item.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_MENU_BUTTON_ITEM_H
+#define DZL_MENU_BUTTON_ITEM_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_MENU_BUTTON_ITEM (dzl_menu_button_item_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlMenuButtonItem, dzl_menu_button_item, DZL, MENU_BUTTON_ITEM, GtkCheckButton)
+
+G_END_DECLS
+
+#endif /* DZL_MENU_BUTTON_ITEM_H */
diff --git a/src/menus/dzl-menu-button-section.c b/src/menus/dzl-menu-button-section.c
new file mode 100644
index 0000000..ab612a3
--- /dev/null
+++ b/src/menus/dzl-menu-button-section.c
@@ -0,0 +1,278 @@
+/* dzl-menu-button-section.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "dzl-menu-button-section"
+
+#include "bindings/dzl-signal-group.h"
+#include "menus/dzl-menu-button-section.h"
+#include "menus/dzl-menu-button-item.h"
+
+struct _DzlMenuButtonSection
+{
+  GtkBox          parent_instance;
+
+  /* Owned references */
+  DzlSignalGroup *menu_signals;
+
+  /* Template references */
+  GtkLabel       *label;
+  GtkBox         *items_box;
+};
+
+enum {
+  PROP_0,
+  PROP_LABEL,
+  PROP_MODEL,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (DzlMenuButtonSection, dzl_menu_button_section, GTK_TYPE_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+static GtkWidget *
+get_nth_child (DzlMenuButtonSection *self,
+               guint                 nth)
+{
+  GList *children = gtk_container_get_children (GTK_CONTAINER (self->items_box));
+  GtkWidget *ret = g_list_nth_data (children, nth);
+  g_list_free (children);
+  return ret;
+}
+
+static void
+update_positions (DzlMenuButtonSection *self)
+{
+  GList *children = gtk_container_get_children (GTK_CONTAINER (self->items_box));
+  guint i = 0;
+
+  for (const GList *iter = children; iter; iter = iter->next)
+    {
+      GtkWidget *widget = iter->data;
+
+      gtk_container_child_set (GTK_CONTAINER (self->items_box), widget,
+                               "position", i++,
+                               NULL);
+    }
+
+  g_list_free (children);
+}
+
+static void
+dzl_menu_button_section_items_changed (DzlMenuButtonSection *self,
+                                       guint                 position,
+                                       guint                 removed,
+                                       guint                 added,
+                                       GMenuModel           *menu)
+{
+  g_assert (DZL_IS_MENU_BUTTON_SECTION (self));
+  g_assert (G_IS_MENU_MODEL (menu));
+
+  for (guint i = 0; i < removed; i++)
+    {
+      GtkWidget *widget = get_nth_child (self, i);
+      gtk_widget_destroy (widget);
+    }
+
+  update_positions (self);
+
+  for (guint i = position; i < position + added; i++)
+    {
+      DzlMenuButtonItem *item;
+      g_autofree gchar *accel = NULL;
+      g_autofree gchar *action = NULL;
+      g_autofree gchar *label = NULL;
+      g_autofree gchar *verb_icon_name = NULL;
+
+      g_menu_model_get_item_attribute (menu, i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
+      g_menu_model_get_item_attribute (menu, i, "verb-icon-name", "s", &verb_icon_name);
+      g_menu_model_get_item_attribute (menu, i, "accel", "s", &accel);
+      g_menu_model_get_item_attribute (menu, i, "action", "s", &action);
+
+      item = g_object_new (DZL_TYPE_MENU_BUTTON_ITEM,
+                           "action-name", action,
+                           "show-image", TRUE,
+                           "show-accel", TRUE,
+                           "icon-name", verb_icon_name,
+                           "text", label,
+                           "accel", accel,
+                           "visible", TRUE,
+                           NULL);
+      gtk_container_add_with_properties (GTK_CONTAINER (self->items_box), GTK_WIDGET (item),
+                                         "position", i,
+                                         NULL);
+    }
+}
+
+static void
+dzl_menu_button_section_bind (DzlMenuButtonSection *self,
+                              GMenuModel           *menu,
+                              DzlSignalGroup       *menu_signals)
+{
+  guint n_items;
+
+  g_assert (DZL_IS_MENU_BUTTON_SECTION (self));
+  g_assert (G_IS_MENU_MODEL (menu));
+  g_assert (DZL_IS_SIGNAL_GROUP (menu_signals));
+
+  n_items = g_menu_model_get_n_items (menu);
+  dzl_menu_button_section_items_changed (self, 0, 0, n_items, menu);
+}
+
+static void
+dzl_menu_button_section_unbind (DzlMenuButtonSection *self,
+                                DzlSignalGroup       *menu_signals)
+{
+  g_assert (DZL_IS_MENU_BUTTON_SECTION (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (menu_signals));
+
+  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
+    return;
+
+  gtk_container_foreach (GTK_CONTAINER (self->items_box),
+                         (GtkCallback) gtk_widget_destroy,
+                         NULL);
+}
+
+static void
+dzl_menu_button_section_destroy (GtkWidget *widget)
+{
+  DzlMenuButtonSection *self = (DzlMenuButtonSection *)widget;
+
+  g_clear_object (&self->menu_signals);
+
+  GTK_WIDGET_CLASS (dzl_menu_button_section_parent_class)->destroy (widget);
+}
+
+static void
+dzl_menu_button_section_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  DzlMenuButtonSection *self = DZL_MENU_BUTTON_SECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, dzl_signal_group_get_target (self->menu_signals));
+      break;
+
+    case PROP_LABEL:
+      g_value_set_string (value, gtk_label_get_label (self->label));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_menu_button_section_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  DzlMenuButtonSection *self = DZL_MENU_BUTTON_SECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      dzl_signal_group_set_target (self->menu_signals, g_value_get_object (value));
+      break;
+
+    case PROP_LABEL:
+      gtk_label_set_label (self->label, g_value_get_string (value));
+      gtk_widget_set_visible (GTK_WIDGET (self->label), !!g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_menu_button_section_class_init (DzlMenuButtonSectionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = dzl_menu_button_section_get_property;
+  object_class->set_property = dzl_menu_button_section_set_property;
+
+  widget_class->destroy = dzl_menu_button_section_destroy;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model", NULL, NULL,
+                         G_TYPE_MENU_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LABEL] =
+    g_param_spec_string ("label", NULL, NULL, NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "dzlmenubuttonsection");
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/dazzle/ui/dzl-menu-button-section.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlMenuButtonSection, label);
+  gtk_widget_class_bind_template_child (widget_class, DzlMenuButtonSection, items_box);
+}
+
+static void
+dzl_menu_button_section_init (DzlMenuButtonSection *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->menu_signals = dzl_signal_group_new (G_TYPE_MENU_MODEL);
+
+  g_signal_connect_swapped (self->menu_signals,
+                            "bind",
+                            G_CALLBACK (dzl_menu_button_section_bind),
+                            self);
+
+  g_signal_connect_swapped (self->menu_signals,
+                            "unbind",
+                            G_CALLBACK (dzl_menu_button_section_unbind),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->menu_signals,
+                                    "items-changed",
+                                    G_CALLBACK (dzl_menu_button_section_items_changed),
+                                    self);
+}
+
+/**
+ * dzl_menu_button_section_new:
+ *
+ * Creates a new #DzlMenuButtonSection.
+ *
+ * Returns: (transfer full): A #DzlMenuButtonSection
+ *
+ * Since: 3.26
+ */
+GtkWidget *
+dzl_menu_button_section_new (GMenuModel  *model,
+                             const gchar *label)
+{
+  return g_object_new (DZL_TYPE_MENU_BUTTON_SECTION,
+                       "model", model,
+                       "label", label,
+                       NULL);
+}
diff --git a/src/menus/dzl-menu-button-section.h b/src/menus/dzl-menu-button-section.h
new file mode 100644
index 0000000..0482ad8
--- /dev/null
+++ b/src/menus/dzl-menu-button-section.h
@@ -0,0 +1,35 @@
+/* dzl-menu-button-section.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_MENU_BUTTON_SECTION_H
+#define DZL_MENU_BUTTON_SECTION_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_MENU_BUTTON_SECTION (dzl_menu_button_section_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlMenuButtonSection, dzl_menu_button_section, DZL, MENU_BUTTON_SECTION, GtkBox)
+
+GtkWidget *dzl_menu_button_section_new (GMenuModel  *model,
+                                        const gchar *label);
+
+G_END_DECLS
+
+#endif /* DZL_MENU_BUTTON_SECTION_H */
diff --git a/src/menus/dzl-menu-button-section.ui b/src/menus/dzl-menu-button-section.ui
new file mode 100644
index 0000000..74b5577
--- /dev/null
+++ b/src/menus/dzl-menu-button-section.ui
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="DzlMenuButtonSection" parent="GtkBox">
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="GtkLabel" id="label">
+        <property name="xalign">0.5</property>
+        <style>
+          <class name="title"/>
+        </style>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox" id="items_box">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/menus/dzl-menu-button.c b/src/menus/dzl-menu-button.c
new file mode 100644
index 0000000..61a8c28
--- /dev/null
+++ b/src/menus/dzl-menu-button.c
@@ -0,0 +1,588 @@
+/* dzl-menu-button.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "dzl-menu-button"
+
+#include "app/dzl-application.h"
+#include "bindings/dzl-signal-group.h"
+#include "menus/dzl-menu-button.h"
+#include "menus/dzl-menu-button-section.h"
+#include "menus/dzl-menu-button-item.h"
+#include "util/dzl-gtk.h"
+
+typedef struct
+{
+  /* Owned references */
+  DzlSignalGroup *menu_signals;
+
+  /* Template references */
+  GtkPopover     *popover;
+  GtkImage       *image;
+  GtkImage       *pan_down_image;
+  GtkBox         *popover_box;
+
+  guint           show_accels : 1;
+  guint           show_icons : 1;
+} DzlMenuButtonPrivate;
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  PROP_MENU_ID,
+  PROP_ICON_NAME,
+  PROP_SHOW_ACCELS,
+  PROP_SHOW_ARROW,
+  PROP_SHOW_ICONS,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (DzlMenuButton, dzl_menu_button, GTK_TYPE_MENU_BUTTON)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+collect_items_sections (GtkWidget *widget,
+                        GPtrArray *ar)
+{
+  GtkWidget *item;
+
+  item = dzl_gtk_widget_find_child_typed (widget, DZL_TYPE_MENU_BUTTON_ITEM);
+  if (item)
+    g_ptr_array_add (ar, item);
+}
+
+static void
+update_image_and_accels (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+  g_autoptr(GPtrArray) ar = g_ptr_array_new ();
+  gboolean show_image = dzl_menu_button_get_show_icons (self);
+  gboolean show_accel = dzl_menu_button_get_show_accels (self);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->popover_box),
+                         (GtkCallback) collect_items_sections,
+                         ar);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      DzlMenuButtonItem *item = g_ptr_array_index (ar, i);
+
+      g_assert (DZL_IS_MENU_BUTTON_ITEM (item));
+
+      g_object_set (item,
+                    "show-image", show_image,
+                    "show-accel", show_accel,
+                    NULL);
+    }
+}
+
+static GtkWidget *
+get_nth_child (DzlMenuButton *self,
+               guint          nth)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+  GList *children = gtk_container_get_children (GTK_CONTAINER (priv->popover_box));
+  GtkWidget *ret = g_list_nth_data (children, nth);
+  g_list_free (children);
+  return ret;
+}
+
+static void
+update_positions (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+  GList *children = gtk_container_get_children (GTK_CONTAINER (priv->popover_box));
+  guint i = 0;
+
+  for (const GList *iter = children; iter; iter = iter->next)
+    {
+      GtkWidget *widget = iter->data;
+
+      gtk_container_child_set (GTK_CONTAINER (priv->popover_box), widget,
+                               "position", i++,
+                               NULL);
+    }
+
+  g_list_free (children);
+}
+
+static void
+dzl_menu_button_add_linked_model (DzlMenuButton *self,
+                                  guint          position,
+                                  GMenuModel    *model,
+                                  const gchar   *label)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+  DzlMenuButtonSection *section;
+
+  g_assert (DZL_IS_MENU_BUTTON (self));
+  g_assert (G_IS_MENU_MODEL (model));
+
+  section = g_object_new (DZL_TYPE_MENU_BUTTON_SECTION,
+                          "label", label,
+                          "model", model,
+                          "visible", TRUE,
+                          NULL);
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->popover_box), GTK_WIDGET (section),
+                                     "position", position,
+                                     NULL);
+}
+
+static void
+dzl_menu_button_items_changed (DzlMenuButton *self,
+                               guint          position,
+                               guint          removed,
+                               guint          added,
+                               GMenuModel    *menu)
+{
+  g_assert (DZL_IS_MENU_BUTTON (self));
+  g_assert (G_IS_MENU_MODEL (menu));
+
+  for (guint i = 0; i < removed; i++)
+    {
+      GtkWidget *widget = get_nth_child (self, i);
+      gtk_widget_destroy (widget);
+    }
+
+  update_positions (self);
+
+  for (guint i = position; i < position + added; i++)
+    {
+      g_autofree gchar *label = NULL;
+      g_autoptr(GMenuModel) linked_model = NULL;
+
+      /* We only support sections at the top-level */
+      g_menu_model_get_item_attribute (menu, i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
+      linked_model = g_menu_model_get_item_link (menu, i, G_MENU_LINK_SECTION);
+
+      if (linked_model != NULL)
+        dzl_menu_button_add_linked_model (self, position, linked_model, label);
+    }
+
+  update_image_and_accels (self);
+}
+
+static void
+dzl_menu_button_menu_signals_bind (DzlMenuButton  *self,
+                                   GMenuModel     *menu,
+                                   DzlSignalGroup *menu_signals)
+{
+  guint n_items;
+
+  g_assert (DZL_IS_MENU_BUTTON (self));
+  g_assert (G_IS_MENU_MODEL (menu));
+  g_assert (DZL_IS_SIGNAL_GROUP (menu_signals));
+
+  n_items = g_menu_model_get_n_items (menu);
+  dzl_menu_button_items_changed (self, 0, 0, n_items, menu);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE);
+}
+
+static void
+dzl_menu_button_menu_signals_unbind (DzlMenuButton  *self,
+                                     DzlSignalGroup *menu_signals)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_assert (DZL_IS_MENU_BUTTON (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (menu_signals));
+
+  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
+    return;
+
+  gtk_container_foreach (GTK_CONTAINER (priv->popover_box),
+                         (GtkCallback) gtk_widget_destroy,
+                         NULL);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+}
+
+static void
+dzl_menu_button_set_menu_id (DzlMenuButton *self,
+                             const gchar   *menu_id)
+{
+  GApplication *app;
+  GMenu *model = NULL;
+
+  g_return_if_fail (DZL_IS_MENU_BUTTON (self));
+
+  app = g_application_get_default ();
+
+  if (DZL_IS_APPLICATION (app))
+    model = dzl_application_get_menu_by_id (DZL_APPLICATION (app), menu_id);
+  else if (GTK_IS_APPLICATION (app))
+    model = gtk_application_get_menu_by_id (GTK_APPLICATION (app), menu_id);
+
+  dzl_menu_button_set_model (self, G_MENU_MODEL (model));
+}
+
+static void
+dzl_menu_button_destroy (GtkWidget *widget)
+{
+  DzlMenuButton *self = (DzlMenuButton *)widget;
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_clear_object (&priv->menu_signals);
+
+  GTK_WIDGET_CLASS (dzl_menu_button_parent_class)->destroy (widget);
+}
+
+static void
+dzl_menu_button_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  DzlMenuButton *self = DZL_MENU_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, dzl_menu_button_get_model (self));
+      break;
+
+    case PROP_SHOW_ARROW:
+      g_value_set_boolean (value, dzl_menu_button_get_show_arrow (self));
+      break;
+
+    case PROP_SHOW_ACCELS:
+      g_value_set_boolean (value, dzl_menu_button_get_show_accels (self));
+      break;
+
+    case PROP_SHOW_ICONS:
+      g_value_set_boolean (value, dzl_menu_button_get_show_icons (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_menu_button_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  DzlMenuButton *self = DZL_MENU_BUTTON (object);
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      dzl_menu_button_set_model (self, g_value_get_object (value));
+      break;
+
+    case PROP_MENU_ID:
+      dzl_menu_button_set_menu_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON_NAME:
+      g_object_set_property (G_OBJECT (priv->image), "icon-name", value);
+      break;
+
+    case PROP_SHOW_ARROW:
+      dzl_menu_button_set_show_arrow (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_ACCELS:
+      dzl_menu_button_set_show_accels (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_ICONS:
+      dzl_menu_button_set_show_icons (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_menu_button_class_init (DzlMenuButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = dzl_menu_button_get_property;
+  object_class->set_property = dzl_menu_button_set_property;
+
+  widget_class->destroy = dzl_menu_button_destroy;
+
+  /**
+   * DzlMenuButton:menu-id:
+   *
+   * The "menu-id" property can be used to automatically load a
+   * #GMenuModel from the applications merged menus. This is
+   * performed via dzl_application_get_menu_by_id().
+   *
+   * Since: 3.26
+   */
+  properties [PROP_MENU_ID] =
+    g_param_spec_string ("menu-id",
+                         "Menu Id",
+                         "The identifier for the menu model to use",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         "Model",
+                         "The GMenuModel to display in the popover",
+                         G_TYPE_MENU_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon-name for the button",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_ACCELS] =
+    g_param_spec_boolean ("show-accels",
+                          "Show Accels",
+                          "If accelerator keys should be shown",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_ARROW] =
+    g_param_spec_boolean ("show-arrow",
+                          "Show Arrow",
+                          "If the down arrow should be shown",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_ICONS] =
+    g_param_spec_boolean ("show-icons",
+                          "Show Icons",
+                          "If icons should be shown next to menu items",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-menu-button.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, image);
+  gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, pan_down_image);
+  gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, popover);
+  gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, popover_box);
+}
+
+static void
+dzl_menu_button_init (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  priv->menu_signals = dzl_signal_group_new (G_TYPE_MENU_MODEL);
+
+  g_signal_connect_swapped (priv->menu_signals,
+                            "bind",
+                            G_CALLBACK (dzl_menu_button_menu_signals_bind),
+                            self);
+
+  g_signal_connect_swapped (priv->menu_signals,
+                            "unbind",
+                            G_CALLBACK (dzl_menu_button_menu_signals_unbind),
+                            self);
+
+  dzl_signal_group_connect_swapped (priv->menu_signals,
+                                    "items-changed",
+                                    G_CALLBACK (dzl_menu_button_items_changed),
+                                    self);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+}
+
+/**
+ * dzl_menu_button_new_with_model:
+ * @icon_name: An icon-name for the button
+ * @model: (nullable): A #GMenuModel or %NULL
+ *
+ * Creates a new #DzlMenuButton with the icon @icon_name and
+ * the menu contents of @model.
+ *
+ * Returns: (transfer full): A #DzlMenuButton
+ */
+GtkWidget *
+dzl_menu_button_new_with_model (const gchar *icon_name,
+                                GMenuModel  *model)
+{
+  g_return_val_if_fail (!model || G_IS_MENU_MODEL (model), NULL);
+
+  return g_object_new (DZL_TYPE_MENU_BUTTON,
+                       "icon-name", icon_name,
+                       "model", model,
+                       NULL);
+}
+
+gboolean
+dzl_menu_button_get_show_arrow (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), FALSE);
+
+  return gtk_widget_get_visible (GTK_WIDGET (priv->pan_down_image));
+}
+
+/**
+ * dzl_menu_button_set_show_arrow:
+ * @self: a #DzlMenuButton
+ *
+ * Sets the #DzlMenuButton:show-arrow property.
+ *
+ * If %TRUE, an pan-down-symbolic image will be displayed next to the
+ * image in the button.
+ *
+ * Since: 3.26
+ */
+void
+dzl_menu_button_set_show_arrow (DzlMenuButton *self,
+                                gboolean       show_arrow)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+  GtkWidget *child;
+
+  g_return_if_fail (DZL_IS_MENU_BUTTON (self));
+
+  child = gtk_bin_get_child (GTK_BIN (self));
+  g_object_set (child,
+                "margin-start", show_arrow ? 3 : 0,
+                "margin-end", show_arrow ? 3 : 0,
+                NULL);
+  gtk_widget_set_visible (GTK_WIDGET (priv->pan_down_image), show_arrow);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_ARROW]);
+}
+
+gboolean
+dzl_menu_button_get_show_icons (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), FALSE);
+
+  return priv->show_icons;
+}
+
+/**
+ * dzl_menu_button_set_show_icons:
+ * @self: a #DzlMenuButton
+ * @show_icons: if icons should be visible
+ *
+ * Sets the #DzlMenuButton:show-icons property.
+ *
+ * If %TRUE, icons will be displayed next to menu items that
+ * contain a shortcut.
+ *
+ * Since: 3.26
+ */
+void
+dzl_menu_button_set_show_icons (DzlMenuButton *self,
+                                gboolean       show_icons)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_if_fail (DZL_IS_MENU_BUTTON (self));
+
+  show_icons = !!show_icons;
+
+  if (priv->show_icons != show_icons)
+    {
+      priv->show_icons = show_icons;
+      update_image_and_accels (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_ICONS]);
+    }
+}
+
+gboolean
+dzl_menu_button_get_show_accels (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), FALSE);
+
+  return priv->show_accels;
+}
+
+/**
+ * dzl_menu_button_set_show_accels:
+ * @self: a #DzlMenuButton
+ * @show_accels: if accelerators should be visible
+ *
+ * Sets the #DzlMenuButton:show-accels property.
+ *
+ * If %TRUE, accelerators will be displayed next to menu items that
+ * contain a shortcut.
+ *
+ * Since: 3.26
+ */
+void
+dzl_menu_button_set_show_accels (DzlMenuButton *self,
+                                 gboolean       show_accels)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_if_fail (DZL_IS_MENU_BUTTON (self));
+
+  show_accels = !!show_accels;
+
+  if (priv->show_accels != show_accels)
+    {
+      priv->show_accels = show_accels;
+      update_image_and_accels (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_ICONS]);
+    }
+}
+
+void
+dzl_menu_button_set_model (DzlMenuButton *self,
+                           GMenuModel    *model)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_if_fail (DZL_IS_MENU_BUTTON (self));
+  g_return_if_fail (!model || G_IS_MENU_MODEL (model));
+
+  if ((gpointer)model != dzl_signal_group_get_target (priv->menu_signals))
+    {
+      dzl_signal_group_set_target (priv->menu_signals, model);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+    }
+}
+
+/**
+ * dzl_menu_button_get_model:
+ * @self: a #DzlMenuButton
+ *
+ * Returns: (transfer none) (nullable): A #DzlMenuButton or %NULL.
+ *
+ * Since: 3.26
+ */
+GMenuModel *
+dzl_menu_button_get_model (DzlMenuButton *self)
+{
+  DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), NULL);
+
+  return dzl_signal_group_get_target (priv->menu_signals);
+}
diff --git a/src/menus/dzl-menu-button.h b/src/menus/dzl-menu-button.h
new file mode 100644
index 0000000..3a94c96
--- /dev/null
+++ b/src/menus/dzl-menu-button.h
@@ -0,0 +1,52 @@
+/* dzl-menu-button.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_MENU_BUTTON_H
+#define DZL_MENU_BUTTON_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_MENU_BUTTON (dzl_menu_button_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DzlMenuButton, dzl_menu_button, DZL, MENU_BUTTON, GtkMenuButton)
+
+struct _DzlMenuButtonClass
+{
+  GtkMenuButtonClass parent_class;
+};
+
+GtkWidget     *dzl_menu_button_new_with_model  (const gchar   *icon_name,
+                                                GMenuModel    *model);
+GMenuModel    *dzl_menu_button_get_model       (DzlMenuButton *self);
+void           dzl_menu_button_set_model       (DzlMenuButton *self,
+                                                GMenuModel    *model);
+gboolean       dzl_menu_button_get_show_arrow  (DzlMenuButton *self);
+void           dzl_menu_button_set_show_arrow  (DzlMenuButton *self,
+                                                gboolean       show_arrow);
+gboolean       dzl_menu_button_get_show_icons  (DzlMenuButton *self);
+void           dzl_menu_button_set_show_icons  (DzlMenuButton *self,
+                                                gboolean       show_icons);
+gboolean       dzl_menu_button_get_show_accels (DzlMenuButton *self);
+void           dzl_menu_button_set_show_accels (DzlMenuButton *self,
+                                                gboolean       show_accels);
+
+G_END_DECLS
+
+#endif /* DZL_MENU_BUTTON_H */
diff --git a/src/menus/dzl-menu-button.ui b/src/menus/dzl-menu-button.ui
new file mode 100644
index 0000000..5c6ba2d
--- /dev/null
+++ b/src/menus/dzl-menu-button.ui
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkPopover" id="popover">
+    <child>
+      <object class="GtkBox" id="popover_box">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </object>
+  <template class="DzlMenuButton" parent="GtkMenuButton">
+    <property name="popover">popover</property>
+    <child>
+      <object class="GtkBox">
+        <property name="halign">center</property>
+        <property name="orientation">horizontal</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage" id="pan_down_image">
+            <property name="icon-name">pan-down-symbolic</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/meson.build b/src/meson.build
index de3279f..ddb0321 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -70,6 +70,9 @@ libdazzle_public_headers = [
   'graphing/dzl-graph-view.h',
 
   'menus/dzl-joined-menu.h',
+  'menus/dzl-menu-button.h',
+  'menus/dzl-menu-button-item.h',
+  'menus/dzl-menu-button-section.h',
   'menus/dzl-menu-manager.h',
 
   'panel/dzl-dock-bin-edge.h',
@@ -217,6 +220,9 @@ libdazzle_public_sources = [
   'graphing/dzl-graph-view.c',
 
   'menus/dzl-joined-menu.c',
+  'menus/dzl-menu-button.c',
+  'menus/dzl-menu-button-item.c',
+  'menus/dzl-menu-button-section.c',
   'menus/dzl-menu-manager.c',
 
   'panel/dzl-dock-bin-edge.c',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]