summaryrefslogtreecommitdiff
path: root/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/modules/menu-item.js
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/modules/menu-item.js')
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/modules/menu-item.js573
1 files changed, 573 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/modules/menu-item.js b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/modules/menu-item.js
new file mode 100644
index 0000000..3a80198
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/modules/menu-item.js
@@ -0,0 +1,573 @@
+// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import Gio from 'gi://Gio';
+import Clutter from 'gi://Clutter';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+import * as Slider from 'resource:///org/gnome/shell/ui/slider.js';
+import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';
+
+import { InfoFetcher } from './info-fetcher.js';
+import { Textbox } from './text-box.js';
+import { gettext as _, ngettext as _n, pgettext as C_ } from './translation.js';
+import * as SessionModeAware from './session-mode-aware.js';
+import { getShutdownScheduleFromSettings } from './schedule-info.js';
+import {
+ ShutdownTimerName,
+ ShutdownTimerObjectPath,
+} from '../dbus-service/shutdown-timer-dbus.js';
+
+import {
+ foregroundActive,
+ observeForegroundActive,
+ unobserveForegroundActive,
+} from './session-mode-aware.js';
+import {
+ durationString,
+ longDurationString,
+ absoluteTimeString,
+ getSliderMinutesFromSettings,
+ ScheduleInfo,
+} from './schedule-info.js';
+
+import { logDebug, proxyPromise } from './util.js';
+import {
+ WAKE_ACTIONS,
+ actionLabel,
+ ACTIONS,
+ mapLegacyAction,
+} from '../dbus-service/action.js';
+
+/**
+ * The ShutdownTimerItem controls wake/shutdown action time and mode.
+ * Additionally, it shows wake and external/internal shutdown schedules.
+ *
+ * The external schedule of the `shutdown` command is fetched by the InfoFetcher.
+ * Note that there is no external schedule for `suspend`.
+ */
+const ShutdownTimerItem = GObject.registerClass(
+ {
+ Properties: {
+ 'shutdown-text': GObject.ParamSpec.string(
+ 'shutdown-text',
+ '',
+ '',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ 'indicator-icon-name': GObject.ParamSpec.string(
+ 'indicator-icon-name',
+ '',
+ '',
+ GObject.ParamFlags.READWRITE,
+ 'go-down-symbolic'
+ ),
+ },
+ Signals: {
+ 'open-preferences': {},
+ shutdown: {
+ param_types: [GObject.TYPE_BOOLEAN, GObject.TYPE_STRING],
+ },
+ wake: {
+ param_types: [GObject.TYPE_BOOLEAN],
+ },
+ },
+ },
+ class ShutdownTimerItem extends QuickSettings.QuickMenuToggle {
+ _init({ path, settings }) {
+ const gicon = Gio.icon_new_for_string(
+ `${path}/icons/shutdown-timer-symbolic.svg`
+ );
+ super._init({ gicon, accessible_name: _('Shutdown Timer') });
+ this.info = {
+ internalShutdown: getShutdownScheduleFromSettings(settings),
+ externalShutdown: new ScheduleInfo({ external: true }),
+ externalWake: new ScheduleInfo({ mode: 'wake' }),
+ state: 'inactive',
+ };
+ this._settings = settings;
+ this.shutdownTimerIcon = gicon;
+ this._updatingSwitcherState = false;
+
+ // submenu in status area menu with slider and toggle button
+ this.sliderItems = {};
+ this.sliders = {};
+ ['shutdown', 'wake'].forEach(prefix => {
+ const [item, slider] = this._createSliderItem(prefix);
+ this.sliderItems[prefix] = item;
+ this.sliders[prefix] = slider;
+ this._onShowSliderChanged(prefix);
+ });
+ this.switcher = new PopupMenu.PopupSwitchMenuItem('', false);
+ // start/stop shutdown timer
+ this.switcher.connect('toggled', () => {
+ if (!this._updatingSwitcherState) {
+ this.emit(
+ 'shutdown',
+ this.switcher.state,
+ this.info.internalShutdown.mode
+ );
+ }
+ });
+ this.switcherSettingsButton = new St.Button({
+ reactive: true,
+ can_focus: true,
+ track_hover: true,
+ accessible_name: _('Settings'),
+ style_class: 'icon-button settings-button',
+ });
+ this.switcherSettingsButton.child = new St.Icon({
+ icon_name: 'applications-system-symbolic',
+ });
+ this.switcherSettingsButton.connect('clicked', () =>
+ this.emit('open-preferences')
+ );
+ this.switcher.add_child(this.switcherSettingsButton);
+
+ this.menu.addMenuItem(this.switcher);
+ // make switcher toggle without popup menu closing
+ this.switcher.activate = __ => {
+ if (this.switcher._switch.mapped) {
+ this.switcher.toggle();
+ }
+ };
+ this.menu.addMenuItem(this.sliderItems['shutdown']);
+
+ this.modeItems = Object.keys(ACTIONS).map(action => {
+ const modeItem = new PopupMenu.PopupMenuItem(actionLabel(action));
+ modeItem.connect('activate', () => {
+ this.emit('shutdown', true, action);
+ });
+ this.menu.addMenuItem(modeItem);
+ return [action, modeItem];
+ });
+
+ this.wakeItems = [
+ new PopupMenu.PopupSeparatorMenuItem(),
+ this.sliderItems['wake'],
+ ...Object.keys(WAKE_ACTIONS).map(action => {
+ const modeItem = new PopupMenu.PopupMenuItem(actionLabel(action));
+ if (action === 'wake') {
+ this.wakeModeItem = modeItem;
+ }
+ modeItem.connect('activate', () =>
+ this.emit('wake', action === 'wake')
+ );
+ return modeItem;
+ }),
+ ];
+ this.wakeItems.forEach(item => {
+ this.menu.addMenuItem(item);
+ });
+ this._updateWakeModeItem();
+ this._updateSwitchLabel();
+
+ // handlers for changed values in settings
+ const settingsHandlerIds = [
+ [
+ [
+ 'shutdown-max-timer-value',
+ 'nonlinear-shutdown-slider-value',
+ 'shutdown-ref-timer-value',
+ 'show-shutdown-absolute-timer-value',
+ 'root-mode-value',
+ 'shutdown-slider-value',
+ ],
+ () => this._updateSwitchLabel(),
+ ],
+ [
+ [
+ 'wake-max-timer-value',
+ 'wake-ref-timer-value',
+ 'show-wake-absolute-timer-value',
+ 'nonlinear-wake-slider-value',
+ 'wake-slider-value',
+ ],
+ () => this._updateWakeModeItem(),
+ ],
+ [['shutdown-slider-value'], () => this._updateSlider('shutdown')],
+ [['wake-slider-value'], () => this._updateSlider('wake')],
+ [
+ [
+ 'show-wake-items-value',
+ 'show-shutdown-mode-value',
+ 'show-shutdown-slider-value',
+ 'show-wake-slider-value',
+ 'show-shutdown-indicator-value',
+ 'show-settings-value',
+ ],
+ () => this._sync(),
+ ],
+ ]
+ .flatMap(([names, func]) => names.map(n => [n, func]))
+ .map(([name, func]) =>
+ this._settings.connect(`changed::${name}`, func)
+ );
+
+ this.connect('clicked', () => this.switcher.toggle());
+ observeForegroundActive(this, () => this._sync());
+ const tickHandlerId = setInterval(() => this.updateShutdownInfo(), 1000);
+ this.connect('destroy', () => {
+ unobserveForegroundActive(this);
+ clearInterval(tickHandlerId);
+ settingsHandlerIds.forEach(handlerId => {
+ this._settings.disconnect(handlerId);
+ });
+ });
+ }
+
+ _sync() {
+ // Update wake mode items
+ this.wakeItems.forEach(item => {
+ item.visible = this._settings.get_boolean('show-wake-items-value');
+ });
+ this._onShowSliderChanged('wake');
+
+ // Update shutdown mode items
+ const activeActions = this._settings
+ .get_string('show-shutdown-mode-value')
+ .split(',')
+ .map(s => mapLegacyAction(s.trim()))
+ .filter(action => action in ACTIONS);
+ logDebug('[menu-item]', activeActions);
+ this.modeItems.forEach(([action, item]) => {
+ const position = activeActions.indexOf(action);
+ if (position > -1) {
+ this.menu.moveMenuItem(item, position + 2);
+ }
+ item.visible = position > -1;
+ });
+
+ this._onShowSliderChanged('shutdown');
+
+ this.switcherSettingsButton.visible =
+ foregroundActive() && this._settings.get_boolean('show-settings-value');
+
+ this.updateShutdownInfo();
+ }
+
+ updateShutdownInfo() {
+ const schedule = this.info.internalShutdown;
+ const externalSchedule = this.info.externalShutdown;
+ const externalWake = this.info.externalWake;
+ const urgendSchedule = externalSchedule.isMoreUrgendThan(schedule)
+ ? externalSchedule
+ : schedule;
+ const active = schedule.scheduled || this.info.state !== 'inactive';
+ const showIndicator = this._settings.get_boolean(
+ 'show-shutdown-indicator-value'
+ );
+
+ // Update Indicator
+ this.set({
+ checked: active,
+ shutdownText:
+ urgendSchedule.scheduled && showIndicator
+ ? urgendSchedule.secondsLeft > 0
+ ? durationString(urgendSchedule.secondsLeft)
+ : _('now')
+ : '',
+ indicatorIconName:
+ showIndicator &&
+ (this.info.internalShutdown.scheduled ||
+ this.info.externalShutdown.scheduled)
+ ? 'go-down-symbolic'
+ : '',
+ });
+
+ // Update selected shutdown mode
+ this.modeItems.forEach(([mode, item]) => {
+ item.setOrnament(
+ mode === schedule.mode
+ ? PopupMenu.Ornament.DOT
+ : PopupMenu.Ornament.NONE
+ );
+ });
+
+ // Update title with scheduled action
+ this.title = actionLabel(schedule.mode);
+
+ // Update toggle state of switcher
+ this._updatingSwitcherState = true;
+ this.switcher.setToggleState(active);
+ this._updatingSwitcherState = false;
+
+ // Update long status description in menu
+ this.menu.setHeader(
+ this.shutdownTimerIcon,
+ _('Shutdown Timer'),
+ [schedule.label, externalWake.label].filter(v => !!v).join('\n')
+ );
+ if (this._settings.get_boolean('show-shutdown-absolute-timer-value')) {
+ this._updateSwitchLabel();
+ }
+ if (this._settings.get_boolean('show-wake-absolute-timer-value')) {
+ this._updateWakeModeItem();
+ }
+ this._updateSubtitle();
+ }
+
+ // update timer value if slider has changed
+ _updateSlider(prefix) {
+ this.sliders[prefix].value =
+ this._settings.get_double(`${prefix}-slider-value`) / 100.0;
+ }
+
+ _createSliderItem(settingsPrefix) {
+ const valueName = `${settingsPrefix}-slider-value`;
+ const sliderValue = this._settings.get_double(valueName) / 100.0;
+ const item = new PopupMenu.PopupBaseMenuItem({ activate: false });
+ const sliderIcon = new St.Icon({
+ icon_name:
+ settingsPrefix === 'wake'
+ ? 'alarm-symbolic'
+ : 'system-shutdown-symbolic',
+ style_class: 'popup-menu-icon',
+ });
+ item.add_child(sliderIcon);
+ const slider = new Slider.Slider(sliderValue);
+ slider.connect('notify::value', () => {
+ const v = slider.value * 100;
+ if (v !== this._settings.get_double(valueName))
+ this._settings.set_double(valueName, v);
+ });
+ item.add_child(slider);
+ return [item, slider];
+ }
+
+ get shutdownTimeStr() {
+ const minutes = Math.abs(
+ getSliderMinutesFromSettings(this._settings, 'shutdown')
+ );
+ return this._settings.get_boolean('show-shutdown-absolute-timer-value')
+ ? absoluteTimeString(minutes, C_('absolute time notation', '%a, %R'))
+ : longDurationString(
+ minutes,
+ h => _n('%s hr', '%s hrs', h),
+ m => _n('%s min', '%s mins', m)
+ );
+ }
+
+ _updateSwitchLabel() {
+ this.switcher.label.text = this._settings.get_boolean('root-mode-value')
+ ? _('%s (protect)').format(this.shutdownTimeStr)
+ : this.shutdownTimeStr;
+
+ if (this._settings.get_string('wake-ref-timer-value') === 'shutdown') {
+ this._updateWakeModeItem();
+ }
+ if (!this.info.internalShutdown.scheduled) {
+ this._updateSubtitle();
+ }
+ }
+
+ _updateSubtitle() {
+ this.subtitle = this.info.internalShutdown.scheduled
+ ? this._settings.get_boolean('show-shutdown-absolute-timer-value')
+ ? this.info.internalShutdown.absoluteTimeString
+ : durationString(this.info.internalShutdown.secondsLeft)
+ : this.shutdownTimeStr;
+ }
+
+ _updateWakeModeItem() {
+ const minutes = getSliderMinutesFromSettings(this._settings, 'wake');
+ const abs = this._settings.get_boolean('show-wake-absolute-timer-value');
+ this.wakeModeItem.label.text = (
+ abs
+ ? C_('WakeButtonText', '%s at %s')
+ : C_('WakeButtonText', '%s after %s')
+ ).format(
+ actionLabel('wake'),
+ abs
+ ? absoluteTimeString(minutes, C_('absolute time notation', '%a, %R'))
+ : longDurationString(
+ minutes,
+ h => _n('%s hour', '%s hours', h),
+ m => _n('%s minute', '%s minutes', m)
+ )
+ );
+ }
+
+ _onShowSliderChanged(settingsPrefix) {
+ this.sliderItems[settingsPrefix].visible =
+ (settingsPrefix !== 'wake' ||
+ this._settings.get_boolean('show-wake-items-value')) &&
+ this._settings.get_boolean(`show-${settingsPrefix}-slider-value`);
+ }
+ }
+);
+
+export const ShutdownTimerIndicator = GObject.registerClass(
+ {
+ Properties: {
+ 'wake-minutes': GObject.ParamSpec.int(
+ 'wake-minutes',
+ '',
+ '',
+ GObject.ParamFlags.READABLE,
+ 0
+ ),
+ 'shutdown-minutes': GObject.ParamSpec.int(
+ 'shutdown-minutes',
+ '',
+ '',
+ GObject.ParamFlags.READABLE,
+ 0
+ ),
+ },
+ Signals: {
+ 'open-preferences': {},
+ shutdown: {
+ param_types: [GObject.TYPE_BOOLEAN, GObject.TYPE_STRING],
+ },
+ wake: {
+ param_types: [GObject.TYPE_BOOLEAN],
+ },
+ },
+ },
+ class ShutdownTimerIndicator extends QuickSettings.SystemIndicator {
+ _init({ path, settings }) {
+ super._init();
+ this._state = 'inactive';
+ this._settings = settings;
+ this._textbox = new Textbox({ settings });
+ const item = new ShutdownTimerItem({ path, settings });
+ const infoFetcher = new InfoFetcher();
+ const proxyCancel = new Gio.Cancellable();
+ this._sdtProxy = null;
+ this._initProxy(item, this._textbox, infoFetcher, proxyCancel).catch(
+ err => {
+ if (!proxyCancel.is_cancelled()) console.error('[sdt-proxy]', err);
+ }
+ );
+ // React to changes in external shutdown and wake schedule
+ infoFetcher.connect('changed', () => this._syncShutdownInfo());
+ this._infoFetcher = infoFetcher;
+ this._shutdownTimerItem = item;
+
+ item.connect('open-preferences', () => this.emit('open-preferences'));
+
+ const icon = new St.Icon({ style_class: 'system-status-icon' });
+ const updateIcon = () => {
+ const name = item.indicatorIconName;
+ icon.visible = !!name;
+ if (icon.visible) icon.iconName = name;
+ this._syncIndicatorsVisible();
+ };
+
+ const scheduleLabel = new St.Label({
+ y_expand: true,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ const updateLabel = () => {
+ const text = item.shutdownText;
+ scheduleLabel.text = text;
+ scheduleLabel.visible = !!text;
+ this._syncIndicatorsVisible();
+ };
+
+ this.add_child(icon);
+ this.add_child(scheduleLabel);
+
+ item.connect('notify::indicator-icon-name', () => updateIcon());
+ item.connect('notify::shutdown-text', () => updateLabel());
+
+ updateLabel();
+ updateIcon();
+
+ this.quickSettingsItems.push(item);
+ this.connect('destroy', () => {
+ proxyCancel.cancel();
+ infoFetcher.destroy();
+ this._infoFetcher = null;
+ this._textbox.destroy();
+ this.quickSettingsItems.forEach(i => i.destroy());
+ this.quickSettingsItems = [];
+ this._shutdownTimerItem = null;
+ if (this._sdtProxy !== null) {
+ this._sdtProxy.destroy();
+ this._sdtProxy = null;
+ }
+ this._settings = null;
+ });
+ item.connect('destroy', () => {
+ // Mitigate already destroyed error when logging out
+ // Does js/ui/layout.js:256 destroy quickSettingsItems?
+ this.quickSettingsItems = [];
+ });
+ }
+
+ async _initProxy(item, textbox, infoFetcher, cancellable) {
+ const proxy = await proxyPromise(
+ ShutdownTimerName,
+ Gio.DBus.session,
+ 'org.gnome.Shell',
+ ShutdownTimerObjectPath,
+ cancellable
+ );
+ if (cancellable?.is_cancelled()) return;
+ item.connect('shutdown', (__, shutdown, action) => {
+ logDebug(
+ '[menu-item] [shutdown-signal] shutdown: ',
+ shutdown,
+ 'action:',
+ action
+ );
+ proxy
+ .ScheduleShutdownAsync(shutdown, action)
+ .catch(err => console.error('[shutdown]', err));
+ });
+ item.connect('wake', (__, wake) => {
+ proxy
+ .ScheduleWakeAsync(wake)
+ .catch(err => console.error('[wake]', err));
+ });
+
+ const signalIds = [
+ proxy.connectSignal('OnMessage', (__, ___, [msg]) => {
+ textbox.showTextbox(msg);
+ }),
+ proxy.connectSignal('OnStateChange', (__, ___, [state]) => {
+ logDebug('[menu-item] [OnStateChange-signal] state:', state);
+ this._state = state;
+ this._syncShutdownInfo();
+ }),
+ proxy.connectSignal('OnExternalChange', () => {
+ infoFetcher.refresh();
+ }),
+ ];
+ proxy.destroy = function () {
+ signalIds.forEach(signalId => this.disconnectSignal(signalId));
+ };
+
+ this._sdtProxy = proxy;
+ [this._state] = await this._sdtProxy.GetStateAsync();
+ if (cancellable?.is_cancelled()) return;
+ logDebug('[sdt-proxy] state', this._state);
+ this._syncShutdownInfo();
+ infoFetcher.refresh();
+ }
+
+ _syncShutdownInfo() {
+ const item = this._shutdownTimerItem;
+ if (this._state === 'action') {
+ if (SessionModeAware.foregroundActive()) Main.overview.hide();
+ this._textbox.hideAll();
+ }
+ item.info = {
+ internalShutdown: getShutdownScheduleFromSettings(this._settings),
+ externalShutdown: item.info.externalShutdown.copy({
+ ...this._infoFetcher.shutdownInfo,
+ }),
+ externalWake: item.info.externalWake.copy({
+ ...this._infoFetcher.wakeInfo,
+ }),
+ state: this._state,
+ };
+ item.updateShutdownInfo();
+ }
+ }
+);