Compare commits
	
		
			2 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3fd70e37bd | ||
|   | 5580cfaf63 | 
| @@ -3,6 +3,7 @@ | ||||
| const Cairo = imports.cairo; | ||||
| const Clutter = imports.gi.Clutter; | ||||
| const Gio = imports.gi.Gio; | ||||
| const GLib = imports.gi.GLib; | ||||
| const Lang = imports.lang; | ||||
| const Mainloop = imports.mainloop; | ||||
| const Pango = imports.gi.Pango; | ||||
| @@ -235,10 +236,12 @@ const AppMenuButton = new Lang.Class({ | ||||
|     Name: 'AppMenuButton', | ||||
|     Extends: PanelMenu.Button, | ||||
|  | ||||
|     _init: function() { | ||||
|         this.parent(0.0); | ||||
|     _init: function(menuManager) { | ||||
|         this.parent(0.0, true); | ||||
|  | ||||
|         this._startingApps = []; | ||||
|  | ||||
|         this._menuManager = menuManager; | ||||
|         this._targetApp = null; | ||||
|  | ||||
|         let bin = new St.Bin({ name: 'appMenu' }); | ||||
| @@ -264,10 +267,6 @@ const AppMenuButton = new Lang.Class({ | ||||
|  | ||||
|         this._iconBottomClip = 0; | ||||
|  | ||||
|         this._quitMenu = new PopupMenu.PopupMenuItem(''); | ||||
|         this.menu.addMenuItem(this._quitMenu); | ||||
|         this._quitMenu.connect('activate', Lang.bind(this, this._onQuit)); | ||||
|  | ||||
|         this._visible = !Main.overview.visible; | ||||
|         if (!this._visible) | ||||
|             this.actor.hide(); | ||||
| @@ -446,12 +445,6 @@ const AppMenuButton = new Lang.Class({ | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     _onQuit: function() { | ||||
|         if (this._targetApp == null) | ||||
|             return; | ||||
|         this._targetApp.request_quit(); | ||||
|     }, | ||||
|  | ||||
|     _onAppStateChanged: function(appSys, app) { | ||||
|         let state = app.state; | ||||
|         if (state != Shell.AppState.STARTING) { | ||||
| @@ -513,8 +506,10 @@ const AppMenuButton = new Lang.Class({ | ||||
|         } | ||||
|  | ||||
|         if (targetApp == this._targetApp) { | ||||
|             if (targetApp && targetApp.get_state() != Shell.AppState.STARTING) | ||||
|             if (targetApp && targetApp.get_state() != Shell.AppState.STARTING) { | ||||
|                 this.stopAnimation(); | ||||
|                 this._maybeSetMenu(); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -528,16 +523,40 @@ const AppMenuButton = new Lang.Class({ | ||||
|         let icon = targetApp.get_faded_icon(2 * PANEL_ICON_SIZE); | ||||
|  | ||||
|         this._label.setText(targetApp.get_name()); | ||||
|         // TODO - _quit() doesn't really work on apps in state STARTING yet | ||||
|         this._quitMenu.label.set_text(_("Quit %s").format(targetApp.get_name())); | ||||
|  | ||||
|         this._iconBox.set_child(icon); | ||||
|         this._iconBox.show(); | ||||
|  | ||||
|         if (targetApp.get_state() == Shell.AppState.STARTING) | ||||
|             this.startAnimation(); | ||||
|         else | ||||
|             this._maybeSetMenu(); | ||||
|  | ||||
|         this.emit('changed'); | ||||
|     }, | ||||
|  | ||||
|     _maybeSetMenu: function() { | ||||
|         let menu; | ||||
|  | ||||
|         if (this._targetApp.action_group) { | ||||
|             if (this.menu instanceof PopupMenu.RemoteMenu && | ||||
|                 this.menu.actionGroup == this._targetApp.action_group) | ||||
|                 return; | ||||
|  | ||||
|             menu = new PopupMenu.RemoteMenu(this.actor, this._targetApp.menu, this._targetApp.action_group); | ||||
|         } else { | ||||
|             if (this.menu && !(this.menu instanceof PopupMenu.RemoteMenu)) | ||||
|                 return; | ||||
|  | ||||
|             // fallback to older menu | ||||
|             menu = new PopupMenu.PopupMenu(this.actor, 0.0, St.Side.TOP, 0); | ||||
|             menu.addAction(_("Quit"), Lang.bind(this, function() { | ||||
|                 this._targetApp.request_quit(); | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         this.setMenu(menu); | ||||
|         this._menuManager.addMenu(menu); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @@ -924,9 +943,8 @@ const Panel = new Lang.Class({ | ||||
|             // more cleanly with the rest of the panel | ||||
|             this._menus.addMenu(this._activitiesButton.menu); | ||||
|  | ||||
|             this._appMenu = new AppMenuButton(); | ||||
|             this._appMenu = new AppMenuButton(this._menus); | ||||
|             this._leftBox.add(this._appMenu.actor); | ||||
|             this._menus.addMenu(this._appMenu.menu); | ||||
|         } | ||||
|  | ||||
|         /* center */ | ||||
|   | ||||
| @@ -96,22 +96,39 @@ const Button = new Lang.Class({ | ||||
|     Name: 'PanelMenuButton', | ||||
|     Extends: ButtonBox, | ||||
|  | ||||
|     _init: function(menuAlignment) { | ||||
|     _init: function(menuAlignment, dontCreateMenu) { | ||||
|         this.parent({ reactive: true, | ||||
|                       can_focus: true, | ||||
|                       track_hover: true }); | ||||
|  | ||||
|         this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); | ||||
|         this.actor.connect('key-press-event', Lang.bind(this, this._onSourceKeyPress)); | ||||
|         this.menu = new PopupMenu.PopupMenu(this.actor, menuAlignment, St.Side.TOP); | ||||
|         this.menu.actor.add_style_class_name('panel-menu'); | ||||
|         this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged)); | ||||
|         this.menu.actor.connect('key-press-event', Lang.bind(this, this._onMenuKeyPress)); | ||||
|         Main.uiGroup.add_actor(this.menu.actor); | ||||
|         this.menu.actor.hide(); | ||||
|  | ||||
|         if (dontCreateMenu) | ||||
|             this.menu = null; | ||||
|         else | ||||
|             this.setMenu(new PopupMenu.PopupMenu(this.actor, menuAlignment, St.Side.TOP, 0)); | ||||
|     }, | ||||
|  | ||||
|     setMenu: function(menu) { | ||||
|         if (this.menu) | ||||
|             this.menu.destroy(); | ||||
|  | ||||
|         this.menu = menu; | ||||
|         if (this.menu) { | ||||
|             this.menu.actor.add_style_class_name('panel-menu'); | ||||
|             this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged)); | ||||
|             this.menu.actor.connect('key-press-event', Lang.bind(this, this._onMenuKeyPress)); | ||||
|  | ||||
|             Main.uiGroup.add_actor(this.menu.actor); | ||||
|             this.menu.actor.hide(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     _onButtonPress: function(actor, event) { | ||||
|         if (!this.menu) | ||||
|             return; | ||||
|  | ||||
|         if (!this.menu.isOpen) { | ||||
|             // Setting the max-height won't do any good if the minimum height of the | ||||
|             // menu is higher then the screen; it's useful if part of the menu is | ||||
| @@ -125,6 +142,9 @@ const Button = new Lang.Class({ | ||||
|     }, | ||||
|  | ||||
|     _onSourceKeyPress: function(actor, event) { | ||||
|         if (!this.menu) | ||||
|             return false; | ||||
|  | ||||
|         let symbol = event.get_key_symbol(); | ||||
|         if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) { | ||||
|             this.menu.toggle(); | ||||
|   | ||||
| @@ -2,7 +2,9 @@ | ||||
|  | ||||
| const Cairo = imports.cairo; | ||||
| const Clutter = imports.gi.Clutter; | ||||
| const GLib = imports.gi.GLib; | ||||
| const Gtk = imports.gi.Gtk; | ||||
| const Gio = imports.gi.Gio; | ||||
| const Lang = imports.lang; | ||||
| const Shell = imports.gi.Shell; | ||||
| const Signals = imports.signals; | ||||
| @@ -1692,6 +1694,254 @@ const PopupComboBoxMenuItem = new Lang.Class({ | ||||
|     } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * RemoteMenu: | ||||
|  * | ||||
|  * A PopupMenu that tracks a GMenuModel and shows its actions | ||||
|  * (exposed by GApplication/GActionGroup) | ||||
|  */ | ||||
| const RemoteMenu = new Lang.Class({ | ||||
|     Name: 'RemoteMenu', | ||||
|     Extends: PopupMenu, | ||||
|  | ||||
|     _init: function(sourceActor, model, actionGroup) { | ||||
|         this.parent(sourceActor, 0.0, St.Side.TOP); | ||||
|  | ||||
|         this.model = model; | ||||
|         this.actionGroup = actionGroup; | ||||
|  | ||||
|         this._actions = { }; | ||||
|         this._modelChanged(this.model, 0, 0, this.model.get_n_items(), this); | ||||
|  | ||||
|         this._actionStateChangeId = this.actionGroup.connect('action-state-changed', Lang.bind(this, this._actionStateChanged)); | ||||
|         this._actionEnableChangeId = this.actionGroup.connect('action-enabled-changed', Lang.bind(this, this._actionEnabledChanged)); | ||||
|     }, | ||||
|  | ||||
|     destroy: function() { | ||||
|         if (this._actionStateChangeId) { | ||||
|             this.actionGroup.disconnect(this._actionStateChangeId); | ||||
|             this._actionStateChangeId = 0; | ||||
|         } | ||||
|  | ||||
|         if (this._actionEnableChangeId) { | ||||
|             this.actionGroup.disconnect(this._actionEnableChangeId); | ||||
|             this._actionEnableChangeId = 0; | ||||
|         } | ||||
|  | ||||
|         this.parent(); | ||||
|     }, | ||||
|  | ||||
|     _createMenuItem: function(model, index) { | ||||
|         let section_link = model.get_item_link(index, Gio.MENU_LINK_SECTION); | ||||
|         if (section_link) { | ||||
|             let item = new PopupMenuSection(); | ||||
|             this._modelChanged(section_link, 0, 0, section_link.get_n_items(), item); | ||||
|             return [item, true, '']; | ||||
|         } | ||||
|  | ||||
|         // labels are not checked for existance, as they're required for all items | ||||
|         let label = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_LABEL, null).deep_unpack(); | ||||
|         // remove all underscores that are not followed by another underscore | ||||
|         label = label.replace(/_([^_])/, '$1'); | ||||
|         let submenu_link = model.get_item_link(index, Gio.MENU_LINK_SUBMENU); | ||||
|  | ||||
|         if (submenu_link) { | ||||
|             let item = new PopupSubMenuMenuItem(label); | ||||
|             this._modelChanged(submenu_link, 0, 0, submenu_link.get_n_items(), item.menu); | ||||
|             return [item, false, '']; | ||||
|         } | ||||
|  | ||||
|         let action_id = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_ACTION, null).deep_unpack(); | ||||
|         if (!this.actionGroup.has_action(action_id)) { | ||||
|             // the action may not be there yet, wait for action-added | ||||
|             return [null, false, 'action-added']; | ||||
|         } | ||||
|  | ||||
|         if (!this._actions[action_id]) | ||||
|             this._actions[action_id] = { enabled: this.actionGroup.get_action_enabled(action_id), | ||||
|                                          state: this.actionGroup.get_action_state(action_id), | ||||
|                                          items: [ ], | ||||
|                                        }; | ||||
|         let action = this._actions[action_id]; | ||||
|         let item, target, destroyId, specificSignalId; | ||||
|  | ||||
|         if (action.state) { | ||||
|             // Docs have get_state_hint(), except that the DBus protocol | ||||
|             // has no provision for it (so ShellApp does not implement it, | ||||
|             // and neither GApplication), and g_action_get_state_hint() | ||||
|             // always returns null | ||||
|             // Funny :) | ||||
|  | ||||
|             switch (String.fromCharCode(action.state.classify())) { | ||||
|             case 'b': | ||||
|                 item = new PopupSwitchMenuItem(label, action.state.get_boolean()); | ||||
|                 action.items.push(item); | ||||
|                 specificSignalId = item.connect('toggled', Lang.bind(this, function(item) { | ||||
|                     this.actionGroup.change_action_state(action_id, GLib.Variant.new_boolean(item.state)); | ||||
|                 })); | ||||
|                 break; | ||||
|             case 'd': | ||||
|                 item = new PopupSliderMenuItem(label, action.state.get_double()); | ||||
|                 action.items.push(item); | ||||
|                 // value-changed is emitted for each motion-event, maybe an idle is more appropriate here? | ||||
|                 specificSignalId = item.connect('value-changed', Lang.bind(this, function(item) { | ||||
|                     this.actionGroup.change_action_state(action_id, GLib.Variant.new_double(item.value)); | ||||
|                 })); | ||||
|                 break; | ||||
|             case 's': | ||||
|                 item = new PopupMenuItem(label); | ||||
|                 item._remoteTarget = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null).deep_unpack(); | ||||
|                 action.items.push(item); | ||||
|                 item.setShowDot(action.state.deep_unpack() == item._remoteTarget); | ||||
|                 specificSignalId = item.connect('activate', Lang.bind(this, function(item) { | ||||
|                     this.actionGroup.change_action_state(action_id, GLib.Variant.new_string(item._remoteTarget)); | ||||
|                 })); | ||||
|                 break; | ||||
|             default: | ||||
|                 log('Action "%s" has state of type %s, which is not supported'.format(action_id, action.state.get_type_string())); | ||||
|                 return [null, false, 'action-state-changed']; | ||||
|             } | ||||
|         } else { | ||||
|             target = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null); | ||||
|             item = new PopupMenuItem(label); | ||||
|             action.items.push(item); | ||||
|             specificSignalId = item.connect('activate', Lang.bind(this, function() { | ||||
|                 this.actionGroup.activate_action(action_id, target); | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         item.actor.reactive = item.actor.can_focus = action.enabled; | ||||
|         if (action.enabled) | ||||
|             item.actor.remove_style_pseudo_class('insensitive'); | ||||
|         else | ||||
|             item.actor.add_style_pseudo_class('insensitive'); | ||||
|  | ||||
|         destroyId = item.connect('destroy', Lang.bind(this, function() { | ||||
|             item.disconnect(destroyId); | ||||
|             item.disconnect(specificSignalId); | ||||
|  | ||||
|             let pos = action.items.indexOf(item); | ||||
|             if (pos != -1) | ||||
|                 action.items.splice(pos, 1); | ||||
|         })); | ||||
|  | ||||
|         return [item, false, '']; | ||||
|     },  | ||||
|  | ||||
|     _modelChanged: function(model, position, removed, added, target) { | ||||
|         let j, k; | ||||
|         let j0, k0; | ||||
|  | ||||
|         let currentItems = target._getMenuItems(); | ||||
|  | ||||
|         for (j0 = 0, k0 = 0; j0 < position; j0++, k0++) { | ||||
|             if (currentItems[k0] instanceof PopupSeparatorMenuItem) | ||||
|                 k0++; | ||||
|         } | ||||
|  | ||||
|         if (removed == -1) { | ||||
|             // special flag to indicate we should destroy everything | ||||
|             for (k = k0; k < currentItems.length; k++) | ||||
|                 currentItems[k].destroy(); | ||||
|         } else { | ||||
|             for (j = j0, k = k0; j < j0 + removed; j++, k++) { | ||||
|                 currentItems[k].destroy(); | ||||
|  | ||||
|                 if (currentItems[k] instanceof PopupSeparatorMenuItem) | ||||
|                     j--; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (j = j0, k = k0; j < j0 + added; j++, k++) { | ||||
|             let [item, addSeparator, changeSignal] = this._createMenuItem(model, j); | ||||
|  | ||||
|             if (item) { | ||||
|                 // separators must be added in the parent to make autohiding work | ||||
|                 if (addSeparator) { | ||||
|                     target.addMenuItem(new PopupSeparatorMenuItem(), k+1); | ||||
|                     k++; | ||||
|                 } | ||||
|  | ||||
|                 target.addMenuItem(item, k); | ||||
|  | ||||
|                 if (addSeparator) { | ||||
|                     target.addMenuItem(new PopupSeparatorMenuItem(), k+1); | ||||
|                     k++; | ||||
|                 } | ||||
|             } else if (changeSignal) { | ||||
|                 let signalId = this.actionGroup.connect(changeSignal, Lang.bind(this, function() { | ||||
|                     this.actionGroup.disconnect(signalId); | ||||
|  | ||||
|                     // force a full update | ||||
|                     this._modelChanged(model, 0, -1, model.get_n_items(), target); | ||||
|                 })); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!model._changedId) { | ||||
|             model._changedId = model.connect('items-changed', Lang.bind(this, this._modelChanged, target)); | ||||
|             model._destroyId = target.connect('destroy', function() { | ||||
|                 if (model._changedId) | ||||
|                     model.disconnect(model._changedId); | ||||
|                 if (model._destroyId) | ||||
|                     target.disconnect(model._destroyId); | ||||
|                 model._changedId = 0; | ||||
|                 model._destroyId = 0; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (target instanceof PopupMenuSection) { | ||||
|             target.actor.visible = target.numMenuItems != 0; | ||||
|         } else { | ||||
|             let sourceItem = target.sourceActor._delegate; | ||||
|             if (sourceItem instanceof PopupSubMenuMenuItem) | ||||
|                 sourceItem.actor.visible = target.numMenuItems != 0; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     _actionStateChanged: function(actionGroup, action_id) { | ||||
|         let action = this._actions[action_id]; | ||||
|         if (!action) | ||||
|             return; | ||||
|  | ||||
|         action.state = actionGroup.get_action_state(action_id); | ||||
|         if (action.items.length) { | ||||
|             switch (String.fromCharCode(action.state.classify())) { | ||||
|             case 'b': | ||||
|                 for (let i = 0; i < action.items.length; i++) | ||||
|                     action.items[i].setToggleState(action.state.get_boolean()); | ||||
|                 break; | ||||
|             case 'd': | ||||
|                 for (let i = 0; i < action.items.length; i++) | ||||
|                     action.items[i].setValue(action.state.get_double()); | ||||
|                 break; | ||||
|             case 's': | ||||
|                 for (let i = 0; i < action.items.length; i++) | ||||
|                     action.items[i].setShowDot(action.items[i]._remoteTarget == action.state.deep_unpack()); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     _actionEnabledChanged: function(actionGroup, action_id) { | ||||
|         let action = this._actions[action_id]; | ||||
|         if (!action) | ||||
|             return; | ||||
|  | ||||
|         action.enabled = actionGroup.get_action_enabled(action_id); | ||||
|         if (action.items.length) { | ||||
|             for (let i = 0; i < action.items.length; i++) { | ||||
|                 let item = action.items[i]; | ||||
|                 item.actor.reactive = item.actor.can_focus = action.enabled; | ||||
|  | ||||
|                 if (action.enabled) | ||||
|                     item.actor.remove_style_pseudo_class('insensitive'); | ||||
|                 else | ||||
|                     item.actor.add_style_pseudo_class('insensitive'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | ||||
| /* Basic implementation of a menu manager. | ||||
|  * Call addMenu to add menus | ||||
|  */ | ||||
|   | ||||
| @@ -27,6 +27,9 @@ void _shell_app_do_match (ShellApp         *app, | ||||
|                           GSList          **prefix_results, | ||||
|                           GSList          **substring_results); | ||||
|  | ||||
| void _shell_app_set_dbus_name (ShellApp   *app, | ||||
|                                const char *name); | ||||
|  | ||||
| G_END_DECLS | ||||
|  | ||||
| #endif /* __SHELL_APP_PRIVATE_H__ */ | ||||
|   | ||||
							
								
								
									
										252
									
								
								src/shell-app.c
									
									
									
									
									
								
							
							
						
						
									
										252
									
								
								src/shell-app.c
									
									
									
									
									
								
							| @@ -37,6 +37,13 @@ typedef struct { | ||||
|  | ||||
|   /* Whether or not we need to resort the windows; this is done on demand */ | ||||
|   gboolean window_sort_stale : 1; | ||||
|  | ||||
|   /* See GApplication documentation */ | ||||
|   guint             name_watcher_id; | ||||
|   gchar            *dbus_name; | ||||
|   GDBusActionGroup *remote_actions; | ||||
|   GMenuProxy       *remote_menu; | ||||
|   GCancellable     *dbus_cancellable; | ||||
| } ShellAppRunningState; | ||||
|  | ||||
| /** | ||||
| @@ -72,11 +79,13 @@ struct _ShellApp | ||||
|   char *casefolded_exec; | ||||
| }; | ||||
|  | ||||
| G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT); | ||||
|  | ||||
| enum { | ||||
|   PROP_0, | ||||
|   PROP_STATE | ||||
|   PROP_STATE, | ||||
|   PROP_ID, | ||||
|   PROP_DBUS_ID, | ||||
|   PROP_ACTION_GROUP, | ||||
|   PROP_MENU | ||||
| }; | ||||
|  | ||||
| enum { | ||||
| @@ -89,6 +98,8 @@ static guint shell_app_signals[LAST_SIGNAL] = { 0 }; | ||||
| static void create_running_state (ShellApp *app); | ||||
| static void unref_running_state (ShellAppRunningState *state); | ||||
|  | ||||
| G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT) | ||||
|  | ||||
| static void | ||||
| shell_app_get_property (GObject    *gobject, | ||||
|                         guint       prop_id, | ||||
| @@ -102,6 +113,20 @@ shell_app_get_property (GObject    *gobject, | ||||
|     case PROP_STATE: | ||||
|       g_value_set_enum (value, app->state); | ||||
|       break; | ||||
|     case PROP_ID: | ||||
|       g_value_set_string (value, shell_app_get_id (app)); | ||||
|       break; | ||||
|     case PROP_DBUS_ID: | ||||
|       g_value_set_string (value, shell_app_get_dbus_id (app)); | ||||
|       break; | ||||
|     case PROP_ACTION_GROUP: | ||||
|       if (app->running_state) | ||||
|         g_value_set_object (value, app->running_state->remote_actions); | ||||
|       break; | ||||
|     case PROP_MENU: | ||||
|       if (app->running_state) | ||||
|         g_value_set_object (value, app->running_state->remote_menu); | ||||
|       break; | ||||
|     default: | ||||
|       G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); | ||||
|       break; | ||||
| @@ -151,6 +176,15 @@ window_backed_app_get_icon (ShellApp *app, | ||||
|   return actor; | ||||
| } | ||||
|  | ||||
| const char * | ||||
| shell_app_get_dbus_id (ShellApp *app) | ||||
| { | ||||
|   if (app->running_state) | ||||
|     return app->running_state->dbus_name; | ||||
|   else | ||||
|     return NULL; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * shell_app_create_icon_texture: | ||||
|  * | ||||
| @@ -948,6 +982,142 @@ _shell_app_remove_window (ShellApp   *app, | ||||
|   g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); | ||||
| } | ||||
|  | ||||
| static void | ||||
| on_action_group_acquired (GObject      *object, | ||||
|                           GAsyncResult *result, | ||||
|                           gpointer      user_data) | ||||
| { | ||||
|   ShellApp *self = SHELL_APP (user_data); | ||||
|   ShellAppRunningState *state = self->running_state; | ||||
|   GError *error = NULL; | ||||
|   char *object_path; | ||||
|  | ||||
|   state->remote_actions = g_dbus_action_group_new_finish (result, | ||||
|                                                           &error); | ||||
|  | ||||
|   if (error) | ||||
|     { | ||||
|       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && | ||||
|           !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) | ||||
|         { | ||||
|           g_warning ("Unexpected error while reading application actions: %s", error->message); | ||||
|         } | ||||
|  | ||||
|       g_clear_error (&error); | ||||
|       g_clear_object (&state->dbus_cancellable); | ||||
|  | ||||
|       if (state->name_watcher_id) | ||||
|         { | ||||
|           g_bus_unwatch_name (state->name_watcher_id); | ||||
|           state->name_watcher_id = 0; | ||||
|         } | ||||
|  | ||||
|       g_free (state->dbus_name); | ||||
|       state->dbus_name = NULL; | ||||
|  | ||||
|       g_object_unref (self); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|   object_path = g_strconcat ("/", state->dbus_name, NULL); | ||||
|   g_strdelimit (object_path, ".", '/'); | ||||
|  | ||||
|   state->remote_menu = g_menu_proxy_get (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), | ||||
|                                          state->dbus_name, | ||||
|                                          object_path); | ||||
|  | ||||
|   g_object_notify (G_OBJECT (self), "dbus-id"); | ||||
|   g_object_notify (G_OBJECT (self), "action-group"); | ||||
|   g_object_notify (G_OBJECT (self), "menu"); | ||||
|  | ||||
|   g_object_unref (self); | ||||
|   g_free (object_path); | ||||
| } | ||||
|  | ||||
| static void | ||||
| on_dbus_name_appeared (GDBusConnection *bus, | ||||
|                        const gchar     *name, | ||||
|                        const gchar     *name_owner, | ||||
|                        gpointer         user_data) | ||||
| { | ||||
|   ShellApp *self = SHELL_APP (user_data); | ||||
|   ShellAppRunningState *state = self->running_state; | ||||
|   char *object_path; | ||||
|  | ||||
|   g_assert (state != NULL); | ||||
|  | ||||
|   object_path = g_strconcat ("/", name, NULL); | ||||
|   g_strdelimit (object_path, ".", '/'); | ||||
|  | ||||
|   if (!state->dbus_cancellable) | ||||
|     state->dbus_cancellable = g_cancellable_new (); | ||||
|  | ||||
|   g_dbus_action_group_new (bus, | ||||
|                            name, | ||||
|                            object_path, | ||||
|                            G_DBUS_ACTION_GROUP_FLAGS_NONE, | ||||
|                            state->dbus_cancellable, | ||||
|                            on_action_group_acquired, | ||||
|                            g_object_ref (self)); | ||||
|  | ||||
|   g_free (object_path); | ||||
| } | ||||
|  | ||||
| static void | ||||
| on_dbus_name_disappeared (GDBusConnection *bus, | ||||
|                           const gchar     *name, | ||||
|                           gpointer         user_data) | ||||
| { | ||||
|   ShellApp *self = SHELL_APP (user_data); | ||||
|   ShellAppRunningState *state = self->running_state; | ||||
|  | ||||
|   g_assert (state != NULL); | ||||
|  | ||||
|   if (state->dbus_cancellable) | ||||
|     { | ||||
|       g_cancellable_cancel (state->dbus_cancellable); | ||||
|       g_clear_object (&state->dbus_cancellable); | ||||
|     } | ||||
|  | ||||
|   g_clear_object (&state->remote_actions); | ||||
|   g_clear_object (&state->remote_menu); | ||||
|  | ||||
|   g_free (state->dbus_name); | ||||
|   state->dbus_name = NULL; | ||||
|  | ||||
|   g_bus_unwatch_name (state->name_watcher_id); | ||||
|   state->name_watcher_id = 0; | ||||
| } | ||||
|  | ||||
| void | ||||
| _shell_app_set_dbus_name (ShellApp   *app, | ||||
|                           const char *bus_name) | ||||
| { | ||||
|   g_return_if_fail (app->running_state != NULL); | ||||
|  | ||||
|   if (app->running_state->dbus_name != NULL) | ||||
|     { | ||||
|       /* already associating with another name | ||||
|          (can only happen if you restart the shell in the | ||||
|          middle of the session, in which case it will try | ||||
|          all names seen on the bus; otherwise, it uses | ||||
|          the Hello signal from GApplication and thus knows | ||||
|          for sure which name is the right one) | ||||
|       */ | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|   app->running_state->dbus_name = g_strdup (bus_name); | ||||
|   app->running_state->name_watcher_id = g_bus_watch_name (G_BUS_TYPE_SESSION, | ||||
|                                                           bus_name, | ||||
|                                                           G_BUS_NAME_WATCHER_FLAGS_NONE, | ||||
|                                                           on_dbus_name_appeared, | ||||
|                                                           on_dbus_name_disappeared, | ||||
|                                                           g_object_ref (app), | ||||
|                                                           g_object_unref); | ||||
|  | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * shell_app_get_pids: | ||||
|  * @app: a #ShellApp | ||||
| @@ -1167,13 +1337,28 @@ unref_running_state (ShellAppRunningState *state) | ||||
| { | ||||
|   MetaScreen *screen; | ||||
|  | ||||
|   g_assert (state->refcount > 0); | ||||
|  | ||||
|   state->refcount--; | ||||
|   if (state->refcount > 0) | ||||
|     return; | ||||
|  | ||||
|   screen = shell_global_get_screen (shell_global_get ()); | ||||
|  | ||||
|   g_signal_handler_disconnect (screen, state->workspace_switch_id); | ||||
|  | ||||
|   if (state->dbus_cancellable) | ||||
|     { | ||||
|       g_cancellable_cancel (state->dbus_cancellable); | ||||
|       g_object_unref (state->dbus_cancellable); | ||||
|     } | ||||
|  | ||||
|   g_clear_object (&state->remote_actions); | ||||
|   g_clear_object (&state->remote_menu); | ||||
|   g_free (state->dbus_name); | ||||
|  | ||||
|   if (state->name_watcher_id) | ||||
|     g_bus_unwatch_name (state->name_watcher_id); | ||||
|  | ||||
|   g_slice_free (ShellAppRunningState, state); | ||||
| } | ||||
|  | ||||
| @@ -1349,6 +1534,9 @@ shell_app_dispose (GObject *object) | ||||
|       while (app->running_state->windows) | ||||
|         _shell_app_remove_window (app, app->running_state->windows->data); | ||||
|     } | ||||
|   /* We should have been transitioned when we removed all of our windows */ | ||||
|   g_assert (app->state == SHELL_APP_STATE_STOPPED); | ||||
|   g_assert (app->running_state == NULL); | ||||
|  | ||||
|   G_OBJECT_CLASS(shell_app_parent_class)->dispose (object); | ||||
| } | ||||
| @@ -1399,4 +1587,60 @@ shell_app_class_init(ShellAppClass *klass) | ||||
|                                                       SHELL_TYPE_APP_STATE, | ||||
|                                                       SHELL_APP_STATE_STOPPED, | ||||
|                                                       G_PARAM_READABLE)); | ||||
|  | ||||
|   /** | ||||
|    * ShellApp:id: | ||||
|    * | ||||
|    * The id of this application (a desktop filename, or a special string | ||||
|    * like window:0xabcd1234) | ||||
|    */ | ||||
|   g_object_class_install_property (gobject_class, | ||||
|                                    PROP_ID, | ||||
|                                    g_param_spec_string ("id", | ||||
|                                                         "Application id", | ||||
|                                                         "The desktop file id of this ShellApp", | ||||
|                                                         NULL, | ||||
|                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); | ||||
|  | ||||
|   /** | ||||
|    * ShellApp:dbus-id: | ||||
|    * | ||||
|    * The DBus well-known name of the application, if one can be associated | ||||
|    * to this ShellApp (it means that the application is using GApplication) | ||||
|    */ | ||||
|   g_object_class_install_property (gobject_class, | ||||
|                                    PROP_DBUS_ID, | ||||
|                                    g_param_spec_string ("dbus-id", | ||||
|                                                         "Application DBus Id", | ||||
|                                                         "The DBus well-known name of the application", | ||||
|                                                         NULL, | ||||
|                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); | ||||
|  | ||||
|   /** | ||||
|    * ShellApp:action-group: | ||||
|    * | ||||
|    * The #GDBusActionGroup associated with this ShellApp, if any. See the | ||||
|    * documentation of #GApplication and #GActionGroup for details. | ||||
|    */ | ||||
|   g_object_class_install_property (gobject_class, | ||||
|                                    PROP_ACTION_GROUP, | ||||
|                                    g_param_spec_object ("action-group", | ||||
|                                                         "Application Action Group", | ||||
|                                                         "The action group exported by the remote application", | ||||
|                                                         G_TYPE_DBUS_ACTION_GROUP, | ||||
|                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); | ||||
|   /** | ||||
|    * ShellApp:menu: | ||||
|    * | ||||
|    * The #GMenuProxy associated with this ShellApp, if any. See the | ||||
|    * documentation of #GMenuModel for details. | ||||
|    */ | ||||
|   g_object_class_install_property (gobject_class, | ||||
|                                    PROP_MENU, | ||||
|                                    g_param_spec_object ("menu", | ||||
|                                                         "Application Menu", | ||||
|                                                         "The primary menu exported by the remote application", | ||||
|                                                         G_TYPE_MENU_PROXY, | ||||
|                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ G_BEGIN_DECLS | ||||
| typedef struct _ShellApp ShellApp; | ||||
| typedef struct _ShellAppClass ShellAppClass; | ||||
| typedef struct _ShellAppPrivate ShellAppPrivate; | ||||
| typedef struct _ShellAppAction ShellAppAction; | ||||
|  | ||||
| #define SHELL_TYPE_APP              (shell_app_get_type ()) | ||||
| #define SHELL_APP(object)           (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP, ShellApp)) | ||||
| @@ -36,9 +37,12 @@ typedef enum { | ||||
| GType shell_app_get_type (void) G_GNUC_CONST; | ||||
|  | ||||
| const char *shell_app_get_id (ShellApp *app); | ||||
|  | ||||
| GMenuTreeEntry *shell_app_get_tree_entry (ShellApp *app); | ||||
| GDesktopAppInfo *shell_app_get_app_info (ShellApp *app); | ||||
|  | ||||
| const char *shell_app_get_dbus_id (ShellApp *app); | ||||
|  | ||||
| ClutterActor *shell_app_create_icon_texture (ShellApp *app, int size); | ||||
| ClutterActor *shell_app_get_faded_icon (ShellApp *app, int size); | ||||
| const char *shell_app_get_name (ShellApp *app); | ||||
|   | ||||
| @@ -49,6 +49,9 @@ struct _ShellWindowTracker | ||||
|   /* <MetaWindow * window, ShellApp *app> */ | ||||
|   GHashTable *window_to_app; | ||||
|  | ||||
|   /* <int, gchar *> */ | ||||
|   GHashTable *pid_to_dbus_connection; | ||||
|  | ||||
|   /* <int, ShellApp *app> */ | ||||
|   GHashTable *launched_pid_to_app; | ||||
| }; | ||||
| @@ -436,6 +439,8 @@ track_window (ShellWindowTracker *self, | ||||
|               MetaWindow      *window) | ||||
| { | ||||
|   ShellApp *app; | ||||
|   GPid pid; | ||||
|   gchar *dbus_name; | ||||
|  | ||||
|   if (!shell_window_tracker_is_window_interesting (window)) | ||||
|     return; | ||||
| @@ -451,6 +456,15 @@ track_window (ShellWindowTracker *self, | ||||
|  | ||||
|   _shell_app_add_window (app, window); | ||||
|  | ||||
|   /* Try to associate this ShellApp with a GApplication id, if one exists */ | ||||
|   pid = meta_window_get_pid (window); | ||||
|   dbus_name = g_hash_table_lookup (self->pid_to_dbus_connection, GINT_TO_POINTER ((int) pid)); | ||||
|   if (dbus_name != NULL) | ||||
|     { | ||||
|       _shell_app_set_dbus_name (app, dbus_name); | ||||
|       g_hash_table_remove (self->pid_to_dbus_connection, GINT_TO_POINTER ((int) pid)); | ||||
|     } | ||||
|  | ||||
|   g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0); | ||||
| } | ||||
|  | ||||
| @@ -581,13 +595,161 @@ on_startup_sequence_changed (MetaScreen            *screen, | ||||
|   g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence); | ||||
| } | ||||
|  | ||||
| typedef struct { | ||||
|   ShellWindowTracker *tracker; | ||||
|   gchar *bus_name; | ||||
| } LookupAppDBusData; | ||||
|  | ||||
| static void | ||||
| on_get_connection_unix_pid_reply (GObject      *connection, | ||||
|                                   GAsyncResult *result, | ||||
|                                   gpointer      user_data) | ||||
| { | ||||
|   LookupAppDBusData *data = user_data; | ||||
|   GError *error = NULL; | ||||
|   GVariant *reply; | ||||
|   guint32 pid; | ||||
|   ShellApp *app; | ||||
|  | ||||
|   reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (connection), result, &error); | ||||
|   if (!reply) | ||||
|     { | ||||
|       if (!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER)) | ||||
|           g_warning ("%s\n", error->message); | ||||
|  | ||||
|       g_clear_error (&error); | ||||
|       goto out; | ||||
|     } | ||||
|   if (!g_variant_is_of_type (reply, G_VARIANT_TYPE ("(u)"))) | ||||
|     { | ||||
|       g_variant_unref (reply); | ||||
|       goto out; | ||||
|     } | ||||
|   g_variant_get (reply, "(u)", &pid); | ||||
|   g_variant_unref (reply); | ||||
|  | ||||
|   app = shell_window_tracker_get_app_from_pid (data->tracker, (int)pid); | ||||
|   if (app) | ||||
|     _shell_app_set_dbus_name (app, data->bus_name); | ||||
|   else | ||||
|     { | ||||
|       g_hash_table_insert (data->tracker->pid_to_dbus_connection, | ||||
|                            GINT_TO_POINTER ((int) pid), | ||||
|                            data->bus_name); | ||||
|       data->bus_name = NULL; | ||||
|     } | ||||
|  | ||||
|  out: | ||||
|   g_object_unref (data->tracker); | ||||
|   g_free (data->bus_name); | ||||
|   g_slice_free (LookupAppDBusData, data); | ||||
| } | ||||
|  | ||||
| static void | ||||
| lookup_application_from_name (ShellWindowTracker *self, | ||||
|                               GDBusConnection    *connection, | ||||
|                               const gchar        *bus_name) | ||||
| { | ||||
|   LookupAppDBusData *data; | ||||
|  | ||||
|   data = g_slice_new0 (LookupAppDBusData); | ||||
|   data->tracker = g_object_ref (self); | ||||
|   data->bus_name = g_strdup (bus_name); | ||||
|  | ||||
|   /* | ||||
|    * TODO: Add something to GtkApplication so it definitely knows the .desktop file. | ||||
|    */ | ||||
|   g_dbus_connection_call (connection, | ||||
|                           "org.freedesktop.DBus", | ||||
|                           "/org/freedesktop/DBus", | ||||
|                           "org.freedesktop.DBus", | ||||
|                           "GetConnectionUnixProcessID", | ||||
|                           g_variant_new ("(s)", bus_name), | ||||
|                           NULL, 0, -1, NULL, on_get_connection_unix_pid_reply, data); | ||||
| } | ||||
|  | ||||
| static void | ||||
| on_application_signal (GDBusConnection  *connection, | ||||
|                        const gchar      *sender_name, | ||||
|                        const gchar      *object_path, | ||||
|                        const gchar      *interface_name, | ||||
|                        const gchar      *signal_name, | ||||
|                        GVariant         *parameters, | ||||
|                        gpointer          user_data) | ||||
| { | ||||
|   ShellWindowTracker *tracker = SHELL_WINDOW_TRACKER (user_data); | ||||
|   gchar *bus_name = NULL; | ||||
|  | ||||
|   g_variant_get (parameters, "(&s)", &bus_name); | ||||
|   lookup_application_from_name (tracker, connection, bus_name); | ||||
|  | ||||
|   g_variant_unref (parameters); | ||||
| } | ||||
|  | ||||
| static void | ||||
| on_list_names_end (GObject      *object, | ||||
|                    GAsyncResult *result, | ||||
|                    gpointer      user_data) | ||||
| { | ||||
|   GDBusConnection *connection = G_DBUS_CONNECTION (object); | ||||
|   ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data); | ||||
|   GError *error = NULL; | ||||
|   GVariantIter iter; | ||||
|   gchar *bus_name = NULL; | ||||
|  | ||||
|   GVariant *res = g_dbus_connection_call_finish (connection, result, &error); | ||||
|  | ||||
|   if (!res) | ||||
|     { | ||||
|       g_warning ("ListNames failed: %s", error->message); | ||||
|       g_error_free (error); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|   g_variant_iter_init (&iter, g_variant_get_child_value (res, 0)); | ||||
|   while (g_variant_iter_loop (&iter, "s", &bus_name)) | ||||
|     { | ||||
|       if (bus_name[0] == ':') | ||||
|         { | ||||
|           /* unique name, uninteresting */ | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|       lookup_application_from_name (self, connection, bus_name); | ||||
|     } | ||||
|  | ||||
|   g_variant_unref (res); | ||||
| } | ||||
|  | ||||
| static void | ||||
| shell_window_tracker_init (ShellWindowTracker *self) | ||||
| { | ||||
|   MetaScreen *screen; | ||||
|  | ||||
|   self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, | ||||
|                                                NULL, (GDestroyNotify) g_object_unref); | ||||
|   g_dbus_connection_signal_subscribe (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), | ||||
|                                       NULL, | ||||
|                                       "org.gtk.Application", | ||||
|                                       "Hello", | ||||
|                                       NULL, | ||||
|                                       NULL, | ||||
|                                       G_DBUS_SIGNAL_FLAGS_NONE, | ||||
|                                       on_application_signal, | ||||
|                                       self, NULL); | ||||
|  | ||||
|   g_dbus_connection_call (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), | ||||
|                           "org.freedesktop.DBus", | ||||
|                           "/org/freedesktop/DBus", | ||||
|                           "org.freedesktop.DBus", | ||||
|                           "ListNames", | ||||
|                           NULL, /* parameters */ | ||||
|                           G_VARIANT_TYPE ("(as)"), | ||||
|                           G_DBUS_CALL_FLAGS_NONE, | ||||
|                           -1, /* timeout */ | ||||
|                           NULL, /* cancellable */ | ||||
|                           on_list_names_end, self); | ||||
|  | ||||
|   self->window_to_app = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); | ||||
|   self->pid_to_dbus_connection = g_hash_table_new_full (NULL, NULL, NULL, g_free); | ||||
|  | ||||
|   self->launched_pid_to_app = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_object_unref); | ||||
|  | ||||
| @@ -607,6 +769,7 @@ shell_window_tracker_finalize (GObject *object) | ||||
|  | ||||
|   g_hash_table_destroy (self->window_to_app); | ||||
|   g_hash_table_destroy (self->launched_pid_to_app); | ||||
|   g_hash_table_destroy (self->pid_to_dbus_connection); | ||||
|  | ||||
|   G_OBJECT_CLASS (shell_window_tracker_parent_class)->finalize(object); | ||||
| } | ||||
|   | ||||
							
								
								
									
										99
									
								
								src/test-gapplication.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										99
									
								
								src/test-gapplication.js
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| #!/usr/bin/env gjs | ||||
|  | ||||
| const Gdk = imports.gi.Gdk; | ||||
| const Gio = imports.gi.Gio; | ||||
| const GLib = imports.gi.GLib; | ||||
| const Gtk = imports.gi.Gtk; | ||||
|  | ||||
| function do_action(action, parameter) { | ||||
|     print ("Action '" + action.name + "' invoked"); | ||||
| } | ||||
|  | ||||
| function do_action_param(action, parameter) { | ||||
|     print ("Action '" + action.name + "' invoked with parameter " + parameter.print(true)); | ||||
| } | ||||
|  | ||||
| function do_action_state_change(action) { | ||||
|     print ("Action '" + action.name + "' has now state '" + action.state.deep_unpack() + "'"); | ||||
| } | ||||
|  | ||||
| function main() { | ||||
|     Gtk.init(null, null); | ||||
|  | ||||
|     let app = new Gtk.Application({ application_id: 'org.gnome.Shell.GtkApplicationTest' }); | ||||
|     app.connect('activate', function() { | ||||
| 	print ("Activated"); | ||||
|     }); | ||||
|  | ||||
|     let group = new Gio.SimpleActionGroup(); | ||||
|  | ||||
|     let action = Gio.SimpleAction.new('one', null); | ||||
|     action.connect('activate', do_action); | ||||
|     group.insert(action); | ||||
|  | ||||
|     let action = Gio.SimpleAction.new('two', null); | ||||
|     action.connect('activate', do_action); | ||||
|     group.insert(action); | ||||
|  | ||||
|     let action = Gio.SimpleAction.new_stateful('toggle', null, GLib.Variant.new('b', false)); | ||||
|     action.connect('activate', do_action); | ||||
|     action.connect('notify::state', do_action_state_change); | ||||
|     group.insert(action); | ||||
|  | ||||
|     let action = Gio.SimpleAction.new('disable', null); | ||||
|     action.set_enabled(false); | ||||
|     action.connect('activate', do_action); | ||||
|     group.insert(action); | ||||
|  | ||||
|     let action = Gio.SimpleAction.new('parameter-int', GLib.VariantType.new('u')); | ||||
|     action.connect('activate', do_action_param); | ||||
|     group.insert(action); | ||||
|  | ||||
|     let action = Gio.SimpleAction.new('parameter-string', GLib.VariantType.new('s')); | ||||
|     action.connect('activate', do_action_param); | ||||
|     group.insert(action); | ||||
|  | ||||
|     app.action_group = group; | ||||
|  | ||||
|     let menu = new Gio.Menu(); | ||||
|     menu.append('An action', 'one'); | ||||
|  | ||||
|     let section = new Gio.Menu(); | ||||
|     section.append('Another action', 'two'); | ||||
|     section.append('Same as above', 'two'); | ||||
|     menu.append_section(null, section); | ||||
|  | ||||
|     // another section, to check separators | ||||
|     section = new Gio.Menu(); | ||||
|     section.append('Checkbox', 'toggle'); | ||||
|     section.append('Disabled', 'disable'); | ||||
|     menu.append_section(null, section); | ||||
|  | ||||
|     // empty sections or submenus should be invisible | ||||
|     menu.append_section('Empty section', new Gio.Menu()); | ||||
|     menu.append_submenu('Empty submenu', new Gio.Menu()); | ||||
|  | ||||
|     let submenu = new Gio.Menu(); | ||||
|     submenu.append('Open c:\\', 'parameter-string::c:\\'); | ||||
|     submenu.append('Open /home', 'parameter-string::/home'); | ||||
|     menu.append_submenu('Recent files', submenu); | ||||
|  | ||||
|     let item = Gio.MenuItem.new('Say 42', null); | ||||
|     item.set_action_and_target_value('parameter-int', GLib.Variant.new('u', 42)); | ||||
|     menu.append_item(item); | ||||
|  | ||||
|     let item = Gio.MenuItem.new('Say 43', null); | ||||
|     item.set_action_and_target_value('parameter-int', GLib.Variant.new('u', 43)); | ||||
|     menu.append_item(item); | ||||
|  | ||||
|     app.menu = menu; | ||||
|  | ||||
|     app.connect('startup', function(app) { | ||||
| 	let window = new Gtk.Window({ title: "Test Application", application: app }); | ||||
| 	window.present(); | ||||
|     }); | ||||
|  | ||||
|     app.run(null); | ||||
| } | ||||
|  | ||||
| main(); | ||||
		Reference in New Issue
	
	Block a user