2025-05-09 09:48:24 +02:00

611 lines
19 KiB
JavaScript

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import St from 'gi://St';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Utils from './utils.js';
import {AnalogClock} from './elements/analogClock.js';
import {CommandLabel} from './elements/commandLabel.js';
import {DigitalClock} from './elements/digitalClock.js';
import {TextLabel} from './elements/textLabel.js';
import {WeatherElement} from './elements/weather.js';
import {UpdateNotification} from './updateNotifier.js';
const DEBUG_LOG = false;
const ElementType = {
DIGITAL_CLOCK: 0,
ANALOG_CLOCK: 1,
TEXT_LABEL: 2,
COMMAND_LABEL: 3,
WEATHER_ELEMENT: 4,
};
let SETTINGS, EXTENSION;
const AnchorPoint = {
TOP_LEFT: 0,
BOTTOM_LEFT: 1,
TOP_RIGHT: 2,
BOTTOM_RIGHT: 3,
CENTER: 4,
};
function debugLog(msg) {
if (DEBUG_LOG)
console.log(msg);
}
var DesktopWidget = GObject.registerClass(
class AzClockDesktopWidget extends St.Widget {
_init(settings, settingsId) {
super._init({
layout_manager: new Clutter.BoxLayout(),
reactive: true,
track_hover: true,
can_focus: true,
x_align: Clutter.ActorAlign.FILL,
y_align: Clutter.ActorAlign.FILL,
pivot_point: new Graphene.Point({x: 0.5, y: .5}),
});
this._settings = settings;
this._settingsId = settingsId;
this._elements = new Map();
this._oldElementIds = [];
this._hasWeatherElement = false;
const isVertical = this._settings.get_boolean('vertical');
this.layout_manager.orientation = isVertical ? Clutter.Orientation.VERTICAL : Clutter.Orientation.HORIZONTAL;
this._menuManager = new PopupMenu.PopupMenuManager(this);
this.connect('notify::hover', () => this._onHover());
this.connect('destroy', () => this._onDestroy());
this._settings.connectObject('changed', () => {
this._setParent();
this._updateDnD();
this.createElements();
this.setStyle();
this.setPositionFromSettings();
}, this);
this._setParent();
this.makeDraggable();
this._updateDnD();
this._createPopUpMenu();
this.createElements();
this.setStyle();
this.setPositionFromSettings();
this.updateClocks(true);
}
_updateDnD() {
// If the widget is locked, block draggable _grabActor()
if (!this._settings.get_boolean('lock-widget'))
this._draggable._grabActor = this._dragGrabActor;
else
this._draggable._grabActor = () => {};
}
_setParent() {
const parent = this.get_parent();
if (parent === Main.layoutManager.uiGroup)
Main.layoutManager.removeChrome(this);
else if (parent === Main.layoutManager._backgroundGroup)
Main.layoutManager._backgroundGroup.remove_child(this);
const alwaysOnTop = this._settings.get_boolean('always-on-top');
if (alwaysOnTop)
Main.layoutManager.addTopChrome(this);
else
Main.layoutManager._backgroundGroup.add_child(this);
}
vfunc_allocate(box) {
if (this._isDragging) {
super.vfunc_allocate(box);
return;
}
const [x, y] = this._settings.get_value('location').deepUnpack();
const width = box.get_width();
const height = box.get_height();
const xAnchorRight = x - width;
const yAnchorBottom = y - height;
const xAnchorCenter = x - (width / 2);
const yAnchorCenter = y - (height / 2);
const anchorPoint = this._settings.get_enum('anchor-point');
if (anchorPoint === AnchorPoint.TOP_LEFT)
box.set_origin(x, y);
else if (anchorPoint === AnchorPoint.BOTTOM_LEFT)
box.set_origin(x, yAnchorBottom);
else if (anchorPoint === AnchorPoint.TOP_RIGHT)
box.set_origin(xAnchorRight, y);
else if (anchorPoint === AnchorPoint.BOTTOM_RIGHT)
box.set_origin(xAnchorRight, yAnchorBottom);
else if (anchorPoint === AnchorPoint.CENTER)
box.set_origin(xAnchorCenter, yAnchorCenter);
super.vfunc_allocate(box);
}
createElements() {
this.remove_all_children();
this._hasWeatherElement = false;
const elementIds = [];
const elements = this._settings.get_value('elements').recursiveUnpack();
elements.forEach(element => {
for (const [elementId, properties] of Object.entries(element)) {
if (properties.enabled)
elementIds.push(elementId);
}
});
const deletedElement = this._oldElementIds.filter(x => !elementIds.includes(x));
for (let i = 0; i < deletedElement.length; i++) {
const elementId = deletedElement[i];
const element = this._elements.get(elementId);
if (element) {
element.destroy();
this._elements.delete(elementId);
}
}
for (let i = 0; i < elementIds.length; i++) {
const elementId = elementIds[i];
const elementSchema = `${SETTINGS.schema_id}.element-data`;
const elementPath = `${this._settings.path}element-data/${elementId}/`;
const elementSettings = Utils.getSettings(EXTENSION, elementSchema, elementPath);
let element = this._elements.get(elementId);
if (!element) {
const elementType = elementSettings.get_enum('element-type');
let elementConstructor;
if (elementType === ElementType.DIGITAL_CLOCK)
elementConstructor = DigitalClock;
else if (elementType === ElementType.ANALOG_CLOCK)
elementConstructor = AnalogClock;
else if (elementType === ElementType.TEXT_LABEL)
elementConstructor = TextLabel;
else if (elementType === ElementType.COMMAND_LABEL)
elementConstructor = CommandLabel;
else if (elementType === ElementType.WEATHER_ELEMENT)
elementConstructor = WeatherElement;
element = new elementConstructor(elementSettings, EXTENSION);
this._elements.set(elementId, element);
}
this.add_child(element);
if (element instanceof WeatherElement)
this._hasWeatherElement = true;
}
if (this._hasWeatherElement)
this._addRefreshWeatherMenuItem();
else
this._removeRefreshWeatherMenuItem();
this._oldElementIds = elementIds;
this.queue_relayout();
}
updateClocks(immediate = false) {
this._elements.forEach(element => {
if (element instanceof DigitalClock || element instanceof AnalogClock)
element.updateClock(immediate);
});
this.queue_relayout();
}
setPositionFromSettings() {
const [x, y] = this._settings.get_value('location').deepUnpack();
debugLog(`set pos from settings - (${x}, ${y})`);
this.set_position(x, y);
this.queue_relayout();
}
setStyle() {
const isVertical = this._settings.get_boolean('vertical');
this.layout_manager.orientation = isVertical ? Clutter.Orientation.VERTICAL : Clutter.Orientation.HORIZONTAL;
const showBorder = this._settings.get_boolean('show-border');
const borderColor = this._settings.get_string('border-color');
const borderWidth = this._settings.get_int('border-width');
const borderRadius = this._settings.get_int('border-radius');
const showBackground = this._settings.get_boolean('show-background');
const backgroundColor = this._settings.get_string('background-color');
const spacing = this._settings.get_int('spacing');
const [paddingTop, paddingRight, paddingBottom, paddingLeft] = this._settings.get_value('padding').deepUnpack();
this.layoutManager.spacing = spacing;
let style = `padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;`;
if (showBackground) {
style += `background-color: ${backgroundColor};
border-radius: ${borderRadius}px;`;
}
if (showBorder) {
style += `border-width: ${borderWidth}px;
border-color: ${borderColor};`;
}
this.style = style;
this.queue_relayout();
}
vfunc_button_press_event() {
const event = Clutter.get_current_event();
if (event.get_button() === 1) {
this._setPopupTimeout();
} else if (event.get_button() === 3) {
this._popupMenu();
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
}
vfunc_button_release_event() {
this._removeMenuTimeout();
return Clutter.EVENT_PROPAGATE;
}
_onDragBegin() {
if (this._menu)
this._menu.close(true);
this._removeMenuTimeout();
this._isDragging = true;
this._dragMonitor = {
dragMotion: this._onDragMotion.bind(this),
};
DND.addDragMonitor(this._dragMonitor);
this._dragX = this.x;
this._dragY = this.y;
}
_onDragMotion(dragEvent) {
this.deltaX = dragEvent.x - (dragEvent.x - this._dragX);
this.deltaY = dragEvent.y - (dragEvent.y - this._dragY);
const [x, y] = this.allocation.get_origin();
this._dragX = x;
this._dragY = y;
return DND.DragMotionResult.CONTINUE;
}
_onDragEnd() {
this._isDragging = false;
if (this._dragMonitor) {
DND.removeDragMonitor(this._dragMonitor);
this._dragMonitor = null;
}
let [x, y] = [Math.round(this.deltaX), Math.round(this.deltaY)];
const width = this.get_width();
const height = this.get_height();
const newX = x + width;
const newY = y + height;
const centerX = x + (width / 2);
const centerY = y + (height / 2);
const anchorPoint = this._settings.get_enum('anchor-point');
if (anchorPoint === AnchorPoint.BOTTOM_LEFT) {
y = newY;
} else if (anchorPoint === AnchorPoint.TOP_RIGHT) {
x = newX;
} else if (anchorPoint === AnchorPoint.BOTTOM_RIGHT) {
x = newX;
y = newY;
} else if (anchorPoint === AnchorPoint.CENTER) {
x = centerX;
y = centerY;
}
this._settings.set_value('location', new GLib.Variant('(ii)', [x, y]));
debugLog(`drag end - (${x}, ${y})`);
}
getDragActorSource() {
return this;
}
makeDraggable() {
this._draggable = DND.makeDraggable(this);
this._draggable._dragActorDropped = () => {
this._draggable._animationInProgress = true;
this._draggable._dragCancellable = false;
this._draggable._dragState = 0; // DND.DragState.INIT;
this._draggable._onAnimationComplete(this._draggable._dragActor, Clutter.get_current_event().get_time());
return true;
};
this._dragGrabActor = this._draggable._grabActor;
this.dragBeginId = this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
this.dragEndId = this._draggable.connect('drag-end', this._onDragEnd.bind(this));
}
_onHover() {
if (!this.hover)
this._removeMenuTimeout();
}
_removeMenuTimeout() {
if (this._menuTimeoutId > 0) {
GLib.source_remove(this._menuTimeoutId);
this._menuTimeoutId = 0;
}
}
_setPopupTimeout() {
this._removeMenuTimeout();
this._menuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 600, () => {
this._menuTimeoutId = 0;
this._popupMenu();
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._menuTimeoutId, '[azclock] this.popupMenu');
}
_popupMenu() {
this._removeMenuTimeout();
this._menu.open();
return false;
}
_createPopUpMenu() {
this._menu = new PopupMenu.PopupMenu(this, 0.5, St.Side.TOP);
const lockWidget = this._settings.get_boolean('lock-widget');
const lockPositionSwitch = new PopupMenu.PopupSwitchMenuItem(_('Lock Position'), lockWidget);
this._menu.addMenuItem(lockPositionSwitch);
lockPositionSwitch.connect('toggled', item => {
this._menu.close();
this._settings.set_boolean('lock-widget', item.state);
});
const alwaysOnTop = this._settings.get_boolean('always-on-top');
const alwaysOnTopSwitch = new PopupMenu.PopupSwitchMenuItem(_('Always on Top'), alwaysOnTop);
this._menu.addMenuItem(alwaysOnTopSwitch);
alwaysOnTopSwitch.connect('toggled', item => {
this._menu.close();
this._settings.set_boolean('always-on-top', item.state);
});
this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._menu.addAction(_('Desktop Widgets Settings'), () => {
EXTENSION.openPreferences();
});
Main.uiGroup.add_child(this._menu.actor);
this._menuManager.addMenu(this._menu);
this._menu.actor.hide();
}
_addRefreshWeatherMenuItem() {
if (this._refreshWeatherMenuItem)
return;
this._refreshWeatherMenuItem = new PopupMenu.PopupMenuItem(_('Refresh Weather'));
this._refreshWeatherMenuItem.connect('activate', () => {
this._elements.forEach(element => {
if (element instanceof WeatherElement)
element.refreshWeather();
});
this._menu.close();
});
this._menu.addMenuItem(this._refreshWeatherMenuItem, 2);
}
_removeRefreshWeatherMenuItem() {
if (!this._refreshWeatherMenuItem)
return;
this._refreshWeatherMenuItem.destroy();
this._refreshWeatherMenuItem = null;
}
_disconnectDnD() {
if (this._draggable && this.dragBeginId) {
this._draggable.disconnect(this.dragBeginId);
this.dragBeginId = null;
}
if (this._draggable && this.dragEndId) {
this._draggable.disconnect(this.dragEndId);
this.dragEndId = null;
}
if (this._dragMonitor) {
DND.removeDragMonitor(this._dragMonitor);
this._dragMonitor = null;
}
this._draggable = null;
}
_onDestroy() {
this._removeMenuTimeout();
if (this._updateClockId) {
GLib.source_remove(this._updateClockId);
this._updateClockId = null;
}
this._removeRefreshWeatherMenuItem();
this._disconnectDnD();
this._settings.disconnectObject(this);
this._settings = null;
this._settingsId = null;
this._elements = null;
this._oldElementIds = null;
this._hasWeatherElement = false;
this._menu.destroy();
this._menu = null;
this._menuManager = null;
}
});
export default class AzClock extends Extension {
enable() {
EXTENSION = this;
this.settings = SETTINGS = this.getSettings();
Utils.createInitialWidget(EXTENSION, SETTINGS);
this.widgets = new Map();
this._oldWidgetIds = [];
this._createWidgets();
this._startClockTimer();
SETTINGS.connectObject('changed::widgets', () => this._createWidgets(), this);
this._updateNotification = new UpdateNotification(this);
}
disable() {
SETTINGS.disconnectObject(this);
if (this._dataChangedTimeoutId) {
GLib.source_remove(this._dataChangedTimeoutId);
this._dataChangedTimeoutId = null;
}
this._updateNotification.destroy();
this._updateNotification = null;
this._removeClockTimer();
this._destroyWidgets();
this._oldWidgetIds = null;
SETTINGS.disconnectObject(this);
EXTENSION = null;
SETTINGS = null;
this.settings = null;
}
_startClockTimer() {
this._updateClockId = GLib.timeout_add(GLib.PRIORITY_HIGH, 1000, () => {
this.widgets.forEach(widget => {
widget.updateClocks();
});
return GLib.SOURCE_CONTINUE;
});
}
_removeClockTimer() {
if (this._updateClockId) {
GLib.source_remove(this._updateClockId);
this._updateClockId = null;
}
}
_destroyWidgets() {
this.widgets.forEach(widget => {
widget.destroy();
});
this.widgets = null;
}
_createWidget(widgetId) {
const widgetSchema = `${SETTINGS.schema_id}.widget-data`;
const widgetPath = `${SETTINGS.path}widget-data/${widgetId}/`;
const widgetSettings = Utils.getSettings(EXTENSION, widgetSchema, widgetPath);
const widget = new DesktopWidget(widgetSettings, widgetId);
return widget;
}
_createWidgets() {
const widgetIds = [];
const widgets = SETTINGS.get_value('widgets').recursiveUnpack();
widgets.forEach(widget => {
for (const [widgetId, properties] of Object.entries(widget)) {
if (properties.enabled)
widgetIds.push(widgetId);
}
});
const deletedWidgets = this._oldWidgetIds.filter(x => !widgetIds.includes(x));
for (let i = 0; i < deletedWidgets.length; i++) {
const widgetId = deletedWidgets[i];
const widget = this.widgets.get(widgetId);
if (widget) {
widget.destroy();
this.widgets.delete(widgetId);
}
}
for (let i = 0; i < widgetIds.length; i++) {
const widgetId = widgetIds[i];
let widget = this.widgets.get(widgetId);
if (!widget) {
widget = this._createWidget(widgetId);
this.widgets.set(widgetId, widget);
}
widget.z_position = widgetIds.length - i;
}
this._oldWidgetIds = widgetIds;
}
openPreferences() {
// Find if an extension preferences window is already open
const prefsWindow = global.get_window_actors().map(wa => wa.meta_window).find(w => w.wm_class === 'org.gnome.Shell.Extensions');
if (!prefsWindow) {
super.openPreferences();
return;
}
// The current prefsWindow belongs to this extension, activate it
if (prefsWindow.title === this.metadata.name) {
Main.activateWindow(prefsWindow);
return;
}
// If another extension's preferences are open, close it and open this extension's preferences
prefsWindow.connectObject('unmanaged', () => {
super.openPreferences();
prefsWindow.disconnectObject(this);
}, this);
prefsWindow.delete(global.get_current_time());
}
}