import Adw from 'gi://Adw'; import Gdk from 'gi://Gdk'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk'; import {AboutPage} from './settings/AboutPage.js'; import {DonatePage} from './settings/DonatePage.js'; import {DialogWindow} from './settings/DialogWindow.js'; import {WidgetSettingsPage} from './settings/WidgetSettingsPage.js'; import * as Utils from './utils.js'; import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; const WidgetType = { DIGITAL: 0, ANALOG: 1, CUSTOM: 2, WEATHER: 3, }; export default class AzClockPrefs extends ExtensionPreferences { fillPreferencesWindow(window) { const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); if (!iconTheme.get_search_path().includes(`${this.path}/media`)) iconTheme.add_search_path(`${this.path}/media`); const settings = this.getSettings(); let pageChangedId = settings.connect('changed::prefs-visible-page', () => { if (settings.get_string('prefs-visible-page') !== '') this._setVisiblePage(window, settings); }); window.connect('close-request', () => { if (pageChangedId) { settings.disconnect(pageChangedId); pageChangedId = null; } }); window.set_default_size(750, 800); const homePage = new HomePage(this, settings); window.add(homePage); this.createRows = () => homePage.createRows(); const donatePage = new DonatePage(this.metadata); window.add(donatePage); const aboutPage = new AboutPage(settings, this.metadata, this.path); window.add(aboutPage); this._setVisiblePage(window, settings); } _setVisiblePage(window, settings) { const prefsVisiblePage = settings.get_string('prefs-visible-page'); window.pop_subpage(); if (prefsVisiblePage === '') { window.set_visible_page_name('HomePage'); } else if (prefsVisiblePage === 'DonatePage') { window.set_visible_page_name('DonatePage'); } else if (prefsVisiblePage === 'WhatsNewPage') { window.set_visible_page_name('AboutPage'); const page = window.get_visible_page(); page.showWhatsNewPage(); } settings.set_string('prefs-visible-page', ''); } } var HomePage = GObject.registerClass( class azClockHomePage extends Adw.PreferencesPage { _init(extension, settings) { super._init({ title: _('Settings'), icon_name: 'preferences-system-symbolic', name: 'HomePage', }); this._extension = extension; this._settings = settings; this._widgetRows = []; const addClockButton = new Gtk.Button({ halign: Gtk.Align.START, icon_name: 'list-add-symbolic', valign: Gtk.Align.CENTER, css_classes: ['suggested-action'], label: _('Add Widget...'), }); addClockButton.connect('clicked', () => { const dialog = new AddWidgetsDialog(this._extension, this._settings, this); dialog.show(); dialog.connect('response', (_w, response) => { if (response === Gtk.ResponseType.APPLY) { this.createRows(); dialog.destroy(); } }); }); this.clocksGroup = new Adw.PreferencesGroup(); this.add(this.clocksGroup); this.clocksGroup.set_header_suffix(addClockButton); this.createRows(); } get settings() { return this._settings; } createRows() { for (let row of this._widgetRows) { this.clocksGroup.remove(row); row = null; } this._widgetRows = []; const widgets = this._settings.get_value('widgets').recursiveUnpack(); widgets.forEach(widget => { for (const [widgetId] of Object.entries(widget)) { const widgetRow = this._createWidgetRow(widgetId); this.clocksGroup.add(widgetRow); } }); } _createWidgetRow(widgetId) { const widgetSchema = `${this._settings.schema_id}.widget-data`; const widgetPath = `${this._settings.path}widget-data/${widgetId}/`; const widgetSettings = Utils.getSettings(this._extension, widgetSchema, widgetPath); const name = widgetSettings.get_string('name'); // Data for the widget is always the first element in array. const widgetRow = new WidgetRow(this._settings, widgetSettings, widgetId, { title: `${_(name)}`, }); widgetRow.use_markup = true; widgetRow.connect('drag-drop-done', (_widget, oldIndex, newIndex) => { const widgets = this._settings.get_value('widgets').deepUnpack(); const movedData = widgets.splice(oldIndex, 1)[0]; widgets.splice(newIndex, 0, movedData); this._settings.set_value('widgets', new GLib.Variant('aa{sv}', widgets)); this.createRows(); }); this._widgetRows.push(widgetRow); return widgetRow; } }); var WidgetRow = GObject.registerClass({ Signals: { 'drag-drop-done': {param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT]}, 'drag-drop-prepare': {}, }, }, class AzClockWidgetRow extends Adw.ActionRow { _init(settings, widgetSettings, schemaId, params) { super._init({ activatable: true, ...params, }); this._params = params; this._settings = settings; this._widgetSettings = widgetSettings; this._schemaId = schemaId; this.dragIcon = new Gtk.Image({ gicon: Gio.icon_new_for_string('list-drag-handle-symbolic'), pixel_size: 12, }); this.add_prefix(this.dragIcon); 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.emit('drag-drop-prepare'); 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, _deleteData) => { this._dragParent.dragWidget = null; this._dragParent.drag_unhighlight_row(); _deleteData = true; }); 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.dragRow; // The row being dragged. const dragRowStartIndex = dragRow.get_index(); const dragRowNewIndex = this.get_index(); gdkDrop.read_value_async(AzClockWidgetRow, 1, null, () => gdkDrop.finish(Gdk.DragAction.MOVE)); // The drag row hasn't moved if (dragRowStartIndex === dragRowNewIndex) return true; this.emit('drag-drop-done', dragRowStartIndex, dragRowNewIndex); return true; }); this.connect('activated', () => { const widgetSettingsWindow = new WidgetSettingsPage(this._settings, this._widgetSettings, { title: this._widgetSettings.get_string('name'), schema_id: this._schemaId, transient_for: this.get_root(), modal: true, resizable: false, default_width: 900, default_height: 700, widget_index: this.get_index(), }); widgetSettingsWindow.connect('notify::title', () => (this.title = `${widgetSettingsWindow.title}`)); widgetSettingsWindow.present(); }); const configureLabel = new Gtk.Label({ label: _('Configure'), halign: Gtk.Align.END, valign: Gtk.Align.CENTER, hexpand: false, vexpand: false, }); this.add_suffix(configureLabel); 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); } createDragRow(alloc) { const dragWidget = new Gtk.ListBox(); dragWidget.set_size_request(alloc.width, -1); const dragRow = new WidgetRow(this._settings, this._widgetSettings, this._schemaId, this._params); dragWidget.append(dragRow); dragWidget.drag_highlight_row(dragRow); return dragWidget; } }); var AddWidgetsDialog = GObject.registerClass( class AzClockAddWidgetsDialog extends DialogWindow { _init(extension, settings, parent) { super._init(_('Add Widget'), parent); this._extension = extension; this._settings = settings; this.search_enabled = false; this.set_default_size(550, -1); this.pageGroup.title = _('Preset Widgets'); this.pageGroup.add(this.addPresetWidget(_('Digital Clock Widget'), WidgetType.DIGITAL)); this.pageGroup.add(this.addPresetWidget(_('Analog Clock Widget'), WidgetType.ANALOG)); this.pageGroup.add(this.addPresetWidget(_('Weather Widget'), WidgetType.WEATHER)); this.pageGroup.add(this.addPresetWidget(_('Custom Widget'), WidgetType.CUSTOM, _('Add your own custom elements'))); this.cloneGroup = new Adw.PreferencesGroup({ title: _('Clone existing Widget'), }); this.cloneGroup.use_markup = true; this.page.add(this.cloneGroup); const widgets = this._settings.get_value('widgets').recursiveUnpack(); widgets.forEach(widget => { for (const [widgetId] of Object.entries(widget)) { const widgetSchema = `${this._settings.schema_id}.widget-data`; const widgetPath = `${this._settings.path}widget-data/${widgetId}/`; const widgetSettings = Utils.getSettings(this._extension, widgetSchema, widgetPath); const name = widgetSettings.get_string('name'); this.cloneGroup.add(this.addPresetWidget(name, widgetSettings)); } }); } addPresetWidget(title, widgetType, subtitle) { const addButton = new Gtk.Button({ icon_name: 'list-add-symbolic', valign: Gtk.Align.CENTER, }); addButton.connect('clicked', () => { const widgetIds = []; const widgets = this._settings.get_value('widgets').deepUnpack(); widgets.forEach(widget => { for (const [widgetId] of Object.entries(widget)) widgetIds.push(widgetId); }); const newWidget = {}; const widgetFolderId = GLib.uuid_string_random(); const widgetSchema = `${this._settings.schema_id}.widget-data`; const widgetPath = `${this._settings.path}widget-data/${widgetFolderId}/`; const widgetSettings = Utils.getSettings(this._extension, widgetSchema, widgetPath); const elements = []; const elementSchema = `${this._settings.schema_id}.element-data`; const elementPath = `${widgetSettings.path}element-data/`; let randomId; if (widgetType === WidgetType.DIGITAL) { try { widgetSettings.set_string('name', _('Digital Clock Widget')); let element = {}; randomId = GLib.uuid_string_random(); let elementSettings = Utils.getSettings(this._extension, elementSchema, `${elementPath}${randomId}/`); element[randomId] = new GLib.Variant('a{sv}', { enabled: GLib.Variant.new_boolean(true), }); elements.push(element); elementSettings.set_string('name', _('Time Label')); element = {}; randomId = GLib.uuid_string_random(); elementSettings = Utils.getSettings(this._extension, elementSchema, `${elementPath}${randomId}/`); element[randomId] = new GLib.Variant('a{sv}', { enabled: GLib.Variant.new_boolean(true), }); elements.push(element); elementSettings.set_string('name', _('Date Label')); elementSettings.set_string('date-format', '%A %b %d'); elementSettings.set_int('font-size', 32); widgetSettings.set_value('elements', new GLib.Variant('aa{sv}', elements)); } catch (e) { console.log(`Error creating New Digital Clock Widget: ${e}`); } } else if (widgetType === WidgetType.ANALOG) { widgetSettings.set_string('name', _('Analog Clock Widget')); const element = {}; randomId = GLib.uuid_string_random(); const elementSettings = Utils.getSettings(this._extension, elementSchema, `${elementPath}${randomId}/`); element[randomId] = new GLib.Variant('a{sv}', { enabled: GLib.Variant.new_boolean(true), }); elements.push(element); elementSettings.set_string('name', _('Analog Clock')); elementSettings.set_enum('element-type', Utils.ElementType.ANALOG_CLOCK); elementSettings.set_value('shadow', new GLib.Variant('(bsiiii)', [true, 'rgba(55, 55, 55, 0.3)', 3, 3, 0, 0])); elementSettings.set_int('border-radius', 999); elementSettings.set_int('border-width', 2); elementSettings.set_string('background-color', 'white'); elementSettings.set_boolean('show-border', true); elementSettings.set_string('border-color', 'black'); elementSettings.set_string('foreground-color', 'black'); widgetSettings.set_value('elements', new GLib.Variant('aa{sv}', elements)); } else if (widgetType === WidgetType.WEATHER) { widgetSettings.set_string('name', _('Weather Widget')); widgetSettings.set_boolean('show-background', true); widgetSettings.set_string('background-color', 'rgba(0, 0, 0, .6)'); const element = {}; randomId = GLib.uuid_string_random(); const elementSettings = Utils.getSettings(this._extension, elementSchema, `${elementPath}${randomId}/`); element[randomId] = new GLib.Variant('a{sv}', { enabled: GLib.Variant.new_boolean(true), }); elements.push(element); elementSettings.set_string('name', _('Weather Forecast')); elementSettings.set_enum('element-type', Utils.ElementType.WEATHER_ELEMENT); elementSettings.set_int('polling-interval', 300); widgetSettings.set_value('elements', new GLib.Variant('aa{sv}', elements)); } else if (widgetType === WidgetType.CUSTOM) { widgetSettings.set_string('name', _('Custom Widget')); } else { const setValue = (copySetting, newSetting, key) => { const defaultValue = copySetting.get_default_value(key); const value = copySetting.get_value(key); if (!defaultValue.equal(value)) newSetting.set_value(key, value); }; const copyWidgetSettings = widgetType; const copyElements = copyWidgetSettings.get_value('elements').deepUnpack(); const copyElementPath = `${copyWidgetSettings.path}element-data/`; const keys = copyWidgetSettings.settings_schema.list_keys(); for (const key of keys) setValue(copyWidgetSettings, widgetSettings, key); copyElements.forEach(element => { for (const [elementId] of Object.entries(element)) { const copyElementSettings = Utils.getSettings(this._extension, elementSchema, `${copyElementPath}${elementId}/`); const elementSettings = Utils.getSettings(this._extension, elementSchema, `${elementPath}${elementId}/`); const elementKeys = copyElementSettings.settings_schema.list_keys(); for (const key of elementKeys) setValue(copyElementSettings, elementSettings, key); } }); } newWidget[widgetFolderId] = new GLib.Variant('a{sv}', { enabled: GLib.Variant.new_boolean(true), }); widgets.push(newWidget); this._settings.set_value('widgets', new GLib.Variant('aa{sv}', widgets)); this.emit('response', Gtk.ResponseType.APPLY); }); const row = new Adw.ActionRow({ subtitle: subtitle ? _(subtitle) : '', title: _(title), activatable_widget: addButton, }); row.add_suffix(addButton); return row; } });