diff options
Diffstat (limited to 'raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js')
| -rw-r--r-- | raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js b/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js new file mode 100644 index 0000000..d992b1a --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js @@ -0,0 +1,280 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +import { + Atk, + Clutter, + Gio, + GLib, + St, +} from './dependencies/gi.js'; + +import {PopupMenu} from './dependencies/shell/ui.js'; + +import {Utils} from './imports.js'; + +// Dbusmenu features not (yet) supported: +// +// * The CHILD_DISPLAY property +// +// This seems to have only one possible value in the Dbusmenu API, so +// there's little point in depending on it--the code in libdbusmenu sets it +// if and only if an item has children, so for our purposes it's simpler +// and more intuitive to just check children.length. (This does ignore the +// possibility of a program not using libdbusmenu and setting CHILD_DISPLAY +// independently, perhaps to indicate that an childless menu item should +// nevertheless be displayed like a submenu.) +// +// * Children more than two levels deep +// +// PopupMenu doesn't seem to support submenus in submenus. +// +// * Shortcut keys +// +// If these keys are supposed to be installed as global shortcuts, we'd +// have to query these aggressively and not wait for the DBus menu to be +// mapped to a popup menu. A shortcut key that only works once the popup +// menu is open and has key focus is possibly of marginal value. + +/** + * + */ +export async function haveDBusMenu() { + try { + const {default: DBusMenu} = await import('gi://Dbusmenu'); + return DBusMenu; + } catch (e) { + log(`Failed to import DBusMenu, quicklists are not available: ${e}`); + return null; + } +} + +const DBusMenu = await haveDBusMenu(); + +/** + * @param dbusmenuItem + * @param deep + */ +export function makePopupMenuItem(dbusmenuItem, deep) { + // These are the only properties guaranteed to be available when the root + // item is first announced. Other properties might be loaded already, but + // be sure to connect to Dbusmenu.MENUITEM_SIGNAL_PROPERTY_CHANGED to get + // the most up-to-date values in case they aren't. + const itemType = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_TYPE); + const label = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_LABEL); + const visible = dbusmenuItem.property_get_bool(DBusMenu.MENUITEM_PROP_VISIBLE); + const enabled = dbusmenuItem.property_get_bool(DBusMenu.MENUITEM_PROP_ENABLED); + const accessibleDesc = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_ACCESSIBLE_DESC); + // const childDisplay = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_CHILD_DISPLAY); + + let item; + const signalsHandler = new Utils.GlobalSignalsHandler(); + const wantIcon = itemType === DBusMenu.CLIENT_TYPES_IMAGE; + + // If the basic type of the menu item needs to change, call this. + const recreateItem = () => { + const newItem = makePopupMenuItem(dbusmenuItem, deep); + const parentMenu = item._parent; + parentMenu.addMenuItem(newItem); + // Reminder: Clutter thinks of later entries in the child list as + // "above" earlier ones, so "above" here means "below" in terms of the + // menu's vertical order. + parentMenu.actor.set_child_above_sibling(newItem.actor, item.actor); + if (newItem.menu) + parentMenu.actor.set_child_above_sibling(newItem.menu.actor, newItem.actor); + + parentMenu.actor.remove_child(item.actor); + item.destroy(); + item = null; + }; + + const updateDisposition = () => { + const disposition = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_DISPOSITION); + let iconName = null; + switch (disposition) { + case DBusMenu.MENUITEM_DISPOSITION_ALERT: + case DBusMenu.MENUITEM_DISPOSITION_WARNING: + iconName = 'dialog-warning-symbolic'; + break; + case DBusMenu.MENUITEM_DISPOSITION_INFORMATIVE: + iconName = 'dialog-information-symbolic'; + break; + } + if (iconName) { + item._dispositionIcon = new St.Icon({ + icon_name: iconName, + style_class: 'popup-menu-icon', + y_align: Clutter.ActorAlign.CENTER, + y_expand: true, + }); + let expander; + for (let child = item.label.get_next_sibling(); ; child = child.get_next_sibling()) { + if (!child) { + expander = new St.Bin({ + style_class: 'popup-menu-item-expander', + x_expand: true, + }); + item.actor.add_child(expander); + break; + } else if (child instanceof St.Widget && + child.has_style_class_name('popup-menu-item-expander')) { + expander = child; + break; + } + } + item.actor.insert_child_above(item._dispositionIcon, expander); + } else if (item._dispositionIcon) { + item.actor.remove_child(item._dispositionIcon); + item._dispositionIcon = null; + } + }; + + const updateIcon = () => { + if (!wantIcon) + return; + + const iconData = dbusmenuItem.property_get_byte_array(DBusMenu.MENUITEM_PROP_ICON_DATA); + const iconName = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_ICON_NAME); + if (iconName) + item.icon.icon_name = iconName; + else if (iconData.length) + item.icon.gicon = Gio.BytesIcon.new(iconData); + }; + + const updateOrnament = () => { + const toggleType = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_TOGGLE_TYPE); + switch (toggleType) { + case DBusMenu.MENUITEM_TOGGLE_CHECK: + item.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM; + break; + case DBusMenu.MENUITEM_TOGGLE_RADIO: + item.actor.accessible_role = Atk.Role.RADIO_MENU_ITEM; + break; + default: + item.actor.accessible_role = Atk.Role.MENU_ITEM; + } + let ornament = PopupMenu.Ornament.NONE; + const state = dbusmenuItem.property_get_int(DBusMenu.MENUITEM_PROP_TOGGLE_STATE); + if (state === DBusMenu.MENUITEM_TOGGLE_STATE_UNKNOWN) { + // PopupMenu doesn't natively support an "unknown" ornament, but we + // can hack one in: + item.setOrnament(ornament); + item.actor.add_accessible_state(Atk.StateType.INDETERMINATE); + item._ornamentLabel.text = '\u2501'; + item.actor.remove_style_pseudo_class('checked'); + } else { + item.actor.remove_accessible_state(Atk.StateType.INDETERMINATE); + if (state === DBusMenu.MENUITEM_TOGGLE_STATE_CHECKED) { + if (toggleType === DBusMenu.MENUITEM_TOGGLE_CHECK) + ornament = PopupMenu.Ornament.CHECK; + else if (toggleType === DBusMenu.MENUITEM_TOGGLE_RADIO) + ornament = PopupMenu.Ornament.DOT; + + item.actor.add_style_pseudo_class('checked'); + } else { + item.actor.remove_style_pseudo_class('checked'); + } + item.setOrnament(ornament); + } + }; + + const onPropertyChanged = (_, name, value) => { + // `value` is null when a property is cleared, so handle those cases + // with sensible defaults. + switch (name) { + case DBusMenu.MENUITEM_PROP_TYPE: + recreateItem(); + break; + case DBusMenu.MENUITEM_PROP_ENABLED: + item.setSensitive(value ? value.unpack() : false); + break; + case DBusMenu.MENUITEM_PROP_LABEL: + item.label.text = value ? value.unpack() : ''; + break; + case DBusMenu.MENUITEM_PROP_VISIBLE: + item.actor.visible = value ? value.unpack() : false; + break; + case DBusMenu.MENUITEM_PROP_DISPOSITION: + updateDisposition(); + break; + case DBusMenu.MENUITEM_PROP_ACCESSIBLE_DESC: + item.actor.get_accessible().accessible_description = value && value.unpack() || ''; + break; + case DBusMenu.MENUITEM_PROP_ICON_DATA: + case DBusMenu.MENUITEM_PROP_ICON_NAME: + updateIcon(); + break; + case DBusMenu.MENUITEM_PROP_TOGGLE_TYPE: + case DBusMenu.MENUITEM_PROP_TOGGLE_STATE: + updateOrnament(); + break; + } + }; + + + // Start actually building the menu item. + const children = dbusmenuItem.get_children(); + if (children.length && !deep) { + // Make a submenu. + item = new PopupMenu.PopupSubMenuMenuItem(label, wantIcon); + const updateChildren = () => { + const itemChildren = dbusmenuItem.get_children(); + if (!itemChildren.length) { + recreateItem(); + return; + } + + item.menu.removeAll(); + itemChildren.forEach(remoteChild => + item.menu.addMenuItem(makePopupMenuItem(remoteChild, true))); + }; + updateChildren(); + signalsHandler.add( + [dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_ADDED, updateChildren], + [dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_MOVED, updateChildren], + [dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_REMOVED, updateChildren]); + } else { + // Don't make a submenu. + if (!deep) { + // We only have the potential to get a submenu if we aren't deep. + signalsHandler.add( + [dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_ADDED, recreateItem], + [dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_MOVED, recreateItem], + [dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_REMOVED, recreateItem]); + } + + if (itemType === DBusMenu.CLIENT_TYPES_SEPARATOR) { + item = new PopupMenu.PopupSeparatorMenuItem(); + } else if (wantIcon) { + item = new PopupMenu.PopupImageMenuItem(label, null); + item.icon = item._icon; + } else { + item = new PopupMenu.PopupMenuItem(label); + } + } + + // Set common initial properties. + item.actor.visible = visible; + item.setSensitive(enabled); + if (accessibleDesc) + item.actor.get_accessible().accessible_description = accessibleDesc; + + updateDisposition(); + updateIcon(); + updateOrnament(); + + // Prevent an initial resize flicker. + if (wantIcon) + item.icon.icon_size = 16; + + + signalsHandler.add(dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_PROPERTY_CHANGED, onPropertyChanged); + + // Connections on item will be lost when item is disposed; there's no need + // to add them to signalsHandler. + item.connect('activate', () => + dbusmenuItem.handle_event(DBusMenu.MENUITEM_EVENT_ACTIVATED, + new GLib.Variant('i', 0), Math.floor(Date.now() / 1000))); + item.connect('destroy', () => signalsHandler.destroy()); + + return item; +} |