aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--debian/changelog32
-rwxr-xr-xexamples/exportmenu.py26
-rw-r--r--examples/unityqmlmenumodel.qml118
-rw-r--r--libqmenumodel/QMenuModel/CMakeLists.txt2
-rw-r--r--libqmenumodel/QMenuModel/plugin.cpp10
-rw-r--r--libqmenumodel/QMenuModel/plugin.h1
-rw-r--r--libqmenumodel/src/CMakeLists.txt19
-rw-r--r--libqmenumodel/src/actionstateparser.cpp34
-rw-r--r--libqmenumodel/src/actionstateparser.h37
-rw-r--r--libqmenumodel/src/converter.cpp132
-rw-r--r--libqmenumodel/src/converter.h4
-rw-r--r--libqmenumodel/src/gtk/config.h0
-rw-r--r--libqmenumodel/src/gtk/gtkactionmuxer.c778
-rw-r--r--libqmenumodel/src/gtk/gtkactionmuxer.h52
-rw-r--r--libqmenumodel/src/gtk/gtkactionobservable.c78
-rw-r--r--libqmenumodel/src/gtk/gtkactionobservable.h60
-rw-r--r--libqmenumodel/src/gtk/gtkactionobserver.c159
-rw-r--r--libqmenumodel/src/gtk/gtkactionobserver.h83
-rw-r--r--libqmenumodel/src/gtk/gtkmenutracker.c495
-rw-r--r--libqmenumodel/src/gtk/gtkmenutracker.h52
-rw-r--r--libqmenumodel/src/gtk/gtkmenutrackeritem.c908
-rw-r--r--libqmenumodel/src/gtk/gtkmenutrackeritem.h103
-rw-r--r--libqmenumodel/src/unitymenumodel.cpp700
-rw-r--r--libqmenumodel/src/unitymenumodel.h78
-rw-r--r--libqmenumodel/src/unitymenumodelevents.cpp63
-rw-r--r--libqmenumodel/src/unitymenumodelevents.h69
-rw-r--r--libqmenumodel/src/unitythemediconprovider.cpp35
-rw-r--r--libqmenumodel/src/unitythemediconprovider.h31
-rw-r--r--tests/client/convertertest.cpp53
30 files changed, 4192 insertions, 22 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f3799b3..95440ca 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,6 +6,8 @@ cmake_minimum_required(VERSION 2.8.9)
include(GNUInstallDirs)
find_package(Qt5Core REQUIRED)
+find_package(Qt5Qml REQUIRED)
+find_package(Qt5Gui REQUIRED)
include(FindPkgConfig)
pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
diff --git a/debian/changelog b/debian/changelog
index d1c3fd1..7c46e30 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,35 @@
+qmenumodel (0.2.7+13.10.20130813-0ubuntu1) saucy; urgency=low
+
+ [ Nick Dedekind ]
+ * Removed UnityMenuAction.
+
+ [ Ubuntu daily release ]
+ * Automatic snapshot from revision 73
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Tue, 13 Aug 2013 10:06:45 +0000
+
+qmenumodel (0.2.7) saucy; urgency=low
+
+ * Releasing version 0.2.7
+
+ -- Nicholas Dedekind <nicholas.dedekind@gmail.com> Tue, 13 Aug 2013 10:11:24 +0100
+
+qmenumodel (0.2.6+13.10.20130812-0ubuntu1) saucy; urgency=low
+
+ [ Nick Dedekind ]
+ * Added UnityMenuModel.
+
+ [ Mirco Müller ]
+ * Added UnityMenuModel.
+
+ [ Lars Uebernickel ]
+ * Added UnityMenuModel.
+
+ [ Ubuntu daily release ]
+ * Automatic snapshot from revision 70
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 12 Aug 2013 18:38:17 +0000
+
qmenumodel (0.2.6daily13.06.07-0ubuntu1) saucy; urgency=low
[ Alberto Mardegan ]
diff --git a/examples/exportmenu.py b/examples/exportmenu.py
index 4be3deb..b7c37b1 100755
--- a/examples/exportmenu.py
+++ b/examples/exportmenu.py
@@ -32,29 +32,14 @@ from gi.repository import GLib
BUS_NAME = 'com.canonical.testmenu'
BUS_OBJECT_PATH = '/com/canonical/testmenu'
-
-if __name__ == '__main__':
- bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
- # Claim well-known bus name and ensure only one instance of self is running
- # at any given time.
- # http://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-names
- proxy = Gio.DBusProxy.new_sync(bus, 0, None,
- 'org.freedesktop.DBus',
- '/org/freedesktop/DBus',
- 'org.freedesktop.DBus', None)
- result = proxy.RequestName('(su)', BUS_NAME, 0x4)
- if result != 1 :
- print >> sys.stderr, ("Name '%s' is already owned on the session bus."
- "Aborting.") % BUS_NAME
- sys.exit(1)
-
+def bus_acquired(bus, name):
menu = Gio.Menu()
foo = Gio.MenuItem.new('foo', 'app.foo')
foo.set_attribute_value('x-additionaltext',
GLib.Variant.new_string('lorem ipsum'))
foo.set_attribute_value('x-enabled', GLib.Variant.new_boolean(True))
menu.append_item(foo)
- bar = Gio.MenuItem.new('bar', 'app.bar')
+ bar = Gio.MenuItem.new('bar', 'bar')
bar.set_attribute_value('x-defaultvalue',
GLib.Variant.new_string('Hello World!'))
bar.set_attribute_value('x-canonical-currentvalue',
@@ -74,5 +59,10 @@ if __name__ == '__main__':
menu.append('baz', 'app.baz')
bus.export_menu_model(BUS_OBJECT_PATH, menu)
- GLib.MainLoop().run()
+ actions = Gio.SimpleActionGroup.new()
+ actions.add_action(Gio.SimpleAction.new("bar", None))
+ bus.export_action_group(BUS_OBJECT_PATH, actions)
+if __name__ == '__main__':
+ Gio.bus_own_name(Gio.BusType.SESSION, BUS_NAME, 0, bus_acquired, None, None)
+ GLib.MainLoop().run()
diff --git a/examples/unityqmlmenumodel.qml b/examples/unityqmlmenumodel.qml
new file mode 100644
index 0000000..c0e5adc
--- /dev/null
+++ b/examples/unityqmlmenumodel.qml
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+import QtQuick 2.0
+import QMenuModel 0.1
+
+Item {
+ width: 400;
+ height: 500;
+
+ UnityMenuModel {
+ id: menu
+ busName: "com.canonical.indicator.sound"
+ actions: { "indicator": "/com/canonical/indicator/sound" }
+ menuObjectPath: "/com/canonical/indicator/sound/desktop"
+ }
+
+ ListView {
+ id: listview
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 3
+ model: menu
+
+ delegate: Loader {
+ sourceComponent: {
+ if (isSeparator) {
+ return separator;
+ }
+ else if (type == "com.canonical.unity.slider") {
+ listview.model.loadExtendedAttributes(index, {'min-icon': 'icon',
+ 'max-icon': 'icon'});
+ return slider;
+ }
+ else {
+ return menuitem;
+ }
+ }
+
+ Component {
+ id: separator
+ Rectangle {
+ width: listview.width
+ height: 4
+ color: "blue"
+ }
+ }
+
+ Component {
+ id: slider
+ Rectangle {
+ width: listview.width
+ color: "#ddd"
+ height: 40
+ Row {
+ anchors.fill: parent
+ Image {
+ source: ext.minIcon
+ }
+ Text {
+ text: model.actionState
+ }
+ Image {
+ source: ext.maxIcon
+ }
+ }
+ }
+ }
+
+ Component {
+ id: menuitem
+ Rectangle {
+ width: listview.width
+ height: 40
+ color: "#ddd"
+ Row {
+ anchors.fill: parent
+ anchors.margins: 5
+ Image {
+ source: icon
+ }
+ Text {
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ color: sensitive ? "black" : "#aaa";
+ text: label
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ var submenu = listview.model.submenu(index);
+ if (submenu)
+ listview.model = submenu;
+ else
+ action.activate();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libqmenumodel/QMenuModel/CMakeLists.txt b/libqmenumodel/QMenuModel/CMakeLists.txt
index 7367e18..78e062b 100644
--- a/libqmenumodel/QMenuModel/CMakeLists.txt
+++ b/libqmenumodel/QMenuModel/CMakeLists.txt
@@ -19,7 +19,7 @@ target_link_libraries(qmenumodel-qml
${GIO_LDFLAGS}
)
-qt5_use_modules(qmenumodel-qml Qml Widgets)
+qt5_use_modules(qmenumodel-qml Qml Quick)
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/qmldir"
"${CMAKE_CURRENT_BINARY_DIR}/qmldir")
diff --git a/libqmenumodel/QMenuModel/plugin.cpp b/libqmenumodel/QMenuModel/plugin.cpp
index ee05fff..0629099 100644
--- a/libqmenumodel/QMenuModel/plugin.cpp
+++ b/libqmenumodel/QMenuModel/plugin.cpp
@@ -22,9 +22,16 @@
#include "qdbusmenumodel.h"
#include "qdbusactiongroup.h"
#include "qstateaction.h"
+#include "unitymenumodel.h"
+#include "unitythemediconprovider.h"
#include <QtQml>
+void QMenuModelQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
+{
+ engine->addImageProvider("theme", new UnityThemedIconProvider);
+}
+
void QMenuModelQmlPlugin::registerTypes(const char *uri)
{
qmlRegisterUncreatableType<QMenuModel>(uri, 0, 1, "QMenuModel",
@@ -36,6 +43,5 @@ void QMenuModelQmlPlugin::registerTypes(const char *uri)
qmlRegisterType<QDBusMenuModel>(uri, 0, 1, "QDBusMenuModel");
qmlRegisterType<QDBusActionGroup>(uri, 0, 1, "QDBusActionGroup");
-
+ qmlRegisterType<UnityMenuModel>(uri, 0, 1, "UnityMenuModel");
}
-
diff --git a/libqmenumodel/QMenuModel/plugin.h b/libqmenumodel/QMenuModel/plugin.h
index fc732d2..3474139 100644
--- a/libqmenumodel/QMenuModel/plugin.h
+++ b/libqmenumodel/QMenuModel/plugin.h
@@ -28,6 +28,7 @@ class QMenuModelQmlPlugin : public QQmlExtensionPlugin
Q_PLUGIN_METADATA(IID "com.canonical.qmenumodel")
public:
+ void initializeEngine(QQmlEngine *engine, const char *uri);
void registerTypes(const char *uri);
};
diff --git a/libqmenumodel/src/CMakeLists.txt b/libqmenumodel/src/CMakeLists.txt
index 2f7aac8..10f59c6 100644
--- a/libqmenumodel/src/CMakeLists.txt
+++ b/libqmenumodel/src/CMakeLists.txt
@@ -1,6 +1,7 @@
project(src)
set(QMENUMODEL_SRC
+ actionstateparser.cpp
converter.cpp
dbus-enums.h
menunode.cpp
@@ -10,6 +11,19 @@ set(QMENUMODEL_SRC
qdbusactiongroup.cpp
qmenumodelevents.cpp
qstateaction.cpp
+ unitymenumodel.cpp
+ unitymenumodelevents.cpp
+ unitythemediconprovider.cpp
+ gtk/gtkactionmuxer.c
+ gtk/gtkactionmuxer.h
+ gtk/gtkactionobservable.c
+ gtk/gtkactionobservable.h
+ gtk/gtkactionobserver.c
+ gtk/gtkactionobserver.h
+ gtk/gtkmenutracker.c
+ gtk/gtkmenutracker.h
+ gtk/gtkmenutrackeritem.c
+ gtk/gtkmenutrackeritem.h
)
set(SHAREDLIBNAME qmenumodel)
@@ -33,17 +47,20 @@ target_link_libraries(${SHAREDLIBNAME}
${GIO_LDFLAGS}
)
-qt5_use_modules(${SHAREDLIBNAME} Core)
+qt5_use_modules(${SHAREDLIBNAME} Core Qml Quick)
install(TARGETS ${SHAREDLIBNAME} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
set(QMENUMODEL_HEADERS
+ actionstateparser.h
dbus-enums.h
qdbusactiongroup.h
qdbusmenumodel.h
qdbusobject.h
qmenumodel.h
qstateaction.h
+ unitymenumodel.h
+ unitythemediconprovider.h
)
set(INCLUDEDIR qmenumodel)
diff --git a/libqmenumodel/src/actionstateparser.cpp b/libqmenumodel/src/actionstateparser.cpp
new file mode 100644
index 0000000..6637b56
--- /dev/null
+++ b/libqmenumodel/src/actionstateparser.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Nick Dedekind <nick.dedekind@canonical.com>
+ */
+
+#include "actionstateparser.h"
+#include "converter.h"
+
+ActionStateParser::ActionStateParser(QObject* parent)
+ : QObject(parent)
+{
+}
+
+QVariant ActionStateParser::toQVariant(GVariant* state) const
+{
+ if (state) {
+ return Converter::toQVariant(state);
+ }
+ return QVariant();
+} \ No newline at end of file
diff --git a/libqmenumodel/src/actionstateparser.h b/libqmenumodel/src/actionstateparser.h
new file mode 100644
index 0000000..044dea1
--- /dev/null
+++ b/libqmenumodel/src/actionstateparser.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Nick Dedekind <nick.dedekind@canonical.com>
+ */
+
+#ifndef ACTIONSTATEPARSER_H
+#define ACTIONSTATEPARSER_H
+
+#include <QObject>
+#include <QVariant>
+
+typedef struct _GVariant GVariant;
+
+class ActionStateParser : public QObject
+{
+ Q_OBJECT
+public:
+ ActionStateParser(QObject* parent = 0);
+
+ virtual QVariant toQVariant(GVariant* state) const;
+};
+
+#endif // ACTIONSTATEPARSER_H
diff --git a/libqmenumodel/src/converter.cpp b/libqmenumodel/src/converter.cpp
index d9d90bd..69e6629 100644
--- a/libqmenumodel/src/converter.cpp
+++ b/libqmenumodel/src/converter.cpp
@@ -195,3 +195,135 @@ GVariant* Converter::toGVariant(const QVariant &value)
return result;
}
+GVariant* Converter::toGVariantWithSchema(const QVariant &value, const char* schema)
+{
+ if (!g_variant_type_string_is_valid(schema)) {
+ return Converter::toGVariant(value);
+ }
+
+ GVariant* result = NULL;
+ const GVariantType* schema_type;
+ schema_type = g_variant_type_new(schema);
+
+ if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_BOOLEAN)) {
+ if (value.canConvert<bool>()) {
+ result = g_variant_new_boolean (value.value<bool>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_BYTE)) {
+ if (value.canConvert<uchar>()) {
+ result = g_variant_new_byte (value.value<uchar>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_INT16)) {
+ if (value.canConvert<qint16>()) {
+ result = g_variant_new_int16 (value.value<qint16>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_UINT16)) {
+ if (value.canConvert<quint16>()) {
+ result = g_variant_new_uint16 (value.value<quint16>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_INT32)) {
+ if (value.canConvert<qint32>()) {
+ result = g_variant_new_int32 (value.value<qint32>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_UINT32)) {
+ if (value.canConvert<quint32>()) {
+ result = g_variant_new_uint32 (value.value<quint32>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_INT64)) {
+ if (value.canConvert<qint64>()) {
+ result = g_variant_new_int64 (value.value<qint64>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_UINT64)) {
+ if (value.canConvert<quint64>()) {
+ result = g_variant_new_uint64 (value.value<quint64>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_DOUBLE)) {
+ if (value.canConvert<double>()) {
+ result = g_variant_new_double (value.value<double>());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_STRING)) {
+ if (value.canConvert<QString>()) {
+ result = g_variant_new_string(value.toString().toUtf8().data());
+ }
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_VARIANT)) {
+ result = Converter::toGVariant(value);
+ } else if (g_variant_type_equal(schema_type, G_VARIANT_TYPE_VARDICT)) {
+ if (value.canConvert(QVariant::Map)) {
+ result = Converter::toGVariant(value.toMap());
+ }
+ } else if (g_variant_type_is_array(schema_type)) {
+ if (value.canConvert(QVariant::List)) {
+
+ const GVariantType* entry_type;
+ GVariant* data;
+ entry_type = g_variant_type_element(schema_type);
+ gchar* entryTypeString = g_variant_type_dup_string(entry_type);
+
+ QVariantList lst = value.toList();
+ GVariant **vars = g_new(GVariant*, lst.size());
+
+ bool ok = true;
+ for (int i=0; i < lst.size(); i++) {
+ data = Converter::toGVariantWithSchema(lst[i], entryTypeString);
+
+ if (data) {
+ vars[i] = data;
+ }
+ else {
+ ok = false;
+ qWarning() << "Failed to convert list to array with schema:" << schema;
+ break;
+ }
+ }
+ if (ok) {
+ result = g_variant_new_array(entry_type, vars, lst.size());
+ }
+ g_free(entryTypeString);
+ g_free(vars);
+ }
+ } else if (g_variant_type_is_tuple(schema_type)) {
+ if (value.canConvert(QVariant::List)) {
+ GVariant* data;
+
+ QVariantList lst = value.toList();
+ GVariant **vars = g_new(GVariant*, lst.size());
+
+ const GVariantType* entry_type = g_variant_type_first(schema_type);
+
+ bool ok = true;
+ for (int i=0; i < lst.size(); i++) {
+
+ gchar* entryTypeString = g_variant_type_dup_string(entry_type);
+
+ data = Converter::toGVariantWithSchema(lst[i], entryTypeString);
+
+ if (data) {
+ vars[i] = data;
+ }
+ else {
+ ok = false;
+ qWarning() << "Failed to convert list to tuple with schema:" << schema;
+ g_free(entryTypeString);
+ break;
+ }
+ g_free(entryTypeString);
+
+ entry_type = g_variant_type_next(entry_type);
+ if (!entry_type) {
+ break;
+ }
+ }
+ if (ok) {
+ result = g_variant_new_tuple(vars, lst.size());
+ }
+ g_free(vars);
+ }
+ }
+
+ // fallback to straight convert.
+ if (!result) {
+ result = Converter::toGVariant(value);
+ }
+ return result;
+}
+
diff --git a/libqmenumodel/src/converter.h b/libqmenumodel/src/converter.h
index 5f05bc7..f47c09e 100644
--- a/libqmenumodel/src/converter.h
+++ b/libqmenumodel/src/converter.h
@@ -28,6 +28,10 @@ class Converter
public:
static QVariant toQVariant(GVariant *value);
static GVariant* toGVariant(const QVariant &value);
+
+ // This converts a QVariant to a GVariant using a provided gvariant schema as
+ // a conversion base (it will attempt to convert to this format).
+ static GVariant* toGVariantWithSchema(const QVariant &value, const char* schema);
};
#endif // CONVERTER_H
diff --git a/libqmenumodel/src/gtk/config.h b/libqmenumodel/src/gtk/config.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libqmenumodel/src/gtk/config.h
diff --git a/libqmenumodel/src/gtk/gtkactionmuxer.c b/libqmenumodel/src/gtk/gtkactionmuxer.c
new file mode 100644
index 0000000..4618564
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkactionmuxer.c
@@ -0,0 +1,778 @@
+/*
+ * Copyright © 2011 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 "config.h"
+
+#include "gtkactionmuxer.h"
+
+#include "gtkactionobservable.h"
+#include "gtkactionobserver.h"
+
+#include <string.h>
+
+/**
+ * SECTION:gtkactionmuxer
+ * @short_description: Aggregate and monitor several action groups
+ *
+ * #GtkActionMuxer is a #GActionGroup and #GtkActionObservable that is
+ * capable of containing other #GActionGroup instances.
+ *
+ * The typical use is aggregating all of the actions applicable to a
+ * particular context into a single action group, with namespacing.
+ *
+ * Consider the case of two action groups -- one containing actions
+ * applicable to an entire application (such as 'quit') and one
+ * containing actions applicable to a particular window in the
+ * application (such as 'fullscreen').
+ *
+ * In this case, each of these action groups could be added to a
+ * #GtkActionMuxer with the prefixes "app" and "win", respectively. This
+ * would expose the actions as "app.quit" and "win.fullscreen" on the
+ * #GActionGroup interface presented by the #GtkActionMuxer.
+ *
+ * Activations and state change requests on the #GtkActionMuxer are wired
+ * through to the underlying action group in the expected way.
+ *
+ * This class is typically only used at the site of "consumption" of
+ * actions (eg: when displaying a menu that contains many actions on
+ * different objects).
+ */
+
+static void gtk_action_muxer_group_iface_init (GActionGroupInterface *iface);
+static void gtk_action_muxer_observable_iface_init (GtkActionObservableInterface *iface);
+
+typedef GObjectClass GtkActionMuxerClass;
+
+struct _GtkActionMuxer
+{
+ GObject parent_instance;
+
+ GHashTable *observed_actions;
+ GHashTable *groups;
+ GtkActionMuxer *parent;
+};
+
+G_DEFINE_TYPE_WITH_CODE (GtkActionMuxer, gtk_action_muxer, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, gtk_action_muxer_group_iface_init)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVABLE, gtk_action_muxer_observable_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_PARENT,
+ NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES];
+
+typedef struct
+{
+ GtkActionMuxer *muxer;
+ GSList *watchers;
+ gchar *fullname;
+} Action;
+
+typedef struct
+{
+ GtkActionMuxer *muxer;
+ GActionGroup *group;
+ gchar *prefix;
+ gulong handler_ids[4];
+} Group;
+
+static void
+gtk_action_muxer_append_group_actions (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ const gchar *prefix = key;
+ Group *group = value;
+ GArray *actions = user_data;
+ gchar **group_actions;
+ gchar **action;
+
+ group_actions = g_action_group_list_actions (group->group);
+ for (action = group_actions; *action; action++)
+ {
+ gchar *fullname;
+
+ fullname = g_strconcat (prefix, ".", *action, NULL);
+ g_array_append_val (actions, fullname);
+ }
+
+ g_strfreev (group_actions);
+}
+
+static gchar **
+gtk_action_muxer_list_actions (GActionGroup *action_group)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group);
+ GArray *actions;
+
+ actions = g_array_new (TRUE, FALSE, sizeof (gchar *));
+
+ for ( ; muxer != NULL; muxer = muxer->parent)
+ {
+ g_hash_table_foreach (muxer->groups,
+ gtk_action_muxer_append_group_actions,
+ actions);
+ }
+
+ return (gchar **) g_array_free (actions, FALSE);
+}
+
+static Group *
+gtk_action_muxer_find_group (GtkActionMuxer *muxer,
+ const gchar *full_name,
+ const gchar **action_name)
+{
+ const gchar *dot;
+ gchar *prefix;
+ Group *group;
+
+ dot = strchr (full_name, '.');
+
+ if (!dot)
+ return NULL;
+
+ prefix = g_strndup (full_name, dot - full_name);
+ group = g_hash_table_lookup (muxer->groups, prefix);
+ g_free (prefix);
+
+ if (action_name)
+ *action_name = dot + 1;
+
+ return group;
+}
+
+static void
+gtk_action_muxer_action_enabled_changed (GtkActionMuxer *muxer,
+ const gchar *action_name,
+ gboolean enabled)
+{
+ Action *action;
+ GSList *node;
+
+ action = g_hash_table_lookup (muxer->observed_actions, action_name);
+ for (node = action ? action->watchers : NULL; node; node = node->next)
+ gtk_action_observer_action_enabled_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name, enabled);
+ g_action_group_action_enabled_changed (G_ACTION_GROUP (muxer), action_name, enabled);
+}
+
+static void
+gtk_action_muxer_group_action_enabled_changed (GActionGroup *action_group,
+ const gchar *action_name,
+ gboolean enabled,
+ gpointer user_data)
+{
+ Group *group = user_data;
+ gchar *fullname;
+
+ fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+ gtk_action_muxer_action_enabled_changed (group->muxer, fullname, enabled);
+
+ g_free (fullname);
+}
+
+static void
+gtk_action_muxer_parent_action_enabled_changed (GActionGroup *action_group,
+ const gchar *action_name,
+ gboolean enabled,
+ gpointer user_data)
+{
+ GtkActionMuxer *muxer = user_data;
+
+ gtk_action_muxer_action_enabled_changed (muxer, action_name, enabled);
+}
+
+static void
+gtk_action_muxer_action_state_changed (GtkActionMuxer *muxer,
+ const gchar *action_name,
+ GVariant *state)
+{
+ Action *action;
+ GSList *node;
+
+ action = g_hash_table_lookup (muxer->observed_actions, action_name);
+ for (node = action ? action->watchers : NULL; node; node = node->next)
+ gtk_action_observer_action_state_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name, state);
+ g_action_group_action_state_changed (G_ACTION_GROUP (muxer), action_name, state);
+}
+
+static void
+gtk_action_muxer_group_action_state_changed (GActionGroup *action_group,
+ const gchar *action_name,
+ GVariant *state,
+ gpointer user_data)
+{
+ Group *group = user_data;
+ gchar *fullname;
+
+ fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+ gtk_action_muxer_action_state_changed (group->muxer, fullname, state);
+
+ g_free (fullname);
+}
+
+static void
+gtk_action_muxer_parent_action_state_changed (GActionGroup *action_group,
+ const gchar *action_name,
+ GVariant *state,
+ gpointer user_data)
+{
+ GtkActionMuxer *muxer = user_data;
+
+ gtk_action_muxer_action_state_changed (muxer, action_name, state);
+}
+
+static void
+gtk_action_muxer_action_added (GtkActionMuxer *muxer,
+ const gchar *action_name,
+ GActionGroup *original_group,
+ const gchar *orignal_action_name)
+{
+ const GVariantType *parameter_type;
+ gboolean enabled;
+ GVariant *state;
+ Action *action;
+
+ action = g_hash_table_lookup (muxer->observed_actions, action_name);
+
+ if (action && action->watchers &&
+ g_action_group_query_action (original_group, orignal_action_name,
+ &enabled, &parameter_type, NULL, NULL, &state))
+ {
+ GSList *node;
+
+ for (node = action->watchers; node; node = node->next)
+ gtk_action_observer_action_added (node->data,
+ GTK_ACTION_OBSERVABLE (muxer),
+ action_name, parameter_type, enabled, state);
+
+ if (state)
+ g_variant_unref (state);
+ }
+
+ g_action_group_action_added (G_ACTION_GROUP (muxer), action_name);
+}
+
+static void
+gtk_action_muxer_action_added_to_group (GActionGroup *action_group,
+ const gchar *action_name,
+ gpointer user_data)
+{
+ Group *group = user_data;
+ gchar *fullname;
+
+ fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+ gtk_action_muxer_action_added (group->muxer, fullname, action_group, action_name);
+
+ g_free (fullname);
+}
+
+static void
+gtk_action_muxer_action_added_to_parent (GActionGroup *action_group,
+ const gchar *action_name,
+ gpointer user_data)
+{
+ GtkActionMuxer *muxer = user_data;
+
+ gtk_action_muxer_action_added (muxer, action_name, action_group, action_name);
+}
+
+static void
+gtk_action_muxer_action_removed (GtkActionMuxer *muxer,
+ const gchar *action_name)
+{
+ Action *action;
+ GSList *node;
+
+ action = g_hash_table_lookup (muxer->observed_actions, action_name);
+ for (node = action ? action->watchers : NULL; node; node = node->next)
+ gtk_action_observer_action_removed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name);
+ g_action_group_action_removed (G_ACTION_GROUP (muxer), action_name);
+}
+
+static void
+gtk_action_muxer_action_removed_from_group (GActionGroup *action_group,
+ const gchar *action_name,
+ gpointer user_data)
+{
+ Group *group = user_data;
+ gchar *fullname;
+
+ fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+ gtk_action_muxer_action_removed (group->muxer, fullname);
+
+ g_free (fullname);
+}
+
+static void
+gtk_action_muxer_action_removed_from_parent (GActionGroup *action_group,
+ const gchar *action_name,
+ gpointer user_data)
+{
+ GtkActionMuxer *muxer = user_data;
+
+ gtk_action_muxer_action_removed (muxer, action_name);
+}
+
+static gboolean
+gtk_action_muxer_query_action (GActionGroup *action_group,
+ const gchar *action_name,
+ gboolean *enabled,
+ const GVariantType **parameter_type,
+ const GVariantType **state_type,
+ GVariant **state_hint,
+ GVariant **state)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group);
+ Group *group;
+ const gchar *unprefixed_name;
+
+ group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name);
+
+ if (group)
+ return g_action_group_query_action (group->group, unprefixed_name, enabled,
+ parameter_type, state_type, state_hint, state);
+
+ if (muxer->parent)
+ return g_action_group_query_action (G_ACTION_GROUP (muxer->parent), action_name,
+ enabled, parameter_type,
+ state_type, state_hint, state);
+
+ return FALSE;
+}
+
+static void
+gtk_action_muxer_activate_action (GActionGroup *action_group,
+ const gchar *action_name,
+ GVariant *parameter)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group);
+ Group *group;
+ const gchar *unprefixed_name;
+
+ group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name);
+
+ if (group)
+ g_action_group_activate_action (group->group, unprefixed_name, parameter);
+ else if (muxer->parent)
+ g_action_group_activate_action (G_ACTION_GROUP (muxer->parent), action_name, parameter);
+}
+
+static void
+gtk_action_muxer_change_action_state (GActionGroup *action_group,
+ const gchar *action_name,
+ GVariant *state)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group);
+ Group *group;
+ const gchar *unprefixed_name;
+
+ group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name);
+
+ if (group)
+ g_action_group_change_action_state (group->group, unprefixed_name, state);
+ else if (muxer->parent)
+ g_action_group_change_action_state (G_ACTION_GROUP (muxer->parent), action_name, state);
+}
+
+static void
+gtk_action_muxer_unregister_internal (Action *action,
+ gpointer observer)
+{
+ GtkActionMuxer *muxer = action->muxer;
+ GSList **ptr;
+
+ for (ptr = &action->watchers; *ptr; ptr = &(*ptr)->next)
+ if ((*ptr)->data == observer)
+ {
+ *ptr = g_slist_remove (*ptr, observer);
+
+ if (action->watchers == NULL)
+ g_hash_table_remove (muxer->observed_actions, action->fullname);
+
+ break;
+ }
+}
+
+static void
+gtk_action_muxer_weak_notify (gpointer data,
+ GObject *where_the_object_was)
+{
+ Action *action = data;
+
+ gtk_action_muxer_unregister_internal (action, where_the_object_was);
+}
+
+static void
+gtk_action_muxer_register_observer (GtkActionObservable *observable,
+ const gchar *name,
+ GtkActionObserver *observer)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable);
+ Action *action;
+
+ action = g_hash_table_lookup (muxer->observed_actions, name);
+
+ if (action == NULL)
+ {
+ action = g_slice_new (Action);
+ action->muxer = muxer;
+ action->fullname = g_strdup (name);
+ action->watchers = NULL;
+
+ g_hash_table_insert (muxer->observed_actions, action->fullname, action);
+ }
+
+ action->watchers = g_slist_prepend (action->watchers, observer);
+ g_object_weak_ref (G_OBJECT (observer), gtk_action_muxer_weak_notify, action);
+}
+
+static void
+gtk_action_muxer_unregister_observer (GtkActionObservable *observable,
+ const gchar *name,
+ GtkActionObserver *observer)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable);
+ Action *action;
+
+ action = g_hash_table_lookup (muxer->observed_actions, name);
+ g_object_weak_unref (G_OBJECT (observer), gtk_action_muxer_weak_notify, action);
+ gtk_action_muxer_unregister_internal (action, observer);
+}
+
+static void
+gtk_action_muxer_free_group (gpointer data)
+{
+ Group *group = data;
+ gint i;
+
+ /* 'for loop' or 'four loop'? */
+ for (i = 0; i < 4; i++)
+ g_signal_handler_disconnect (group->group, group->handler_ids[i]);
+
+ g_object_unref (group->group);
+ g_free (group->prefix);
+
+ g_slice_free (Group, group);
+}
+
+static void
+gtk_action_muxer_free_action (gpointer data)
+{
+ Action *action = data;
+ GSList *it;
+
+ for (it = action->watchers; it; it = it->next)
+ g_object_weak_unref (G_OBJECT (it->data), gtk_action_muxer_weak_notify, action);
+
+ g_slist_free (action->watchers);
+ g_free (action->fullname);
+
+ g_slice_free (Action, action);
+}
+
+static void
+gtk_action_muxer_finalize (GObject *object)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (object);
+
+ g_assert_cmpint (g_hash_table_size (muxer->observed_actions), ==, 0);
+ g_hash_table_unref (muxer->observed_actions);
+ g_hash_table_unref (muxer->groups);
+
+ G_OBJECT_CLASS (gtk_action_muxer_parent_class)
+ ->finalize (object);
+}
+
+static void
+gtk_action_muxer_dispose (GObject *object)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (object);
+
+ if (muxer->parent)
+ {
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_added_to_parent, muxer);
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_removed_from_parent, muxer);
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_enabled_changed, muxer);
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_state_changed, muxer);
+
+ g_clear_object (&muxer->parent);
+ }
+
+ g_hash_table_remove_all (muxer->observed_actions);
+
+ G_OBJECT_CLASS (gtk_action_muxer_parent_class)
+ ->dispose (object);
+}
+
+static void
+gtk_action_muxer_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (object);
+
+ switch (property_id)
+ {
+ case PROP_PARENT:
+ g_value_set_object (value, gtk_action_muxer_get_parent (muxer));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtk_action_muxer_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtkActionMuxer *muxer = GTK_ACTION_MUXER (object);
+
+ switch (property_id)
+ {
+ case PROP_PARENT:
+ gtk_action_muxer_set_parent (muxer, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtk_action_muxer_init (GtkActionMuxer *muxer)
+{
+ muxer->observed_actions = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gtk_action_muxer_free_action);
+ muxer->groups = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gtk_action_muxer_free_group);
+}
+
+static void
+gtk_action_muxer_observable_iface_init (GtkActionObservableInterface *iface)
+{
+ iface->register_observer = gtk_action_muxer_register_observer;
+ iface->unregister_observer = gtk_action_muxer_unregister_observer;
+}
+
+static void
+gtk_action_muxer_group_iface_init (GActionGroupInterface *iface)
+{
+ iface->list_actions = gtk_action_muxer_list_actions;
+ iface->query_action = gtk_action_muxer_query_action;
+ iface->activate_action = gtk_action_muxer_activate_action;
+ iface->change_action_state = gtk_action_muxer_change_action_state;
+}
+
+static void
+gtk_action_muxer_class_init (GObjectClass *class)
+{
+ class->get_property = gtk_action_muxer_get_property;
+ class->set_property = gtk_action_muxer_set_property;
+ class->finalize = gtk_action_muxer_finalize;
+ class->dispose = gtk_action_muxer_dispose;
+
+ properties[PROP_PARENT] = g_param_spec_object ("parent", "Parent",
+ "The parent muxer",
+ GTK_TYPE_ACTION_MUXER,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (class, NUM_PROPERTIES, properties);
+}
+
+/**
+ * gtk_action_muxer_insert:
+ * @muxer: a #GtkActionMuxer
+ * @prefix: the prefix string for the action group
+ * @action_group: a #GActionGroup
+ *
+ * Adds the actions in @action_group to the list of actions provided by
+ * @muxer. @prefix is prefixed to each action name, such that for each
+ * action <varname>x</varname> in @action_group, there is an equivalent
+ * action @prefix<literal>.</literal><varname>x</varname> in @muxer.
+ *
+ * For example, if @prefix is "<literal>app</literal>" and @action_group
+ * contains an action called "<literal>quit</literal>", then @muxer will
+ * now contain an action called "<literal>app.quit</literal>".
+ *
+ * If any #GtkActionObservers are registered for actions in the group,
+ * "action_added" notifications will be emitted, as appropriate.
+ *
+ * @prefix must not contain a dot ('.').
+ */
+void
+gtk_action_muxer_insert (GtkActionMuxer *muxer,
+ const gchar *prefix,
+ GActionGroup *action_group)
+{
+ gchar **actions;
+ Group *group;
+ gint i;
+
+ /* TODO: diff instead of ripout and replace */
+ gtk_action_muxer_remove (muxer, prefix);
+
+ group = g_slice_new (Group);
+ group->muxer = muxer;
+ group->group = g_object_ref (action_group);
+ group->prefix = g_strdup (prefix);
+
+ g_hash_table_insert (muxer->groups, group->prefix, group);
+
+ actions = g_action_group_list_actions (group->group);
+ for (i = 0; actions[i]; i++)
+ gtk_action_muxer_action_added_to_group (group->group, actions[i], group);
+ g_strfreev (actions);
+
+ group->handler_ids[0] = g_signal_connect (group->group, "action-added",
+ G_CALLBACK (gtk_action_muxer_action_added_to_group), group);
+ group->handler_ids[1] = g_signal_connect (group->group, "action-removed",
+ G_CALLBACK (gtk_action_muxer_action_removed_from_group), group);
+ group->handler_ids[2] = g_signal_connect (group->group, "action-enabled-changed",
+ G_CALLBACK (gtk_action_muxer_group_action_enabled_changed), group);
+ group->handler_ids[3] = g_signal_connect (group->group, "action-state-changed",
+ G_CALLBACK (gtk_action_muxer_group_action_state_changed), group);
+}
+
+/**
+ * gtk_action_muxer_remove:
+ * @muxer: a #GtkActionMuxer
+ * @prefix: the prefix of the action group to remove
+ *
+ * Removes a #GActionGroup from the #GtkActionMuxer.
+ *
+ * If any #GtkActionObservers are registered for actions in the group,
+ * "action_removed" notifications will be emitted, as appropriate.
+ */
+void
+gtk_action_muxer_remove (GtkActionMuxer *muxer,
+ const gchar *prefix)
+{
+ Group *group;
+
+ group = g_hash_table_lookup (muxer->groups, prefix);
+
+ if (group != NULL)
+ {
+ gchar **actions;
+ gint i;
+
+ g_hash_table_steal (muxer->groups, prefix);
+
+ actions = g_action_group_list_actions (group->group);
+ for (i = 0; actions[i]; i++)
+ gtk_action_muxer_action_removed_from_group (group->group, actions[i], group);
+ g_strfreev (actions);
+
+ gtk_action_muxer_free_group (group);
+ }
+}
+
+/**
+ * gtk_action_muxer_new:
+ *
+ * Creates a new #GtkActionMuxer.
+ */
+GtkActionMuxer *
+gtk_action_muxer_new (void)
+{
+ return g_object_new (GTK_TYPE_ACTION_MUXER, NULL);
+}
+
+/**
+ * gtk_action_muxer_get_parent:
+ * @muxer: a #GtkActionMuxer
+ *
+ * Returns: (transfer none): the parent of @muxer, or NULL.
+ */
+GtkActionMuxer *
+gtk_action_muxer_get_parent (GtkActionMuxer *muxer)
+{
+ g_return_val_if_fail (GTK_IS_ACTION_MUXER (muxer), NULL);
+
+ return muxer->parent;
+}
+
+/**
+ * gtk_action_muxer_set_parent:
+ * @muxer: a #GtkActionMuxer
+ * @parent: (allow-none): the new parent #GtkActionMuxer
+ *
+ * Sets the parent of @muxer to @parent.
+ */
+void
+gtk_action_muxer_set_parent (GtkActionMuxer *muxer,
+ GtkActionMuxer *parent)
+{
+ g_return_if_fail (GTK_IS_ACTION_MUXER (muxer));
+ g_return_if_fail (parent == NULL || GTK_IS_ACTION_MUXER (parent));
+
+ if (muxer->parent == parent)
+ return;
+
+ if (muxer->parent != NULL)
+ {
+ gchar **actions;
+ gchar **it;
+
+ actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent));
+ for (it = actions; *it; it++)
+ gtk_action_muxer_action_removed (muxer, *it);
+ g_strfreev (actions);
+
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_added_to_parent, muxer);
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_removed_from_parent, muxer);
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_enabled_changed, muxer);
+ g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_state_changed, muxer);
+
+ g_object_unref (muxer->parent);
+ }
+
+ muxer->parent = parent;
+
+ if (muxer->parent != NULL)
+ {
+ gchar **actions;
+ gchar **it;
+
+ g_object_ref (muxer->parent);
+
+ actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent));
+ for (it = actions; *it; it++)
+ gtk_action_muxer_action_added (muxer, *it, G_ACTION_GROUP (muxer->parent), *it);
+ g_strfreev (actions);
+
+ g_signal_connect (muxer->parent, "action-added",
+ G_CALLBACK (gtk_action_muxer_action_added_to_parent), muxer);
+ g_signal_connect (muxer->parent, "action-removed",
+ G_CALLBACK (gtk_action_muxer_action_removed_from_parent), muxer);
+ g_signal_connect (muxer->parent, "action-enabled-changed",
+ G_CALLBACK (gtk_action_muxer_parent_action_enabled_changed), muxer);
+ g_signal_connect (muxer->parent, "action-state-changed",
+ G_CALLBACK (gtk_action_muxer_parent_action_state_changed), muxer);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (muxer), properties[PROP_PARENT]);
+}
diff --git a/libqmenumodel/src/gtk/gtkactionmuxer.h b/libqmenumodel/src/gtk/gtkactionmuxer.h
new file mode 100644
index 0000000..4014830
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkactionmuxer.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2011 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>
+ */
+
+#ifndef __GTK_ACTION_MUXER_H__
+#define __GTK_ACTION_MUXER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_ACTION_MUXER (gtk_action_muxer_get_type ())
+#define GTK_ACTION_MUXER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ GTK_TYPE_ACTION_MUXER, GtkActionMuxer))
+#define GTK_IS_ACTION_MUXER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ GTK_TYPE_ACTION_MUXER))
+
+typedef struct _GtkActionMuxer GtkActionMuxer;
+
+GType gtk_action_muxer_get_type (void);
+GtkActionMuxer * gtk_action_muxer_new (void);
+
+void gtk_action_muxer_insert (GtkActionMuxer *muxer,
+ const gchar *prefix,
+ GActionGroup *action_group);
+
+void gtk_action_muxer_remove (GtkActionMuxer *muxer,
+ const gchar *prefix);
+
+GtkActionMuxer * gtk_action_muxer_get_parent (GtkActionMuxer *muxer);
+
+void gtk_action_muxer_set_parent (GtkActionMuxer *muxer,
+ GtkActionMuxer *parent);
+
+G_END_DECLS
+
+#endif /* __GTK_ACTION_MUXER_H__ */
diff --git a/libqmenumodel/src/gtk/gtkactionobservable.c b/libqmenumodel/src/gtk/gtkactionobservable.c
new file mode 100644
index 0000000..ab90df2
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkactionobservable.c
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2011 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/>.
+ *
+ * Authors: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "gtkactionobservable.h"
+
+G_DEFINE_INTERFACE (GtkActionObservable, gtk_action_observable, G_TYPE_OBJECT)
+
+/*
+ * SECTION:gtkactionobserable
+ * @short_description: an interface implemented by objects that report
+ * changes to actions
+ */
+
+void
+gtk_action_observable_default_init (GtkActionObservableInterface *iface)
+{
+}
+
+/**
+ * gtk_action_observable_register_observer:
+ * @observable: a #GtkActionObservable
+ * @action_name: the name of the action
+ * @observer: the #GtkActionObserver to which the events will be reported
+ *
+ * Registers @observer as being interested in changes to @action_name on
+ * @observable.
+ */
+void
+gtk_action_observable_register_observer (GtkActionObservable *observable,
+ const gchar *action_name,
+ GtkActionObserver *observer)
+{
+ g_return_if_fail (GTK_IS_ACTION_OBSERVABLE (observable));
+
+ GTK_ACTION_OBSERVABLE_GET_IFACE (observable)
+ ->register_observer (observable, action_name, observer);
+}
+
+/**
+ * gtk_action_observable_unregister_observer:
+ * @observable: a #GtkActionObservable
+ * @action_name: the name of the action
+ * @observer: the #GtkActionObserver to which the events will be reported
+ *
+ * Removes the registration of @observer as being interested in changes
+ * to @action_name on @observable.
+ *
+ * If the observer was registered multiple times, it must be
+ * unregistered an equal number of times.
+ */
+void
+gtk_action_observable_unregister_observer (GtkActionObservable *observable,
+ const gchar *action_name,
+ GtkActionObserver *observer)
+{
+ g_return_if_fail (GTK_IS_ACTION_OBSERVABLE (observable));
+
+ GTK_ACTION_OBSERVABLE_GET_IFACE (observable)
+ ->unregister_observer (observable, action_name, observer);
+}
diff --git a/libqmenumodel/src/gtk/gtkactionobservable.h b/libqmenumodel/src/gtk/gtkactionobservable.h
new file mode 100644
index 0000000..aa1514b
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkactionobservable.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2011 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/>.
+ *
+ * Authors: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __GTK_ACTION_OBSERVABLE_H__
+#define __GTK_ACTION_OBSERVABLE_H__
+
+#include "gtkactionobserver.h"
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_ACTION_OBSERVABLE (gtk_action_observable_get_type ())
+#define GTK_ACTION_OBSERVABLE(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ GTK_TYPE_ACTION_OBSERVABLE, GtkActionObservable))
+#define GTK_IS_ACTION_OBSERVABLE(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ GTK_TYPE_ACTION_OBSERVABLE))
+#define GTK_ACTION_OBSERVABLE_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \
+ GTK_TYPE_ACTION_OBSERVABLE, \
+ GtkActionObservableInterface))
+
+typedef struct _GtkActionObservableInterface GtkActionObservableInterface;
+
+struct _GtkActionObservableInterface
+{
+ GTypeInterface g_iface;
+
+ void (* register_observer) (GtkActionObservable *observable,
+ const gchar *action_name,
+ GtkActionObserver *observer);
+ void (* unregister_observer) (GtkActionObservable *observable,
+ const gchar *action_name,
+ GtkActionObserver *observer);
+};
+
+GType gtk_action_observable_get_type (void);
+void gtk_action_observable_register_observer (GtkActionObservable *observable,
+ const gchar *action_name,
+ GtkActionObserver *observer);
+void gtk_action_observable_unregister_observer (GtkActionObservable *observable,
+ const gchar *action_name,
+ GtkActionObserver *observer);
+
+G_END_DECLS
+
+#endif /* __GTK_ACTION_OBSERVABLE_H__ */
diff --git a/libqmenumodel/src/gtk/gtkactionobserver.c b/libqmenumodel/src/gtk/gtkactionobserver.c
new file mode 100644
index 0000000..cf70b20
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkactionobserver.c
@@ -0,0 +1,159 @@
+/*
+ * Copyright © 2011 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/>.
+ *
+ * Authors: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "gtkactionobserver.h"
+
+G_DEFINE_INTERFACE (GtkActionObserver, gtk_action_observer, G_TYPE_OBJECT)
+
+/**
+ * SECTION:gtkactionobserver
+ * @short_description: an interface implemented by objects that are
+ * interested in monitoring actions for changes
+ *
+ * GtkActionObserver is a simple interface allowing objects that wish to
+ * be notified of changes to actions to be notified of those changes.
+ *
+ * It is also possible to monitor changes to action groups using
+ * #GObject signals, but there are a number of reasons that this
+ * approach could become problematic:
+ *
+ * - there are four separate signals that must be manually connected
+ * and disconnected
+ *
+ * - when a large number of different observers wish to monitor a
+ * (usually disjoint) set of actions within the same action group,
+ * there is only one way to avoid having all notifications delivered
+ * to all observers: signal detail. In order to use signal detail,
+ * each action name must be quarked, which is not always practical.
+ *
+ * - even if quarking is acceptable, #GObject signal details are
+ * implemented by scanning a linked list, so there is no real
+ * decrease in complexity
+ */
+
+void
+gtk_action_observer_default_init (GtkActionObserverInterface *class)
+{
+}
+
+/**
+ * gtk_action_observer_action_added:
+ * @observer: a #GtkActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ * @enabled: %TRUE if the action is now enabled
+ * @parameter_type: the parameter type for action invocations, or %NULL
+ * if no parameter is required
+ * @state: the current state of the action, or %NULL if the action is
+ * stateless
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for is added.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_added (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ const GVariantType *parameter_type,
+ gboolean enabled,
+ GVariant *state)
+{
+ g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer));
+
+ GTK_ACTION_OBSERVER_GET_IFACE (observer)
+ ->action_added (observer, observable, action_name, parameter_type, enabled, state);
+}
+
+/**
+ * gtk_action_observer_action_enabled_changed:
+ * @observer: a #GtkActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ * @enabled: %TRUE if the action is now enabled
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for becomes enabled or disabled.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_enabled_changed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ gboolean enabled)
+{
+ g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer));
+
+ GTK_ACTION_OBSERVER_GET_IFACE (observer)
+ ->action_enabled_changed (observer, observable, action_name, enabled);
+}
+
+/**
+ * gtk_action_observer_action_state_changed:
+ * @observer: a #GtkActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ * @state: the new state of the action
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for changes to its state.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_state_changed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ GVariant *state)
+{
+ g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer));
+
+ GTK_ACTION_OBSERVER_GET_IFACE (observer)
+ ->action_state_changed (observer, observable, action_name, state);
+}
+
+/**
+ * gtk_action_observer_action_removed:
+ * @observer: a #GtkActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for is removed.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_removed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name)
+{
+ g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer));
+
+ GTK_ACTION_OBSERVER_GET_IFACE (observer)
+ ->action_removed (observer, observable, action_name);
+}
diff --git a/libqmenumodel/src/gtk/gtkactionobserver.h b/libqmenumodel/src/gtk/gtkactionobserver.h
new file mode 100644
index 0000000..83629a7
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkactionobserver.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2011 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/>.
+ *
+ * Authors: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __GTK_ACTION_OBSERVER_H__
+#define __GTK_ACTION_OBSERVER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_ACTION_OBSERVER (gtk_action_observer_get_type ())
+#define GTK_ACTION_OBSERVER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ GTK_TYPE_ACTION_OBSERVER, GtkActionObserver))
+#define GTK_IS_ACTION_OBSERVER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ GTK_TYPE_ACTION_OBSERVER))
+#define GTK_ACTION_OBSERVER_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \
+ GTK_TYPE_ACTION_OBSERVER, GtkActionObserverInterface))
+
+typedef struct _GtkActionObserverInterface GtkActionObserverInterface;
+typedef struct _GtkActionObservable GtkActionObservable;
+typedef struct _GtkActionObserver GtkActionObserver;
+
+struct _GtkActionObserverInterface
+{
+ GTypeInterface g_iface;
+
+ void (* action_added) (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ const GVariantType *parameter_type,
+ gboolean enabled,
+ GVariant *state);
+ void (* action_enabled_changed) (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ gboolean enabled);
+ void (* action_state_changed) (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ GVariant *state);
+ void (* action_removed) (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name);
+};
+
+GType gtk_action_observer_get_type (void);
+void gtk_action_observer_action_added (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ const GVariantType *parameter_type,
+ gboolean enabled,
+ GVariant *state);
+void gtk_action_observer_action_enabled_changed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ gboolean enabled);
+void gtk_action_observer_action_state_changed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ GVariant *state);
+void gtk_action_observer_action_removed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name);
+
+G_END_DECLS
+
+#endif /* __GTK_ACTION_OBSERVER_H__ */
diff --git a/libqmenumodel/src/gtk/gtkmenutracker.c b/libqmenumodel/src/gtk/gtkmenutracker.c
new file mode 100644
index 0000000..ab369ab
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkmenutracker.c
@@ -0,0 +1,495 @@
+/*
+ * 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "gtkmenutracker.h"
+
+/**
+ * SECTION:gtkmenutracker
+ * @Title: GtkMenuTracker
+ * @Short_description: A helper class for interpreting #GMenuModel
+ *
+ * #GtkMenuTracker is a simple object to ease implementations of #GMenuModel.
+ * Given a #GtkActionObservable (usually a #GActionMuxer) along with a
+ * #GMenuModel, it will tell you which menu items to create and where to place
+ * them. If a menu item is removed, it will tell you the position of the menu
+ * item to remove.
+ *
+ * Using #GtkMenuTracker is fairly simple. The only guarantee you must make
+ * to #GtkMenuTracker is that you must obey all insert signals and track the
+ * position of items that #GtkMenuTracker gives you. That is, #GtkMenuTracker
+ * expects positions of all the latter items to change when it calls your
+ * insertion callback with an early position, as it may ask you to remove
+ * an item with a readjusted position later.
+ *
+ * #GtkMenuTracker will give you a #GtkMenuTrackerItem in your callback. You
+ * must hold onto this object until a remove signal is emitted. This item
+ * represents a single menu item, which can be one of three classes: normal item,
+ * separator, or submenu.
+ *
+ * Certain properties on the #GtkMenuTrackerItem are mutable, and you must
+ * listen for changes in the item. For more details, see the documentation
+ * for #GtkMenuTrackerItem along with https://live.gnome.org/GApplication/GMenuModel.
+ *
+ * The idea of @with_separators is for special cases where menu models may
+ * be tracked in places where separators are not available, like in toplevel
+ * "File", "Edit" menu bars. Ignoring separator items is wrong, as #GtkMenuTracker
+ * expects the position to change, so we must tell #GtkMenuTracker to ignore
+ * separators itself.
+ */
+
+typedef struct _GtkMenuTrackerSection GtkMenuTrackerSection;
+
+struct _GtkMenuTracker
+{
+ GtkActionObservable *observable;
+ GtkMenuTrackerInsertFunc insert_func;
+ GtkMenuTrackerRemoveFunc remove_func;
+ gpointer user_data;
+
+ GtkMenuTrackerSection *toplevel;
+};
+
+struct _GtkMenuTrackerSection
+{
+ GMenuModel *model;
+ GSList *items;
+ gchar *action_namespace;
+
+ guint with_separators : 1;
+ guint has_separator : 1;
+
+ gulong handler;
+};
+
+static GtkMenuTrackerSection * gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
+ GMenuModel *model,
+ gboolean with_separators,
+ gint offset,
+ const gchar *action_namespace);
+static void gtk_menu_tracker_section_free (GtkMenuTrackerSection *section);
+
+static GtkMenuTrackerSection *
+gtk_menu_tracker_section_find_model (GtkMenuTrackerSection *section,
+ GMenuModel *model,
+ gint *offset)
+{
+ GSList *item;
+
+ if (section->has_separator)
+ (*offset)++;
+
+ if (section->model == model)
+ return section;
+
+ for (item = section->items; item; item = item->next)
+ {
+ GtkMenuTrackerSection *subsection = item->data;
+
+ if (subsection)
+ {
+ GtkMenuTrackerSection *found_section;
+
+ found_section = gtk_menu_tracker_section_find_model (subsection, model, offset);
+
+ if (found_section)
+ return found_section;
+ }
+ else
+ (*offset)++;
+ }
+
+ return FALSE;
+}
+
+/* this is responsible for syncing the showing of a separator for a
+ * single subsection (and its children).
+ *
+ * we only ever show separators if we have _actual_ children (ie: we do
+ * not show a separator if the section contains only empty child
+ * sections). it's difficult to determine this on-the-fly, so we have
+ * this separate function to come back later and figure it out.
+ *
+ * 'section' is that section.
+ *
+ * 'tracker' is passed in so that we can emit callbacks when we decide
+ * to add/remove separators.
+ *
+ * 'offset' is passed in so we know which position to emit in our
+ * callbacks. ie: if we add a separator right at the top of this
+ * section then we would emit it with this offset. deeper inside, we
+ * adjust accordingly.
+ *
+ * could_have_separator is true in two situations:
+ *
+ * - our parent section had with_separators defined and we are not the
+ * first section (ie: we should add a separator if we have content in
+ * order to divide us from the items above)
+ *
+ * - if we had a 'label' attribute set for this section
+ *
+ * parent_model and parent_index are passed in so that we can give them
+ * to the insertion callback so that it can see the label (and anything
+ * else that happens to be defined on the section).
+ *
+ * we iterate each item in ourselves. for subsections, we recursively
+ * run ourselves to sync separators. after we are done, we notice if we
+ * have any items in us or if we are completely empty and sync if our
+ * separator is shown or not.
+ */
+static gint
+gtk_menu_tracker_section_sync_separators (GtkMenuTrackerSection *section,
+ GtkMenuTracker *tracker,
+ gint offset,
+ gboolean could_have_separator,
+ GMenuModel *parent_model,
+ gint parent_index)
+{
+ gboolean should_have_separator;
+ gint n_items = 0;
+ GSList *item;
+ gint i = 0;
+
+ for (item = section->items; item; item = item->next)
+ {
+ GtkMenuTrackerSection *subsection = item->data;
+
+ if (subsection)
+ {
+ gboolean could_have_separator;
+
+ could_have_separator = (section->with_separators && i > 0) ||
+ g_menu_model_get_item_attribute (section->model, i, "label", "s", NULL);
+
+ n_items += gtk_menu_tracker_section_sync_separators (subsection, tracker, offset + n_items,
+ could_have_separator, section->model, i);
+ }
+ else
+ n_items++;
+
+ i++;
+ }
+
+ should_have_separator = could_have_separator && n_items != 0;
+
+ if (should_have_separator > section->has_separator)
+ {
+ /* Add a separator */
+ GtkMenuTrackerItem *item;
+
+ item = _gtk_menu_tracker_item_new (tracker->observable, parent_model, parent_index, NULL, TRUE);
+ (* tracker->insert_func) (item, offset, tracker->user_data);
+ g_object_unref (item);
+
+ section->has_separator = TRUE;
+ }
+ else if (should_have_separator < section->has_separator)
+ {
+ /* Remove a separator */
+ (* tracker->remove_func) (offset, tracker->user_data);
+ section->has_separator = FALSE;
+ }
+
+ n_items += section->has_separator;
+
+ return n_items;
+}
+
+static gint
+gtk_menu_tracker_section_measure (GtkMenuTrackerSection *section)
+{
+ GSList *item;
+ gint n_items;
+
+ if (section == NULL)
+ return 1;
+
+ n_items = 0;
+
+ if (section->has_separator)
+ n_items++;
+
+ for (item = section->items; item; item = item->next)
+ n_items += gtk_menu_tracker_section_measure (item->data);
+
+ return n_items;
+}
+
+static void
+gtk_menu_tracker_remove_items (GtkMenuTracker *tracker,
+ GSList **change_point,
+ gint offset,
+ gint n_items)
+{
+ gint i;
+
+ for (i = 0; i < n_items; i++)
+ {
+ GtkMenuTrackerSection *subsection;
+ gint n;
+
+ subsection = (*change_point)->data;
+ *change_point = g_slist_delete_link (*change_point, *change_point);
+
+ n = gtk_menu_tracker_section_measure (subsection);
+ gtk_menu_tracker_section_free (subsection);
+
+ while (n--)
+ (* tracker->remove_func) (offset, tracker->user_data);
+ }
+}
+
+static void
+gtk_menu_tracker_add_items (GtkMenuTracker *tracker,
+ GtkMenuTrackerSection *section,
+ GSList **change_point,
+ gint offset,
+ GMenuModel *model,
+ gint position,
+ gint n_items)
+{
+ while (n_items--)
+ {
+ GMenuModel *submenu;
+
+ submenu = g_menu_model_get_item_link (model, position + n_items, G_MENU_LINK_SECTION);
+ g_assert (submenu != model);
+ if (submenu != NULL)
+ {
+ GtkMenuTrackerSection *subsection;
+ gchar *action_namespace = NULL;
+
+ g_menu_model_get_item_attribute (model, position + n_items,
+ G_MENU_ATTRIBUTE_ACTION_NAMESPACE, "s", &action_namespace);
+
+ if (section->action_namespace)
+ {
+ gchar *namespace;
+
+ namespace = g_strjoin (".", section->action_namespace, action_namespace, NULL);
+ subsection = gtk_menu_tracker_section_new (tracker, submenu, FALSE, offset, namespace);
+ g_free (namespace);
+ }
+ else
+ subsection = gtk_menu_tracker_section_new (tracker, submenu, FALSE, offset, section->action_namespace);
+
+ *change_point = g_slist_prepend (*change_point, subsection);
+ g_free (action_namespace);
+ g_object_unref (submenu);
+ }
+ else
+ {
+ GtkMenuTrackerItem *item;
+
+ item = _gtk_menu_tracker_item_new (tracker->observable, model, position + n_items,
+ section->action_namespace, FALSE);
+ (* tracker->insert_func) (item, offset, tracker->user_data);
+ g_object_unref (item);
+
+ *change_point = g_slist_prepend (*change_point, NULL);
+ }
+ }
+}
+
+static void
+gtk_menu_tracker_model_changed (GMenuModel *model,
+ gint position,
+ gint removed,
+ gint added,
+ gpointer user_data)
+{
+ GtkMenuTracker *tracker = user_data;
+ GtkMenuTrackerSection *section;
+ GSList **change_point;
+ gint offset = 0;
+ gint i;
+
+ /* First find which section the changed model corresponds to, and the
+ * position of that section within the overall menu.
+ */
+ section = gtk_menu_tracker_section_find_model (tracker->toplevel, model, &offset);
+
+ /* Next, seek through that section to the change point. This gives us
+ * the correct GSList** to make the change to and also finds the final
+ * offset at which we will make the changes (by measuring the number
+ * of items within each item of the section before the change point).
+ */
+ change_point = &section->items;
+ for (i = 0; i < position; i++)
+ {
+ offset += gtk_menu_tracker_section_measure ((*change_point)->data);
+ change_point = &(*change_point)->next;
+ }
+
+ /* We remove items in order and add items in reverse order. This
+ * means that the offset used for all inserts and removes caused by a
+ * single change will be the same.
+ *
+ * This also has a performance advantage: GtkMenuShell stores the
+ * menu items in a linked list. In the case where we are creating a
+ * menu for the first time, adding the items in reverse order means
+ * that we only ever insert at index zero, prepending the list. This
+ * means that we can populate in O(n) time instead of O(n^2) that we
+ * would do by appending.
+ */
+ gtk_menu_tracker_remove_items (tracker, change_point, offset, removed);
+ gtk_menu_tracker_add_items (tracker, section, change_point, offset, model, position, added);
+
+ /* The offsets for insertion/removal of separators will be all over
+ * the place, however...
+ */
+ gtk_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
+}
+
+static void
+gtk_menu_tracker_section_free (GtkMenuTrackerSection *section)
+{
+ if (section == NULL)
+ return;
+
+ g_signal_handler_disconnect (section->model, section->handler);
+ g_slist_free_full (section->items, (GDestroyNotify) gtk_menu_tracker_section_free);
+ g_free (section->action_namespace);
+ g_object_unref (section->model);
+ g_slice_free (GtkMenuTrackerSection, section);
+}
+
+static GtkMenuTrackerSection *
+gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
+ GMenuModel *model,
+ gboolean with_separators,
+ gint offset,
+ const gchar *action_namespace)
+{
+ GtkMenuTrackerSection *section;
+
+ section = g_slice_new0 (GtkMenuTrackerSection);
+ section->model = g_object_ref (model);
+ section->with_separators = with_separators;
+ section->action_namespace = g_strdup (action_namespace);
+
+ gtk_menu_tracker_add_items (tracker, section, &section->items, offset, model, 0, g_menu_model_get_n_items (model));
+ section->handler = g_signal_connect (model, "items-changed", G_CALLBACK (gtk_menu_tracker_model_changed), tracker);
+
+ return section;
+}
+
+/*< private >
+ * gtk_menu_tracker_new:
+ * @model: the model to flatten
+ * @with_separators: if the toplevel should have separators (ie: TRUE
+ * for menus, FALSE for menubars)
+ * @action_namespace: the passed-in action namespace
+ * @insert_func: insert callback
+ * @remove_func: remove callback
+ * @user_data user data for callbacks
+ *
+ * Creates a GtkMenuTracker for @model, holding a ref on @model for as
+ * long as the tracker is alive.
+ *
+ * This flattens out the model, merging sections and inserting
+ * separators where appropriate. It monitors for changes and performs
+ * updates on the fly. It also handles action_namespace for subsections
+ * (but you will need to handle it yourself for submenus).
+ *
+ * When the tracker is first created, @insert_func will be called many
+ * times to populate the menu with the initial contents of @model
+ * (unless it is empty), before gtk_menu_tracker_new() returns. For
+ * this reason, the menu that is using the tracker ought to be empty
+ * when it creates the tracker.
+ *
+ * Future changes to @model will result in more calls to @insert_func
+ * and @remove_func.
+ *
+ * The position argument to both functions is the linear 0-based
+ * position in the menu at which the item in question should be inserted
+ * or removed.
+ *
+ * For @insert_func, @model and @item_index are used to get the
+ * information about the menu item to insert. @action_namespace is the
+ * action namespace that actions referred to from that item should place
+ * themselves in. Note that if the item is a submenu and the
+ * "action-namespace" attribute is defined on the item, it will _not_ be
+ * applied to the @action_namespace argument as it is meant for the
+ * items inside of the submenu, not the submenu item itself.
+ *
+ * @is_separator is set to %TRUE in case the item being added is a
+ * separator. @model and @item_index will still be meaningfully set in
+ * this case -- to the section menu item corresponding to the separator.
+ * This is useful if the section specifies a label, for example. If
+ * there is an "action-namespace" attribute on this menu item then it
+ * should be ignored by the consumer because #GtkMenuTracker has already
+ * handled it.
+ *
+ * When using #GtkMenuTracker there is no need to hold onto @model or
+ * monitor it for changes. The model will be unreffed when
+ * gtk_menu_tracker_free() is called.
+ */
+GtkMenuTracker *
+gtk_menu_tracker_new (GtkActionObservable *observable,
+ GMenuModel *model,
+ gboolean with_separators,
+ const gchar *action_namespace,
+ GtkMenuTrackerInsertFunc insert_func,
+ GtkMenuTrackerRemoveFunc remove_func,
+ gpointer user_data)
+{
+ GtkMenuTracker *tracker;
+
+ tracker = g_slice_new (GtkMenuTracker);
+ tracker->observable = g_object_ref (observable);
+ tracker->insert_func = insert_func;
+ tracker->remove_func = remove_func;
+ tracker->user_data = user_data;
+
+ tracker->toplevel = gtk_menu_tracker_section_new (tracker, model, with_separators, 0, action_namespace);
+ gtk_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
+
+ return tracker;
+}
+
+GtkMenuTracker *
+gtk_menu_tracker_new_for_item_submenu (GtkMenuTrackerItem *item,
+ GtkMenuTrackerInsertFunc insert_func,
+ GtkMenuTrackerRemoveFunc remove_func,
+ gpointer user_data)
+{
+ return gtk_menu_tracker_new (_gtk_menu_tracker_item_get_observable (item),
+ _gtk_menu_tracker_item_get_submenu (item),
+ TRUE,
+ _gtk_menu_tracker_item_get_submenu_namespace (item),
+ insert_func, remove_func, user_data);
+}
+
+/*< private >
+ * gtk_menu_tracker_free:
+ * @tracker: a #GtkMenuTracker
+ *
+ * Frees the tracker, ...
+ */
+void
+gtk_menu_tracker_free (GtkMenuTracker *tracker)
+{
+ gtk_menu_tracker_section_free (tracker->toplevel);
+ g_object_unref (tracker->observable);
+ g_slice_free (GtkMenuTracker, tracker);
+}
diff --git a/libqmenumodel/src/gtk/gtkmenutracker.h b/libqmenumodel/src/gtk/gtkmenutracker.h
new file mode 100644
index 0000000..96370ad
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkmenutracker.h
@@ -0,0 +1,52 @@
+/*
+ * 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __GTK_MENU_TRACKER_H__
+#define __GTK_MENU_TRACKER_H__
+
+#include "gtkmenutrackeritem.h"
+
+typedef struct _GtkMenuTracker GtkMenuTracker;
+
+typedef void (* GtkMenuTrackerInsertFunc) (GtkMenuTrackerItem *item,
+ gint position,
+ gpointer user_data);
+
+typedef void (* GtkMenuTrackerRemoveFunc) (gint position,
+ gpointer user_data);
+
+
+GtkMenuTracker * gtk_menu_tracker_new (GtkActionObservable *observer,
+ GMenuModel *model,
+ gboolean with_separators,
+ const gchar *action_namespace,
+ GtkMenuTrackerInsertFunc insert_func,
+ GtkMenuTrackerRemoveFunc remove_func,
+ gpointer user_data);
+
+GtkMenuTracker * gtk_menu_tracker_new_for_item_submenu (GtkMenuTrackerItem *item,
+ GtkMenuTrackerInsertFunc insert_func,
+ GtkMenuTrackerRemoveFunc remove_func,
+ gpointer user_data);
+
+void gtk_menu_tracker_free (GtkMenuTracker *tracker);
+
+#endif /* __GTK_MENU_TRACKER_H__ */
diff --git a/libqmenumodel/src/gtk/gtkmenutrackeritem.c b/libqmenumodel/src/gtk/gtkmenutrackeritem.c
new file mode 100644
index 0000000..e2ed1f0
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkmenutrackeritem.c
@@ -0,0 +1,908 @@
+/*
+ * 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "gtkmenutrackeritem.h"
+
+/**
+ * SECTION:gtkmenutrackeritem
+ * @Title: GtkMenuTrackerItem
+ * @Short_description: Small helper for model menu items
+ *
+ * A #GtkMenuTrackerItem is a small helper class used by #GtkMenuTracker to
+ * represent menu items. It has one of three classes: normal item, separator,
+ * or submenu.
+ *
+ * If an item is one of the non-normal classes (submenu, separator), only the
+ * label of the item needs to be respected. Otherwise, all the properties
+ * of the item contribute to the item's appearance and state.
+ *
+ * Implementing the appearance of the menu item is up to toolkits, and certain
+ * toolkits may choose to ignore certain properties, like icon or accel. The
+ * role of the item determines its accessibility role, along with its
+ * decoration if the GtkMenuTrackerItem::toggled property is true. As an
+ * example, if the item has the role %GTK_MENU_TRACKER_ITEM_ROLE_CHECK and
+ * GtkMenuTrackerItem::toggled is %FALSE, its accessible role should be that of
+ * a check menu item, and no decoration should be drawn. But if
+ * GtkMenuTrackerItem::toggled is %TRUE, a checkmark should be drawn.
+ *
+ * All properties except for the two class-determining properties,
+ * GtkMenuTrackerItem::is-separator and GtkMenuTrackerItem::has-submenu are
+ * allowed to change, so listen to the notify signals to update your item's
+ * appearance. When using a GObject library, this can conveniently be done
+ * with g_object_bind_property() and #GBinding, and this is how this is
+ * implemented in GTK+; the appearance side is implemented in #GtkModelMenuItem.
+ *
+ * When an item is clicked, simply call gtk_menu_tracker_item_activated() in
+ * response. The #GtkMenuTrackerItem will take care of everything related to
+ * activating the item and will itself update the state of all items in
+ * response.
+ *
+ * Submenus are a special case of menu item. When an item is a submenu, you
+ * should create a submenu for it with gtk_menu_tracker_new_item_for_submenu(),
+ * and apply the same menu tracking logic you would for a toplevel menu.
+ * Applications using submenus may want to lazily build their submenus in
+ * response to the user clicking on it, as building a submenu may be expensive.
+ *
+ * Thus, the submenu has two special controls -- the submenu's visibility
+ * should be controlled by the GtkMenuTrackerItem::submenu-shown property,
+ * and if a user clicks on the submenu, do not immediately show the menu,
+ * but call gtk_menu_tracker_item_request_submenu_shown() and wait for the
+ * GtkMenuTrackerItem::submenu-shown property to update. If the user navigates,
+ * the application may want to be notified so it can cancel the expensive
+ * operation that it was using to build the submenu. Thus,
+ * gtk_menu_tracker_item_request_submenu_shown() takes a boolean parameter.
+ * Use %TRUE when the user wants to open the submenu, and %FALSE when the
+ * user wants to close the submenu.
+ */
+
+typedef GObjectClass GtkMenuTrackerItemClass;
+
+struct _GtkMenuTrackerItem
+{
+ GObject parent_instance;
+
+ GtkActionObservable *observable;
+ gchar *action_namespace;
+ GMenuItem *item;
+ GtkMenuTrackerItemRole role : 4;
+ guint is_separator : 1;
+ guint can_activate : 1;
+ guint sensitive : 1;
+ guint toggled : 1;
+ guint submenu_shown : 1;
+ guint submenu_requested : 1;
+ GVariant *action_state;
+};
+
+enum {
+ PROP_0,
+ PROP_IS_SEPARATOR,
+ PROP_HAS_SUBMENU,
+ PROP_LABEL,
+ PROP_ICON,
+ PROP_SENSITIVE,
+ PROP_VISIBLE,
+ PROP_ROLE,
+ PROP_TOGGLED,
+ PROP_ACCEL,
+ PROP_SUBMENU_SHOWN,
+ PROP_ACTION_NAME,
+ PROP_ACTION_STATE,
+ N_PROPS
+};
+
+static GParamSpec *gtk_menu_tracker_item_pspecs[N_PROPS];
+
+static void gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface);
+G_DEFINE_TYPE_WITH_CODE (GtkMenuTrackerItem, gtk_menu_tracker_item, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER, gtk_menu_tracker_item_init_observer_iface))
+
+GType
+gtk_menu_tracker_item_role_get_type (void)
+{
+ static gsize gtk_menu_tracker_item_role_type;
+
+ if (g_once_init_enter (&gtk_menu_tracker_item_role_type))
+ {
+ static const GEnumValue values[] = {
+ { GTK_MENU_TRACKER_ITEM_ROLE_NORMAL, "GTK_MENU_TRACKER_ITEM_ROLE_NORMAL", "normal" },
+ { GTK_MENU_TRACKER_ITEM_ROLE_CHECK, "GTK_MENU_TRACKER_ITEM_ROLE_CHECK", "check" },
+ { GTK_MENU_TRACKER_ITEM_ROLE_RADIO, "GTK_MENU_TRACKER_ITEM_ROLE_RADIO", "radio" },
+ { 0, NULL, NULL }
+ };
+ GType type;
+
+ type = g_enum_register_static ("GtkMenuTrackerItemRole", values);
+
+ g_once_init_leave (&gtk_menu_tracker_item_role_type, type);
+ }
+
+ return gtk_menu_tracker_item_role_type;
+}
+
+static void
+gtk_menu_tracker_item_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
+
+ switch (prop_id)
+ {
+ case PROP_IS_SEPARATOR:
+ g_value_set_boolean (value, gtk_menu_tracker_item_get_is_separator (self));
+ break;
+ case PROP_HAS_SUBMENU:
+ g_value_set_boolean (value, gtk_menu_tracker_item_get_has_submenu (self));
+ break;
+ case PROP_LABEL:
+ g_value_set_string (value, gtk_menu_tracker_item_get_label (self));
+ break;
+ case PROP_ICON:
+ g_value_set_object (value, gtk_menu_tracker_item_get_icon (self));
+ break;
+ case PROP_SENSITIVE:
+ g_value_set_boolean (value, gtk_menu_tracker_item_get_sensitive (self));
+ break;
+ case PROP_VISIBLE:
+ g_value_set_boolean (value, gtk_menu_tracker_item_get_visible (self));
+ break;
+ case PROP_ROLE:
+ g_value_set_enum (value, gtk_menu_tracker_item_get_role (self));
+ break;
+ case PROP_TOGGLED:
+ g_value_set_boolean (value, gtk_menu_tracker_item_get_toggled (self));
+ break;
+ case PROP_ACCEL:
+ g_value_set_string (value, gtk_menu_tracker_item_get_accel (self));
+ break;
+ case PROP_SUBMENU_SHOWN:
+ g_value_set_boolean (value, gtk_menu_tracker_item_get_submenu_shown (self));
+ break;
+ case PROP_ACTION_NAME:
+ g_value_set_string (value, gtk_menu_tracker_item_get_action_name (self));
+ case PROP_ACTION_STATE:
+ g_value_set_variant (value, self->action_state);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtk_menu_tracker_item_finalize (GObject *object)
+{
+ GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
+
+ g_free (self->action_namespace);
+
+ if (self->observable)
+ g_object_unref (self->observable);
+
+ if (self->action_state)
+ g_variant_unref (self->action_state);
+
+ g_object_unref (self->item);
+
+ G_OBJECT_CLASS (gtk_menu_tracker_item_parent_class)->finalize (object);
+}
+
+static void
+gtk_menu_tracker_item_init (GtkMenuTrackerItem * self)
+{
+}
+
+static void
+gtk_menu_tracker_item_class_init (GtkMenuTrackerItemClass *class)
+{
+ class->get_property = gtk_menu_tracker_item_get_property;
+ class->finalize = gtk_menu_tracker_item_finalize;
+
+ gtk_menu_tracker_item_pspecs[PROP_IS_SEPARATOR] =
+ g_param_spec_boolean ("is-separator", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_HAS_SUBMENU] =
+ g_param_spec_boolean ("has-submenu", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_LABEL] =
+ g_param_spec_string ("label", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_ICON] =
+ g_param_spec_object ("icon", "", "", G_TYPE_ICON, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_SENSITIVE] =
+ g_param_spec_boolean ("sensitive", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_VISIBLE] =
+ g_param_spec_boolean ("visible", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_ROLE] =
+ g_param_spec_enum ("role", "", "",
+ GTK_TYPE_MENU_TRACKER_ITEM_ROLE, GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_TOGGLED] =
+ g_param_spec_boolean ("toggled", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_ACCEL] =
+ g_param_spec_string ("accel", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN] =
+ g_param_spec_boolean ("submenu-shown", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_ACTION_NAME] =
+ g_param_spec_boolean ("action-name", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ gtk_menu_tracker_item_pspecs[PROP_ACTION_STATE] =
+ g_param_spec_boolean ("action-state", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+
+ g_object_class_install_properties (class, N_PROPS, gtk_menu_tracker_item_pspecs);
+}
+
+static void
+gtk_menu_tracker_item_action_added (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ const GVariantType *parameter_type,
+ gboolean enabled,
+ GVariant *state)
+{
+ GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+ GVariant *action_target;
+
+ action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+
+ self->can_activate = (action_target == NULL && parameter_type == NULL) ||
+ (action_target != NULL && parameter_type != NULL &&
+ g_variant_is_of_type (action_target, parameter_type));
+
+ if (!self->can_activate)
+ {
+ if (action_target)
+ g_variant_unref (action_target);
+ return;
+ }
+
+ self->sensitive = enabled;
+
+ if (action_target != NULL && state != NULL)
+ {
+ self->toggled = g_variant_equal (state, action_target);
+ self->role = GTK_MENU_TRACKER_ITEM_ROLE_RADIO;
+ }
+
+ else if (state != NULL && g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+ {
+ self->toggled = g_variant_get_boolean (state);
+ self->role = GTK_MENU_TRACKER_ITEM_ROLE_CHECK;
+ }
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ if (self->sensitive)
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+
+ if (self->toggled)
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+
+ if (self->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ROLE]);
+
+ if (state != NULL)
+ {
+ self->action_state = g_variant_ref (state);
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ACTION_STATE]);
+ }
+
+ g_object_thaw_notify (G_OBJECT (self));
+
+ if (action_target)
+ g_variant_unref (action_target);
+}
+
+static void
+gtk_menu_tracker_item_action_enabled_changed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ gboolean enabled)
+{
+ GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+
+ if (!self->can_activate)
+ return;
+
+ if (self->sensitive == enabled)
+ return;
+
+ self->sensitive = enabled;
+
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+}
+
+static void
+gtk_menu_tracker_item_action_state_changed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name,
+ GVariant *state)
+{
+ GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+ GVariant *action_target;
+ gboolean was_toggled;
+
+ if (!self->can_activate)
+ return;
+
+ action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+ was_toggled = self->toggled;
+
+ if (action_target)
+ {
+ self->toggled = g_variant_equal (state, action_target);
+ g_variant_unref (action_target);
+ }
+
+ else if (g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+ self->toggled = g_variant_get_boolean (state);
+
+ else
+ self->toggled = FALSE;
+
+ if (self->toggled != was_toggled)
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+
+ if (self->action_state)
+ g_variant_unref (self->action_state);
+ self->action_state = g_variant_ref (state);
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ACTION_STATE]);
+}
+
+static void
+gtk_menu_tracker_item_action_removed (GtkActionObserver *observer,
+ GtkActionObservable *observable,
+ const gchar *action_name)
+{
+ GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+
+ if (!self->can_activate)
+ return;
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ if (self->sensitive)
+ {
+ self->sensitive = FALSE;
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+ }
+
+ if (self->toggled)
+ {
+ self->toggled = FALSE;
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+ }
+
+ if (self->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
+ {
+ self->role = GTK_MENU_TRACKER_ITEM_ROLE_NORMAL;
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ROLE]);
+ }
+
+ if (self->action_state != NULL)
+ {
+ g_variant_unref (self->action_state);
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ACTION_STATE]);
+ }
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface)
+{
+ iface->action_added = gtk_menu_tracker_item_action_added;
+ iface->action_enabled_changed = gtk_menu_tracker_item_action_enabled_changed;
+ iface->action_state_changed = gtk_menu_tracker_item_action_state_changed;
+ iface->action_removed = gtk_menu_tracker_item_action_removed;
+}
+
+GtkMenuTrackerItem *
+_gtk_menu_tracker_item_new (GtkActionObservable *observable,
+ GMenuModel *model,
+ gint item_index,
+ const gchar *action_namespace,
+ gboolean is_separator)
+{
+ GtkMenuTrackerItem *self;
+ const gchar *action_name;
+
+ g_return_val_if_fail (GTK_IS_ACTION_OBSERVABLE (observable), NULL);
+ g_return_val_if_fail (G_IS_MENU_MODEL (model), NULL);
+
+ self = g_object_new (GTK_TYPE_MENU_TRACKER_ITEM, NULL);
+ self->item = g_menu_item_new_from_model (model, item_index);
+ self->action_namespace = g_strdup (action_namespace);
+ self->observable = g_object_ref (observable);
+ self->is_separator = is_separator;
+
+ if (!is_separator && g_menu_item_get_attribute (self->item, "action", "&s", &action_name))
+ {
+ GActionGroup *group = G_ACTION_GROUP (observable);
+ const GVariantType *parameter_type;
+ gboolean enabled;
+ GVariant *state;
+ gboolean found;
+
+ state = NULL;
+
+ if (action_namespace)
+ {
+ gchar *full_action;
+
+ full_action = g_strjoin (".", action_namespace, action_name, NULL);
+ gtk_action_observable_register_observer (self->observable, full_action, GTK_ACTION_OBSERVER (self));
+ found = g_action_group_query_action (group, full_action, &enabled, &parameter_type, NULL, NULL, &state);
+ g_free (full_action);
+ }
+ else
+ {
+ gtk_action_observable_register_observer (self->observable, action_name, GTK_ACTION_OBSERVER (self));
+ found = g_action_group_query_action (group, action_name, &enabled, &parameter_type, NULL, NULL, &state);
+ }
+
+ if (found)
+ gtk_menu_tracker_item_action_added (GTK_ACTION_OBSERVER (self), observable, NULL, parameter_type, enabled, state);
+ else
+ gtk_menu_tracker_item_action_removed (GTK_ACTION_OBSERVER (self), observable, NULL);
+
+ if (state)
+ g_variant_unref (state);
+ }
+ else
+ self->sensitive = TRUE;
+
+ return self;
+}
+
+GtkActionObservable *
+_gtk_menu_tracker_item_get_observable (GtkMenuTrackerItem *self)
+{
+ return self->observable;
+}
+
+/**
+ * gtk_menu_tracker_item_get_is_separator:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns whether the menu item is a separator. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #GtkMenuTrackerItem.
+ */
+gboolean
+gtk_menu_tracker_item_get_is_separator (GtkMenuTrackerItem *self)
+{
+ return self->is_separator;
+}
+
+/**
+ * gtk_menu_tracker_item_get_has_submenu:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns whether the menu item has a submenu. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #GtkMenuTrackerItem.
+ */
+gboolean
+gtk_menu_tracker_item_get_has_submenu (GtkMenuTrackerItem *self)
+{
+ GMenuModel *link;
+
+ link = g_menu_item_get_link (self->item, G_MENU_LINK_SUBMENU);
+
+ if (link)
+ {
+ g_object_unref (link);
+ return TRUE;
+ }
+ else
+ return FALSE;
+}
+
+const gchar *
+gtk_menu_tracker_item_get_label (GtkMenuTrackerItem *self)
+{
+ const gchar *label = NULL;
+
+ g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_LABEL, "&s", &label);
+
+ return label;
+}
+
+/**
+ * gtk_menu_tracker_item_get_icon:
+ *
+ * Returns: (transfer full):
+ */
+GIcon *
+gtk_menu_tracker_item_get_icon (GtkMenuTrackerItem *self)
+{
+ GVariant *icon_data;
+ GIcon *icon;
+
+ icon_data = g_menu_item_get_attribute_value (self->item, "icon", NULL);
+
+ if (icon_data == NULL)
+ return NULL;
+
+ icon = g_icon_deserialize (icon_data);
+ g_variant_unref (icon_data);
+
+ return icon;
+}
+
+gboolean
+gtk_menu_tracker_item_get_sensitive (GtkMenuTrackerItem *self)
+{
+ return self->sensitive;
+}
+
+gboolean
+gtk_menu_tracker_item_get_visible (GtkMenuTrackerItem *self)
+{
+ return TRUE;
+}
+
+GtkMenuTrackerItemRole
+gtk_menu_tracker_item_get_role (GtkMenuTrackerItem *self)
+{
+ return self->role;
+}
+
+gboolean
+gtk_menu_tracker_item_get_toggled (GtkMenuTrackerItem *self)
+{
+ return self->toggled;
+}
+
+const gchar *
+gtk_menu_tracker_item_get_accel (GtkMenuTrackerItem *self)
+{
+ const gchar *accel = NULL;
+
+ g_menu_item_get_attribute (self->item, "accel", "&s", &accel);
+
+ return accel;
+}
+
+GMenuModel *
+_gtk_menu_tracker_item_get_submenu (GtkMenuTrackerItem *self)
+{
+ return g_menu_item_get_link (self->item, "submenu");
+}
+
+gchar *
+_gtk_menu_tracker_item_get_submenu_namespace (GtkMenuTrackerItem *self)
+{
+ const gchar *namespace;
+
+ if (g_menu_item_get_attribute (self->item, "action-namespace", "&s", &namespace))
+ {
+ if (self->action_namespace)
+ return g_strjoin (".", self->action_namespace, namespace, NULL);
+ else
+ return g_strdup (namespace);
+ }
+ else
+ return g_strdup (self->action_namespace);
+}
+
+gboolean
+gtk_menu_tracker_item_get_should_request_show (GtkMenuTrackerItem *self)
+{
+ return g_menu_item_get_attribute (self->item, "submenu-action", "&s", NULL);
+}
+
+gboolean
+gtk_menu_tracker_item_get_submenu_shown (GtkMenuTrackerItem *self)
+{
+ return self->submenu_shown;
+}
+
+/**
+ * gtk_menu_tracker_item_get_action_name:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns the action name
+ */
+const gchar *
+gtk_menu_tracker_item_get_action_name (GtkMenuTrackerItem *self)
+{
+ const gchar *action_name = NULL;
+
+ g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_ACTION, "&s", &action_name);
+
+ return action_name;
+}
+
+GVariant *
+gtk_menu_tracker_item_get_action_state (GtkMenuTrackerItem *self)
+{
+ if (self->action_state != NULL)
+ return g_variant_ref (self->action_state);
+
+ return NULL;
+}
+
+static void
+gtk_menu_tracker_item_set_submenu_shown (GtkMenuTrackerItem *self,
+ gboolean submenu_shown)
+{
+ if (submenu_shown == self->submenu_shown)
+ return;
+
+ self->submenu_shown = submenu_shown;
+ g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN]);
+}
+
+void
+gtk_menu_tracker_item_activated (GtkMenuTrackerItem *self)
+{
+ const gchar *action_name;
+ GVariant *action_target;
+
+ g_return_if_fail (GTK_IS_MENU_TRACKER_ITEM (self));
+
+ if (!self->can_activate)
+ return;
+
+ g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_ACTION, "&s", &action_name);
+ action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+
+ if (self->action_namespace)
+ {
+ gchar *full_action;
+
+ full_action = g_strjoin (".", self->action_namespace, action_name, NULL);
+ g_action_group_activate_action (G_ACTION_GROUP (self->observable), full_action, action_target);
+ g_free (full_action);
+ }
+ else
+ g_action_group_activate_action (G_ACTION_GROUP (self->observable), action_name, action_target);
+
+ if (action_target)
+ g_variant_unref (action_target);
+}
+
+void
+gtk_menu_tracker_item_change_state (GtkMenuTrackerItem *self,
+ GVariant *value)
+{
+ const gchar *action_name;
+
+ g_return_if_fail (GTK_IS_MENU_TRACKER_ITEM (self));
+
+ g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_ACTION, "&s", &action_name);
+
+ if (self->action_namespace)
+ {
+ gchar *full_action;
+
+ full_action = g_strjoin (".", self->action_namespace, action_name, NULL);
+ g_action_group_change_action_state (G_ACTION_GROUP (self->observable), full_action, g_variant_ref(value));
+ g_free (full_action);
+ }
+ else
+ g_action_group_change_action_state (G_ACTION_GROUP (self->observable), action_name, g_variant_ref(value));
+}
+
+typedef struct
+{
+ GtkMenuTrackerItem *item;
+ gchar *submenu_action;
+ gboolean first_time;
+} GtkMenuTrackerOpener;
+
+static void
+gtk_menu_tracker_opener_update (GtkMenuTrackerOpener *opener)
+{
+ GActionGroup *group = G_ACTION_GROUP (opener->item->observable);
+ gboolean is_open = TRUE;
+
+ /* We consider the menu as being "open" if the action does not exist
+ * or if there is another problem (no state, wrong state type, etc.).
+ * If the action exists, with the correct state then we consider it
+ * open if we have ever seen this state equal to TRUE.
+ *
+ * In the event that we see the state equal to FALSE, we force it back
+ * to TRUE. We do not signal that the menu was closed because this is
+ * likely to create UI thrashing.
+ *
+ * The only way the menu can have a true-to-false submenu-shown
+ * transition is if the user calls _request_submenu_shown (FALSE).
+ * That is handled in _free() below.
+ */
+
+ if (g_action_group_has_action (group, opener->submenu_action))
+ {
+ GVariant *state = g_action_group_get_action_state (group, opener->submenu_action);
+
+ if (state)
+ {
+ if (g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+ is_open = g_variant_get_boolean (state);
+ g_variant_unref (state);
+ }
+ }
+
+ /* If it is already open, signal that.
+ *
+ * If it is not open, ask it to open.
+ */
+ if (is_open)
+ gtk_menu_tracker_item_set_submenu_shown (opener->item, TRUE);
+
+ if (!is_open || opener->first_time)
+ {
+ g_action_group_change_action_state (group, opener->submenu_action, g_variant_new_boolean (TRUE));
+ opener->first_time = FALSE;
+ }
+}
+
+static void
+gtk_menu_tracker_opener_added (GActionGroup *group,
+ const gchar *action_name,
+ gpointer user_data)
+{
+ GtkMenuTrackerOpener *opener = user_data;
+
+ if (g_str_equal (action_name, opener->submenu_action))
+ gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_removed (GActionGroup *action_group,
+ const gchar *action_name,
+ gpointer user_data)
+{
+ GtkMenuTrackerOpener *opener = user_data;
+
+ if (g_str_equal (action_name, opener->submenu_action))
+ gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_changed (GActionGroup *action_group,
+ const gchar *action_name,
+ GVariant *new_state,
+ gpointer user_data)
+{
+ GtkMenuTrackerOpener *opener = user_data;
+
+ if (g_str_equal (action_name, opener->submenu_action))
+ gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_free (gpointer data)
+{
+ GtkMenuTrackerOpener *opener = data;
+
+ g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_added, opener);
+ g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_removed, opener);
+ g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_changed, opener);
+
+ g_action_group_change_action_state (G_ACTION_GROUP (opener->item->observable),
+ opener->submenu_action,
+ g_variant_new_boolean (FALSE));
+
+ gtk_menu_tracker_item_set_submenu_shown (opener->item, FALSE);
+
+ g_free (opener->submenu_action);
+
+ g_slice_free (GtkMenuTrackerOpener, opener);
+}
+
+static GtkMenuTrackerOpener *
+gtk_menu_tracker_opener_new (GtkMenuTrackerItem *item,
+ const gchar *submenu_action)
+{
+ GtkMenuTrackerOpener *opener;
+
+ opener = g_slice_new (GtkMenuTrackerOpener);
+ opener->first_time = TRUE;
+ opener->item = item;
+
+ if (item->action_namespace)
+ opener->submenu_action = g_strjoin (".", item->action_namespace, submenu_action, NULL);
+ else
+ opener->submenu_action = g_strdup (submenu_action);
+
+ g_signal_connect (item->observable, "action-added", G_CALLBACK (gtk_menu_tracker_opener_added), opener);
+ g_signal_connect (item->observable, "action-removed", G_CALLBACK (gtk_menu_tracker_opener_removed), opener);
+ g_signal_connect (item->observable, "action-state-changed", G_CALLBACK (gtk_menu_tracker_opener_changed), opener);
+
+ gtk_menu_tracker_opener_update (opener);
+
+ return opener;
+}
+
+void
+gtk_menu_tracker_item_request_submenu_shown (GtkMenuTrackerItem *self,
+ gboolean shown)
+{
+ const gchar *submenu_action;
+ gboolean has_submenu_action;
+
+ if (shown == self->submenu_requested)
+ return;
+
+ has_submenu_action = g_menu_item_get_attribute (self->item, "submenu-action", "&s", &submenu_action);
+
+ self->submenu_requested = shown;
+
+ /* If we have a submenu action, start a submenu opener and wait
+ * for the reply from the client. Otherwise, simply open the
+ * submenu immediately.
+ */
+ if (has_submenu_action)
+ {
+ if (shown)
+ g_object_set_data_full (G_OBJECT (self), "submenu-opener",
+ gtk_menu_tracker_opener_new (self, submenu_action),
+ gtk_menu_tracker_opener_free);
+ else
+ g_object_set_data (G_OBJECT (self), "submenu-opener", NULL);
+ }
+ else
+ gtk_menu_tracker_item_set_submenu_shown (self, shown);
+}
+
+gboolean
+gtk_menu_tracker_item_get_attribute (GtkMenuTrackerItem *self,
+ const gchar *attribute,
+ const gchar *format,
+ ...)
+{
+ gboolean success = FALSE;
+ GVariant *value;
+
+ g_return_val_if_fail (GTK_IS_MENU_TRACKER_ITEM (self), FALSE);
+ g_return_val_if_fail (attribute != NULL, FALSE);
+ g_return_val_if_fail (format != NULL, FALSE);
+
+ value = g_menu_item_get_attribute_value (self->item, attribute, NULL);
+ if (value)
+ {
+ if (g_variant_check_format_string (value, format, TRUE))
+ {
+ va_list args;
+
+ va_start (args, format);
+ g_variant_get_va (value, format, NULL, &args);
+ va_end (args);
+
+ success = TRUE;
+ }
+
+ g_variant_unref (value);
+ }
+
+ return success;
+}
+
+GVariant *
+gtk_menu_tracker_item_get_attribute_value (GtkMenuTrackerItem *self,
+ const gchar *attribute,
+ const GVariantType *expected_type)
+{
+ return g_menu_item_get_attribute_value (self->item, attribute, expected_type);
+}
diff --git a/libqmenumodel/src/gtk/gtkmenutrackeritem.h b/libqmenumodel/src/gtk/gtkmenutrackeritem.h
new file mode 100644
index 0000000..59a7080
--- /dev/null
+++ b/libqmenumodel/src/gtk/gtkmenutrackeritem.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright © 2011, 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>
+ */
+
+#ifndef __GTK_MENU_TRACKER_ITEM_H__
+#define __GTK_MENU_TRACKER_ITEM_H__
+
+#include "gtkactionobservable.h"
+
+#define GTK_TYPE_MENU_TRACKER_ITEM (gtk_menu_tracker_item_get_type ())
+#define GTK_MENU_TRACKER_ITEM(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ GTK_TYPE_MENU_TRACKER_ITEM, GtkMenuTrackerItem))
+#define GTK_IS_MENU_TRACKER_ITEM(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ GTK_TYPE_MENU_TRACKER_ITEM))
+
+typedef struct _GtkMenuTrackerItem GtkMenuTrackerItem;
+
+#define GTK_TYPE_MENU_TRACKER_ITEM_ROLE (gtk_menu_tracker_item_role_get_type ())
+
+typedef enum {
+ GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
+ GTK_MENU_TRACKER_ITEM_ROLE_CHECK,
+ GTK_MENU_TRACKER_ITEM_ROLE_RADIO,
+} GtkMenuTrackerItemRole;
+
+GType gtk_menu_tracker_item_get_type (void) G_GNUC_CONST;
+
+GType gtk_menu_tracker_item_role_get_type (void) G_GNUC_CONST;
+
+GtkMenuTrackerItem * _gtk_menu_tracker_item_new (GtkActionObservable *observable,
+ GMenuModel *model,
+ gint item_index,
+ const gchar *action_namespace,
+ gboolean is_separator);
+
+GtkActionObservable * _gtk_menu_tracker_item_get_observable (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_is_separator (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_has_submenu (GtkMenuTrackerItem *self);
+
+const gchar * gtk_menu_tracker_item_get_label (GtkMenuTrackerItem *self);
+
+GIcon * gtk_menu_tracker_item_get_icon (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_sensitive (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_visible (GtkMenuTrackerItem *self);
+
+GtkMenuTrackerItemRole gtk_menu_tracker_item_get_role (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_toggled (GtkMenuTrackerItem *self);
+
+const gchar * gtk_menu_tracker_item_get_accel (GtkMenuTrackerItem *self);
+
+GMenuModel * _gtk_menu_tracker_item_get_submenu (GtkMenuTrackerItem *self);
+
+gchar * _gtk_menu_tracker_item_get_submenu_namespace (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_should_request_show (GtkMenuTrackerItem *self);
+
+void gtk_menu_tracker_item_activated (GtkMenuTrackerItem *self);
+
+void gtk_menu_tracker_item_change_state (GtkMenuTrackerItem *self,
+ GVariant *value);
+
+
+
+
+void gtk_menu_tracker_item_request_submenu_shown (GtkMenuTrackerItem *self,
+ gboolean shown);
+
+gboolean gtk_menu_tracker_item_get_submenu_shown (GtkMenuTrackerItem *self);
+
+const gchar * gtk_menu_tracker_item_get_action_name (GtkMenuTrackerItem *self);
+
+GVariant * gtk_menu_tracker_item_get_action_state (GtkMenuTrackerItem *self);
+
+gboolean gtk_menu_tracker_item_get_attribute (GtkMenuTrackerItem *self,
+ const gchar *attribute,
+ const gchar *format,
+ ...);
+
+GVariant * gtk_menu_tracker_item_get_attribute_value (GtkMenuTrackerItem *self,
+ const gchar *attribute,
+ const GVariantType *expected_type);
+
+#endif
diff --git a/libqmenumodel/src/unitymenumodel.cpp b/libqmenumodel/src/unitymenumodel.cpp
new file mode 100644
index 0000000..3ea61ec
--- /dev/null
+++ b/libqmenumodel/src/unitymenumodel.cpp
@@ -0,0 +1,700 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+#include "unitymenumodel.h"
+#include "converter.h"
+#include "actionstateparser.h"
+#include "unitymenumodelevents.h"
+
+#include <QIcon>
+#include <QQmlComponent>
+#include <QCoreApplication>
+
+extern "C" {
+ #include "gtk/gtkactionmuxer.h"
+ #include "gtk/gtkmenutracker.h"
+}
+
+G_DEFINE_QUARK (UNITY_MENU_MODEL, unity_menu_model)
+G_DEFINE_QUARK (UNITY_SUBMENU_MODEL, unity_submenu_model)
+G_DEFINE_QUARK (UNITY_MENU_ITEM_EXTENDED_ATTRIBUTES, unity_menu_item_extended_attributes)
+
+enum MenuRoles {
+ LabelRole = Qt::DisplayRole + 1,
+ SensitiveRole,
+ IsSeparatorRole,
+ IconRole,
+ TypeRole,
+ ExtendedAttributesRole,
+ ActionRole,
+ ActionStateRole,
+ IsCheckRole,
+ IsRadioRole,
+ IsToggledRole
+};
+
+class UnityMenuModelPrivate
+{
+public:
+ UnityMenuModelPrivate(UnityMenuModel *model);
+ ~UnityMenuModelPrivate();
+
+ void clearItems(bool resetModel=true);
+ void clearName();
+ void updateActions();
+ void updateMenuModel();
+ QVariant itemState(GtkMenuTrackerItem *item);
+
+ UnityMenuModel *model;
+ GtkActionMuxer *muxer;
+ GtkMenuTracker *menutracker;
+ GSequence *items;
+ GDBusConnection *connection;
+ QByteArray busName;
+ QByteArray nameOwner;
+ guint nameWatchId;
+ QVariantMap actions;
+ QByteArray menuObjectPath;
+ QHash<QByteArray, int> roles;
+ ActionStateParser* actionStateParser;
+
+ static void nameAppeared(GDBusConnection *connection, const gchar *name, const gchar *owner, gpointer user_data);
+ static void nameVanished(GDBusConnection *connection, const gchar *name, gpointer user_data);
+ static void menuItemInserted(GtkMenuTrackerItem *item, gint position, gpointer user_data);
+ static void menuItemRemoved(gint position, gpointer user_data);
+ static void menuItemChanged(GObject *object, GParamSpec *pspec, gpointer user_data);
+};
+
+void menu_item_free (gpointer data)
+{
+ GtkMenuTrackerItem *item = (GtkMenuTrackerItem *) data;
+
+ g_signal_handlers_disconnect_by_func (item, (gpointer) UnityMenuModelPrivate::menuItemChanged, NULL);
+ g_object_unref (item);
+}
+
+UnityMenuModelPrivate::UnityMenuModelPrivate(UnityMenuModel *model)
+{
+ this->model = model;
+ this->menutracker = NULL;
+ this->connection = NULL;
+ this->nameWatchId = 0;
+ this->actionStateParser = new ActionStateParser(model);
+
+ this->muxer = gtk_action_muxer_new ();
+
+ this->items = g_sequence_new (menu_item_free);
+}
+
+UnityMenuModelPrivate::~UnityMenuModelPrivate()
+{
+ this->clearItems(false);
+
+ g_clear_pointer (&this->menutracker, gtk_menu_tracker_free);
+ g_clear_object (&this->muxer);
+ g_clear_object (&this->connection);
+
+ if (this->nameWatchId)
+ g_bus_unwatch_name (this->nameWatchId);
+}
+
+void UnityMenuModelPrivate::clearItems(bool resetModel)
+{
+ UnityMenuModelClearEvent ummce(resetModel);
+ QCoreApplication::sendEvent(model, &ummce);
+}
+
+void UnityMenuModelPrivate::clearName()
+{
+ this->clearItems();
+
+ this->nameOwner = QByteArray();
+
+ this->updateActions();
+ this->updateMenuModel();
+}
+
+void UnityMenuModelPrivate::updateActions()
+{
+ Q_FOREACH (QString prefix, this->actions.keys())
+ gtk_action_muxer_remove (this->muxer, prefix.toUtf8());
+
+ if (this->nameOwner.isEmpty())
+ return;
+
+ for (QVariantMap::const_iterator it = this->actions.constBegin(); it != this->actions.constEnd(); ++it) {
+ GDBusActionGroup *actions;
+
+ actions = g_dbus_action_group_get (this->connection, this->nameOwner, it.value().toByteArray());
+ gtk_action_muxer_insert (this->muxer, it.key().toUtf8(), G_ACTION_GROUP (actions));
+
+ g_object_unref (actions);
+ }
+}
+
+void UnityMenuModelPrivate::updateMenuModel()
+{
+ this->clearItems();
+ g_clear_pointer (&this->menutracker, gtk_menu_tracker_free);
+
+ if (!this->nameOwner.isEmpty()) {
+ GDBusMenuModel *menu;
+
+ menu = g_dbus_menu_model_get (this->connection, this->nameOwner, this->menuObjectPath.constData());
+ this->menutracker = gtk_menu_tracker_new (GTK_ACTION_OBSERVABLE (this->muxer),
+ G_MENU_MODEL (menu), TRUE, NULL,
+ menuItemInserted, menuItemRemoved, this);
+
+ g_object_unref (menu);
+ }
+}
+
+QVariant UnityMenuModelPrivate::itemState(GtkMenuTrackerItem *item)
+{
+ QVariant result;
+
+ GVariant *state = gtk_menu_tracker_item_get_action_state (item);
+ if (state != NULL) {
+ if (actionStateParser != NULL) {
+ result = actionStateParser->toQVariant(state);
+ }
+ g_variant_unref (state);
+ }
+
+ return result;
+}
+
+void UnityMenuModelPrivate::nameAppeared(GDBusConnection *connection, const gchar *name, const gchar *owner, gpointer user_data)
+{
+ UnityMenuModelPrivate *priv = (UnityMenuModelPrivate *)user_data;
+
+ priv->connection = (GDBusConnection *) g_object_ref (connection);
+ priv->nameOwner = owner;
+
+ priv->updateActions();
+ priv->updateMenuModel();
+}
+
+void UnityMenuModelPrivate::nameVanished(GDBusConnection *connection, const gchar *name, gpointer user_data)
+{
+ UnityMenuModelPrivate *priv = (UnityMenuModelPrivate *)user_data;
+
+ priv->clearName();
+}
+
+void UnityMenuModelPrivate::menuItemInserted(GtkMenuTrackerItem *item, gint position, gpointer user_data)
+{
+ UnityMenuModelPrivate *priv = (UnityMenuModelPrivate *)user_data;
+
+ UnityMenuModelAddRowEvent ummare(item, position);
+ QCoreApplication::sendEvent(priv->model, &ummare);
+}
+
+void UnityMenuModelPrivate::menuItemRemoved(gint position, gpointer user_data)
+{
+ UnityMenuModelPrivate *priv = (UnityMenuModelPrivate *)user_data;
+
+ UnityMenuModelRemoveRowEvent ummrre(position);
+ QCoreApplication::sendEvent(priv->model, &ummrre);
+}
+
+void UnityMenuModelPrivate::menuItemChanged(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+ GSequenceIter *it = (GSequenceIter *) user_data;
+ GtkMenuTrackerItem *item;
+ GtkActionObservable *muxer;
+ UnityMenuModel *model;
+ gint position;
+
+ item = (GtkMenuTrackerItem *) g_sequence_get (it);
+ muxer = _gtk_menu_tracker_item_get_observable (item);
+ model = (UnityMenuModel *) g_object_get_qdata (G_OBJECT (item), unity_menu_model_quark ());
+ position = g_sequence_iter_get_position (it);
+
+ UnityMenuModelDataChangeEvent ummdce(position);
+ QCoreApplication::sendEvent(model, &ummdce);
+}
+
+UnityMenuModel::UnityMenuModel(QObject *parent):
+ QAbstractListModel(parent)
+{
+ priv = new UnityMenuModelPrivate(this);
+}
+
+UnityMenuModel::~UnityMenuModel()
+{
+ delete priv;
+}
+
+QByteArray UnityMenuModel::busName() const
+{
+ return priv->busName;
+}
+
+void UnityMenuModel::setBusName(const QByteArray &name)
+{
+ priv->clearName();
+
+ if (priv->nameWatchId)
+ g_bus_unwatch_name (priv->nameWatchId);
+
+ priv->nameWatchId = g_bus_watch_name (G_BUS_TYPE_SESSION, name.constData(), G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ UnityMenuModelPrivate::nameAppeared, UnityMenuModelPrivate::nameVanished,
+ priv, NULL);
+ priv->busName = name;
+}
+
+QVariantMap UnityMenuModel::actions() const
+{
+ return priv->actions;
+}
+
+void UnityMenuModel::setActions(const QVariantMap &actions)
+{
+ priv->actions = actions;
+ priv->updateActions();
+}
+
+QByteArray UnityMenuModel::menuObjectPath() const
+{
+ return priv->menuObjectPath;
+}
+
+void UnityMenuModel::setMenuObjectPath(const QByteArray &path)
+{
+ priv->menuObjectPath = path;
+ priv->updateMenuModel();
+}
+
+ActionStateParser* UnityMenuModel::actionStateParser() const
+{
+ return priv->actionStateParser;
+}
+
+void UnityMenuModel::setActionStateParser(ActionStateParser* actionStateParser)
+{
+ if (priv->actionStateParser != actionStateParser) {
+ if (priv->actionStateParser && priv->actionStateParser->parent() == this) {
+ delete priv->actionStateParser;
+ }
+ priv->actionStateParser = actionStateParser;
+ Q_EMIT actionStateParserChanged(actionStateParser);
+ }
+}
+
+int UnityMenuModel::rowCount(const QModelIndex &parent) const
+{
+ return !parent.isValid() ? g_sequence_get_length (priv->items) : 0;
+}
+
+int UnityMenuModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+static QString iconUri(GIcon *icon)
+{
+ QString uri;
+
+ if (G_IS_THEMED_ICON (icon)) {
+ const gchar* const* iconNames = g_themed_icon_get_names (G_THEMED_ICON (icon));
+ guint index = 0;
+ while(iconNames[index] != NULL) {
+ if (QIcon::hasThemeIcon(iconNames[index])) {
+ uri = QString("image://theme/") + iconNames[index];
+ break;
+ }
+ index++;
+ }
+ }
+ else if (G_IS_FILE_ICON (icon)) {
+ GFile *file;
+
+ file = g_file_icon_get_file (G_FILE_ICON (icon));
+ if (g_file_is_native (file)) {
+ gchar *fileuri;
+
+ fileuri = g_file_get_path (file);
+ uri = QString(fileuri);
+
+ g_free (fileuri);
+ }
+ }
+ else if (G_IS_BYTES_ICON (icon)) {
+ gsize size;
+ gconstpointer data;
+ gchar *base64;
+
+ data = g_bytes_get_data (g_bytes_icon_get_bytes (G_BYTES_ICON (icon)), &size);
+ base64 = g_base64_encode ((const guchar *) data, size);
+
+ uri = QString("data://");
+ uri.append (base64);
+
+ g_free (base64);
+ }
+
+ return uri;
+}
+
+QVariant UnityMenuModel::data(const QModelIndex &index, int role) const
+{
+ GtkMenuTrackerItem *item;
+
+ item = (GtkMenuTrackerItem *) g_sequence_get (g_sequence_get_iter_at_pos (priv->items, index.row()));
+
+ switch (role) {
+ case LabelRole:
+ return gtk_menu_tracker_item_get_label (item);
+
+ case SensitiveRole:
+ return gtk_menu_tracker_item_get_sensitive (item) == TRUE ? true : false;
+
+ case IsSeparatorRole:
+ return gtk_menu_tracker_item_get_is_separator (item) == TRUE ? true : false;
+
+ case IconRole: {
+ GIcon *icon = gtk_menu_tracker_item_get_icon (item);
+ if (icon) {
+ QString uri = iconUri(icon);
+ g_object_unref (icon);
+ return uri;
+ }
+ else
+ return QString();
+ }
+
+ case TypeRole: {
+ gchar *type;
+ if (gtk_menu_tracker_item_get_attribute (item, "x-canonical-type", "s", &type)) {
+ QVariant v(type);
+ g_free (type);
+ return v;
+ }
+ else
+ return QVariant();
+ }
+
+ case ExtendedAttributesRole: {
+ QVariantMap *map = (QVariantMap *) g_object_get_qdata (G_OBJECT (item), unity_menu_item_extended_attributes_quark ());
+ return map ? *map : QVariant();
+ }
+
+ case ActionRole:
+ return gtk_menu_tracker_item_get_action_name (item);
+
+ case ActionStateRole:
+ return priv->itemState(item);
+
+ case IsCheckRole:
+ return gtk_menu_tracker_item_get_role (item) == GTK_MENU_TRACKER_ITEM_ROLE_CHECK;
+
+ case IsRadioRole:
+ return gtk_menu_tracker_item_get_role (item) == GTK_MENU_TRACKER_ITEM_ROLE_RADIO;
+
+ case IsToggledRole:
+ return gtk_menu_tracker_item_get_toggled (item) == TRUE ? true : false;
+
+ default:
+ return QVariant();
+ }
+}
+
+QModelIndex UnityMenuModel::index(int row, int column, const QModelIndex &parent) const
+{
+ return createIndex(row, column);
+}
+
+QModelIndex UnityMenuModel::parent(const QModelIndex &index) const
+{
+ return QModelIndex();
+}
+
+#include <QtDebug>
+QHash<int, QByteArray> UnityMenuModel::roleNames() const
+{
+ QHash<int, QByteArray> names;
+
+ names[LabelRole] = "label";
+ names[SensitiveRole] = "sensitive";
+ names[IsSeparatorRole] = "isSeparator";
+ names[IconRole] = "icon";
+ names[TypeRole] = "type";
+ names[ExtendedAttributesRole] = "ext";
+ names[ActionRole] = "action";
+ names[ActionStateRole] = "actionState";
+ names[IsCheckRole] = "isCheck";
+ names[IsRadioRole] = "isRadio";
+ names[IsToggledRole] = "isToggled";
+
+ return names;
+}
+
+QObject * UnityMenuModel::submenu(int position, QQmlComponent* actionStateParser)
+{
+ GSequenceIter *it;
+ GtkMenuTrackerItem *item;
+ UnityMenuModel *model;
+
+ it = g_sequence_get_iter_at_pos (priv->items, position);
+ if (g_sequence_iter_is_end (it))
+ return NULL;
+
+ item = (GtkMenuTrackerItem *) g_sequence_get (it);
+ if (!gtk_menu_tracker_item_get_has_submenu (item))
+ return NULL;
+
+ model = (UnityMenuModel *) g_object_get_qdata (G_OBJECT (item), unity_submenu_model_quark ());
+ if (model == NULL) {
+ model = new UnityMenuModel(this);
+
+ if (actionStateParser) {
+ ActionStateParser* parser = qobject_cast<ActionStateParser*>(actionStateParser->create());
+ if (parser) {
+ model->setActionStateParser(parser);
+ }
+ }
+
+ model->priv->menutracker = gtk_menu_tracker_new_for_item_submenu (item,
+ UnityMenuModelPrivate::menuItemInserted,
+ UnityMenuModelPrivate::menuItemRemoved,
+ model->priv);
+ g_object_set_qdata (G_OBJECT (item), unity_submenu_model_quark (), model);
+ }
+
+ return model;
+}
+
+static void freeExtendedAttrs(gpointer data)
+{
+ QVariantMap *extendedAttrs = (QVariantMap *) data;
+ delete extendedAttrs;
+}
+
+static QVariant attributeToQVariant(GVariant *value, const QString &type)
+{
+ QVariant result;
+
+ if (type == "int") {
+ if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT32)) {
+ result = QVariant(g_variant_get_int32(value));
+ }
+ }
+ else if (type == "int64") {
+ if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT64)) {
+ result = QVariant((qlonglong)g_variant_get_int64(value));
+ }
+ }
+ else if (type == "bool") {
+ if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN)) {
+ result = QVariant(g_variant_get_boolean(value));
+ }
+ }
+ else if (type == "string") {
+ if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
+ result = QVariant(g_variant_get_string(value, NULL));
+ }
+ }
+ else if (type == "double") {
+ if (g_variant_is_of_type (value, G_VARIANT_TYPE_DOUBLE)) {
+ result = QVariant(g_variant_get_double(value));
+ }
+ }
+ else if (type == "variant") {
+ if (g_variant_is_of_type (value, G_VARIANT_TYPE_VARIANT)) {
+ result = Converter::toQVariant(value);
+ }
+ }
+ else if (type == "icon") {
+ GIcon *icon = g_icon_deserialize (value);
+ if (icon) {
+ result = iconUri(icon);
+ g_object_unref (icon);
+ }
+ else {
+ result = QVariant("");
+ }
+ }
+
+ return result;
+}
+
+/* convert 'some-key' to 'someKey' or 'SomeKey'. (from dconf-qt) */
+static QString qtify_name(const char *name)
+{
+ bool next_cap = false;
+ QString result;
+
+ while (*name) {
+ if (*name == '-') {
+ next_cap = true;
+ } else if (next_cap) {
+ result.append(toupper(*name));
+ next_cap = false;
+ } else {
+ result.append(*name);
+ }
+
+ name++;
+ }
+
+ return result;
+}
+
+bool UnityMenuModel::loadExtendedAttributes(int position, const QVariantMap &schema)
+{
+ GtkMenuTrackerItem *item;
+ QVariantMap *extendedAttrs;
+
+ item = (GtkMenuTrackerItem *) g_sequence_get (g_sequence_get_iter_at_pos (priv->items, position));
+
+ extendedAttrs = new QVariantMap;
+
+ for (QVariantMap::const_iterator it = schema.constBegin(); it != schema.constEnd(); ++it) {
+ QString name = it.key();
+ QString type = it.value().toString();
+
+ GVariant *value = gtk_menu_tracker_item_get_attribute_value (item, name.toUtf8(), NULL);
+ if (value == NULL) {
+ qWarning("loadExtendedAttributes: menu item does not contain '%s'", it.key().toUtf8().constData());
+ continue;
+ }
+
+ QVariant qvalue = attributeToQVariant(value, type);
+ if (qvalue.isValid())
+ extendedAttrs->insert(qtify_name (name.toUtf8()), qvalue);
+ else
+ qWarning("loadExtendedAttributes: key '%s' is of type '%s' (expected '%s')",
+ name.toUtf8().constData(), g_variant_get_type_string(value), type.constData());
+
+ g_variant_unref (value);
+ }
+
+ g_object_set_qdata_full (G_OBJECT (item), unity_menu_item_extended_attributes_quark (),
+ extendedAttrs, freeExtendedAttrs);
+}
+
+QVariant UnityMenuModel::get(int row, const QByteArray &role)
+{
+ if (priv->roles.isEmpty()) {
+ QHash<int, QByteArray> names = roleNames();
+ Q_FOREACH (int role, names.keys())
+ priv->roles.insert(names[role], role);
+ }
+
+ return this->data(this->index(row, 0), priv->roles[role]);
+}
+
+void UnityMenuModel::activate(int index, const QVariant& parameter)
+{
+ GtkMenuTrackerItem *item;
+
+ item = (GtkMenuTrackerItem *) g_sequence_get (g_sequence_get_iter_at_pos (priv->items, index));
+
+ if (parameter.isValid()) {
+ gchar *action;
+
+ gtk_menu_tracker_item_get_attribute (item, "action", "s", &action);
+ g_action_group_activate_action (G_ACTION_GROUP (priv->muxer), action, Converter::toGVariant(parameter));
+
+ g_free (action);
+ } else {
+ gtk_menu_tracker_item_activated (item);
+ }
+}
+
+void UnityMenuModel::changeState(int index, const QVariant& parameter)
+{
+ GtkMenuTrackerItem* item;
+ GVariant* data;
+ GVariant* current_state;
+
+ item = (GtkMenuTrackerItem *) g_sequence_get (g_sequence_get_iter_at_pos (priv->items, index));
+ if (!item) return;
+
+ current_state = gtk_menu_tracker_item_get_action_state (item);
+ if (current_state) {
+ // Attempt to convert the parameter to the expected type
+ data = Converter::toGVariantWithSchema(parameter, g_variant_get_type_string(current_state));
+ g_variant_unref (current_state);
+ } else {
+ data = Converter::toGVariant(parameter);
+ }
+
+ gtk_menu_tracker_item_change_state (item, data);
+ if (data) {
+ g_variant_unref(data);
+ }
+}
+
+
+bool UnityMenuModel::event(QEvent* e)
+{
+ if (e->type() == UnityMenuModelClearEvent::eventType) {
+ UnityMenuModelClearEvent *emmce = static_cast<UnityMenuModelClearEvent*>(e);
+
+ GSequenceIter *begin;
+ GSequenceIter *end;
+
+ if (emmce->reset)
+ beginResetModel();
+
+ begin = g_sequence_get_begin_iter (priv->items);
+ end = g_sequence_get_end_iter (priv->items);
+ g_sequence_remove_range (begin, end);
+
+ if (emmce->reset)
+ endResetModel();
+
+ return true;
+ } else if (e->type() == UnityMenuModelAddRowEvent::eventType) {
+ UnityMenuModelAddRowEvent *ummrce = static_cast<UnityMenuModelAddRowEvent*>(e);
+
+ GSequenceIter *it;
+ it = g_sequence_get_iter_at_pos (priv->items, ummrce->position);
+ if (it) {
+ beginInsertRows(QModelIndex(), ummrce->position, ummrce->position);
+
+ it = g_sequence_insert_before (it, g_object_ref (ummrce->item));
+ g_object_set_qdata (G_OBJECT (ummrce->item), unity_menu_model_quark (), this);
+ g_signal_connect (ummrce->item, "notify", G_CALLBACK (UnityMenuModelPrivate::menuItemChanged), it);
+
+ endInsertRows();
+ }
+ return true;
+ } else if (e->type() == UnityMenuModelRemoveRowEvent::eventType) {
+ UnityMenuModelRemoveRowEvent *ummrre = static_cast<UnityMenuModelRemoveRowEvent*>(e);
+
+ GSequenceIter *it;
+ it = g_sequence_get_iter_at_pos (priv->items, ummrre->position);
+ if (it) {
+ beginRemoveRows(QModelIndex(), ummrre->position, ummrre->position);
+
+ g_sequence_remove (it);
+
+ endRemoveRows();
+ }
+ return true;
+ } else if (e->type() == UnityMenuModelDataChangeEvent::eventType) {
+ UnityMenuModelDataChangeEvent *ummdce = static_cast<UnityMenuModelDataChangeEvent*>(e);
+
+ Q_EMIT dataChanged(index(ummdce->position, 0), index(ummdce->position, 0));
+ return true;
+ }
+ return QAbstractListModel::event(e);
+}
diff --git a/libqmenumodel/src/unitymenumodel.h b/libqmenumodel/src/unitymenumodel.h
new file mode 100644
index 0000000..df35f08
--- /dev/null
+++ b/libqmenumodel/src/unitymenumodel.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+#ifndef UNITYMENUMODEL_H
+#define UNITYMENUMODEL_H
+
+#include <QAbstractListModel>
+class ActionStateParser;
+class QQmlComponent;
+
+class UnityMenuModel: public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QByteArray busName READ busName WRITE setBusName NOTIFY busNameChanged)
+ Q_PROPERTY(QVariantMap actions READ actions WRITE setActions NOTIFY actionsChanged)
+ Q_PROPERTY(QByteArray menuObjectPath READ menuObjectPath WRITE setMenuObjectPath NOTIFY menuObjectPathChanged)
+ Q_PROPERTY(ActionStateParser* actionStateParser READ actionStateParser WRITE setActionStateParser NOTIFY actionStateParserChanged)
+
+public:
+ UnityMenuModel(QObject *parent = NULL);
+ virtual ~UnityMenuModel();
+
+ QByteArray busName() const;
+ void setBusName(const QByteArray &name);
+
+ QVariantMap actions() const;
+ void setActions(const QVariantMap &actions);
+
+ QByteArray menuObjectPath() const;
+ void setMenuObjectPath(const QByteArray &path);
+
+ ActionStateParser* actionStateParser() const;
+ void setActionStateParser(ActionStateParser* actionStateParser);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const;
+ QModelIndex parent(const QModelIndex &index) const;
+ QHash<int, QByteArray> roleNames() const;
+
+ Q_INVOKABLE QObject * submenu(int position, QQmlComponent* actionStateParser = NULL);
+ Q_INVOKABLE bool loadExtendedAttributes(int position, const QVariantMap &schema);
+ Q_INVOKABLE QVariant get(int row, const QByteArray &role);
+
+ Q_INVOKABLE void activate(int index, const QVariant& parameter = QVariant());
+ Q_INVOKABLE void changeState(int index, const QVariant& parameter);
+
+Q_SIGNALS:
+ void busNameChanged(const QByteArray &name);
+ void actionsChanged(const QByteArray &path);
+ void menuObjectPathChanged(const QByteArray &path);
+ void actionStateParserChanged(ActionStateParser* parser);
+
+protected:
+ virtual bool event(QEvent* e);
+
+private:
+ class UnityMenuModelPrivate *priv;
+ friend class UnityMenuModelPrivate;
+};
+
+#endif
diff --git a/libqmenumodel/src/unitymenumodelevents.cpp b/libqmenumodel/src/unitymenumodelevents.cpp
new file mode 100644
index 0000000..e03d1c7
--- /dev/null
+++ b/libqmenumodel/src/unitymenumodelevents.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Nicholas Dedekind <nick.dedekind@canonical.com
+ */
+
+extern "C" {
+#include <glib-object.h>
+#include <gio/gio.h>
+}
+
+#include "unitymenumodelevents.h"
+#include "unitymenumodel.h"
+
+const QEvent::Type UnityMenuModelClearEvent::eventType = static_cast<QEvent::Type>(QEvent::registerEventType());
+const QEvent::Type UnityMenuModelAddRowEvent::eventType = static_cast<QEvent::Type>(QEvent::registerEventType());
+const QEvent::Type UnityMenuModelRemoveRowEvent::eventType = static_cast<QEvent::Type>(QEvent::registerEventType());
+const QEvent::Type UnityMenuModelDataChangeEvent::eventType = static_cast<QEvent::Type>(QEvent::registerEventType());
+
+UnityMenuModelClearEvent::UnityMenuModelClearEvent(bool _reset)
+ : QEvent(UnityMenuModelClearEvent::eventType),
+ reset(_reset)
+{}
+
+UnityMenuModelAddRowEvent::UnityMenuModelAddRowEvent(GtkMenuTrackerItem *_item, int _position)
+ : QEvent(UnityMenuModelAddRowEvent::eventType),
+ item(_item),
+ position(_position)
+{
+ if (item) {
+ g_object_ref(item);
+ }
+}
+
+UnityMenuModelAddRowEvent::~UnityMenuModelAddRowEvent()
+{
+ if (item) {
+ g_object_unref(item);
+ }
+}
+
+UnityMenuModelRemoveRowEvent::UnityMenuModelRemoveRowEvent(int _position)
+ : QEvent(UnityMenuModelRemoveRowEvent::eventType),
+ position(_position)
+{}
+
+UnityMenuModelDataChangeEvent::UnityMenuModelDataChangeEvent(int _position)
+ : QEvent(UnityMenuModelDataChangeEvent::eventType),
+ position(_position)
+{}
diff --git a/libqmenumodel/src/unitymenumodelevents.h b/libqmenumodel/src/unitymenumodelevents.h
new file mode 100644
index 0000000..dcb27ff
--- /dev/null
+++ b/libqmenumodel/src/unitymenumodelevents.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Nicholas Dedekind <nick.dedekind@canonical.com
+ */
+
+#ifndef UNITYMENUMODELEVENTS_H
+#define UNITYMENUMODELEVENTS_H
+
+#include <QEvent>
+
+typedef struct _GtkMenuTrackerItem GtkMenuTrackerItem;
+
+/* Event for a unitymenumodel clear */
+class UnityMenuModelClearEvent : public QEvent
+{
+public:
+ static const QEvent::Type eventType;
+ UnityMenuModelClearEvent(bool reset);
+
+ bool reset;
+};
+
+/* Event for a row add for unitymenumodel */
+class UnityMenuModelAddRowEvent : public QEvent
+{
+public:
+ static const QEvent::Type eventType;
+ UnityMenuModelAddRowEvent(GtkMenuTrackerItem *item, int position);
+ ~UnityMenuModelAddRowEvent();
+
+ GtkMenuTrackerItem *item;
+ int position;
+};
+
+/* Event for a row remove for unitymenumodel */
+class UnityMenuModelRemoveRowEvent : public QEvent
+{
+public:
+ static const QEvent::Type eventType;
+ UnityMenuModelRemoveRowEvent(int position);
+
+ int position;
+};
+
+/* Event for a row data change for unitymenumodel */
+class UnityMenuModelDataChangeEvent : public QEvent
+{
+public:
+ static const QEvent::Type eventType;
+ UnityMenuModelDataChangeEvent(int position);
+
+ int position;
+};
+
+#endif //UNITYMENUMODELEVENTS_H
diff --git a/libqmenumodel/src/unitythemediconprovider.cpp b/libqmenumodel/src/unitythemediconprovider.cpp
new file mode 100644
index 0000000..69afd76
--- /dev/null
+++ b/libqmenumodel/src/unitythemediconprovider.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+#include "unitythemediconprovider.h"
+
+#include <QIcon>
+
+UnityThemedIconProvider::UnityThemedIconProvider():
+ QQuickImageProvider(QQuickImageProvider::Pixmap)
+{
+}
+
+#include <QtDebug>
+
+QPixmap UnityThemedIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
+{
+ QPixmap pixmap = QIcon::fromTheme(id).pixmap(requestedSize.isValid() ? requestedSize : QSize(32, 32));
+ *size = pixmap.size();
+ return pixmap;
+}
diff --git a/libqmenumodel/src/unitythemediconprovider.h b/libqmenumodel/src/unitythemediconprovider.h
new file mode 100644
index 0000000..7e71ea8
--- /dev/null
+++ b/libqmenumodel/src/unitythemediconprovider.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program 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; version 3.
+ *
+ * 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+#ifndef UNITY_THEMED_ICON_PROVIDER_H
+#define UNITY_THEMED_ICON_PROVIDER_H
+
+#include <QQuickImageProvider>
+
+class UnityThemedIconProvider: public QQuickImageProvider
+{
+public:
+ UnityThemedIconProvider();
+ QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize);
+};
+
+#endif
diff --git a/tests/client/convertertest.cpp b/tests/client/convertertest.cpp
index f382332..db2807f 100644
--- a/tests/client/convertertest.cpp
+++ b/tests/client/convertertest.cpp
@@ -45,6 +45,22 @@ private:
g_variant_unref(gv);
return result;
}
+ bool compareWithSchema(const QVariant &qv, const QString strType)
+ {
+ GVariantType* expected_type;
+ expected_type = g_variant_type_new(strType.toUtf8().data());
+
+ bool result;
+ GVariant *gv = Converter::toGVariantWithSchema(qv, strType.toUtf8().data());
+ result = g_variant_type_equal(g_variant_get_type(gv), expected_type);
+ if (!result) {
+ qWarning() << "types are different: QVariant:" << qv.typeName()
+ << "Result:" << (const char*) g_variant_get_type(gv)
+ << "Expected:"<< (const char*) expected_type;
+ }
+ g_variant_unref(gv);
+ return result;
+ }
private Q_SLOTS:
@@ -118,6 +134,43 @@ private Q_SLOTS:
g_variant_unref(gTuple);
}
+ void testSchemaConvert()
+ {
+ // convert to integer
+ compareWithSchema(QVariant::fromValue<int>(1), "i");
+ compareWithSchema(QVariant::fromValue<double>(1.1), "i");
+
+ // convert to integer
+ compareWithSchema(QVariant::fromValue<bool>(true), "b");
+ compareWithSchema(QVariant::fromValue<int>(1), "b");
+
+ // convert to double
+ compareWithSchema(QVariant::fromValue<double>(1.0), "d");
+ compareWithSchema(QVariant::fromValue<int>(1), "d");
+
+ // convert to string
+ compareWithSchema(QVariant::fromValue<int>(1), "s");
+ compareWithSchema(QVariant::fromValue<double>(1.1), "s");
+
+ // convert to tuple
+ compareWithSchema(QVariantList() << QVariant::fromValue<bool>(true) << QVariant::fromValue<int>(1) << QVariant::fromValue<int>(1) << QVariant::fromValue<QString>("test1"), "(bdis)");
+
+ // convert to array
+ compareWithSchema(QVariantList() << QVariant::fromValue<int>(1) << QVariant::fromValue<int>(1), "ad");
+ compareWithSchema(QVariantList() << QVariant::fromValue<QString>("test1") << QVariant::fromValue<QString>("test2"), "as");
+
+ // convert to array of tuple
+ QVariantList si1(QVariantList() << QVariant::fromValue<QString>("test1") << QVariant::fromValue<int>(1));
+ QVariantList si2(QVariantList() << QVariant::fromValue<QString>("test1") << QVariant::fromValue<int>(1));
+ compareWithSchema(QVariantList() << QVariant::fromValue(si1) << QVariant::fromValue(si2), "a(sd)");
+
+ // convert to vardict
+ QVariantMap map;
+ map["test1"] = QVariant::fromValue<int>(1);
+ map["test2"] = QVariant::fromValue<double>(1);
+ compareWithSchema(map, "a{sv}");
+ }
+
};
QTEST_MAIN(ConverterTest)