import Adw from 'gi://Adw'; import Gdk from 'gi://Gdk'; import GdkPixbuf from 'gi://GdkPixbuf'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk'; import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; export const DialogWindow = GObject.registerClass({ Signals: { 'response': {param_types: [GObject.TYPE_INT]}, }, }, class ArcMenuDialogWindow extends Adw.PreferencesWindow { _init(title, parent) { super._init({ title, transient_for: parent.get_root(), modal: true, search_enabled: true, }); this.page = new Adw.PreferencesPage(); this.pageGroup = new Adw.PreferencesGroup(); this.add(this.page); this.page.add(this.pageGroup); } }); export const SettingRow = GObject.registerClass( class ArcMenuSettingRow extends Adw.ActionRow { _init(params) { super._init({ activatable: true, ...params, }); const goNextImage = new Gtk.Image({ gicon: Gio.icon_new_for_string('go-next-symbolic'), halign: Gtk.Align.END, valign: Gtk.Align.CENTER, hexpand: false, vexpand: false, }); this.add_suffix(goNextImage); } }); export const DragRow = GObject.registerClass({ Properties: { 'shortcut-name': GObject.ParamSpec.string( 'shortcut-name', 'shortcut-name', 'shortcut-name', GObject.ParamFlags.READWRITE, ''), 'shortcut-icon': GObject.ParamSpec.string( 'shortcut-icon', 'shortcut-icon', 'shortcut-icon', GObject.ParamFlags.READWRITE, ''), 'shortcut-command': GObject.ParamSpec.string( 'shortcut-command', 'shortcut-command', 'shortcut-command', GObject.ParamFlags.READWRITE, ''), 'gicon': GObject.ParamSpec.object( 'gicon', 'gicon', 'gicon', GObject.ParamFlags.READWRITE, Gio.Icon.$gtype), 'pixbuf': GObject.ParamSpec.object( 'pixbuf', 'pixbuf', 'pixbuf', GObject.ParamFlags.READWRITE, GdkPixbuf.Pixbuf.$gtype), 'icon-pixel-size': GObject.ParamSpec.int( 'icon-pixel-size', 'icon-pixel-size', 'icon-pixel-size', GObject.ParamFlags.READWRITE, 1, GLib.MAXINT32, 22), 'switch-enabled': GObject.ParamSpec.boolean( 'switch-enabled', 'switch-enabled', 'switch-enabled', GObject.ParamFlags.READWRITE, false), 'switch-active': GObject.ParamSpec.boolean( 'switch-active', 'switch-active', 'switch-active', GObject.ParamFlags.READWRITE, false), }, Signals: { 'drag-drop-done': { }, 'change-button-clicked': { }, 'switch-toggled': { }, }, }, class ArcMenuDragRow extends Adw.ActionRow { _init(params) { super._init(params); this._params = params; this.icon = new Gtk.Image({ gicon: this.gicon, pixel_size: this.icon_pixel_size, }); this.add_prefix(this.icon); if (this.pixbuf) this.icon.set_from_pixbuf(this.pixbuf); this.connect('notify::gicon', () => (this.icon.gicon = this.gicon)); this.dragIcon = new Gtk.Image({ gicon: Gio.icon_new_for_string('list-drag-handle-symbolic'), pixel_size: 12, }); this.add_prefix(this.dragIcon); if (this.switch_enabled) { this.switch = new Gtk.Switch({ valign: Gtk.Align.CENTER, vexpand: false, margin_start: 10, active: this.switch_active, }); this.switch.connect('notify::active', () => { this.switch_active = this.switch.get_active(); this.emit('switch-toggled'); }); this.add_suffix(this.switch); this.add_suffix(new Gtk.Separator({ orientation: Gtk.Orientation.VERTICAL, margin_top: 10, margin_bottom: 10, })); } const dragSource = new Gtk.DragSource({actions: Gdk.DragAction.MOVE}); this.add_controller(dragSource); const dropTarget = new Gtk.DropTargetAsync({actions: Gdk.DragAction.MOVE}); this.add_controller(dropTarget); dragSource.connect('drag-begin', (self, gdkDrag) => { this._dragParent = self.get_widget().get_parent(); this._dragParent.dragRow = this; const alloc = this.get_allocation(); const dragWidget = self.get_widget().createDragRow(alloc); this._dragParent.dragWidget = dragWidget; const icon = Gtk.DragIcon.get_for_drag(gdkDrag); icon.set_child(dragWidget); gdkDrag.set_hotspot(this._dragParent.dragX, this._dragParent.dragY); }); dragSource.connect('prepare', (self, x, y) => { this.set_state_flags(Gtk.StateFlags.NORMAL, true); const parent = self.get_widget().get_parent(); // store drag start cursor location parent.dragX = x; parent.dragY = y; return new Gdk.ContentProvider(); }); dragSource.connect('drag-end', (_self, _gdkDrag) => { this._dragParent.dragWidget = null; this._dragParent.drag_unhighlight_row(); }); dropTarget.connect('drag-enter', self => { const parent = self.get_widget().get_parent(); const widget = self.get_widget(); parent.drag_highlight_row(widget); }); dropTarget.connect('drag-leave', self => { const parent = self.get_widget().get_parent(); parent.drag_unhighlight_row(); }); dropTarget.connect('drop', (_self, gdkDrop) => { const parent = this.get_parent(); const {dragRow} = parent; // The row being dragged. const dragRowStartIndex = dragRow.get_index(); const dragRowNewIndex = this.get_index(); gdkDrop.read_value_async(ArcMenuDragRow, 1, null, () => gdkDrop.finish(Gdk.DragAction.MOVE)); // The drag row hasn't moved if (dragRowStartIndex === dragRowNewIndex) return true; parent.remove(dragRow); parent.show(); parent.insert(dragRow, dragRowNewIndex); this.emit('drag-drop-done'); return true; }); } createDragRow(alloc) { const dragWidget = new Gtk.ListBox(); dragWidget.set_size_request(alloc.width, alloc.height); const dragRow = new DragRow(this._params); dragWidget.append(dragRow); dragWidget.drag_highlight_row(dragRow); dragRow.title = _(this.title); dragRow.css_classes = this.css_classes; dragRow.icon.gicon = this.gicon; if (this.pixbuf) dragRow.icon.set_from_pixbuf(this.pixbuf); const editButton = new Gtk.Button({ icon_name: 'view-more-symbolic', valign: Gtk.Align.CENTER, }); dragRow.add_suffix(editButton); return dragWidget; } }); const ModifyEntryType = { MOVE_UP: 0, MOVE_DOWN: 1, REMOVE: 2, }; export const EditEntriesBox = GObject.registerClass({ Properties: { 'allow-modify': GObject.ParamSpec.boolean( 'allow-modify', 'allow-modify', 'allow-modify', GObject.ParamFlags.READWRITE, false), 'allow-remove': GObject.ParamSpec.boolean( 'allow-remove', 'allow-remove', 'allow-remove', GObject.ParamFlags.READWRITE, false), 'row': GObject.ParamSpec.object( 'row', 'row', 'row', GObject.ParamFlags.READWRITE, Gtk.Widget.$gtype), }, Signals: { 'modify-button-clicked': {}, 'entry-modified': {param_types: [GObject.TYPE_INT, GObject.TYPE_INT]}, }, }, class ArcMenuEditEntriesBox extends Gtk.MenuButton { _init(params) { super._init({ icon_name: 'view-more-symbolic', valign: Gtk.Align.CENTER, popover: new Gtk.Popover(), ...params, }); const popoverBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 3, }); this.popover.set_child(popoverBox); const modifyEntryButton = new Gtk.Button({ label: _('Modify'), has_frame: false, visible: this.allow_modify, }); modifyEntryButton.connect('clicked', () => { this.popover.popdown(); this.emit('modify-button-clicked'); }); popoverBox.append(modifyEntryButton); const topSeparator = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL); topSeparator.visible = this.allow_modify; popoverBox.append(topSeparator); const moveUpButton = new Gtk.Button({ label: _('Move Up'), has_frame: false, }); moveUpButton.connect('clicked', () => this.modifyEntry(ModifyEntryType.MOVE_UP)); popoverBox.append(moveUpButton); const moveDownButton = new Gtk.Button({ label: _('Move Down'), has_frame: false, }); moveDownButton.connect('clicked', () => this.modifyEntry(ModifyEntryType.MOVE_DOWN)); popoverBox.append(moveDownButton); const removeEntryButton = new Gtk.Button({ label: _('Remove'), has_frame: false, visible: this.allow_remove, }); removeEntryButton.connect('clicked', () => this.modifyEntry(ModifyEntryType.REMOVE)); const bottomSeparator = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL); bottomSeparator.visible = this.allow_remove; popoverBox.append(bottomSeparator); popoverBox.append(removeEntryButton); this.connect('notify::allow-modify', () => { modifyEntryButton.visible = this.allow_modify; topSeparator.visible = this.allow_modify; }); this.connect('notify::allow-remove', () => { removeEntryButton.visible = this.allow_remove; bottomSeparator.visible = this.allow_remove; }); } modifyEntry(modifyEntryType) { this.popover.popdown(); const startIndex = this.row.get_index(); const parent = this.row.get_parent(); const children = [...parent]; let indexModification; if (modifyEntryType === ModifyEntryType.MOVE_DOWN) { if (startIndex >= children.length - 1) return; indexModification = 1; } else if (modifyEntryType === ModifyEntryType.MOVE_UP) { if (startIndex <= 0) return; indexModification = -1; } if (modifyEntryType === ModifyEntryType.REMOVE) indexModification = (startIndex + 1) * -1; // we want newIndex == -1 for a remove const newIndex = startIndex + indexModification; parent.remove(this.row); if (newIndex !== -1) parent.insert(this.row, newIndex); parent.show(); this.emit('entry-modified', startIndex, newIndex); } }); export const IconGrid = GObject.registerClass(class ArcMenuIconGrid extends Gtk.FlowBox { _init(spacing = 4) { super._init({ max_children_per_line: 15, row_spacing: spacing, column_spacing: spacing, valign: Gtk.Align.START, halign: Gtk.Align.CENTER, homogeneous: true, selection_mode: Gtk.SelectionMode.SINGLE, }); this._spacing = spacing; this.childrenCount = 0; this.connect('child-activated', (_self, child) => { this.setActiveChild(child); }); } setActiveChild(child) { if (this._previousSelectedChild) this._previousSelectedChild.setActive(false); child.setActive(true); this._previousSelectedChild = child; } unselect_all() { if (this._previousSelectedChild) this._previousSelectedChild.setActive(false); super.unselect_all(); } select_child(child) { this.setActiveChild(child); super.select_child(child); } add(widget) { widget.margin_top = widget.margin_bottom = widget.margin_start = widget.margin_end = this._spacing; this.append(widget); this.childrenCount++; } }); export const MenuButtonIconTile = GObject.registerClass(class ArcMenuMenuButtonIconTile extends Gtk.FlowBoxChild { _init(icon, name) { super._init({ css_classes: ['card', 'activatable'], }); const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 4, margin_top: 4, margin_bottom: 4, margin_start: 4, margin_end: 4, }); this.set_child(box); const ICON_SIZE = 32; this._image = new Gtk.Image({ gicon: Gio.icon_new_for_string(icon), pixel_size: ICON_SIZE, }); this._label = new Gtk.Label({ label: name ? _(name) : '', hexpand: true, css_classes: ['caption'], visible: !!name, }); box.append(this._image); box.append(this._label); } setIcon(icon) { this._image.gicon = Gio.icon_new_for_string(icon); } setActive(active) { if (active) { this._image.css_classes = ['accent']; this._label.css_classes = ['caption', 'accent']; } else { this._image.css_classes = []; this._label.css_classes = ['caption']; } } }); export const MenuLayoutTile = GObject.registerClass(class ArcMenuMenuLayoutTile extends Gtk.FlowBoxChild { _init(styleInfo) { super._init({ css_classes: ['card', 'activatable'], margin_top: 4, margin_bottom: 4, margin_start: 4, margin_end: 4, halign: Gtk.Align.FILL, hexpand: true, }); const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin_top: 4, margin_bottom: 4, margin_start: 8, margin_end: 8, }); this.set_child(box); this.name = styleInfo.TITLE; this.layout = styleInfo.LAYOUT; this._image = new Gtk.Image({ gicon: Gio.icon_new_for_string(styleInfo.IMAGE), pixel_size: 145, }); this._label = new Gtk.Label({ label: _(this.name), hexpand: true, css_classes: ['caption'], }); box.append(this._image); box.append(this._label); } setActive(active) { if (active) { this._image.css_classes = ['accent']; this._label.css_classes = ['caption', 'accent']; } else { this._image.css_classes = []; this._label.css_classes = ['caption']; } } });