diff options
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, ¶meter_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 = §ion->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, §ion->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 (>k_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 (>k_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, ¶meter_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, ¶meter_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) |