611 lines
19 KiB
JavaScript
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());
|
|
}
|
|
}
|