/* -*- mode: js2 - indent-tabs-mode: nil - js2-basic-offset: 4 -*- */ /* jshint multistr:true */ /* jshint esnext:true */ /* exported CaffeineExtension */ /** * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . **/ 'use strict'; import Gio from 'gi://Gio'; import GObject from 'gi://GObject'; import Shell from 'gi://Shell'; import St from 'gi://St'; import Meta from 'gi://Meta'; import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js'; const QuickSettingsMenu = Main.panel.statusArea.quickSettings; const INHIBIT_APPS_KEY = 'inhibit-apps'; const SHOW_INDICATOR_KEY = 'show-indicator'; const SHOW_NOTIFICATIONS_KEY = 'show-notifications'; const SHOW_TIMER_KEY = 'show-timer'; const DURATION_TIMER_INDEX = 'duration-timer'; const TOGGLE_STATE_KEY = 'toggle-state'; const USER_ENABLED_KEY = 'user-enabled'; const RESTORE_KEY = 'restore-state'; const FULLSCREEN_KEY = 'enable-fullscreen'; const NIGHT_LIGHT_KEY = 'nightlight-control'; const TOGGLE_SHORTCUT = 'toggle-shortcut'; const TIMER_KEY = 'countdown-timer'; const SCREEN_BLANK = 'screen-blank'; const TRIGGER_APPS_MODE = 'trigger-apps-mode'; const INDICATOR_POSITION = 'indicator-position'; const INDICATOR_INDEX = 'indicator-position-index'; const INDICATOR_POS_MAX = 'indicator-position-max'; const ColorInterface = ' \ \ \ \ \ '; const ColorProxy = Gio.DBusProxy.makeProxyWrapper(ColorInterface); const DBusSessionManagerIface = '\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ '; const DBusSessionManagerProxy = Gio.DBusProxy.makeProxyWrapper(DBusSessionManagerIface); const DBusSessionManagerInhibitorIface = '\ \ \ \ \ \ '; const DBusSessionManagerInhibitorProxy = Gio.DBusProxy.makeProxyWrapper(DBusSessionManagerInhibitorIface); const DisabledIcon = 'my-caffeine-off-symbolic'; const EnabledIcon = 'my-caffeine-on-symbolic'; const TimerMenuIcon = 'stopwatch-symbolic'; const ControlContext = { NEVER: 0, ALWAYS: 1, FOR_APPS: 2 }; const ShowIndicator = { ONLY_ACTIVE: 0, ALWAYS: 1, NEVER: 2 }; const AppsTrigger = { ON_RUNNING: 0, ON_FOCUS: 1, ON_ACTIVE_WORKSPACE: 2 }; const TIMERS = [ [5, 10, 15, 20, 30, 'caffeine-short-timer-symbolic'], [10, 20, 30, 40, 50, 'caffeine-medium-timer-symbolic'], [30, 45, 60, 75, 80, 'caffeine-long-timer-symbolic'], [0, 0, 0, 0, 0, 'caffeine-infinite-timer-symbolic'] ]; const CaffeineToggle = GObject.registerClass( class CaffeineToggle extends QuickSettings.QuickMenuToggle { _init(settings, path) { super._init({ 'title': _('Caffeine'), toggleMode: true }); this._settings = settings; this._path = path; // Icons this.finalTimerMenuIcon = TimerMenuIcon; let iconTheme = new St.IconTheme(); if (!iconTheme.has_icon(TimerMenuIcon)) { this.finalTimerMenuIcon = Gio.icon_new_for_string(`${this._path}/icons/${TimerMenuIcon}.svg`); } this._iconActivated = Gio.icon_new_for_string(`${this._path}/icons/${EnabledIcon}.svg`); this._iconDeactivated = Gio.icon_new_for_string(`${this._path}/icons/${DisabledIcon}.svg`); this._iconName(); // Menu this.menu.setHeader(this.finalTimerMenuIcon, _('Caffeine Timer'), null); // Add elements this._itemsSection = new PopupMenu.PopupMenuSection(); this.menu.addMenuItem(this._itemsSection); // Init Timers this._timerItems = new Map(); this._syncTimers(false); // Bind signals this._settings.bind(`${TOGGLE_STATE_KEY}`, this, 'checked', Gio.SettingsBindFlags.DEFAULT); this._settings.connect(`changed::${TOGGLE_STATE_KEY}`, () => { this._iconName(); }); this._settings.connect(`changed::${TIMER_KEY}`, () => { this._sync(); }); this._settings.connect(`changed::${DURATION_TIMER_INDEX}`, () => { this._syncTimers(true); }); this.connect('destroy', () => { this._iconActivated = null; this._iconDeactivated = null; this.gicon = null; }); } _syncTimers(resetDefault) { this._itemsSection.removeAll(); this._timerItems.clear(); const durationIndex = this._settings.get_int(DURATION_TIMER_INDEX); for (const timer of TIMERS) { let label = null; if (timer[0] === 0) { label = _('Infinite'); } else { label = parseInt(timer[durationIndex]) + _(' minutes'); } if (!label) { continue; } const icon = Gio.icon_new_for_string(`${this._path}/icons/${timer[5]}.svg`); const item = new PopupMenu.PopupImageMenuItem(label, icon); item.connect('activate', () => { this._checkTimer(timer[durationIndex]); }); this._timerItems.set(timer[durationIndex], item); this._itemsSection.addMenuItem(item); } this.menuEnabled = TIMERS.length > 2; // Select active duration if (resetDefault && this._settings.get_int(TIMER_KEY) !== 0) { // Set default duration to 0 this._settings.set_int(TIMER_KEY, 0); } else { this._sync(); } } _sync() { const activeTimerId = this._settings.get_int(TIMER_KEY); for (const [timerId, item] of this._timerItems) { item.setOrnament(timerId === activeTimerId ? PopupMenu.Ornament.CHECK : PopupMenu.Ornament.NONE); } } _checkTimer(timerId) { this._settings.set_int(TIMER_KEY, timerId); this._settings.set_boolean(TOGGLE_STATE_KEY, true); } _iconName() { if (this._settings.get_boolean(TOGGLE_STATE_KEY)) { this.gicon = this._iconActivated; } else { this.gicon = this._iconDeactivated; } } }); const Caffeine = GObject.registerClass( class Caffeine extends QuickSettings.SystemIndicator { _init(settings, path, name) { super._init(); this._indicator = this._addIndicator(); this._settings = settings; this._name = name; // D-bus this._proxy = new ColorProxy( Gio.DBus.session, 'org.gnome.SettingsDaemon.Color', '/org/gnome/SettingsDaemon/Color', (proxy, error) => { if (error) { log(error.message); } } ); this._sessionManager = new DBusSessionManagerProxy(Gio.DBus.session, 'org.gnome.SessionManager', '/org/gnome/SessionManager'); // From auto-move-windows@gnome-shell-extensions.gcampax.github.com this._appSystem = Shell.AppSystem.get_default(); this._activeWorkspace = null; // Init Apps Signals Id this._appStateChangedSignalId = 0; this._appDisplayChangedSignalId = 0; this._appWorkspaceChangedSignalId = 0; this._appAddWindowSignalId = 0; this._appRemoveWindowSignalId = 0; // ("screen" in global) is false on 3.28, although global.screen exists if (typeof global.screen !== 'undefined') { this._screen = global.screen; this._display = this._screen.get_display(); } else { this._screen = global.display; this._display = this._screen; } // Add indicator label for the timer this._timerLabel = new St.Label({ y_expand: true, y_align: Clutter.ActorAlign.CENTER }); this._timerLabel.visible = false; this.add_child(this._timerLabel); // Icons this._iconActivated = Gio.icon_new_for_string(`${path}/icons/${EnabledIcon}.svg`); this._iconDeactivated = Gio.icon_new_for_string(`${path}/icons/${DisabledIcon}.svg`); this._indicator.gicon = this._iconDeactivated; // Manage night light this._nightLight = false; /* Inhibited flag value * - 4: Inhibit suspending the session or computer * - 12: Inhibit the session being marked as idle */ this.inhibitFlags = 12; // Caffeine state this._state = false; this._userState = false; // Store the inhibition requests until processed this._inhibitionAddedFifo = []; this._inhibitionRemovedFifo = []; // Init Timers this._timeOut = null; this._timePrint = null; this._timerEnable = false; this._timeFullscreen = null; this._timeWorkspaceAdd = null; this._timeWorkspaceRemove = null; this._timeAppUnblock = null; // Show icon this._manageShowIndicator(); // Init app list this._appConfigs = []; this._appInhibitedData = new Map(); this._updateAppConfigs(); // Enable caffeine when fullscreen app is running if (this._settings.get_boolean(FULLSCREEN_KEY)) { this._inFullscreenId = this._screen.connect('in-fullscreen-changed', this.toggleFullscreen.bind(this)); this.toggleFullscreen(); } // QuickSettings this._caffeineToggle = new CaffeineToggle(this._settings, path); this.quickSettingsItems.push(this._caffeineToggle); this._updateTimerSubtitle(); // Init settings keys and restore user state this._settings.reset(TOGGLE_STATE_KEY); if (this._settings.get_boolean(USER_ENABLED_KEY) && this._settings.get_boolean(RESTORE_KEY)) { this.toggleState(); } else { // reset user state this._settings.reset(USER_ENABLED_KEY); } // Bind signals this._inhibitorAddedId = this._sessionManager.connectSignal( 'InhibitorAdded', this._inhibitorAdded.bind(this)); this._inhibitorRemovedId = this._sessionManager.connectSignal( 'InhibitorRemoved', this._inhibitorRemoved.bind(this)); this.inhibitId = this._settings.connect(`changed::${INHIBIT_APPS_KEY}`, this._updateAppConfigs.bind(this)); this.stateId = this._settings.connect(`changed::${TOGGLE_STATE_KEY}`, this._updateMainState.bind(this)); this.timerId = this._settings.connect(`changed::${TIMER_KEY}`, this._startTimer.bind(this)); this.showTimerId = this._settings.connect(`changed::${SHOW_TIMER_KEY}`, this._showIndicatorLabel.bind(this)); this.indicatorId = this._settings.connect(`changed::${INDICATOR_POSITION}`, this._updateIndicatorPosition.bind(this)); this.showIndicatorId = this._settings.connect(`changed::${SHOW_INDICATOR_KEY}`, () => { this._manageShowIndicator(); this._showIndicatorLabel(); }); this.triggerId = this._settings.connect(`changed::${TRIGGER_APPS_MODE}`, () => { this._resetAppSignalId(); this._updateAppEventMode(); }); // Change user state on icon scroll event this._indicator.reactive = true; this._indicator.connect('scroll-event', (actor, event) => this._handleScrollEvent(event)); // Init position and index of indicator icon this.indicatorPosition = this._settings.get_int(INDICATOR_POSITION); this.indicatorIndex = this._settings.get_int(INDICATOR_INDEX); this.lastIndicatorPosition = this.indicatorPosition; // Add indicator and toggle QuickSettingsMenu.addExternalIndicator(this); QuickSettingsMenu._indicators.remove_actor(this); QuickSettingsMenu._indicators.insert_child_at_index(this, this.indicatorIndex); this._updateLastIndicatorPosition(); } get inFullscreen() { let nbMonitors = this._screen.get_n_monitors(); let inFullscreen = false; for (let i = 0; i < nbMonitors; i++) { if (this._screen.get_monitor_in_fullscreen(i)) { inFullscreen = true; break; } } return inFullscreen; } toggleFullscreen() { /* Reset previous FullScreen delay * This prevent multiple inhibitors to be created in toggleFullscreen() * if a previous timer is still running. */ if (this._timeFullscreen !== null) { GLib.Source.remove(this._timeFullscreen); this._timeFullscreen = null; } this._manageScreenBlankState(false); // Add 2 second delay before adding inhibitor this._timeFullscreen = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => { if (this.inFullscreen && !this._appInhibitedData.has('fullscreen')) { this.addInhibit('fullscreen'); this._manageNightLight(false, false); } this._timeFullscreen = null; return GLib.SOURCE_REMOVE; }); if (!this.inFullscreen && this._appInhibitedData.has('fullscreen')) { this.removeInhibit('fullscreen'); this._manageNightLight(true, false); } } toggleState() { this._manageScreenBlankState(false); if (this._state) { this._removeTimer(); this._appInhibitedData.forEach((data, appId) => this.removeInhibit(appId) ); this._manageNightLight(true, false); } else { this.addInhibit('user'); this._manageNightLight(false, false); // Enable timer when duration isn't null if (this._settings.get_int(TIMER_KEY) !== 0 && !this._timerEnable) { this._startTimer(); } } } addInhibit(appId) { this._sessionManager.InhibitRemote(appId, 0, 'Inhibit by %s'.format(this._name), this.inhibitFlags, (cookie) => { this._inhibitionAddedFifo.push(appId); // Init app data let data = { cookie, isToggled: true, isInhibited: false, object: '' }; this._appInhibitedData.set(appId, data); } ); } removeInhibit(appId) { let appData = this._appInhibitedData.get(appId); if (appData && appData.isInhibited) { this._inhibitionRemovedFifo.push(appId); this._sessionManager.UninhibitRemote(appData.cookie); appData.isToggled = false; this._appInhibitedData.set(appId, appData); } } _updateLastIndicatorPosition() { let pos = -1; let nbItems = QuickSettingsMenu._indicators.get_n_children(); let targetIndicator = null; // Count only the visible item in indicator bar for (let i = 0; i < nbItems; i++) { targetIndicator = QuickSettingsMenu._indicators.get_child_at_index(i); if (targetIndicator.is_visible()) { pos += 1; } } this._settings.set_int(INDICATOR_POS_MAX, pos); } _incrementIndicatorPosIndex() { if (this.lastIndicatorPosition < this.indicatorPosition) { this.indicatorIndex += 1; } else { this.indicatorIndex -= 1; } } _updateIndicatorPosition() { this._updateLastIndicatorPosition(); const newPosition = this._settings.get_int(INDICATOR_POSITION); if (this.indicatorPosition !== newPosition) { this.indicatorPosition = newPosition; this._incrementIndicatorPosIndex(); // Skip invisible indicator let targetIndicator = QuickSettingsMenu._indicators.get_child_at_index(this.indicatorIndex); let maxIndex = QuickSettingsMenu._indicators.get_n_children(); while (this.indicatorIndex < maxIndex && !targetIndicator.is_visible() && this.indicatorIndex > -1) { this._incrementIndicatorPosIndex(); targetIndicator = QuickSettingsMenu._indicators.get_child_at_index(this.indicatorIndex); } // Always reset index to 0 on position 0 if (this.indicatorPosition === 0) { this.indicatorIndex = 0; } // Update last position this.lastIndicatorPosition = newPosition; // Update indicator index QuickSettingsMenu._indicators.remove_actor(this); QuickSettingsMenu._indicators.insert_child_at_index(this, this.indicatorIndex); this._settings.set_int(INDICATOR_INDEX, this.indicatorIndex); } } _showIndicatorLabel() { if (this._settings.get_boolean(SHOW_TIMER_KEY) && (this._settings.get_enum(SHOW_INDICATOR_KEY) !== ShowIndicator.NEVER) && this._timerEnable) { this._timerLabel.visible = true; } else { this._timerLabel.visible = false; } } _startTimer() { // Reset timer this._removeTimer(); this._timerEnable = true; // Get duration let timerDelay = this._settings.get_int(TIMER_KEY) * 60; // Execute Timer only if duration isn't set on infinite time if (timerDelay !== 0) { let secondLeft = timerDelay; this._showIndicatorLabel(); this._printTimer(secondLeft); this._timePrint = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { secondLeft -= 1; this._printTimer(secondLeft); return GLib.SOURCE_CONTINUE; }); this._timeOut = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timerDelay * 1000, () => { // Disable Caffeine when timer ended this._removeTimer(); this._settings.set_boolean(TOGGLE_STATE_KEY, false); return GLib.SOURCE_REMOVE; }); } } _printTimer(second) { const min = Math.floor(second / 60); const minS = Math.floor(second % 60).toLocaleString('en-US', { minimumIntegerDigits: 2, useGrouping: false }); // Print Timer in system Indicator and Toggle menu subLabel this._updateLabelTimer(min + ':' + minS); } _removeTimer() { // End timer this._timerEnable = false; // Flush and hide timer label this._updateLabelTimer(null); this._timerLabel.visible = false; // Remove timer if ((this._timeOut !== null) || (this._timePrint !== null)) { GLib.Source.remove(this._timeOut); GLib.Source.remove(this._timePrint); this._timeOut = null; this._timePrint = null; } } _updateLabelTimer(text) { this._timerLabel.text = text; this._caffeineToggle.menu.setHeader(this._caffeineToggle.finalTimerMenuIcon, _('Caffeine Timer'), text); this._caffeineToggle.subtitle = text; } _handleScrollEvent(event) { switch (event.get_scroll_direction()) { case Clutter.ScrollDirection.UP: if (!this._state) { // User state on - UP this._settings.set_boolean(TOGGLE_STATE_KEY, true); } break; case Clutter.ScrollDirection.DOWN: if (this._state) { // Stop timer this._removeTimer(); // User state off - DOWN this._settings.set_boolean(TOGGLE_STATE_KEY, false); } break; } } _inhibitorAdded(proxy, sender, [object]) { this._sessionManager.GetInhibitorsRemote(([inhibitors]) => { // Get the first added request let requestedId = this._inhibitionAddedFifo.shift(); for (let i of inhibitors) { let inhibitor = new DBusSessionManagerInhibitorProxy(Gio.DBus.session, 'org.gnome.SessionManager', i); inhibitor.GetAppIdRemote((appId) => { appId = String(appId); let appData = this._appInhibitedData.get(appId); if (appId !== '' && requestedId === appId && appData) { appData.isInhibited = true; appData.object = object; this._appInhibitedData.set(appId, appData); if (appId === 'user') { this._saveUserState(true); } else { this._updateAppSubtitle(appId); } // Update state if (this._state === false) { this._saveMainState(true); // Indicator icon this._manageShowIndicator(); this._indicator.gicon = this._iconActivated; // Shell OSD notifications if (this._settings.get_boolean(SHOW_NOTIFICATIONS_KEY) && !this.inFullscreen) { this._sendOSDNotification(true); } } } }); } }); } /* eslint-disable no-unused-vars */ _inhibitorRemoved(proxy, sender, [object]) { /* eslint-enable no-unused-vars */ // Get the first removed request let appId = this._inhibitionRemovedFifo.shift(); if (appId) { let appData = this._appInhibitedData.get(appId); if (appData) { // Remove app from list this._appInhibitedData.delete(appId); if (appId === 'user') { this._saveUserState(false); } else { this._updateAppSubtitle(null); } // Update state if (this._appInhibitedData.size === 0) { this._saveMainState(false); // Indicator icon this._manageShowIndicator(); this._indicator.gicon = this._iconDeactivated; // Shell OSD notifications if (this._settings.get_boolean(SHOW_NOTIFICATIONS_KEY)) { this._sendOSDNotification(false); } } } } } _isToggleInhibited(appId) { let appData = this._appInhibitedData.get(appId); if (appData && appData.isToggled) { return true; } else { return false; } } _manageShowIndicator() { if (this._state) { this._indicator.visible = this._settings.get_enum(SHOW_INDICATOR_KEY) !== ShowIndicator.NEVER; } else { this._indicator.visible = this._settings.get_enum(SHOW_INDICATOR_KEY) === ShowIndicator.ALWAYS; } } _manageScreenBlankState(isApp) { let blankState = this._settings.get_enum(SCREEN_BLANK) === ControlContext.ALWAYS; if (isApp) { blankState = this._settings.get_enum(SCREEN_BLANK) > ControlContext.NEVER; } if (blankState) { this.inhibitFlags = 4; } else { this.inhibitFlags = 12; } } _manageNightLight(isEnable, isApp) { let nightLightPref = this._settings.get_enum(NIGHT_LIGHT_KEY) === ControlContext.ALWAYS; if (isApp) { nightLightPref = this._settings.get_enum(NIGHT_LIGHT_KEY) > ControlContext.NEVER; } if (isEnable && (nightLightPref || this._nightLight && this._proxy.DisabledUntilTomorrow)) { this._proxy.DisabledUntilTomorrow = false; this._nightLight = false; } else if (!isEnable && nightLightPref) { this._proxy.DisabledUntilTomorrow = true; this._nightLight = true; } } _sendOSDNotification(state) { const nightLightPref = this._settings.get_enum(NIGHT_LIGHT_KEY) !== ControlContext.NEVER; if (state) { let message = _('Caffeine enabled'); if (nightLightPref && this._nightLight && this._proxy.NightLightActive) { message = message + '. ' + _('Night Light paused'); } Main.osdWindowManager.show(-1, this._iconActivated, message, null, null); } else { let message = _('Caffeine disabled'); if (nightLightPref && !this._nightLight && this._proxy.NightLightActive) { message = message + '. ' + _('Night Light resumed'); } Main.osdWindowManager.show(-1, this._iconDeactivated, message, null, null); } } // Add the name of App as subtitle _updateAppSubtitle(id) { const listAppId = this._appInhibitedData.keys(); let appId = id !== null ? id : listAppId.next().value; if (appId !== undefined) { let appInfo = Gio.DesktopAppInfo.new(appId); this._caffeineToggle.subtitle = appInfo !== null ? appInfo.get_display_name() : null; } } // Add the timer duration selected as subtitle _updateTimerSubtitle() { if (!this._settings.get_boolean(TOGGLE_STATE_KEY)) { const timerDuration = this._settings.get_int(TIMER_KEY); this._caffeineToggle.subtitle = timerDuration !== 0 ? parseInt(timerDuration) + _(' minutes') : null; } } _updateAppConfigs() { this._appConfigs.length = 0; this._settings.get_strv(INHIBIT_APPS_KEY).forEach((appId) => { // Check if app still exist const appInfo = Gio.DesktopAppInfo.new(appId); if (appInfo) { this._appConfigs.push(appId); } }); // Remove inhibited app that are not in the list anymore let inhibitedAppsToRemove = [...this._appInhibitedData.keys()] .filter((id) => !this._appConfigs.includes(id)); inhibitedAppsToRemove.forEach((id) => { this._manageScreenBlankState(true); // Allow blank screen this._manageNightLight(true, true); this.removeInhibit(id); // Uninhibit app }); this._updateAppEventMode(); } _updateMainState() { if (this._settings.get_boolean(TOGGLE_STATE_KEY) !== this._state) { this.toggleState(); } // Add timer duration as Subtitle when disable this._updateTimerSubtitle(); } _saveUserState(state) { this._userState = state; this._settings.set_boolean(USER_ENABLED_KEY, state); } _saveMainState(state) { this._state = state; this._settings.set_boolean(TOGGLE_STATE_KEY, state); } _resetAppSignalId() { if (this._appStateChangedSignalId > 0) { this._appSystem.disconnect(this._appStateChangedSignalId); this._appStateChangedSignalId = 0; } if (this._appDisplayChangedSignalId > 0) { global.display.disconnect(this._appDisplayChangedSignalId); this._appDisplayChangedSignalId = 0; } if (this._appWorkspaceChangedSignalId > 0) { global.workspace_manager.disconnect(this._appWorkspaceChangedSignalId); this._appWorkspaceChangedSignalId = 0; } if (this._appAddWindowSignalId > 0) { this._activeWorkspace.disconnect(this._appAddWindowSignalId); this._appAddWindowSignalId = 0; } if (this._appRemoveWindowSignalId > 0) { this._activeWorkspace.disconnect(this._appRemoveWindowSignalId); this._appRemoveWindowSignalId = 0; } } _updateAppEventMode() { let appsTriggeredMode = this._settings.get_enum(TRIGGER_APPS_MODE); if (this._appConfigs.length === 0) { this._resetAppSignalId(); } else { switch (appsTriggeredMode) { // TRIGGER APPS MODE: ON RUNNING case AppsTrigger.ON_RUNNING: if (this._appStateChangedSignalId === 0) { this._appStateChangedSignalId = this._appSystem.connect('app-state-changed', this._appStateChanged.bind(this)); } // Check if currently running App this._appConfigs.forEach((id) => { let app = this._appSystem.lookup_app(id); if (app && app.get_state() !== Shell.AppState.STOPPED) { this._appStateChanged(this._appSystem, app); } }); break; // TRIGGER APPS MODE: ON FOCUS case AppsTrigger.ON_FOCUS: if (this._appDisplayChangedSignalId === 0) { this._appDisplayChangedSignalId = global.display.connect('notify::focus-window', this._appWindowFocusChanged.bind(this)); } // Check if currently focused App this._appWindowFocusChanged(); break; // TRIGGER APPS MODE: ON ACTIVE WORKSPACE case AppsTrigger.ON_ACTIVE_WORKSPACE: if (this._appWorkspaceChangedSignalId === 0) { this._appWorkspaceChangedSignalId = global.workspace_manager.connect('workspace-switched', this._appWorkspaceChanged.bind(this)); } // Check if App is currently on active workspace this._appWorkspaceChanged(); break; } } } _toggleWorkspace() { // Search for triggered apps on active workspace this._appConfigs.forEach((appId) => { let app = this._appSystem.lookup_app(appId); let isOnWorkspace = app.is_on_workspace(this._activeWorkspace); if (isOnWorkspace && !this._isToggleInhibited(appId)) { this._manageScreenBlankState(true); // Allow blank screen this._manageNightLight(false, true); this.addInhibit(appId); // Inhibit app } else if (!isOnWorkspace && this._isToggleInhibited(appId)) { this._manageScreenBlankState(true); // Allow blank screen this._manageNightLight(true, true); this.removeInhibit(appId); // Uninhibit app } }); } _appWorkspaceChanged() { // Reset signal for Add/remove windows on workspace if (this._appAddWindowSignalId > 0) { this._activeWorkspace.disconnect(this._appAddWindowSignalId); this._appAddWindowSignalId = 0; } if (this._appRemoveWindowSignalId > 0) { this._activeWorkspace.disconnect(this._appRemoveWindowSignalId); this._appRemoveWindowSignalId = 0; } // Get active workspace this._activeWorkspace = global.workspace_manager.get_active_workspace(); // Add signal listener on add/remove windows for the active workspace this._appAddWindowSignalId = this._activeWorkspace.connect('window-added', (wkspace, window) => { const type = window.get_window_type(); // Accept only normal window, ignore all other type (dialog, menu,...) if (type === 0) { // Add 100 ms delay to handle window detection this._timeWorkspaceAdd = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { this._toggleWorkspace(); this._timeWorkspaceAdd = null; return GLib.SOURCE_REMOVE; }); } }); this._appRemoveWindowSignalId = this._activeWorkspace.connect('window-removed', (wkspace, window) => { const type = window.get_window_type(); // Accept only normal window, ignore all other type (dialog, menu,...) if (type === 0) { // Add 100 ms delay to handle window detection this._timeWorkspaceRemove = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { this._toggleWorkspace(); this._timeWorkspaceRemove = null; return GLib.SOURCE_REMOVE; }); } }); // Check and toggle Caffeine this._toggleWorkspace(); } _appWindowFocusChanged() { let winTrack = Shell.WindowTracker.get_default(); let appId = null; let app = winTrack.focus_app; if (app) { appId = app.get_id(); } if (this._appConfigs.includes(appId) && !this._isToggleInhibited(appId)) { this._manageScreenBlankState(true); // Allow blank screen this._manageNightLight(false, true); this.addInhibit(appId); // Inhibit app // Uninhibit previous focused apps this._appInhibitedData.forEach((data, id) => { if (id !== appId && id !== 'user') { this.removeInhibit(id); } }); } else if (!this._appConfigs.includes(appId) && this._appInhibitedData.size !== 0) { this._manageScreenBlankState(true); // Allow blank screen this._manageNightLight(true, true); // Uninhibit all apps this._appInhibitedData.forEach((data, id) => { if (id !== 'user') { this.removeInhibit(id); } }); } } _appStateChanged(appSys, app) { let appId = app.get_id(); let appState = app.get_state(); if (this._appConfigs.includes(appId)) { // Block App state signal appSys.block_signal_handler(this._appStateChangedSignalId); // Allow blank screen this._manageScreenBlankState(true); if (appState === Shell.AppState.STOPPED && this._isToggleInhibited(appId)) { this._manageNightLight(true, true); this.removeInhibit(appId); // Uninhibit app } else if (appState !== Shell.AppState.STOPPED && !this._isToggleInhibited(appId)) { this._manageNightLight(false, true); this.addInhibit(appId); // Inhibit app } // Add 200 ms delay before unblock state signal this._timeAppUnblock = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { appSys.unblock_signal_handler(this._appStateChangedSignalId); this._timeAppUnblock = null; return GLib.SOURCE_REMOVE; }); } } destroy() { // Remove all inhibitors this._appInhibitedData.forEach((data, appId) => this.removeInhibit(appId)); this._appInhibitedData.clear(); // Remove ToggleMenu this.quickSettingsItems.forEach((item) => item.destroy()); // Disconnect from signals if (this._settings.get_boolean(FULLSCREEN_KEY)) { this._screen.disconnect(this._inFullscreenId); } if (this._inhibitorAddedId) { this._sessionManager.disconnectSignal(this._inhibitorAddedId); this._inhibitorAddedId = 0; } if (this._inhibitorRemovedId) { this._sessionManager.disconnectSignal(this._inhibitorRemovedId); this._inhibitorRemovedId = 0; } if (this._windowCreatedId) { this._display.disconnect(this._windowCreatedId); this._windowCreatedId = 0; } if (this._windowDestroyedId) { global.window_manager.disconnect(this._windowDestroyedId); this._windowDestroyedId = 0; } if (this._timeOut) { GLib.Source.remove(this._timeOut); this._timeOut = null; } if (this._timePrint) { GLib.Source.remove(this._timePrint); this._timePrint = null; } if (this._timeFullscreen) { GLib.Source.remove(this._timeFullscreen); this._timeFullscreen = null; } if (this._timeWorkspaceAdd) { GLib.Source.remove(this._timeWorkspaceAdd); this._timeWorkspaceAdd = null; } if (this._timeWorkspaceRemove) { GLib.Source.remove(this._timeWorkspaceRemove); this._timeWorkspaceRemove = null; } if (this._timeAppUnblock) { GLib.Source.remove(this._timeAppUnblock); this._timeAppUnblock = null; } this._resetAppSignalId(); // Disconnect settings signals if (this.inhibitId) { this._settings.disconnect(this.inhibitId); this.inhibitId = undefined; } if (this.stateId) { this._settings.disconnect(this.stateId); this.stateId = undefined; } if (this.timerId) { this._settings.disconnect(this.timerId); this.timerId = undefined; } if (this.showTimerId) { this._settings.disconnect(this.showTimerId); this.showTimerId = undefined; } if (this.indicatorId) { this._settings.disconnect(this.indicatorId); this.indicatorId = undefined; } if (this.showIndicatorId) { this._settings.disconnect(this.showIndicatorId); this.showIndicatorId = undefined; } if (this.triggerId) { this._settings.disconnect(this.triggerId); this.triggerId = undefined; } this._appConfigs.length = 0; this._settings = null; super.destroy(); } }); export default class CaffeineExtension extends Extension { enable() { this._settings = this.getSettings(); this._caffeineIndicator = new Caffeine(this._settings, this.path); // Register shortcut Main.wm.addKeybinding(TOGGLE_SHORTCUT, this._settings, Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, Shell.ActionMode.ALL, () => { this._caffeineIndicator.toggleState(); }); } disable() { this._caffeineIndicator.destroy(); this._caffeineIndicator = null; // Unregister shortcut Main.wm.removeKeybinding(TOGGLE_SHORTCUT); } }