From 9dc9d0713f214aaeb893bf56e1f77e3fa6b4b180 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Fri, 12 Mar 2021 08:35:22 +0100 Subject: [PATCH 1/7] :beetle: Fix updating menus --- src/extension/Menu.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/extension/Menu.js b/src/extension/Menu.js index e95b9427..c7923b70 100644 --- a/src/extension/Menu.js +++ b/src/extension/Menu.js @@ -637,7 +637,9 @@ var Menu = class Menu { item.name = newConfig.name; item.icon = newConfig.icon; item.angle = newConfig.angle; - item.setActivationCallback(newConfig.activate || null); + item.setSelectionCallback(newConfig.onSelect || null); + item.setHoverCallback(newConfig.onHover || null); + item.setUnhoverCallback(newConfig.onUnhover || null); const children = new Set(item.getChildMenuItems()); @@ -680,7 +682,9 @@ var Menu = class Menu { icon: newChild.icon, angle: newChild.angle, }); - newChild.matchingChild.setActivationCallback(newConfig.activate || null); + newChild.matchingChild.setSelectionCallback(newConfig.onSelect || null); + newChild.matchingChild.setHoverCallback(newConfig.onHover || null); + newChild.matchingChild.setUnhoverCallback(newConfig.onUnhover || null); item.addMenuItem(newChild.matchingChild); newChild.matchingChild.onSettingsChange(this._settings); @@ -689,7 +693,9 @@ var Menu = class Menu { newChild.matchingChild.name = newChild.name; newChild.matchingChild.icon = newChild.icon; newChild.matchingChild.angle = newChild.angle; - newChild.matchingChild.setActivationCallback(newConfig.activate || null); + newChild.matchingChild.setSelectionCallback(newConfig.onSelect || null); + newChild.matchingChild.setHoverCallback(newConfig.onHover || null); + newChild.matchingChild.setUnhoverCallback(newConfig.onUnhover || null); } }); From 1ff947e4ebda4cde8200d785904fe491fc5305cf Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sun, 14 Mar 2021 09:44:16 +0100 Subject: [PATCH 2/7] :recycle: Use custom widgets for item configuration --- assets/settings.ui | 414 +++-------------------------- src/common/ConfigWidgetFactory.js | 321 ++++++++++++++++++++++ src/common/ItemRegistry.js | 53 ++-- src/common/actions/Command.js | 58 ++-- src/common/actions/DBusSignal.js | 57 ++-- src/common/actions/File.js | 58 ++-- src/common/actions/InsertText.js | 55 ++-- src/common/actions/Shortcut.js | 57 ++-- src/common/actions/Uri.js | 57 ++-- src/common/menus/FrequentlyUsed.js | 55 ++-- src/common/menus/RecentFiles.js | 57 ++-- src/prefs/MenuEditorPage.js | 312 ++++++---------------- 12 files changed, 751 insertions(+), 803 deletions(-) create mode 100644 src/common/ConfigWidgetFactory.js diff --git a/assets/settings.ui b/assets/settings.ui index 652d1d1b..88c65602 100644 --- a/assets/settings.ui +++ b/assets/settings.ui @@ -827,67 +827,11 @@ Whenever you encounter a 🐞️ bug or have 🎉️ feature request, feel free 1 10 - - False - - - True - False - 5 - 5 - 5 - 5 - 5 - - - True - False - False - True - - - 0 - 0 - - - - - 100 1 10 - - 400 - False - True - - - True - False - 5 - 5 - 5 - 5 - True - True - 5 - - - True - False - True - True - - - 0 - 0 - - - - - False @@ -8536,8 +8480,7 @@ This can be used if you want to capture a video of Fly-Pie. True False - center - gtk-find + gtk-find @@ -8668,49 +8611,13 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& - + True False slide-up 200 - - True - False - 20 - 5 - 10 - - - True - False - start - label - - - False - True - 0 - - - - - True - False - end - label - - - - False - True - end - 1 - - - + @@ -8719,213 +8626,6 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& 4 - - - True - False - slide-up - 200 - - - True - False - - - True - True - False - True - none - item-application-popover - - - True - False - center - gtk-find - - - - - False - True - 0 - - - - - True - True - - - True - True - 1 - - - - - - - - False - True - 5 - - - - - True - False - slide-up - 200 - - - True - False - - - True - True - False - True - none - item-file-popover - - - True - False - center - gtk-find - - - - - False - True - 0 - - - - - True - True - - - True - True - 1 - - - - - - - - False - True - 6 - - - - - True - False - slide-up - 200 - - - True - True - - - - - False - True - 7 - - - - - True - False - slide-up - 200 - - - True - True - 4 - alpha - item-count - True - 4 - - - - - False - True - 8 - - - - - True - False - slide-up - 200 - - - True - False - 0 - in - - - True - False - - - 50 - True - True - - - True - False - center - center - 5 - 5 - vertical - 5 - - - - - - - - - - - - - - - - - False - True - 9 - - True @@ -8941,16 +8641,46 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& vertical 5 - + True False - 10 + vertical + 5 - + True False - start - Shortcut + 10 + + + True + False + start + Shortcut + + + False + True + 0 + + + + + True + False + end + This will open the menu. + + + + False + True + end + 1 + + False @@ -8959,71 +8689,13 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& - - True - False - end - This will open the menu. - - - - False - True - end - 1 - - - - - False - True - 0 - - - - - True - False - 0 - in - - - True - False - - - 50 - True - True - - - True - False - center - center - 5 - 5 - vertical - 5 - - - - - - - - - - False True - 1 + 0 @@ -9060,7 +8732,7 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& False True - 2 + 3 @@ -9069,7 +8741,7 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& False True - 10 + 5 @@ -9089,7 +8761,7 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& False True - 11 + 6 @@ -9116,7 +8788,7 @@ In order to prevent weird behavior, <b>fixed angles will be removed</b& True True - 12 + 7 diff --git a/src/common/ConfigWidgetFactory.js b/src/common/ConfigWidgetFactory.js new file mode 100644 index 00000000..c8eb90ed --- /dev/null +++ b/src/common/ConfigWidgetFactory.js @@ -0,0 +1,321 @@ +////////////////////////////////////////////////////////////////////////////////////////// +// ___ _ ___ // +// | | \/ | ) | | This software may be modified and distri- // +// O- |- | | - | | |- -O buted under the terms of the MIT license. // +// | |_ | | | |_ See the LICENSE file for details. // +// // +////////////////////////////////////////////////////////////////////////////////////////// + +'use strict'; + +const {Gtk, Gdk} = imports.gi; + +const _ = imports.gettext.domain('flypie').gettext; + +////////////////////////////////////////////////////////////////////////////////////////// +// This class contains some static utility functions which can be used to create // +// configuration widgets for the items in the menu editor. // +// // +// Each menu item may have additional properties set by the user. The Gtk.Widgets // +// required to do this are created by the config.getWidget() methods of the individual // +// item types. While all the code below could be directly put into these methods, they // +// are oftentimes quite similar and therefore in this file here. // +////////////////////////////////////////////////////////////////////////////////////////// + +var ConfigWidgetFactory = class ConfigWidgetFactory { + + // ---------------------------------------------------------------------- static methods + + // This creates a widget which can be used to adjust a line of text. The 'name' and + // 'description' are shown above and 'text' is the initial value. 'callback(text)' will + // be fired whenever the text is edited. The function returns a Gtk.Box containing all + // the required widgets. + static createTextWidget(name, description, text, callback) { + const box = this.createConfigWidgetCaption(name, description); + + const entry = new Gtk.Entry({text: text}); + box.pack_start(entry, false, false, 0); + + entry.connect('notify::text', (widget) => { + callback(widget.text); + }); + + return box; + } + + // This creates a widget which can be used to adjust a number. The 'name' and + // 'description' are shown above, 'min' and 'max' define the allowed value range, 'step' + // the allowed increment and 'value' is the initial value. 'callback(number)' will be + // fired whenever a new number is chosen. The function returns a Gtk.Box + // containing all the required widgets. + static createCountWidget(name, description, min, max, step, value, callback) { + const box = this.createConfigWidgetCaption(name, description); + + const entry = Gtk.SpinButton.new_with_range(min, max, step); + entry.value = value; + box.pack_start(entry, false, false, 0); + + entry.connect('notify::value', (widget) => { + callback(widget.value); + }); + + return box; + } + + // This creates a widget which can be used to select a file. The 'name' and + // 'description' are shown above, 'file' is the initial value, and 'callback(file, + // name, icon)' will be fired whenever a new application is selected. The function + // returns a Gtk.Box containing all the required widgets. Note that 'icon' and 'name' + // passed to the callback may be undefined when the user directly edited the file path. + static createFileWidget(name, description, file, callback) { + const box = this.createConfigWidgetCaption(name, description); + + const entryBox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL}); + entryBox.get_style_context().add_class('linked'); + box.pack_start(entryBox, false, false, 0); + + const button = new Gtk.MenuButton(); + button.image = Gtk.Image.new_from_icon_name('gtk-find', Gtk.IconSize.BUTTON); + + const popover = new Gtk.Popover(); + button.set_popover(popover); + + const fileChooser = new Gtk.FileChooserWidget({ + action: Gtk.FileChooserAction.OPEN, + margin: 5, + height_request: 375, + width_request: 600 + }); + + popover.add(fileChooser); + + fileChooser.show(); + + entryBox.pack_start(button, false, false, 0); + + const entry = new Gtk.Entry({text: file}); + entryBox.pack_start(entry, true, true, 0); + + // Initialize the file-select-popover. When a file is activated, the popover is + // hidden, when a file is selected, the item's name, icon and file input fields are + // updated accordingly. + fileChooser.connect('file-activated', () => { + popover.popdown(); + }); + + fileChooser.connect('selection-changed', (widget) => { + if (widget.get_file()) { + const info = widget.get_file().query_info('standard::icon', 0, null); + callback( + widget.get_filename(), widget.get_file().get_basename(), + info.get_icon().to_string()); + entry.text = widget.get_filename(); + } + }); + + entry.connect('notify::text', (widget) => { + callback(widget.text); + }); + + return box; + } + + // This creates a widget which can be used to select an application. The 'name' and + // 'description' are shown above, 'command' is the initial value, and 'callback(command, + // name, icon)' will be fired whenever a new application is selected. The function + // returns a Gtk.Box containing all the required widgets. Note that 'icon' and 'name' + // passed to the callback may be undefined when the user directly edited the command. + static createCommandWidget(name, description, command, callback) { + const box = this.createConfigWidgetCaption(name, description); + + const entryBox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL}); + entryBox.get_style_context().add_class('linked'); + box.pack_start(entryBox, false, false, 0); + + const button = new Gtk.MenuButton(); + button.image = Gtk.Image.new_from_icon_name('gtk-find', Gtk.IconSize.BUTTON); + + const popover = new Gtk.Popover(); + button.set_popover(popover); + + const appChooser = new Gtk.AppChooserWidget({show_all: true, margin: 5}); + popover.add(appChooser); + + appChooser.show(); + + entryBox.pack_start(button, false, false, 0); + + const entry = new Gtk.Entry({text: command}); + entryBox.pack_start(entry, true, true, 0); + + // Initialize the application-select-popover. On mouse-up the popover is hidden, + // whenever an application is selected, the item's name, icon and command input fields + // are updated accordingly. + appChooser.connect('button-release-event', () => { + popover.popdown(); + }); + + appChooser.connect('application-selected', (widget, app) => { + callback(app.get_commandline(), app.get_display_name(), app.get_icon().to_string()); + entry.text = app.get_commandline(); + }); + + entry.connect('notify::text', (widget) => { + callback(widget.text); + }); + + return box; + } + + // This creates a widget which can be used to select a shortcut. The 'name' and + // 'description' are shown above, 'shortcut' is the initial value, and + // 'callback(shortcut)' will be fired whenever a new shortcut is selected. The function + // returns a Gtk.Box containing all the required widgets. + static createShortcutWidget(name, description, shortcut, callback) { + + const [container, label] = this.createShortcutLabel(true, callback); + label.set_accelerator(shortcut); + + const box = this.createConfigWidgetCaption(name, description); + box.pack_start(container, false, false, 0); + + return box; + } + + // This is used by all the function above to create the header of the configuration + // widget. It returns a vertical Gtk.Box containing a horizontal box with the name and + // the dimmed description. + static createConfigWidgetCaption(name, description) { + const vBox = + new Gtk.Box({orientation: Gtk.Orientation.VERTICAL, spacing: 5, margin_top: 20}); + const hBox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL}); + + vBox.pack_start(hBox, false, false, 0); + + // This is shown on the left above the data widget. + const nameLabel = new Gtk.Label({label: name}); + + // This is shown on the right above the data widget. + const descriptionLabel = new Gtk.Label({label: description}); + descriptionLabel.get_style_context().add_class('dim-label'); + + hBox.pack_start(nameLabel, false, false, 0); + hBox.pack_end(descriptionLabel, false, false, 0); + + return vBox; + } + + + // This creates a widget which can be used to select a shortcut. A Gtk.ShortcutLabel is + // used to visualize the shortcut. + // The doFullGrab parameter enables selection of shortcuts which are already bound to + // something else. For example, imagine you have configured opening a terminal via + // Ctrl+Alt+T in your system settings. Now if doFullGrab == false, selecting Ctrl+Alt+T + // will not work; it will open the terminal instead. However, if doFullGrab == true, you + // will be able to select Ctrl+Alt+T. This is very important - we do not want to + // bind menus to shortcuts which are bound to something else - but we want menu + // items to simulate shortcut presses which are actually bound to something else! + // The onSelect callback will be fired whenever a new shortcut is select and will + // receive the shortcut as a string parameter. + // The function returns two things [Gtk.Frame, Gtk.ShortcutLabel]. The former is the + // container which should be added to something, the latter is the internal label. You + // can use the set_accelerator method of this label to adjust the currently shown + // shortcut programmatically. + static createShortcutLabel(doFullGrab, onSelect) { + const frame = new Gtk.Frame({shadow_type: Gtk.ShadowType.IN}); + const listBox = new Gtk.ListBox(); + const row = new Gtk.ListBoxRow({height_request: 50}); + + frame.add(listBox); + listBox.add(row); + + const label = new Gtk.ShortcutLabel({ + // Translators: This is shown on the shortcut-buttons when no shortcut is selected. + disabled_text: _('Not bound.'), + halign: Gtk.Align.CENTER, + valign: Gtk.Align.CENTER + }); + row.add(label); + + // Whenever the widget is in the please-select-something-state, the label is cleared + // and a text indicating that the user should press the shortcut is shown. To be able + // to reset to the state before (e.g. when ESC is pressed), this stores the previous + // value. + let lastAccelerator = ''; + + // This function grabs the keyboard input. If doFullGrab == true, the complete + // keyboard input of the default Seat will be grabbed. Else only a Gtk grab is + // performed. The text of the Gtk.ShortcutLabel is changed to indicate that the widget + // is waiting for input. + const grabKeyboard = () => { + if (doFullGrab) { + const seat = Gdk.Display.get_default().get_default_seat(); + seat.grab( + row.get_window(), Gdk.SeatCapabilities.KEYBOARD, false, null, null, null); + } + row.grab_add(); + lastAccelerator = label.get_accelerator(); + label.set_accelerator(''); + label.set_disabled_text( + _('Press the shortcut!\nESC to cancel, BackSpace to unbind')); + }; + + // This function cancels any previous grab. The label's disabled-text is reset to "Not + // bound". + const cancelGrab = () => { + if (doFullGrab) { + const seat = Gdk.Display.get_default().get_default_seat(); + seat.ungrab(); + } + row.grab_remove(); + row.parent.unselect_all(); + label.set_disabled_text(_('Not bound.')); + }; + + // When the row is activated, the input is grabbed. + row.parent.connect('row-activated', (row) => { + grabKeyboard(); + }); + + // Key input events are received once the input is grabbed. + row.connect('key-press-event', (row, event) => { + if (row.is_selected()) { + const keyval = event.get_keyval()[1]; + const mods = event.get_state()[1] & Gtk.accelerator_get_default_mod_mask(); + + if (keyval == Gdk.KEY_Escape) { + // Escape cancels the shortcut selection. + label.set_accelerator(lastAccelerator); + cancelGrab(); + + } else if (keyval == Gdk.KEY_BackSpace) { + // BackSpace removes any bindings. + label.set_accelerator(''); + onSelect(''); + cancelGrab(); + + } else if (Gtk.accelerator_valid(keyval, mods)) { + // Else, if a valid accelerator was pressed, we store it. + const accelerator = Gtk.accelerator_name(keyval, mods); + onSelect(accelerator); + label.set_accelerator(accelerator); + cancelGrab(); + } + + return true; + } + return false; + }); + + // Clicking with the mouse cancels the shortcut selection. + row.connect('button-press-event', () => { + if (row.has_grab()) { + label.set_accelerator(lastAccelerator); + cancelGrab(); + } + return true; + }); + + return [frame, label]; + } +} \ No newline at end of file diff --git a/src/common/ItemRegistry.js b/src/common/ItemRegistry.js index 3c6ffb12..79980ba0 100644 --- a/src/common/ItemRegistry.js +++ b/src/common/ItemRegistry.js @@ -33,22 +33,6 @@ const utils = Me.imports.src.common.utils; var ItemClass = {MENU: 0, ACTION: 1}; -////////////////////////////////////////////////////////////////////////////////////////// -// Each menu item type has a data type - this determines which widgets are visible // -// when an item of this type is selected in the settings dialog. If you create a new // -// item type, this list may have to be extended. This will also require some changes to // -// the MenuEditor.js as this is responsible for showing and hiding the widgets // -// accordingly. // -////////////////////////////////////////////////////////////////////////////////////////// - -var ItemDataType = { - TEXT: 0, // A Gtk.Entry for arbitrary text. - SHORTCUT: 1, // A shortcut-selector is shown. - COMMAND: 2, // A Gtk.Entry with an additional button for searching installed apps. - FILE: 3, // A Gtk.Entry with an additional file chooser button. - COUNT: 4 // A Gtk.SpinButton for selecting numbers. -}; - ////////////////////////////////////////////////////////////////////////////////////////// // The getItemTypes() of the ItemRegistry can be used to access all available action // // and menu types. Each item type should have eight properties: // @@ -65,23 +49,24 @@ var ItemDataType = { // This should be translatable. // // description: This will be shown in the right hand side settings when an item of // // this type is selected. This should be translatable. // -// data: An optional object which defines some additional data which can be // -// chosen by the user. The additional data is always stored as a // -// string. If you actually need to store a number (e.g. for // -// ItemDataType.COUNT, you will have to use parseInt(data) in the // -// createItem() method. // -// type: This determines which widget is shown to select the // -// data in the settings dialog. Possible values are // -// listed in ItemDataType above. // -// name: This will be shown on the left above the data widget // -// in the settings dialog. // -// description: This will be shown on the right above the data widget // -// in the settings dialog. // -// default: This string value will be used as data for newly // -// created items. // +// config: An optional object which defines additional data which can be // +// configured by the user. The object must have to properties: // +// defaultData: A object defining the default data which will be // +// stored for newly created items of this type. When a // +// new item is selected, the getWidget() method below // +// will be executed and the defaultData object will be // +// passed as first parameter. // +// getWidget: A function which returns a Gtk.Widget which will be // +// shown in the menu editor when an item of this type // +// is selected. The function receives two arguments: // +// First an object containing the currently configured // +// item data, second a callback which should be fired // +// whenever the user changes the state of the widget. // +// The new data should be passed as object to the // +// callback. // // createItem: A function which will be called whenever a menu is opened containing // // an item of this kind. The data value chosen by the user will be // -// passed to this function. // +// passed to this function as object. // ////////////////////////////////////////////////////////////////////////////////////////// let _itemTypes = null; @@ -191,10 +176,10 @@ var ItemRegistry = class ItemRegistry { // Assign default data. if (config.data == undefined) { - if (this.getItemTypes()[config.type].data != undefined) { - config.data = this.getItemTypes()[config.type].data.default; + if (this.getItemTypes()[config.type].config != undefined) { + config.data = this.getItemTypes()[config.type].config.defaultData; } else { - config.data = ''; + config.data = {}; } } diff --git a/src/common/actions/Command.js b/src/common/actions/Command.js index 67636b37..977fe2d3 100644 --- a/src/common/actions/Command.js +++ b/src/common/actions/Command.js @@ -12,9 +12,10 @@ const Gio = imports.gi.Gio; const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const utils = Me.imports.src.common.utils; -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.common.utils; +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; ////////////////////////////////////////////////////////////////////////////////////////// // The command actions executes a shell command when activated. This can be used to // @@ -43,35 +44,50 @@ var action = { description: _( 'The Launch Application action executes any given command. This is primarily used to open applications but may have plenty of other use cases as well.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.COMMAND, - - // This is shown on the left above the data widget in the settings dialog. - name: _('Command'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('Use the button to list installed apps!'), + // Items of this type have an additional text configuration parameter which represents + // the command to execute. + config: { + // This is used as data for newly created items of this type. + defaultData: {command: ''}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "command" property. + // To stay backwards compatible with Fly-Pie 4, we have to also handle the case + // where the command is given as a simple string value. + let command = ''; + if (typeof data === 'string') { + command = data; + } else if (data.command != undefined) { + command = data.command; + } - // This is be used as data for newly created items. - default: '', + return ConfigWidgetFactory.createCommandWidget( + _('Command'), _('Use the button to list installed apps!'), command, + (command, name, icon) => { + updateCallback({command: command}, name, icon); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "command" property. + // To stay backwards compatible with Fly-Pie 4, we have to also handle the case + // where the command is given as a simple string value. createItem: (data) => { + let command = ''; + if (typeof data === 'string') { + command = data; + } else if (data.command != undefined) { + command = data.command; + } + // The onSelect() function will be called when the user selects this action. return { onSelect: () => { try { const ctx = global.create_app_launch_context(0, -1); const item = Gio.AppInfo.create_from_commandline( - data, null, Gio.AppInfoCreateFlags.NONE); + command, null, Gio.AppInfoCreateFlags.NONE); item.launch([], ctx); } catch (error) { utils.debug('Failed to execute command: ' + error); diff --git a/src/common/actions/DBusSignal.js b/src/common/actions/DBusSignal.js index ef2f0e90..3a9dac2c 100644 --- a/src/common/actions/DBusSignal.js +++ b/src/common/actions/DBusSignal.js @@ -10,8 +10,9 @@ const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; ////////////////////////////////////////////////////////////////////////////////////////// // The D-Bus signal action does nothing. It is actually something like a dummy action. // @@ -41,28 +42,42 @@ var action = { description: _( 'The D-Bus Signal action does nothing on its own. But you can listen on the D-Bus for its activation. This can be very useful in custom menus opened via the command line.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.TEXT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('ID'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('This will be passed to the D-Bus signal.'), - - // This is be used as data for newly created items. - default: '', + // Items of this type have an additional text configuration parameter which is passed as + // ID to the D-Bus signals. + config: { + // This is used as data for newly created items of this type. + defaultData: {id: ''}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single ID property. To stay + // backwards compatible with Fly-Pie 4, we have to also handle the case where the ID + // is given as a simple string value. + let id = ''; + if (typeof data === 'string') { + id = data; + } else if (data.id != undefined) { + id = data.id; + } + + return ConfigWidgetFactory.createTextWidget( + _('ID'), _('This will be passed to the D-Bus signal.'), id, (id) => { + updateCallback({id: id}); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single ID property. To stay + // backwards compatible with Fly-Pie 4, we have to also handle the case where the ID is + // given as a simple string value. createItem: (data) => { - return {id: data, onSelect: () => {}}; + let id = ''; + if (typeof data === 'string') { + id = data; + } else if (data.id != undefined) { + id = data.id; + } + + return {id: id, onSelect: () => {}}; } }; \ No newline at end of file diff --git a/src/common/actions/File.js b/src/common/actions/File.js index 42643e16..6dfa57ae 100644 --- a/src/common/actions/File.js +++ b/src/common/actions/File.js @@ -12,9 +12,10 @@ const Gio = imports.gi.Gio; const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const utils = Me.imports.src.common.utils; -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.common.utils; +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; ////////////////////////////////////////////////////////////////////////////////////////// // The file action is very similar to the Url action, but only works for files. // @@ -43,33 +44,48 @@ var action = { description: _( 'The Open File action will open the file specified above with your system\'s default application.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.FILE, - - // This is shown on the left above the data widget in the settings dialog. - name: _('File'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('It will be opened with the default app.'), + // Items of this type have an additional text configuration parameter which represents + // the command to execute. + config: { + // This is used as data for newly created items of this type. + defaultData: {file: ''}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "file" property. + // To stay backwards compatible with Fly-Pie 4, we have to also handle the case + // where the file is given as a simple string value. + let file = ''; + if (typeof data === 'string') { + file = data; + } else if (data.file != undefined) { + file = data.file; + } - // This is be used as data for newly created items. - default: '' + return ConfigWidgetFactory.createFileWidget( + _('File'), _('It will be opened with the default app.'), file, + (file, name, icon) => { + updateCallback({file: file}, name, icon); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "file" property. + // To stay backwards compatible with Fly-Pie 4, we have to also handle the case + // where the file is given as a simple string value. createItem: (data) => { + let file = ''; + if (typeof data === 'string') { + file = data; + } else if (data.file != undefined) { + file = data.file; + } + // The onSelect() function will be called when the user selects this action. return { onSelect: () => { try { - Gio.AppInfo.launch_default_for_uri('file://' + data, null); + Gio.AppInfo.launch_default_for_uri('file://' + file, null); } catch (error) { utils.debug('Failed to open file: ' + error); } diff --git a/src/common/actions/InsertText.js b/src/common/actions/InsertText.js index fd916d3c..ebe566f9 100644 --- a/src/common/actions/InsertText.js +++ b/src/common/actions/InsertText.js @@ -12,8 +12,9 @@ const {Gdk, Gtk} = imports.gi; const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; // We import the InputManipulator optionally. When this file is included from the daemon // side, it is available and can be used in the activation code of the action defined @@ -54,33 +55,47 @@ var action = { description: _( 'The Insert Text action copies the given text to the clipboard and then simulates a Ctrl+V. This can be useful if you realize that you often write the same things.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.TEXT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('Text'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('This text will be inserted.'), + // Items of this type have an additional text configuration parameter which is the text + // which is to be inserted. + config: { + // This is used as data for newly created items of this type. + defaultData: {text: ''}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "text" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the text is given as a simple string value. + let text = ''; + if (typeof data === 'string') { + text = data; + } else if (data.text != undefined) { + text = data.text; + } - // This is be used as data for newly created items. - default: '', + return ConfigWidgetFactory.createTextWidget( + _('Text'), _('This text will be inserted.'), text, (text) => { + updateCallback({text: text}); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "text" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the text is given as a simple string value. createItem: (data) => { + let text = ''; + if (typeof data === 'string') { + text = data; + } else if (data.text != undefined) { + text = data.text; + } + // The onSelect() function will be called when the user selects this action. return { onSelect: () => { const clipboard = Gtk.Clipboard.get_default(Gdk.Display.get_default()); - clipboard.set_text(data, -1); + clipboard.set_text(text, -1); InputManipulator.activateAccelerator('v'); } }; diff --git a/src/common/actions/Shortcut.js b/src/common/actions/Shortcut.js index 6ec8ae3c..dc614480 100644 --- a/src/common/actions/Shortcut.js +++ b/src/common/actions/Shortcut.js @@ -10,8 +10,9 @@ const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; // We have to import the InputManipulator optionally. This is because this file is // included from both sides: From prefs.js and from extension.js. When included from @@ -52,29 +53,43 @@ var action = { description: _( 'The Activate Shortcut action simulates a key combination when activated. For example, this can be used to switch virtual desktops, control multimedia playback or to undo / redo operations.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.SHORTCUT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('Shortcut'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('This shortcut will be simulated.'), - - // This is be used as data for newly created items. - default: '', + // Items of this type have an additional text configuration parameter which represents + // the command to execute. + config: { + // This is used as data for newly created items of this type. + defaultData: {shortcut: ''}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "shortcut" property. + // To stay backwards compatible with Fly-Pie 4, we have to also handle the case + // where the shortcut is given as a simple string value. + let shortcut = ''; + if (typeof data === 'string') { + shortcut = data; + } else if (data.shortcut != undefined) { + shortcut = data.shortcut; + } + + return ConfigWidgetFactory.createShortcutWidget( + _('Shortcut'), _('This shortcut will be simulated.'), shortcut, (shortcut) => { + updateCallback({shortcut: shortcut}); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "shortcut" property. + // To stay backwards compatible with Fly-Pie 4, we have to also handle the case + // where the shortcut is given as a simple string value. createItem: (data) => { + let shortcut = ''; + if (typeof data === 'string') { + shortcut = data; + } else if (data.shortcut != undefined) { + shortcut = data.shortcut; + } + // The onSelect() function will be called when the user selects this action. - return {onSelect: () => InputManipulator.activateAccelerator(data)}; + return {onSelect: () => InputManipulator.activateAccelerator(shortcut)}; } }; diff --git a/src/common/actions/Uri.js b/src/common/actions/Uri.js index ca3d589f..8fd2d665 100644 --- a/src/common/actions/Uri.js +++ b/src/common/actions/Uri.js @@ -12,9 +12,10 @@ const Gio = imports.gi.Gio; const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const utils = Me.imports.src.common.utils; -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.common.utils; +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; ////////////////////////////////////////////////////////////////////////////////////////// // The Uri action opens the defined URI with the system's default application. // @@ -42,33 +43,47 @@ var action = { description: _( 'When the Open URI action is activated, the above URI is opened with the default application. For http URLs, this will be your web browser. However, it is also possible to open other URIs such as "mailto:foo@bar.org".'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.TEXT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('URI'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('It will be opened with the default app.'), + // Items of this type have an additional text configuration parameter which is the URI + // which is to be opened. + config: { + // This is used as data for newly created items of this type. + defaultData: {uri: ''}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "uri" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the URI is given as a simple string value. + let uri = ''; + if (typeof data === 'string') { + uri = data; + } else if (data.uri != undefined) { + uri = data.uri; + } - // This is be used as data for newly created items. - default: '', + return ConfigWidgetFactory.createTextWidget( + _('URI'), _('It will be opened with the default app.'), uri, (uri) => { + updateCallback({uri: uri}); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "uri" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the URI is given as a simple string value. createItem: (data) => { + let uri = ''; + if (typeof data === 'string') { + uri = data; + } else if (data.uri != undefined) { + uri = data.uri; + } + // The onSelect() function will be called when the user selects this action. return { onSelect: () => { try { - Gio.AppInfo.launch_default_for_uri(data, null); + Gio.AppInfo.launch_default_for_uri(uri, null); } catch (error) { utils.debug('Failed to open URL: ' + error); } diff --git a/src/common/menus/FrequentlyUsed.js b/src/common/menus/FrequentlyUsed.js index b90a0003..c2f6b8a3 100644 --- a/src/common/menus/FrequentlyUsed.js +++ b/src/common/menus/FrequentlyUsed.js @@ -10,8 +10,9 @@ const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; // We have to import the Shell module optionally. This is because this file is included // from both sides: From prefs.js and from extension.js. When included from prefs.js, the @@ -53,29 +54,43 @@ var menu = { description: _( 'The Frequently Used menu shows a list of frequently used applications. For efficient selections, you should limit the maximum number of shown applications to about twelve.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.COUNT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('Max Item Count'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('Limits the number of children.'), + // Items of this type have an additional count configuration parameter which is the + // maximum number of items to display. + config: { + // This is used as data for newly created items of this type. + defaultData: {maxNum: 7}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "maxNum" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the maxNum is given as a simple string value. + let maxNum = 12; + if (typeof data === 'string') { + maxNum = parseInt(data); + } else if (data.maxNum != undefined) { + maxNum = data.maxNum; + } - // This is be used as data for newly created items. - default: '7' + return ConfigWidgetFactory.createCountWidget( + _('Max Item Count'), _('Limits the number of children.'), 1, 20, 1, maxNum, + (value) => { + updateCallback({maxNum: value}); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "maxNum" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the maxNum is given as a simple string value. createItem: (data) => { - const maxNum = parseInt(data); + let maxNum = 12; + if (typeof data === 'string') { + maxNum = parseInt(data); + } else if (data.maxNum != undefined) { + maxNum = data.maxNum; + } + const apps = Shell.AppUsage.get_default().get_most_used().slice(0, maxNum); const result = {children: []}; diff --git a/src/common/menus/RecentFiles.js b/src/common/menus/RecentFiles.js index f5f1c33c..3a51671e 100644 --- a/src/common/menus/RecentFiles.js +++ b/src/common/menus/RecentFiles.js @@ -12,9 +12,10 @@ const {Gio, Gtk} = imports.gi; const _ = imports.gettext.domain('flypie').gettext; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const utils = Me.imports.src.common.utils; -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.common.utils; +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; ////////////////////////////////////////////////////////////////////////////////////////// // Returns an item with entries for each recently used file, as reported by // @@ -43,29 +44,43 @@ var menu = { description: _( 'The Recent Files menu shows a list of recently used files. For efficient selections, you should limit the maximum number of shown files to about twelve.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.COUNT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('Max Item Count'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('Limits the number of children.'), + // Items of this type have an additional count configuration parameter which is the + // maximum number of items to display. + config: { + // This is used as data for newly created items of this type. + defaultData: {maxNum: 7}, + + getWidget(data, updateCallback) { + // The data paramter *should* be an object containing a single "maxNum" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the maxNum is given as a simple string value. + let maxNum = 12; + if (typeof data === 'string') { + maxNum = parseInt(data); + } else if (data.maxNum != undefined) { + maxNum = data.maxNum; + } - // This is be used as data for newly created items. - default: '7' + return ConfigWidgetFactory.createCountWidget( + _('Max Item Count'), _('Limits the number of children.'), 1, 20, 1, maxNum, + (value) => { + updateCallback({maxNum: value}); + }); + } }, // This will be called whenever a menu is opened containing an item of this kind. - // The data value chosen by the user will be passed to this function. + // The data paramter *should* be an object containing a single "maxNum" property. To + // stay backwards compatible with Fly-Pie 4, we have to also handle the case where + // the maxNum is given as a simple string value. createItem: (data) => { - const maxNum = parseInt(data); + let maxNum = 12; + if (typeof data === 'string') { + maxNum = parseInt(data); + } else if (data.maxNum != undefined) { + maxNum = data.maxNum; + } + const recentFiles = Gtk.RecentManager.get_default().get_items(); const num = recentFiles.length; const result = {children: []}; diff --git a/src/prefs/MenuEditorPage.js b/src/prefs/MenuEditorPage.js index 72429a41..698ddfce 100644 --- a/src/prefs/MenuEditorPage.js +++ b/src/prefs/MenuEditorPage.js @@ -11,13 +11,13 @@ const Cairo = imports.cairo; const {GObject, Gdk, GLib, Gtk, Gio} = imports.gi; -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const utils = Me.imports.src.common.utils; -const DBusInterface = Me.imports.src.common.DBusInterface.DBusInterface; -const Statistics = Me.imports.src.common.Statistics.Statistics; -const ItemRegistry = Me.imports.src.common.ItemRegistry.ItemRegistry; -const ItemClass = Me.imports.src.common.ItemRegistry.ItemClass; -const ItemDataType = Me.imports.src.common.ItemRegistry.ItemDataType; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.common.utils; +const DBusInterface = Me.imports.src.common.DBusInterface.DBusInterface; +const Statistics = Me.imports.src.common.Statistics.Statistics; +const ItemRegistry = Me.imports.src.common.ItemRegistry.ItemRegistry; +const ItemClass = Me.imports.src.common.ItemRegistry.ItemClass; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; const DBusWrapper = Gio.DBusProxy.makeProxyWrapper(DBusInterface.description); @@ -33,7 +33,7 @@ let ColumnTypes = { ICON: GObject.TYPE_STRING, // The string representation of the icon. NAME: GObject.TYPE_STRING, // The name without any markup. TYPE: GObject.TYPE_STRING, // The item type. Like 'Shortcut' or 'Bookmarks'. - DATA: GObject.TYPE_STRING, // Used for the command, file, application, ... + DATA: GObject.TYPE_STRING, // JSON string containing item-specific data. SHORTCUT: GObject.TYPE_STRING, // Hotkey to open top-level menus. CENTERED: GObject.TYPE_BOOLEAN, // Wether a menu should be opened centered. ANGLE: GObject.TYPE_INT, // The fixed angle for items. @@ -485,10 +485,12 @@ var MenuEditorPage = class MenuEditorPage { } if (info != null) { + const data = JSON.stringify({command: info.get_commandline()}); + this._set(newIter, 'DATA', data); this._set(newIter, 'ICON', icon); this._set(newIter, 'NAME', info.get_display_name()); this._set(newIter, 'TYPE', newType); - this._set(newIter, 'DATA', info.get_commandline()); + success = true; } } @@ -500,10 +502,12 @@ var MenuEditorPage = class MenuEditorPage { const info = file.query_info('standard::icon', 0, null); if (info != null) { + // Skip the file:// + const data = JSON.stringify({file: text.substring(7)}); + this._set(newIter, 'DATA', data); this._set(newIter, 'ICON', info.get_icon().to_string()); this._set(newIter, 'NAME', file.get_basename()); this._set(newIter, 'TYPE', newType); - this._set(newIter, 'DATA', text.substring(7)); // Skip the file:// success = true; } @@ -517,10 +521,10 @@ var MenuEditorPage = class MenuEditorPage { const newType = 'Uri'; const name = text.length < 20 ? text : text.substring(0, 20) + '...'; + this._set(newIter, 'DATA', JSON.stringify({uri: text})); this._set(newIter, 'ICON', ItemRegistry.getItemTypes()[newType].icon); this._set(newIter, 'NAME', name); this._set(newIter, 'TYPE', newType); - this._set(newIter, 'DATA', text); success = true; } } @@ -530,10 +534,10 @@ var MenuEditorPage = class MenuEditorPage { const newType = 'InsertText'; const name = text.length < 20 ? text : text.substring(0, 20) + '...'; + this._set(newIter, 'DATA', JSON.stringify({text: text})); this._set(newIter, 'ICON', ItemRegistry.getItemTypes()[newType].icon); this._set(newIter, 'NAME', 'Insert: ' + name); this._set(newIter, 'TYPE', newType); - this._set(newIter, 'DATA', text); } return true; @@ -729,40 +733,12 @@ var MenuEditorPage = class MenuEditorPage { this._builder.get_object('item-icon-drawingarea').queue_draw(); }); - - // Now we initialize all other widgets of the item settings. That is, for example, the - // name, the url, command, or file input fields. - - // Store the item's name in the tree store when the text of the input field is + // Store the item's name in the tree store when the text of the name input field is // changed. this._builder.get_object('item-name').connect('notify::text', (widget) => { this._setSelected('NAME', widget.text); }); - // Store the item's file path in the tree store's DATA column when the text of the - // corresponding input field is changed. - this._builder.get_object('item-file').connect('notify::text', (widget) => { - this._setSelected('DATA', widget.text); - }); - - // Store the item's command in the tree store's DATA column when the text of the - // corresponding input field is changed. - this._builder.get_object('item-command').connect('notify::text', (widget) => { - this._setSelected('DATA', widget.text); - }); - - // Store the item's maximum item count in the tree store's DATA column when the - // corresponding input field is changed. - this._builder.get_object('item-count').connect('value-changed', (adjustment) => { - this._setSelected('DATA', adjustment.value); - }); - - // Store the item's text in the tree store's DATA column when the text of the - // corresponding input field is changed. - this._builder.get_object('item-text').connect('notify::text', (widget) => { - this._setSelected('DATA', widget.text); - }); - // For top-level menus, store whether they should be opened in the center of the // screen. this._builder.get_object('menu-centered').connect('notify::active', (widget) => { @@ -810,44 +786,16 @@ var MenuEditorPage = class MenuEditorPage { } }); - - // Initialize the file-select-popover. When a file is activated, the popover is - // hidden, when a file is selected, the item's name, icon and file input fields are - // updated accordingly. - this._builder.get_object('item-file-chooser').connect('file-activated', () => { - this._builder.get_object('item-file-popover').popdown(); - }); - - this._builder.get_object('item-file-chooser') - .connect('selection-changed', (widget) => { - const info = widget.get_file().query_info('standard::icon', 0, null); - this._builder.get_object('icon-name').text = info.get_icon().to_string(); - this._builder.get_object('item-name').text = widget.get_file().get_basename(); - this._builder.get_object('item-file').text = widget.get_filename(); - }); - - // Initialize the application-select-popover. On mouse-up the popover is hidden, - // whenever an application is selected, the item's name, icon and command input fields - // are updated accordingly. - this._builder.get_object('application-popover-list') - .connect('button-release-event', () => { - this._builder.get_object('item-application-popover').popdown(); - }); - - this._builder.get_object('application-popover-list') - .connect('application-selected', (widget, app) => { - this._builder.get_object('icon-name').text = app.get_icon().to_string(); - this._builder.get_object('item-name').text = app.get_display_name(); - this._builder.get_object('item-command').text = app.get_commandline(); - }); - - // Initialize the two shortcut-select elements. See the documentation of - // _initShortcutSelect for details. - this._itemShortcutLabel = - this._initShortcutSelect('item-shortcut-select', true, 'DATA'); - this._menuShortcutLabel = - this._initShortcutSelect('menu-shortcut-select', false, 'SHORTCUT'); - + // Initialize the menu shortcut-select element. See the documentation of + // createShortcutLabel for details. + { + const [box, label] = ConfigWidgetFactory.createShortcutLabel(false, (shortcut) => { + this._setSelected('SHORTCUT', shortcut); + }); + this._builder.get_object('menu-shortcut-box').pack_start(box, false, false, 0); + this._menuShortcutLabel = label; + box.show_all(); + } // When the currently selected menu item changes, the content of the settings widgets // must be updated accordingly. @@ -866,21 +814,14 @@ var MenuEditorPage = class MenuEditorPage { this._builder.get_object('action-types-list').sensitive = actionsSensitive; // There are multiple Gtk.Revealers involved. Based on the selected item's type - // their content is either shown or hidden. First we assume that all are hidden - // and selectively set them to be shown. All settings are invisible if nothing is + // their content is either shown or hidden. All settings are invisible if nothing is // selected, the menu settings (shortcut, centered) are visible if a top-level // element is selected, for all other items the fixed angle can be set. - const revealers = { - 'item-settings-revealer': somethingSelected, - 'item-settings-menu-revealer': this._isToplevelSelected(), - 'item-settings-angle-revealer': !this._isToplevelSelected(), - 'item-settings-data-caption-revealer': false, - 'item-settings-item-shortcut-revealer': false, - 'item-settings-count-revealer': false, - 'item-settings-command-revealer': false, - 'item-settings-file-revealer': false, - 'item-settings-text-revealer': false - }; + this._builder.get_object('item-settings-revealer').reveal_child = somethingSelected; + this._builder.get_object('item-settings-menu-revealer').reveal_child = + this._isToplevelSelected(); + this._builder.get_object('item-settings-angle-revealer').reveal_child = + !this._isToplevelSelected(); if (somethingSelected) { @@ -915,52 +856,58 @@ var MenuEditorPage = class MenuEditorPage { this._builder.get_object('item-angle').value = this._getSelected('ANGLE'); } - // Now we check whether the selected item has a data property. - const data = ItemRegistry.getItemTypes()[selectedType].data; - if (data) { + // Now we check whether the selected item has a config property. + const config = ItemRegistry.getItemTypes()[selectedType].config; + + // If it has a config property, we can show the revealer for the config widget. + const revealer = this._builder.get_object('item-settings-config-revealer'); + revealer.reveal_child = config != null; - // If it has a data property, we can show the revealer for the name and the - // description of the associated data. - revealers['item-settings-data-caption-revealer'] = true; + // In this case, we also ask the config object to create a new configuration + // widget for the selected type. + if (config) { - // Then we set the content of the name and description labels. - this._builder.get_object('item-settings-data-name').label = data.name; - this._builder.get_object('item-settings-data-description').label = - data.description; + // First we remove any previous configuration widget. + revealer.foreach((oldChild) => {revealer.remove(oldChild)}); - // Finally we update one of the data widgets based on the data type of the - // selected item. - if (data.type == ItemDataType.SHORTCUT) { - this._itemShortcutLabel.set_accelerator(this._getSelected('DATA')); - revealers['item-settings-item-shortcut-revealer'] = true; + // To populate the new configuration widget with data, we retrieve the data from + // the tree store's data column. This **should** be a JSON string, but if + // someone tries to load a config from Fly-Pie 4 or older, this may not be the + // case. So we print a warning in this case. + let data = this._getSelected('DATA'); + try { + data = JSON.parse(data); + } catch (error) { + utils.debug( + 'Warning: Invalid configuration data is stored for the selected item: ' + + error); + } - } else if (data.type == ItemDataType.FILE) { - this._builder.get_object('item-file').text = this._getSelected('DATA'); - revealers['item-settings-file-revealer'] = true; + // Then we create and add the new configuration widget. The callback will be + // fired when the user changes the data. "data" will contain an object which is + // to be stored as JSON string, optionally the name and icon of the currently + // selected item can be changed as well (e.g. when an application is selected, + // we want to change the item's name and icon accordingly). + const newChild = config.getWidget(data, (data, name, icon) => { + this._setSelected('DATA', JSON.stringify(data)); - } else if (data.type == ItemDataType.COMMAND) { - this._builder.get_object('item-command').text = this._getSelected('DATA'); - revealers['item-settings-command-revealer'] = true; + if (name) { + this._builder.get_object('item-name').text = name; + } - } else if (data.type == ItemDataType.COUNT) { - this._builder.get_object('item-count').value = this._getSelected('DATA'); - revealers['item-settings-count-revealer'] = true; + if (icon) { + this._builder.get_object('icon-name').text = icon; + } + }); - } else if (data.type == ItemDataType.TEXT) { - this._builder.get_object('item-text').text = this._getSelected('DATA'); - revealers['item-settings-text-revealer'] = true; - } + revealer.add(newChild); + newChild.show_all(); } // All modifications are done, all future modifications will come from the user // and should result in saving the configuration again. this._menuSavingAllowed = true; } - - // Finally update the state of all revealers. - for (const revealer in revealers) { - this._builder.get_object(revealer).reveal_child = revealers[revealer]; - } }); // Initialize the tip-display label. @@ -999,107 +946,6 @@ var MenuEditorPage = class MenuEditorPage { iconList.set_sort_column_id(0, Gtk.SortType.ASCENDING); } - - // This creates / initializes a Gtk.ListBoxRow which can be used to select a shortcut. A - // Gtk.ShortcutLabel is used to visualize the shortcut - this element is not yet - // available in Glade, therefore it's created here in code. This makes everything a bit - // hard-wired, which could be improved in the future. - // The functionality is added to a Gtk.ListBoxRow identified via rowName. This row is - // expected to have single Gtk.Box as child; the Gtk.ShortcutLabel will be packed to the - // end of this Gtk.Box. - // The doFullGrab parameters enables selection of shortcuts which are already bound to - // something else. For example, imagine you have configured opening a terminal via - // Ctrl+Alt+T in your system settings. Now if doFullGrab == false, selecting Ctrl+Alt+T - // will not work; it will open the terminal instead. However, if doFullGrab == true, you - // will be able to select Ctrl+Alt+T. This is very important - we do not want to bind - // menus to shortcuts which are bound to something else - but we want menu items to - // simulate shortcut presses which are actually bound to something else! - _initShortcutSelect(rowName, doFullGrab, dataColumn) { - - const row = this._builder.get_object(rowName); - - // Translators: This is shown on the shortcut-buttons when no shortcut is selected. - const label = new Gtk.ShortcutLabel({disabled_text: _('Not bound.')}); - row.get_child().pack_end(label, false, false, 0); - label.show(); - - // This function grabs the keyboard input. If doFullGrab == true, the complete - // keyboard input of the default Seat will be grabbed. Else only a Gtk grab is - // performed. The text of the Gtk.ShortcutLabel is changed to indicate that the widget - // is waiting for input. - const grabKeyboard = () => { - if (doFullGrab) { - const seat = Gdk.Display.get_default().get_default_seat(); - seat.grab( - row.get_window(), Gdk.SeatCapabilities.KEYBOARD, false, null, null, null); - } - row.grab_add(); - label.set_accelerator(''); - label.set_disabled_text( - _('Press the shortcut!\nESC to cancel, BackSpace to unbind')); - }; - - // This function cancels any previous grab. The label's disabled-text is reset to "Not - // bound". - const cancelGrab = () => { - if (doFullGrab) { - const seat = Gdk.Display.get_default().get_default_seat(); - seat.ungrab(); - } - row.grab_remove(); - row.parent.unselect_all(); - label.set_disabled_text(_('Not bound.')); - }; - - // When the row is activated, the input is grabbed. - row.parent.connect('row-activated', (row) => { - grabKeyboard(); - }); - - // Key input events are received once the input is grabbed. - row.connect('key-press-event', (row, event) => { - if (row.is_selected()) { - const keyval = event.get_keyval()[1]; - const mods = event.get_state()[1] & Gtk.accelerator_get_default_mod_mask(); - - if (keyval == Gdk.KEY_Escape) { - // Escape cancels the shortcut selection. - label.set_accelerator(this._getSelected(dataColumn)); - cancelGrab(); - - } else if (keyval == Gdk.KEY_BackSpace) { - // BackSpace removes any bindings. - label.set_accelerator(''); - this._setSelected(dataColumn, ''); - cancelGrab(); - - } else if (Gtk.accelerator_valid(keyval, mods)) { - // Else, if a valid accelerator was pressed, we store it. - const accelerator = Gtk.accelerator_name(keyval, mods); - this._setSelected(dataColumn, accelerator); - label.set_accelerator(accelerator); - cancelGrab(); - } - - return true; - } - return false; - }); - - // Clicking with the mouse cancels the shortcut selection. - row.connect('button-press-event', () => { - if (row.has_grab()) { - label.set_accelerator(this._getSelected(dataColumn)); - cancelGrab(); - } - return true; - }); - - // Return the label - this wouldn't be necessary if we could create the - // Gtk.ShortcutLabel directly in Glade. - return label; - } - // There is a small label in the menu editor which shows random tips at regular // intervals. _initInfoLabel() { @@ -1185,8 +1031,10 @@ var MenuEditorPage = class MenuEditorPage { this._set(iter, 'ANGLE', -1); this._set(iter, 'SHORTCUT', ''); - if (ItemRegistry.getItemTypes()[newType].data != undefined) { - this._set(iter, 'DATA', ItemRegistry.getItemTypes()[newType].data.default); + if (ItemRegistry.getItemTypes()[newType].config != undefined) { + this._set( + iter, 'DATA', + JSON.stringify(ItemRegistry.getItemTypes()[newType].config.defaultData)); } } @@ -1406,7 +1254,7 @@ var MenuEditorPage = class MenuEditorPage { name: this._get(iter, 'NAME'), icon: this._get(iter, 'ICON'), type: this._get(iter, 'TYPE'), - data: this._get(iter, 'DATA'), + data: JSON.parse(this._get(iter, 'DATA')), angle: this._get(iter, 'ANGLE') }; @@ -1425,7 +1273,7 @@ var MenuEditorPage = class MenuEditorPage { name: this._get(iter, 'NAME'), icon: this._get(iter, 'ICON'), type: this._get(iter, 'TYPE'), - data: this._get(iter, 'DATA'), + data: JSON.parse(this._get(iter, 'DATA')), shortcut: this._get(iter, 'SHORTCUT'), id: this._get(iter, 'ID'), centered: this._get(iter, 'CENTERED'), @@ -1466,7 +1314,7 @@ var MenuEditorPage = class MenuEditorPage { this._set(iter, 'ICON', child.icon); this._set(iter, 'NAME', child.name); this._set(iter, 'TYPE', child.type); - this._set(iter, 'DATA', child.data); + this._set(iter, 'DATA', JSON.stringify(child.data)); this._set(iter, 'ANGLE', child.angle); this._set(iter, 'SHORTCUT', ''); @@ -1487,7 +1335,7 @@ var MenuEditorPage = class MenuEditorPage { this._set(iter, 'ICON', config.icon); this._set(iter, 'NAME', config.name); this._set(iter, 'TYPE', config.type); - this._set(iter, 'DATA', config.data); + this._set(iter, 'DATA', JSON.stringify(config.data)); this._set(iter, 'SHORTCUT', config.shortcut); this._set(iter, 'CENTERED', config.centered); this._set(iter, 'ID', config.id != undefined ? config.id : this._getNewID()); From 11640e1faa79c5dd4e699beac02ca2e5e47b7874 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sun, 14 Mar 2021 10:16:33 +0100 Subject: [PATCH 3/7] :memo: Update action / menu creation docs --- docs/creating-actions.md | 66 ++++++++++++++++++++++++++-------------- docs/creating-menus.md | 11 ++++--- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/docs/creating-actions.md b/docs/creating-actions.md index ff9acd39..6fd70e79 100644 --- a/docs/creating-actions.md +++ b/docs/creating-actions.md @@ -10,7 +10,8 @@ Actions have an `onSelect()` method which is called when the user selects them; If you want to create a new Action type for Fly-Pie, this guide is made for you! As an example, we will create an Action which shows a notification with a user-defined message whenever it is selected. -First, create a file `src/common/actions/ExampleAction.js` with the following content. +Before you start, you should read the [Software Architecture Page](software-architecture.md) to get an overview of the components of Fly-Pie. +Then create a file `src/common/actions/ExampleAction.js` with the following content. You should read the code, most of it is explained with inline comments! ```javascript @@ -41,9 +42,10 @@ try { } // Some extension-local imports we will use further down. -const Me = imports.misc.extensionUtils.getCurrentExtension(); -const utils = Me.imports.src.common.utils; -const ItemRegistry = Me.imports.src.common.ItemRegistry; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.common.utils; +const ItemRegistry = Me.imports.src.common.ItemRegistry; +const ConfigWidgetFactory = Me.imports.src.common.ConfigWidgetFactory.ConfigWidgetFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This simple example action shows a desktop notification when selected. The text of // @@ -72,23 +74,40 @@ var action = { // This is the (long) description shown when an item of this type is selected. description: _('Bar bar bar bar.'), - // Items of this type have an additional data property which can be set by the user. The - // data value chosen by the user is passed to the createItem() method further below. - data: { - - // The data type determines which widget is visible when an item of this type is - // selected in the settings dialog. - type: ItemRegistry.ItemDataType.TEXT, - - // This is shown on the left above the data widget in the settings dialog. - name: _('Message'), - - // Translators: Please keep this short. - // This is shown on the right above the data widget in the settings dialog. - description: _('Shown when this is activated.'), - - // This is be used as data for newly created items. - default: _('Hello World!'), + // Items of this type have a custom user setting for the text to show in the + // notification. The 'config' property below defines how this data can be set. + config: { + + // This is used as data for newly created items of this type. You can add any + // number of properties to this defaultData object. Just make sure that you use + // the same properties in the updateCallback further below. + defaultData: {message: _('Hello World!')}, + + // This is called whenever an item of this type is selected in the menu editor. + // It should return a Gtk.Widget which will be shown in the sidebar of the menu + // editor. The currently configured data object will be passed as first parameter, + // the second parameter is a callback which should be fired whenever the user + // changes something in the widgets. + getWidget(data, updateCallback) { + + // Our data parameter *should* be an object containing a single "message" + // property (like the defaultData above). In order to prevent a crash + // when that's not the case (e.g. when the user edited the menu configuration + // by hand and made a mistake) we check this here. + let message = data.message || ''; + + // You can use Gtk here to create any widget you want. In this tutorial we will + // use the ConfigWidgetFactory to do this job. Feel free to look into this method + // to learn the details. + return ConfigWidgetFactory.createTextWidget( + _('Message'), // Shown on the left above the text entry. + _('Shown when this is activated.'), // Shown on the right above the text entry. + message, // The initial value of the entry. + (message) => { // Called whenever the text is modified. + updateCallback({message: message}); // We call the updateCallback with a new + } // data object. + ); + } }, // This will be called whenever a menu is opened containing an item of this kind. @@ -97,10 +116,13 @@ var action = { // This will be printed to the log when a menu is opened containing such an action. utils.debug('ExampleAction Created!'); + // Handle invalid data. + let message = data.message || ''; + // The onSelect() function will be called when the user selects this action. return { onSelect: () => { - Main.notify(_('ExampleAction Selected!'), data); + Main.notify(_('ExampleAction Selected!'), message); } }; } diff --git a/docs/creating-menus.md b/docs/creating-menus.md index 31b4e8ed..d70ccae6 100644 --- a/docs/creating-menus.md +++ b/docs/creating-menus.md @@ -10,7 +10,8 @@ Actions have an `onSelect()` method which is called when the user selects them; If you want to create a new Menu type for Fly-Pie, this guide is made for you! As an example, we will create a Menu which contains three actions, each of which shows a desktop notification when selected. -First, create a file `src/common/menus/ExampleMenu.js` with the following content. +Before you start, you should read the [Software Architecture Page](software-architecture.md) to get an overview of the components of Fly-Pie. +Then create a file `src/common/menus/ExampleMenu.js` with the following content. You should read the code, most of it is explained with inline comments! ```javascript @@ -72,9 +73,9 @@ var menu = { // This is the (long) description shown when an item of this type is selected. description: _('Bar bar bar bar.'), - // Menus can also have a data field. See the documentation on how-to create custom - // actions for details. This example menu does not use a data field. - // data: { ... } + // Menus can also have a config field like actions. See the documentation on how-to + // create custom actions for details. This example menu does not use a config field. + // config: { ... } // This will be called whenever a menu is opened containing an item of this kind. createItem: () => { @@ -117,7 +118,7 @@ Once this file is in place, you just need to add the new Action to the `src/comm To do this, add the following line to the other, similar-looking lines in `getItemTypes()`. ```javascript -ExampleMenu: actions.ExampleMenu.menu, +ExampleMenu: menus.ExampleMenu.menu, ``` Finally you can restart GNOME Shell with Alt + F2, r + Enter (or logout / login on Wayland). From 44da6c83748083984e3600d354f0f8d8785466e1 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sun, 14 Mar 2021 10:30:40 +0100 Subject: [PATCH 4/7] :beetle: Fix default values --- src/common/menus/FrequentlyUsed.js | 4 ++-- src/common/menus/RecentFiles.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/menus/FrequentlyUsed.js b/src/common/menus/FrequentlyUsed.js index c2f6b8a3..3098145a 100644 --- a/src/common/menus/FrequentlyUsed.js +++ b/src/common/menus/FrequentlyUsed.js @@ -64,7 +64,7 @@ var menu = { // The data paramter *should* be an object containing a single "maxNum" property. To // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the maxNum is given as a simple string value. - let maxNum = 12; + let maxNum = 7; if (typeof data === 'string') { maxNum = parseInt(data); } else if (data.maxNum != undefined) { @@ -84,7 +84,7 @@ var menu = { // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the maxNum is given as a simple string value. createItem: (data) => { - let maxNum = 12; + let maxNum = 7; if (typeof data === 'string') { maxNum = parseInt(data); } else if (data.maxNum != undefined) { diff --git a/src/common/menus/RecentFiles.js b/src/common/menus/RecentFiles.js index 3a51671e..42d05dfe 100644 --- a/src/common/menus/RecentFiles.js +++ b/src/common/menus/RecentFiles.js @@ -54,7 +54,7 @@ var menu = { // The data paramter *should* be an object containing a single "maxNum" property. To // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the maxNum is given as a simple string value. - let maxNum = 12; + let maxNum = 7; if (typeof data === 'string') { maxNum = parseInt(data); } else if (data.maxNum != undefined) { @@ -74,7 +74,7 @@ var menu = { // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the maxNum is given as a simple string value. createItem: (data) => { - let maxNum = 12; + let maxNum = 7; if (typeof data === 'string') { maxNum = parseInt(data); } else if (data.maxNum != undefined) { From 1376444e028f2e3ebdac93ed2e0b88bf38b70906 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sun, 14 Mar 2021 10:31:03 +0100 Subject: [PATCH 5/7] :memo: Update D-Bus docs --- docs/dbus-interface.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/dbus-interface.md b/docs/dbus-interface.md index 9de5406f..3db22c32 100644 --- a/docs/dbus-interface.md +++ b/docs/dbus-interface.md @@ -50,13 +50,17 @@ gdbus call --session --dest org.gnome.Shell \ "name": "Up", \ "icon": "🔼️", \ "type": "Shortcut", \ - "data": "Up" \ + "data": { \ + "shortcut": "Up" \ + } \ }, \ { \ "name": "Down", \ "icon": "🔽️", \ "type": "Shortcut", \ - "data": "Down" \ + "data": { \ + "shortcut": "Down" \ + } \ } \ ] \ }' @@ -86,20 +90,20 @@ The table below lists all possible item types. Some of the types require that th | Actions | Default `data` | Description | |------------|-----------------------|-------------| -| **`"Command"`** | `""` | This action executes a command given in `data`. This is primarily used to open applications but may have plenty of other use cases as well. | -| **`"DBusSignal"`** | `""` | This action does nothing on its own. But you can listen on the D-Bus for its activation. This can be very useful in custom menus opened via the command line. The string given in `data` will be passed as `itemID` to the `OnHover`, `OnUnhover` and `OnSelect` signals. Below this table you will find an example! | -| **`"File"`** | `""` | This action will open a file given with an absolute path in `data` with your system\'s default application. | -| **`"InsertText"`** | `""` | This action copies the text given in `data` to the clipboard and then simulates a Ctrl+V. This can be useful if you realize that you often write the same things. | -| **`"Shortcut"`** | `""` | This action simulates a key combination when activated. For example, this can be used to switch virtual desktops, control multimedia playback or to undo / redo operations. `data` should be something like `"space"`. | -| **`"Uri"`** | `""` | When this action is activated, the URI given in `data` is opened with the default application. For http URLs, this will be your web browser. However, it is also possible to open other URIs such as `"mailto:foo@bar.org"`. | +| **`"Command"`** | `{"command":""}` | This action executes a command given in `data`. This is primarily used to open applications but may have plenty of other use cases as well. | +| **`"DBusSignal"`** | `{"id":""}` | This action does nothing on its own. But you can listen on the D-Bus for its activation. This can be very useful in custom menus opened via the command line. The ID string given in `data` will be passed as `itemID` to the `OnHover`, `OnUnhover` and `OnSelect` signals. Below this table you will find an example! | +| **`"File"`** | `{"file":""}` | This action will open a file given with an absolute path in `data` with your system\'s default application. | +| **`"InsertText"`** | `{"text":""}` | This action copies the text given in `data` to the clipboard and then simulates a Ctrl+V. This can be useful if you realize that you often write the same things. | +| **`"Shortcut"`** | `{"shortcut":""}` | This action simulates a key combination when activated. For example, this can be used to switch virtual desktops, control multimedia playback or to undo / redo operations. `data` should be something like `{"shortcut":"space"}`. | +| **`"Uri"`** | `{"uri":""}` | When this action is activated, the URI given in `data` is opened with the default application. For http URLs, this will be your web browser. However, it is also possible to open other URIs such as `{"uri":"mailto:foo@bar.org"}`. | | **Menus** | | | | **`"CustomMenu"`** | _not used_ | Use the `"children"` property to add as many actions or submenus as you want! | | **`"Bookmarks"`** | _not used_ | This menu shows an item for the trash, your desktop and each bookmarked directory. | | **`"Devices"`** | _not used_ | This menu shows an item for each mounted volume, like USB-Sticks. | | **`"Favorites"`** | _not used_ | This menu shows the applications you have pinned to GNOME Shell's Dash. | -| **`"FrequentlyUsed"`** | `"7"` | This menu shows a list of frequently used applications. You should limit the maximum number of shown applications to a reasonable number given in `data`. | +| **`"FrequentlyUsed"`** | `{"maxNum":7}` | This menu shows a list of frequently used applications. You should limit the maximum number of shown applications to a reasonable number given in `data`. | | **`"MainMenu"`** | _not used_ | This menu shows all installed applications. Usually, this is very cluttered as many sections contain too many items to be used efficiently. You should rather setup your own menus! This menu is only available if the typelib for GMenu is installed on the system. Usually the package is called something like `gir1.2-gmenu-3.0`. | -| **`"RecentFiles"`** | `"7"` | This menu shows a list of recently used files. You should limit the maximum number of shown files to a reasonable number given in `data`. | +| **`"RecentFiles"`** | `{"maxNum":7}` | This menu shows a list of recently used files. You should limit the maximum number of shown files to a reasonable number given in `data`. | | **`"RunningApps"`** | _not used_ | This menu shows all currently running applications. This is similar to the Alt+Tab window selection. As the entries change position frequently, this is actually not very effective. | | **`"System"`** | _not used_ | This menu shows an items for screen-lock, shutdown, settings, etc. | @@ -157,7 +161,7 @@ gdbus call --session --dest org.gnome.Shell \ { \ "name": "Cat", \ "icon":"🐈", \ - "data": "cat!!" \ + "data": {"id": "cat!!"} \ } \ ] \ } \ From b16b23fcd1b5ea7b88f2cb8018a2eb71a7cbcd13 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sun, 14 Mar 2021 10:32:44 +0100 Subject: [PATCH 6/7] :beetle: Fix link --- src/common/actions/DBusSignal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/actions/DBusSignal.js b/src/common/actions/DBusSignal.js index 3a9dac2c..1e4cf223 100644 --- a/src/common/actions/DBusSignal.js +++ b/src/common/actions/DBusSignal.js @@ -40,7 +40,7 @@ var action = { // This is the (long) description shown when an item of this type is selected. description: _( - 'The D-Bus Signal action does nothing on its own. But you can listen on the D-Bus for its activation. This can be very useful in custom menus opened via the command line.'), + 'The D-Bus Signal action does nothing on its own. But you can listen on the D-Bus for its activation. This can be very useful in custom menus opened via the command line.'), // Items of this type have an additional text configuration parameter which is passed as // ID to the D-Bus signals. From bcb941d21bddc4861ab1e47aa66f1be322d409a8 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sun, 14 Mar 2021 12:50:02 +0100 Subject: [PATCH 7/7] :memo: Update comments --- src/common/actions/Command.js | 12 ++++++++---- src/common/actions/DBusSignal.js | 12 ++++++++---- src/common/actions/File.js | 14 +++++++++----- src/common/actions/InsertText.js | 12 ++++++++---- src/common/actions/Shortcut.js | 16 ++++++++++------ src/common/actions/Uri.js | 12 ++++++++---- src/common/menus/FrequentlyUsed.js | 12 ++++++++---- src/common/menus/RecentFiles.js | 12 ++++++++---- 8 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/common/actions/Command.js b/src/common/actions/Command.js index 977fe2d3..5a0c7975 100644 --- a/src/common/actions/Command.js +++ b/src/common/actions/Command.js @@ -50,10 +50,14 @@ var action = { // This is used as data for newly created items of this type. defaultData: {command: ''}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "command" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the command is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "command" property. - // To stay backwards compatible with Fly-Pie 4, we have to also handle the case - // where the command is given as a simple string value. let command = ''; if (typeof data === 'string') { command = data; @@ -70,7 +74,7 @@ var action = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "command" property. + // The data parameter *should* be an object containing a single "command" property. // To stay backwards compatible with Fly-Pie 4, we have to also handle the case // where the command is given as a simple string value. createItem: (data) => { diff --git a/src/common/actions/DBusSignal.js b/src/common/actions/DBusSignal.js index 1e4cf223..597a7fdd 100644 --- a/src/common/actions/DBusSignal.js +++ b/src/common/actions/DBusSignal.js @@ -48,10 +48,14 @@ var action = { // This is used as data for newly created items of this type. defaultData: {id: ''}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "id" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the ID is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single ID property. To stay - // backwards compatible with Fly-Pie 4, we have to also handle the case where the ID - // is given as a simple string value. let id = ''; if (typeof data === 'string') { id = data; @@ -67,7 +71,7 @@ var action = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single ID property. To stay + // The data parameter *should* be an object containing a single ID property. To stay // backwards compatible with Fly-Pie 4, we have to also handle the case where the ID is // given as a simple string value. createItem: (data) => { diff --git a/src/common/actions/File.js b/src/common/actions/File.js index 6dfa57ae..1ec111b3 100644 --- a/src/common/actions/File.js +++ b/src/common/actions/File.js @@ -45,15 +45,19 @@ var action = { 'The Open File action will open the file specified above with your system\'s default application.'), // Items of this type have an additional text configuration parameter which represents - // the command to execute. + // the file path to open. config: { // This is used as data for newly created items of this type. defaultData: {file: ''}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "file" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the file is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "file" property. - // To stay backwards compatible with Fly-Pie 4, we have to also handle the case - // where the file is given as a simple string value. let file = ''; if (typeof data === 'string') { file = data; @@ -70,7 +74,7 @@ var action = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "file" property. + // The data parameter *should* be an object containing a single "file" property. // To stay backwards compatible with Fly-Pie 4, we have to also handle the case // where the file is given as a simple string value. createItem: (data) => { diff --git a/src/common/actions/InsertText.js b/src/common/actions/InsertText.js index ebe566f9..72b64d11 100644 --- a/src/common/actions/InsertText.js +++ b/src/common/actions/InsertText.js @@ -61,10 +61,14 @@ var action = { // This is used as data for newly created items of this type. defaultData: {text: ''}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "text" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the text is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "text" property. To - // stay backwards compatible with Fly-Pie 4, we have to also handle the case where - // the text is given as a simple string value. let text = ''; if (typeof data === 'string') { text = data; @@ -80,7 +84,7 @@ var action = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "text" property. To + // The data parameter *should* be an object containing a single "text" property. To // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the text is given as a simple string value. createItem: (data) => { diff --git a/src/common/actions/Shortcut.js b/src/common/actions/Shortcut.js index dc614480..138651e3 100644 --- a/src/common/actions/Shortcut.js +++ b/src/common/actions/Shortcut.js @@ -53,16 +53,20 @@ var action = { description: _( 'The Activate Shortcut action simulates a key combination when activated. For example, this can be used to switch virtual desktops, control multimedia playback or to undo / redo operations.'), - // Items of this type have an additional text configuration parameter which represents - // the command to execute. + // Items of this type have an additional configuration parameter which represents + // the shortcut to simulate. config: { // This is used as data for newly created items of this type. defaultData: {shortcut: ''}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "shortcut" property. To stay backwards compatible + // with Fly-Pie 4, we have to also handle the case where the shortcut is given as a + // simple string value. The second parameter is a callback which is fired whenever the + // user changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "shortcut" property. - // To stay backwards compatible with Fly-Pie 4, we have to also handle the case - // where the shortcut is given as a simple string value. let shortcut = ''; if (typeof data === 'string') { shortcut = data; @@ -78,7 +82,7 @@ var action = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "shortcut" property. + // The data parameter *should* be an object containing a single "shortcut" property. // To stay backwards compatible with Fly-Pie 4, we have to also handle the case // where the shortcut is given as a simple string value. createItem: (data) => { diff --git a/src/common/actions/Uri.js b/src/common/actions/Uri.js index 8fd2d665..57a66ec4 100644 --- a/src/common/actions/Uri.js +++ b/src/common/actions/Uri.js @@ -49,10 +49,14 @@ var action = { // This is used as data for newly created items of this type. defaultData: {uri: ''}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "uri" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the URI is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "uri" property. To - // stay backwards compatible with Fly-Pie 4, we have to also handle the case where - // the URI is given as a simple string value. let uri = ''; if (typeof data === 'string') { uri = data; @@ -68,7 +72,7 @@ var action = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "uri" property. To + // The data parameter *should* be an object containing a single "uri" property. To // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the URI is given as a simple string value. createItem: (data) => { diff --git a/src/common/menus/FrequentlyUsed.js b/src/common/menus/FrequentlyUsed.js index 3098145a..4b440cc3 100644 --- a/src/common/menus/FrequentlyUsed.js +++ b/src/common/menus/FrequentlyUsed.js @@ -60,10 +60,14 @@ var menu = { // This is used as data for newly created items of this type. defaultData: {maxNum: 7}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "maxNum" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the number is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "maxNum" property. To - // stay backwards compatible with Fly-Pie 4, we have to also handle the case where - // the maxNum is given as a simple string value. let maxNum = 7; if (typeof data === 'string') { maxNum = parseInt(data); @@ -80,7 +84,7 @@ var menu = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "maxNum" property. To + // The data parameter *should* be an object containing a single "maxNum" property. To // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the maxNum is given as a simple string value. createItem: (data) => { diff --git a/src/common/menus/RecentFiles.js b/src/common/menus/RecentFiles.js index 42d05dfe..0fcb5aa7 100644 --- a/src/common/menus/RecentFiles.js +++ b/src/common/menus/RecentFiles.js @@ -50,10 +50,14 @@ var menu = { // This is used as data for newly created items of this type. defaultData: {maxNum: 7}, + // This is called whenever an item of this type is selected in the menu editor. It + // returns a Gtk.Widget which will be shown in the sidebar of the menu editor. The + // currently configured data object will be passed as first parameter and *should* be + // an object containing a single "maxNum" property. To stay backwards compatible with + // Fly-Pie 4, we have to also handle the case where the number is given as a simple + // string value. The second parameter is a callback which is fired whenever the user + // changes something in the widgets. getWidget(data, updateCallback) { - // The data paramter *should* be an object containing a single "maxNum" property. To - // stay backwards compatible with Fly-Pie 4, we have to also handle the case where - // the maxNum is given as a simple string value. let maxNum = 7; if (typeof data === 'string') { maxNum = parseInt(data); @@ -70,7 +74,7 @@ var menu = { }, // This will be called whenever a menu is opened containing an item of this kind. - // The data paramter *should* be an object containing a single "maxNum" property. To + // The data parameter *should* be an object containing a single "maxNum" property. To // stay backwards compatible with Fly-Pie 4, we have to also handle the case where // the maxNum is given as a simple string value. createItem: (data) => {