diff --git a/src/appIcons.js b/src/appIcons.js index 9e5a34d..9009258 100644 --- a/src/appIcons.js +++ b/src/appIcons.js @@ -43,7 +43,6 @@ import { EventEmitter } from 'resource:///org/gnome/shell/misc/signals.js' import * as Utils from './utils.js' import * as PanelSettings from './panelSettings.js' import * as Taskbar from './taskbar.js' -import * as Progress from './progress.js' import { DTP_EXTENSION, SETTINGS, @@ -121,6 +120,7 @@ export const TaskbarAppIcon = GObject.registerClass( _init(appInfo, panel, iconParams, previewMenu, iconAnimator) { this.dtpPanel = panel this._nWindows = 0 + this._notifications = {} this.window = appInfo.window this.isLauncher = appInfo.isLauncher this._previewMenu = previewMenu @@ -132,6 +132,7 @@ export const TaskbarAppIcon = GObject.registerClass( super._init(appInfo.app, iconParams) + this._signalsHandler = new Utils.GlobalSignalsHandler() this._timeoutsHandler = new Utils.TimeoutsHandler() // Fix touchscreen issues before the listener is added by the parent constructor. @@ -176,6 +177,7 @@ export const TaskbarAppIcon = GObject.registerClass( }) this.remove_child(this._iconContainer) + this.icon._iconBin.set_pivot_point(0.5, 0.5) this._dtpIconContainer.add_child(this._iconContainer) @@ -191,11 +193,6 @@ export const TaskbarAppIcon = GObject.registerClass( this._updateWindowTitle() this._updateWindowTitleStyle() - this._scaleFactorChangedId = Utils.getStageTheme().connect( - 'changed', - () => this._updateWindowTitleStyle(), - ) - box.add_child(this._dtpIconContainer) box.add_child(this._windowTitle) @@ -223,35 +220,33 @@ export const TaskbarAppIcon = GObject.registerClass( this._setAppIconPadding() this._setAppIconStyle() this._showDots() + this._numberOverlay() - this._focusWindowChangedId = global.display.connect( - 'notify::focus-window', - this._onFocusAppChanged.bind(this), - ) - - this._windowEnteredMonitorId = this._windowLeftMonitorId = 0 - this._stateChangedId = this.app.connect( - 'windows-changed', - this.onWindowsChanged.bind(this), + this._signalsHandler.add( + [ + Utils.getStageTheme(), + 'changed', + this._updateWindowTitleStyle.bind(this), + ], + [ + global.display, + 'notify::focus-window', + this._onFocusAppChanged.bind(this), + ], + [this.app, 'windows-changed', this.onWindowsChanged.bind(this)], ) if (!this.window) { if (SETTINGS.get_boolean('isolate-monitors')) { - this._windowEnteredMonitorId = - Utils.DisplayWrapper.getScreen().connect( - 'window-entered-monitor', - this.onWindowEnteredOrLeft.bind(this), - ) - this._windowLeftMonitorId = Utils.DisplayWrapper.getScreen().connect( - 'window-left-monitor', + this._signalsHandler.add([ + Utils.DisplayWrapper.getScreen(), + ['window-entered-monitor', 'window-left-monitor'], this.onWindowEnteredOrLeft.bind(this), - ) + ]) } - this._titleWindowChangeId = 0 - this._minimizedWindowChangeId = 0 - - this._fullscreenId = Utils.DisplayWrapper.getScreen().connect( + this._signalsHandler.add([ + Utils.DisplayWrapper.getScreen(), 'in-fullscreen-changed', () => { if ( @@ -263,188 +258,110 @@ export const TaskbarAppIcon = GObject.registerClass( this._displayProperIndicator() } }, - ) + ]) } else { - this._titleWindowChangeId = this.window.connect( - 'notify::title', - this._updateWindowTitle.bind(this), - ) - - this._minimizedWindowChangeId = this.window.connect( - 'notify::minimized', - this._updateWindowTitleStyle.bind(this), + this._signalsHandler.add( + [this.window, 'notify::title', this._updateWindowTitle.bind(this)], + [ + this.window, + 'notify::minimized', + this._updateWindowTitleStyle.bind(this), + ], ) } - this._scrollEventId = this.connect( - 'scroll-event', - this._onMouseScroll.bind(this), - ) - - this._overviewWindowDragEndId = Main.overview.connect( - 'window-drag-end', - this._onOverviewWindowDragEnd.bind(this), - ) - - this._switchWorkspaceId = global.window_manager.connect( - 'switch-workspace', - this._onSwitchWorkspace.bind(this), - ) - - this._hoverChangeId = this.connect('notify::hover', () => - this._onAppIconHoverChanged(), - ) - - this._hoverChangeId2 = this.connect('notify::hover', () => - this._onAppIconHoverChanged_GtkWorkaround(), - ) - this._pressedChangedId = this.connect('notify::pressed', () => - this._onAppIconPressedChanged_GtkWorkaround(), - ) - - this._dtpSettingsSignalIds = [ - SETTINGS.connect( - 'changed::animate-appicon-hover', - this._onAnimateAppiconHoverChanged.bind(this), - ), - SETTINGS.connect( + this._signalsHandler.add( + [this, 'scroll-event', this._onMouseScroll.bind(this)], + [ + Main.overview, + 'window-drag-end', + this._onOverviewWindowDragEnd.bind(this), + ], + [ + global.window_manager, + 'switch-workspace', + this._onSwitchWorkspace.bind(this), + ], + [ + this, + 'notify::hover', + () => { + this._onAppIconHoverChanged() + this._onAppIconHoverChanged_GtkWorkaround() + }, + ], + [ + this, + 'notify::pressed', + this._onAppIconPressedChanged_GtkWorkaround.bind(this), + ], + [ + this.dtpPanel.panelManager.notificationsMonitor, + `update-${this.app.id}`, + this._handleNotifications.bind(this), + ], + [ + SETTINGS, 'changed::animate-appicon-hover', + () => { + this._onAnimateAppiconHoverChanged() + this._onAppIconHoverHighlightChanged() + }, + ], + [ + SETTINGS, + [ + 'changed::highlight-appicon-hover', + 'changed::highlight-appicon-hover-background-color', + 'changed::highlight-appicon-pressed-background-color', + 'changed::highlight-appicon-hover-border-radius', + ], this._onAppIconHoverHighlightChanged.bind(this), - ), - SETTINGS.connect( - 'changed::highlight-appicon-hover', - this._onAppIconHoverHighlightChanged.bind(this), - ), - SETTINGS.connect( - 'changed::highlight-appicon-hover-background-color', - this._onAppIconHoverHighlightChanged.bind(this), - ), - SETTINGS.connect( - 'changed::highlight-appicon-pressed-background-color', - this._onAppIconHoverHighlightChanged.bind(this), - ), - SETTINGS.connect( + ], + [ + SETTINGS, + [ + 'changed::dot-position', + 'changed::dot-size', + 'changed::dot-style-focused', + 'changed::dot-style-unfocused', + 'changed::dot-color-dominant', + 'changed::dot-color-override', + 'changed::dot-color-1', + 'changed::dot-color-2', + 'changed::dot-color-3', + 'changed::dot-color-4', + 'changed::dot-color-unfocused-different', + 'changed::dot-color-unfocused-1', + 'changed::dot-color-unfocused-2', + 'changed::dot-color-unfocused-3', + 'changed::dot-color-unfocused-4', + 'changed::focus-highlight', + 'changed::focus-highlight-dominant', + 'changed::focus-highlight-color', + 'changed::focus-highlight-opacity', + 'changed::group-apps-underline-unfocused', + ], + this._settingsChangeRefresh.bind(this), + ], + [ + SETTINGS, + [ + 'changed::group-apps-label-font-size', + 'changed::group-apps-label-font-weight', + 'changed::group-apps-label-font-color', + 'changed::group-apps-label-font-color-minimized', + 'changed::group-apps-label-max-width', + 'changed::group-apps-use-fixed-width', + ], + this._updateWindowTitleStyle.bind(this), + ], + [ + SETTINGS, 'changed::highlight-appicon-hover-border-radius', - this._onAppIconHoverHighlightChanged.bind(this), - ), - SETTINGS.connect( - 'changed::dot-position', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-size', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-style-focused', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-style-unfocused', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-dominant', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-override', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-1', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-2', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-3', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-4', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-unfocused-different', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-unfocused-1', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-unfocused-2', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-unfocused-3', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::dot-color-unfocused-4', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::focus-highlight', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::focus-highlight-dominant', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::focus-highlight-color', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::focus-highlight-opacity', - this._settingsChangeRefresh.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-label-font-size', - this._updateWindowTitleStyle.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-label-font-weight', - this._updateWindowTitleStyle.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-label-font-color', - this._updateWindowTitleStyle.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-label-font-color-minimized', - this._updateWindowTitleStyle.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-label-max-width', - this._updateWindowTitleStyle.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-use-fixed-width', - this._updateWindowTitleStyle.bind(this), - ), - SETTINGS.connect( - 'changed::group-apps-underline-unfocused', - this._settingsChangeRefresh.bind(this), - ), - ] - - this._dtpSettingsSignalIds = this._dtpSettingsSignalIds.concat([ - SETTINGS.connect('changed::highlight-appicon-hover-border-radius', () => - this._setIconStyle(this._isFocusedWindow()), - ), - ]) - - this._progressIndicator = new Progress.ProgressIndicator( - this, - panel.progressManager, + () => this._setIconStyle(this._isFocusedWindow()), + ], ) - - this._numberOverlay() } getDragActor() { @@ -520,60 +437,9 @@ export const TaskbarAppIcon = GObject.registerClass( super._onDestroy() this._timeoutsHandler.destroy() + this._signalsHandler.destroy() this._previewMenu.close(true) - - // Disconect global signals - if (this._stateChangedId > 0) { - this.app.disconnect(this._stateChangedId) - this._stateChangedId = 0 - } - - if (this._overviewWindowDragEndId) - Main.overview.disconnect(this._overviewWindowDragEndId) - - if (this._focusWindowChangedId) - global.display.disconnect(this._focusWindowChangedId) - - if (this._fullscreenId) - Utils.DisplayWrapper.getScreen().disconnect(this._fullscreenId) - - if (this._titleWindowChangeId) - this.window.disconnect(this._titleWindowChangeId) - - if (this._minimizedWindowChangeId) - this.window.disconnect(this._minimizedWindowChangeId) - - if (this._windowEnteredMonitorId) { - Utils.DisplayWrapper.getScreen().disconnect( - this._windowEnteredMonitorId, - ) - Utils.DisplayWrapper.getScreen().disconnect(this._windowLeftMonitorId) - } - - if (this._switchWorkspaceId) - global.window_manager.disconnect(this._switchWorkspaceId) - - if (this._scaleFactorChangedId) - Utils.getStageTheme().disconnect(this._scaleFactorChangedId) - - if (this._hoverChangeId) { - this.disconnect(this._hoverChangeId) - } - if (this._hoverChangeId2) { - this.disconnect(this._hoverChangeId2) - } - if (this._pressedChangedId) { - this.disconnect(this._pressedChangedId) - } - - if (this._scrollEventId) { - this.disconnect(this._scrollEventId) - } - - for (let i = 0; i < this._dtpSettingsSignalIds.length; ++i) { - SETTINGS.disconnect(this._dtpSettingsSignalIds[i]) - } } onWindowsChanged() { @@ -1009,16 +875,18 @@ export const TaskbarAppIcon = GObject.registerClass( if (!this._menu) { this._menu = new TaskbarSecondaryMenu(this, this.dtpPanel.geom.position) this._menu.setApp(this.app) - this._menu.connect('open-state-changed', (menu, isPoppedUp) => { - if (!isPoppedUp) this._onMenuPoppedDown() - else this._previewMenu.close(true) - }) - let id = Main.overview.connect('hiding', () => { - this._menu.close() - }) - this.connect('destroy', () => { - Main.overview.disconnect(id) - }) + + this._signalsHandler.add( + [ + this._menu, + 'open-state-changed', + (menu, isPoppedUp) => { + if (!isPoppedUp) this._onMenuPoppedDown() + else this._previewMenu.close(true) + }, + ], + [Main.overview, 'hiding', () => this._menu.close()], + ) // We want to keep the item hovered while the menu is up this._menu.blockSourceEvents = true @@ -1731,25 +1599,89 @@ export const TaskbarAppIcon = GObject.registerClass( cr.$dispose() } + _handleNotifications(notificationsMonitor, state) { + if (!this._nWindows && !this.window) { + delete this._notifications.total + return + } + + let urgent = + 'urgent' in state ? state.urgent : this._notifications.urgent || false + let formatCount = (count) => { + if (!count) return 0 + + return count > 10 ? '10+' : count + } + + if ('count-visible' in state) + this._notifications.countVisible = state['count-visible'] + + if ('count' in state) this._notifications.count = state.count + + if ('trayCount' in state) + this._notifications.trayCount = this._checkIfFocusedApp() + ? 0 + : state.trayCount + + this._notifications.total = + SETTINGS.get_boolean('progress-show-count') && + ((this._notifications.countVisible || 0) && + (this._notifications.count || 0)) + + (this._notifications.trayCount || 0) + + urgent = urgent && !!this._notifications.total + + if (urgent !== this._notifications.urgent) + this.iconAnimator[`${urgent ? 'add' : 'remove'}Animation`]( + this.icon._iconBin, + 'dance', + ) + + this._notifications.urgent = urgent + + // restore hotkeys number if no more notifications + this._maybeToggleNumberOverlay( + formatCount(this._notifications.total) || + this._numberHotkeysOverlayLabel, + ) + } + + _maybeToggleNumberOverlay(labelNumber) { + let visible = this._numberOverlayBin.visible + let shouldBeVisible = + this._hotkeysOverlayActive || this._notifications.total + + this._numberOverlayLabel[ + `${this._notifications.total ? 'add' : 'remove'}_style_class_name` + ]('notification-badge') + + if ( + shouldBeVisible && + labelNumber != this._numberOverlayLabel.get_text() + ) { + this._numberOverlayLabel.set_text(labelNumber.toString()) + this._updateNumberOverlay() + } + + if (visible && !shouldBeVisible) this._numberOverlayBin.hide() + else if (!visible && shouldBeVisible) this._numberOverlayBin.show() + } + _numberOverlay() { - // Add label for a Hot-Key visual aid + // Add label for a numeric visual aid (hotkeys or notification) this._numberOverlayLabel = new St.Label({ style_class: 'badge' }) this._numberOverlayBin = new St.Bin({ child: this._numberOverlayLabel, y: 2, }) this._numberOverlayLabel.add_style_class_name('number-overlay') - this._numberOverlayOrder = -1 + this._numberHotkeysOverlayLabel = -1 this._numberOverlayBin.hide() this._dtpIconContainer.add_child(this._numberOverlayBin) } - updateHotkeyNumberOverlay() { - this.updateNumberOverlay(this._numberOverlayBin, true) - } - - updateNumberOverlay(bin, fixedSize) { + _updateNumberOverlay() { // We apply an overall scale factor that might come from a HiDPI monitor. // Clutter dimensions are in physical pixels, but CSS measures are in logical // pixels, so make sure to consider the scale. @@ -1760,7 +1692,6 @@ export const TaskbarAppIcon = GObject.registerClass( Math.max(12, 0.3 * natWidth) / Utils.getScaleFactor(), ) let size = Math.round(font_size * 1.3) - let label = bin.child let style = 'font-size: ' + font_size + @@ -1772,25 +1703,25 @@ export const TaskbarAppIcon = GObject.registerClass( size + 'px;' - if (fixedSize || label.get_text().length == 1) { + if (this._numberOverlayLabel.get_text().length == 1) { style += 'width: ' + size + 'px;' } else { style += 'padding: 0 2px;' } - bin.x = 2 - label.set_style(style) + this._numberOverlayLabel.set_style(style) } - setNumberOverlay(number) { - this._numberOverlayOrder = number - this._numberOverlayLabel.set_text(number.toString()) + setHotkeysNumberOverlayLabel(number) { + this._numberHotkeysOverlayLabel = number } - toggleNumberOverlay(activate) { - if (activate && this._numberOverlayOrder > -1) - this._numberOverlayBin.show() - else this._numberOverlayBin.hide() + toggleHotkeysNumberOverlay(activate) { + this._hotkeysOverlayActive = + activate && this._numberHotkeysOverlayLabel > -1 + + if (!this._notifications.total) + this._maybeToggleNumberOverlay(this._numberHotkeysOverlayLabel) } handleDragOver(source) { diff --git a/src/notificationsMonitor.js b/src/notificationsMonitor.js new file mode 100644 index 0000000..803685b --- /dev/null +++ b/src/notificationsMonitor.js @@ -0,0 +1,159 @@ +/* + * This file is part of the Dash-To-Panel extension for Gnome 3 + * + * 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 . + */ + +import Gio from 'gi://Gio' +import Shell from 'gi://Shell' + +import * as Main from 'resource:///org/gnome/shell/ui/main.js' +import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js' +import { EventEmitter } from 'resource:///org/gnome/shell/misc/signals.js' + +import * as Utils from './utils.js' + +const tracker = Shell.WindowTracker.get_default() +const knownCorrespondances = { + 'org.gnome.Evolution': [/^org\.gnome\.[eE]volution([.-].+)?$/g], +} + +export const NotificationsMonitor = class extends EventEmitter { + constructor() { + super() + + this._signalsHandler = new Utils.GlobalSignalsHandler() + + // pretty much useless, but might as well keep it for now + this._launcherEntryId = Gio.DBus.session.signal_subscribe( + null, // sender + 'com.canonical.Unity.LauncherEntry', // iface + 'Update', // member + null, // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + ( + connection, + senderName, + objectPath, + interfaceName, + signalName, + parameters, + ) => this._handleLauncherUpdate(senderName, parameters), + ) + + this._signalsHandler.add([ + tracker, + 'notify::focus-app', + () => { + // reset notifications from message tray on app focus + if (tracker.focus_app) + this.dispatch(tracker.focus_app.id, { trayCount: 0 }, true) + }, + ]) + this._acquireUnityDBus() + + this._checkNotifications() + } + + destroy() { + if (this._launcherEntryId) + Gio.DBus.session.signal_unsubscribe(this._launcherEntryId) + + this._releaseUnityDBus() + this._signalsHandler.destroy() + } + + dispatch(appId, state, ignoreMapping) { + // depending of the notification source, some app id end + // with ".desktop" and some don't ¯\_(ツ)_/¯ + appId = appId.replace('.desktop', '') + + // some app have different source app id, deamon and such, + // but it maps to a desktop app so match those here + if (!ignoreMapping && !knownCorrespondances[appId]) + appId = + Object.keys(knownCorrespondances).find((k) => + knownCorrespondances[k].some((regex) => appId.match(regex)), + ) || appId + + this.emit(`update-${appId}.desktop`, state) + } + + _acquireUnityDBus() { + if (!this._unityBusId) { + this._unityBusId = Gio.DBus.session.own_name( + 'com.canonical.Unity', + Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT, + null, + null, + ) + } + } + + _releaseUnityDBus() { + if (this._unityBusId) { + Gio.DBus.session.unown_name(this._unityBusId) + this._unityBusId = 0 + } + } + + _handleLauncherUpdate(senderName, parameters) { + if (!senderName || !parameters) return + + let [appUri, properties] = parameters.deep_unpack() + let appId = appUri.replace(/(^\w+:|^)\/\//, '') + let updates = {} + + // https://wiki.ubuntu.com/Unity/LauncherAPI#Low_level_DBus_API:_com.canonical.Unity.LauncherEntry + for (let property in properties) + updates[property] = properties[property].unpack() + + this.dispatch(appId, updates) + } + + _checkNotifications() { + let addSource = (tray, source) => { + let appId = source?._appId || source?.app?.id + + if (!appId) return + + this._signalsHandler.addWithLabel(appId, [ + source, + 'notify::count', + () => + this.dispatch(appId, { + trayCount: source.count, // source.unseenCount might be less annoying + urgent: !!source.notifications.find( + (n) => n.urgency > MessageTray.Urgency.NORMAL, + ), + }), + ]) + } + + this._signalsHandler.add( + [Main.messageTray, 'source-added', addSource], + [ + Main.messageTray, + 'source-removed', + (tray, source) => { + if (source?._appId) + this._signalsHandler.removeWithLabel(source._appId) + }, + ], + ) + + Main.messageTray.getSources().forEach((s) => addSource(null, s)) + } +} diff --git a/src/overview.js b/src/overview.js index 026e7cb..ea8706c 100644 --- a/src/overview.js +++ b/src/overview.js @@ -373,7 +373,7 @@ export const Overview = class { this._hotKeysEnabled = true if (SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS') - this.taskbar.toggleNumberOverlay(true) + this.taskbar.toggleHotkeysNumberOverlay(true) } _disableHotKeys() { @@ -418,7 +418,7 @@ export const Overview = class { this._hotKeysEnabled = false - this.taskbar.toggleNumberOverlay(false) + this.taskbar.toggleHotkeysNumberOverlay(false) } _optionalNumberOverlay() { @@ -435,8 +435,8 @@ export const Overview = class { SETTINGS.get_boolean('hot-keys') && SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS' ) - this.taskbar.toggleNumberOverlay(true) - else this.taskbar.toggleNumberOverlay(false) + this.taskbar.toggleHotkeysNumberOverlay(true) + else this.taskbar.toggleHotkeysNumberOverlay(false) }, ], [SETTINGS, 'changed::shortcut-num-keys', () => this._resetHotkeys()], @@ -468,7 +468,7 @@ export const Overview = class { if (hotkey_option === 'NEVER') return if (hotkey_option === 'TEMPORARILY' || overlayFromShortcut) - this.taskbar.toggleNumberOverlay(true) + this.taskbar.toggleHotkeysNumberOverlay(true) this._panel.intellihide.revealAndHold(Intellihide.Hold.TEMPORARY) @@ -484,7 +484,7 @@ export const Overview = class { timeout, () => { if (hotkey_option != 'ALWAYS') { - this.taskbar.toggleNumberOverlay(false) + this.taskbar.toggleHotkeysNumberOverlay(false) } this._panel.intellihide.release(Intellihide.Hold.TEMPORARY) diff --git a/src/panel.js b/src/panel.js index 3aa215b..b0d465f 100644 --- a/src/panel.js +++ b/src/panel.js @@ -48,7 +48,6 @@ import Shell from 'gi://Shell' import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js' import * as DateMenu from 'resource:///org/gnome/shell/ui/dateMenu.js' import * as Volume from 'resource:///org/gnome/shell/ui/status/volume.js' -import * as Progress from './progress.js' import * as Intellihide from './intellihide.js' import * as Transparency from './transparency.js' @@ -359,8 +358,6 @@ export const Panel = GObject.registerClass( // most repaint requests don't actually require us to repaint anything. // This saves significant CPU when repainting the screen. this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS) - - this._initProgressManager() } disable() { @@ -377,8 +374,6 @@ export const Panel = GObject.registerClass( this.dynamicTransparency.destroy() - this.progressManager.destroy() - this.taskbar.destroy() this.showAppsIconWrapper.destroy() @@ -610,16 +605,6 @@ export const Panel = GObject.registerClass( } }, ], - [ - SETTINGS, - 'changed::progress-show-bar', - () => this._initProgressManager(), - ], - [ - SETTINGS, - 'changed::progress-show-count', - () => this._initProgressManager(), - ], ) if (isVertical) { @@ -1494,19 +1479,6 @@ export const Panel = GObject.registerClass( ignoredConstr.indexOf(source.constructor.name) >= 0 ) } - - _initProgressManager() { - const progressVisible = SETTINGS.get_boolean('progress-show-bar') - const countVisible = SETTINGS.get_boolean('progress-show-count') - const pm = this.progressManager - - if (!pm && (progressVisible || countVisible)) - this.progressManager = new Progress.ProgressManager() - else if (pm) - Object.keys(pm._entriesByDBusName).forEach((k) => - pm._entriesByDBusName[k].setCountVisible(countVisible), - ) - } }, ) diff --git a/src/panelManager.js b/src/panelManager.js index dd14070..9e8c9c5 100755 --- a/src/panelManager.js +++ b/src/panelManager.js @@ -45,6 +45,7 @@ import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js' import * as LookingGlass from 'resource:///org/gnome/shell/ui/lookingGlass.js' import * as Main from 'resource:///org/gnome/shell/ui/main.js' import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js' +import { NotificationsMonitor } from './notificationsMonitor.js' import * as Layout from 'resource:///org/gnome/shell/ui/layout.js' import { InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js' import { SETTINGS } from './extension.js' @@ -133,6 +134,8 @@ export const PanelManager = class { if (reset) return + this.notificationsMonitor = new NotificationsMonitor() + this._desktopIconsUsableArea = new DesktopIconsIntegration.DesktopIconsUsableAreaClass() @@ -337,6 +340,8 @@ export const PanelManager = class { this._setKeyBindings(false) + this.notificationsMonitor.destroy() + this._signalsHandler.destroy() Main.layoutManager._updateHotCorners = this._oldUpdateHotCorners @@ -716,6 +721,9 @@ export const IconAnimator = class { if (this._started && this._count === 0) { this._timeline.stop() } + + if (name == 'dance') target.rotation_angle_z = 0 + return } } diff --git a/src/prefs.js b/src/prefs.js index 7778766..36a2c5e 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -255,18 +255,18 @@ const Preferences = class { this._setMonitorsInfo() }, ) - + let maybeGoToPage = () => { let targetPageName = settings.get_string('target-prefs-page') - + if (targetPageName) { window.set_visible_page_name(targetPageName) settings.set_string('target-prefs-page', '') } } - + settings.connect('changed::target-prefs-page', maybeGoToPage) - + maybeGoToPage() }) } diff --git a/src/progress.js b/src/progress.js deleted file mode 100644 index dc84120..0000000 --- a/src/progress.js +++ /dev/null @@ -1,699 +0,0 @@ -/* - * This file is part of the Dash-To-Panel extension for Gnome 3 - * - * 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 . - * - * - * Credits: - * This file is based on code from the Dash to Dock extension by micheleg - */ - -import Cairo from 'cairo' -import Gio from 'gi://Gio' -import Clutter from 'gi://Clutter' -import Pango from 'gi://Pango' -import St from 'gi://St' -import * as Utils from './utils.js' -import { SETTINGS } from './extension.js' - -import { EventEmitter } from 'resource:///org/gnome/shell/misc/signals.js' - -export const ProgressManager = class extends EventEmitter { - constructor() { - super() - - this._entriesByDBusName = {} - - this._launcher_entry_dbus_signal_id = Gio.DBus.session.signal_subscribe( - null, // sender - 'com.canonical.Unity.LauncherEntry', // iface - null, // member - null, // path - null, // arg0 - Gio.DBusSignalFlags.NONE, - this._onEntrySignalReceived.bind(this), - ) - - this._dbus_name_owner_changed_signal_id = Gio.DBus.session.signal_subscribe( - 'org.freedesktop.DBus', // sender - 'org.freedesktop.DBus', // interface - 'NameOwnerChanged', // member - '/org/freedesktop/DBus', // path - null, // arg0 - Gio.DBusSignalFlags.NONE, - this._onDBusNameOwnerChanged.bind(this), - ) - - this._acquireUnityDBus() - } - - destroy() { - if (this._launcher_entry_dbus_signal_id) { - Gio.DBus.session.signal_unsubscribe(this._launcher_entry_dbus_signal_id) - } - - if (this._dbus_name_owner_changed_signal_id) { - Gio.DBus.session.signal_unsubscribe( - this._dbus_name_owner_changed_signal_id, - ) - } - - this._releaseUnityDBus() - } - - size() { - return Object.keys(this._entriesByDBusName).length - } - - lookupByDBusName(dbusName) { - return Object.hasOwn(this._entriesByDBusName, dbusName) - ? this._entriesByDBusName[dbusName] - : null - } - - lookupById(appId) { - let ret = [] - for (let dbusName in this._entriesByDBusName) { - let entry = this._entriesByDBusName[dbusName] - if (entry && entry.appId() == appId) { - ret.push(entry) - } - } - - return ret - } - - addEntry(entry) { - let existingEntry = this.lookupByDBusName(entry.dbusName()) - if (existingEntry) { - existingEntry.update(entry) - } else { - this._entriesByDBusName[entry.dbusName()] = entry - this.emit('progress-entry-added', entry) - } - } - - removeEntry(entry) { - delete this._entriesByDBusName[entry.dbusName()] - this.emit('progress-entry-removed', entry) - } - - _acquireUnityDBus() { - if (!this._unity_bus_id) { - Gio.DBus.session.own_name( - 'com.canonical.Unity', - Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT, - null, - null, - ) - } - } - - _releaseUnityDBus() { - if (this._unity_bus_id) { - Gio.DBus.session.unown_name(this._unity_bus_id) - this._unity_bus_id = 0 - } - } - - _onEntrySignalReceived( - connection, - sender_name, - object_path, - interface_name, - signal_name, - parameters, - ) { - if (!parameters || !signal_name) return - - if (signal_name == 'Update') { - if (!sender_name) { - return - } - - this._handleUpdateRequest(sender_name, parameters) - } - } - - _onDBusNameOwnerChanged( - connection, - sender_name, - object_path, - interface_name, - signal_name, - parameters, - ) { - if (!parameters || !this.size()) return - - let [, before, after] = parameters.deep_unpack() - - if (!after) { - if (Object.hasOwn(this._entriesByDBusName, before)) { - this.removeEntry(this._entriesByDBusName[before]) - } - } - } - - _handleUpdateRequest(senderName, parameters) { - if (!senderName || !parameters) { - return - } - - let [appUri, properties] = parameters.deep_unpack() - let appId = appUri.replace(/(^\w+:|^)\/\//, '') - let entry = this.lookupByDBusName(senderName) - - if (entry) { - entry.setDBusName(senderName) - entry.update(properties) - } else { - let entry = new AppProgress(senderName, appId, properties) - this.addEntry(entry) - } - } -} - -export class AppProgress extends EventEmitter { - constructor(dbusName, appId, properties) { - super() - - this._dbusName = dbusName - this._appId = appId - this._count = 0 - this._countVisible = false - this._progress = 0.0 - this._progressVisible = false - this._urgent = false - this.update(properties) - } - - appId() { - return this._appId - } - - dbusName() { - return this._dbusName - } - - count() { - return this._count - } - - setCount(count) { - if (this._count != count) { - this._count = count - this.emit('count-changed', this._count) - } - } - - countVisible() { - return this._countVisible - } - - setCountVisible(countVisible) { - if (this._countVisible != countVisible) { - this._countVisible = countVisible - this.emit('count-visible-changed', this._countVisible) - } - } - - progress() { - return this._progress - } - - setProgress(progress) { - if (this._progress != progress) { - this._progress = progress - this.emit('progress-changed', this._progress) - } - } - - progressVisible() { - return this._progressVisible - } - - setProgressVisible(progressVisible) { - if (this._progressVisible != progressVisible) { - this._progressVisible = progressVisible - this.emit('progress-visible-changed', this._progressVisible) - } - } - - urgent() { - return this._urgent - } - - setUrgent(urgent) { - if (this._urgent != urgent) { - this._urgent = urgent - this.emit('urgent-changed', this._urgent) - } - } - - setDBusName(dbusName) { - if (this._dbusName != dbusName) { - let oldName = this._dbusName - this._dbusName = dbusName - this.emit('dbus-name-changed', oldName) - } - } - - update(other) { - if (other instanceof AppProgress) { - this.setDBusName(other.dbusName()) - this.setCount(other.count()) - this.setCountVisible(other.countVisible()) - this.setProgress(other.progress()) - this.setProgressVisible(other.progressVisible()) - this.setUrgent(other.urgent()) - } else { - for (let property in other) { - if (Object.hasOwn(other, property)) { - if (property == 'count') { - this.setCount(other[property].get_int64()) - } else if (property == 'count-visible') { - this.setCountVisible( - SETTINGS.get_boolean('progress-show-count') && - other[property].get_boolean(), - ) - } else if (property == 'progress') { - this.setProgress(other[property].get_double()) - } else if (property == 'progress-visible') { - this.setProgressVisible( - SETTINGS.get_boolean('progress-show-bar') && - other[property].get_boolean(), - ) - } else if (property == 'urgent') { - this.setUrgent(other[property].get_boolean()) - } else { - // Not implemented yet - } - } - } - } - } -} - -export const ProgressIndicator = class { - constructor(source, progressManager) { - this._source = source - this._progressManager = progressManager - this._signalsHandler = new Utils.GlobalSignalsHandler() - - this._sourceDestroyId = this._source.connect('destroy', () => { - this._signalsHandler.destroy() - }) - - this._notificationBadgeLabel = new St.Label({ style_class: 'badge' }) - this._notificationBadgeBin = new St.Bin({ - child: this._notificationBadgeLabel, - y: 2, - x: 2, - }) - this._notificationBadgeLabel.add_style_class_name('notification-badge') - this._notificationBadgeCount = 0 - this._notificationBadgeBin.hide() - - this._source._dtpIconContainer.add_child(this._notificationBadgeBin) - this._source._dtpIconContainer.connect( - 'notify::allocation', - this.updateNotificationBadge.bind(this), - ) - - this._progressManagerEntries = [] - this._progressManager.lookupById(this._source.app.id).forEach((entry) => { - this.insertEntry(entry) - }) - - this._signalsHandler.add( - [ - this._progressManager, - 'progress-entry-added', - this._onEntryAdded.bind(this), - ], - [ - this._progressManager, - 'progress-entry-removed', - this._onEntryRemoved.bind(this), - ], - ) - } - - destroy() { - this._source.disconnect(this._sourceDestroyId) - this._signalsHandler.destroy() - } - - _onEntryAdded(appProgress, entry) { - if (!entry || !entry.appId()) return - if ( - this._source && - this._source.app && - this._source.app.id == entry.appId() - ) { - this.insertEntry(entry) - } - } - - _onEntryRemoved(appProgress, entry) { - if (!entry || !entry.appId()) return - - if ( - this._source && - this._source.app && - this._source.app.id == entry.appId() - ) { - this.removeEntry(entry) - } - } - - updateNotificationBadge() { - this._source.updateNumberOverlay(this._notificationBadgeBin) - this._notificationBadgeLabel.clutter_text.ellipsize = - Pango.EllipsizeMode.MIDDLE - } - - _notificationBadgeCountToText(count) { - if (count <= 9999) { - return count.toString() - } else if (count < 1e5) { - let thousands = count / 1e3 - return thousands.toFixed(1).toString() + 'k' - } else if (count < 1e6) { - let thousands = count / 1e3 - return thousands.toFixed(0).toString() + 'k' - } else if (count < 1e8) { - let millions = count / 1e6 - return millions.toFixed(1).toString() + 'M' - } else if (count < 1e9) { - let millions = count / 1e6 - return millions.toFixed(0).toString() + 'M' - } else { - let billions = count / 1e9 - return billions.toFixed(1).toString() + 'B' - } - } - - setNotificationBadge(count) { - this._notificationBadgeCount = count - let text = this._notificationBadgeCountToText(count) - this._notificationBadgeLabel.set_text(text) - } - - toggleNotificationBadge(activate) { - if (activate && this._notificationBadgeCount > 0) { - this.updateNotificationBadge() - this._notificationBadgeBin.show() - } else this._notificationBadgeBin.hide() - } - - _showProgressOverlay() { - if (this._progressOverlayArea) { - this._updateProgressOverlay() - return - } - - this._progressOverlayArea = new St.DrawingArea({ - x_expand: true, - y_expand: true, - }) - this._progressOverlayArea.add_style_class_name('progress-bar') - this._progressOverlayArea.connect('repaint', () => { - this._drawProgressOverlay(this._progressOverlayArea) - }) - - this._source._iconContainer.add_child(this._progressOverlayArea) - let node = this._progressOverlayArea.get_theme_node() - - let [hasColor, color] = node.lookup_color('-progress-bar-background', false) - if (hasColor) this._progressbar_background = color - else - this._progressbar_background = new Utils.ColorUtils.Color({ - red: 204, - green: 204, - blue: 204, - alpha: 255, - }) - ;[hasColor, color] = node.lookup_color('-progress-bar-border', false) - if (hasColor) this._progressbar_border = color - else - this._progressbar_border = new Utils.ColorUtils.Color({ - red: 230, - green: 230, - blue: 230, - alpha: 255, - }) - - this._updateProgressOverlay() - } - - _hideProgressOverlay() { - if (this._progressOverlayArea) this._progressOverlayArea.destroy() - - this._progressOverlayArea = null - this._progressbar_background = null - this._progressbar_border = null - } - - _updateProgressOverlay() { - if (this._progressOverlayArea) { - this._progressOverlayArea.queue_repaint() - } - } - - _drawProgressOverlay(area) { - let scaleFactor = Utils.getScaleFactor() - let [surfaceWidth, surfaceHeight] = area.get_surface_size() - let cr = area.get_context() - - let iconSize = this._source.icon.iconSize * scaleFactor - - let x = Math.floor((surfaceWidth - iconSize) / 2) - let y = Math.floor((surfaceHeight - iconSize) / 2) - - let lineWidth = Math.floor(1.0 * scaleFactor) - let padding = Math.floor(iconSize * 0.05) - let width = iconSize - 2.0 * padding - let height = Math.floor(Math.min(18.0 * scaleFactor, 0.2 * iconSize)) - x += padding - y += iconSize - height - padding - - cr.setLineWidth(lineWidth) - - // Draw the outer stroke - let stroke = new Cairo.LinearGradient(0, y, 0, y + height) - let fill = null - stroke.addColorStopRGBA(0.5, 0.5, 0.5, 0.5, 0.1) - stroke.addColorStopRGBA(0.9, 0.8, 0.8, 0.8, 0.4) - Utils.drawRoundedLine( - cr, - x + lineWidth / 2.0, - y + lineWidth / 2.0, - width, - height, - true, - true, - stroke, - fill, - ) - - // Draw the background - x += lineWidth - y += lineWidth - width -= 2.0 * lineWidth - height -= 2.0 * lineWidth - - stroke = Cairo.SolidPattern.createRGBA(0.2, 0.2, 0.2, 0.9) - fill = new Cairo.LinearGradient(0, y, 0, y + height) - fill.addColorStopRGBA(0.4, 0.25, 0.25, 0.25, 1.0) - fill.addColorStopRGBA(0.9, 0.35, 0.35, 0.35, 1.0) - Utils.drawRoundedLine( - cr, - x + lineWidth / 2.0, - y + lineWidth / 2.0, - width, - height, - true, - true, - stroke, - fill, - ) - - // Draw the finished bar - x += lineWidth - y += lineWidth - width -= 2.0 * lineWidth - height -= 2.0 * lineWidth - - let finishedWidth = Math.ceil(this._progress * width) - - let bg = this._progressbar_background - let bd = this._progressbar_border - - stroke = Cairo.SolidPattern.createRGBA( - bd.red / 255, - bd.green / 255, - bd.blue / 255, - bd.alpha / 255, - ) - fill = Cairo.SolidPattern.createRGBA( - bg.red / 255, - bg.green / 255, - bg.blue / 255, - bg.alpha / 255, - ) - - if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) - Utils.drawRoundedLine( - cr, - x + lineWidth / 2.0 + width - finishedWidth, - y + lineWidth / 2.0, - finishedWidth, - height, - true, - true, - stroke, - fill, - ) - else - Utils.drawRoundedLine( - cr, - x + lineWidth / 2.0, - y + lineWidth / 2.0, - finishedWidth, - height, - true, - true, - stroke, - fill, - ) - - cr.$dispose() - } - - setProgress(progress) { - this._progress = Math.min(Math.max(progress, 0.0), 1.0) - this._updateProgressOverlay() - } - - toggleProgressOverlay(activate) { - if (activate) { - this._showProgressOverlay() - } else { - this._hideProgressOverlay() - } - } - - insertEntry(appProgress) { - if ( - !appProgress || - this._progressManagerEntries.indexOf(appProgress) !== -1 - ) - return - - this._progressManagerEntries.push(appProgress) - this._selectEntry(appProgress) - } - - removeEntry(appProgress) { - if (!appProgress || this._progressManagerEntries.indexOf(appProgress) == -1) - return - - this._progressManagerEntries.splice( - this._progressManagerEntries.indexOf(appProgress), - 1, - ) - - if (this._progressManagerEntries.length > 0) { - this._selectEntry( - this._progressManagerEntries[this._progressManagerEntries.length - 1], - ) - } else { - this.setNotificationBadge(0) - this.toggleNotificationBadge(false) - this.setProgress(0) - this.toggleProgressOverlay(false) - this.setUrgent(false) - } - } - - _selectEntry(appProgress) { - if (!appProgress) return - - this._signalsHandler.removeWithLabel('progress-entry') - - this._signalsHandler.addWithLabel( - 'progress-entry', - [ - appProgress, - 'count-changed', - (appProgress, value) => { - this.setNotificationBadge(value) - }, - ], - [ - appProgress, - 'count-visible-changed', - (appProgress, value) => { - this.toggleNotificationBadge(value) - }, - ], - [ - appProgress, - 'progress-changed', - (appProgress, value) => { - this.setProgress(value) - }, - ], - [ - appProgress, - 'progress-visible-changed', - (appProgress, value) => { - this.toggleProgressOverlay(value) - }, - ], - [ - appProgress, - 'urgent-changed', - (appProgress, value) => { - this.setUrgent(value) - }, - ], - ) - - this.setNotificationBadge(appProgress.count()) - this.toggleNotificationBadge(appProgress.countVisible()) - this.setProgress(appProgress.progress()) - this.toggleProgressOverlay(appProgress.progressVisible()) - - this._isUrgent = false - } - - setUrgent(urgent) { - const icon = this._source.icon._iconBin - if (urgent) { - if (!this._isUrgent) { - icon.set_pivot_point(0.5, 0.5) - this._source.iconAnimator.addAnimation(icon, 'dance') - this._isUrgent = true - } - } else { - if (this._isUrgent) { - this._source.iconAnimator.removeAnimation(icon, 'dance') - this._isUrgent = false - } - icon.rotation_angle_z = 0 - } - } -} diff --git a/src/stylesheet.css b/src/stylesheet.css index 8b951bf..96fb7c9 100644 --- a/src/stylesheet.css +++ b/src/stylesheet.css @@ -153,7 +153,7 @@ } #dashtopanelScrollview .notification-badge { - background-color: rgba(255,0,0,0.8); + background-color: rgba(214, 0, 0, 0.8); } #dashtopanelScrollview .progress-bar { diff --git a/src/taskbar.js b/src/taskbar.js index 30ee3d0..d66a993 100644 --- a/src/taskbar.js +++ b/src/taskbar.js @@ -64,6 +64,7 @@ let donateDummyApp = { action == 'opts' ? _('Donation options') : '', }, connect: () => [], + disconnect: () => false, connectObject: () => [], get_id: () => 'dtp_donate', get_windows: () => [], @@ -1197,10 +1198,8 @@ export const Taskbar = class extends EventEmitter { // This is required for icon reordering when the scrollview is used. this._updateAppIcons() - // This will update the size, and the corresponding number for each icon on the primary panel - if (this.dtpPanel.isPrimary) { - this._updateNumberOverlay() - } + // This will update the size, and the corresponding number for each icon + this._updateHotkeysNumberOverlay() this._shownInitially = true } @@ -1276,7 +1275,7 @@ export const Taskbar = class extends EventEmitter { } } - _updateNumberOverlay() { + _updateHotkeysNumberOverlay() { let seenApps = {} let counter = 0 @@ -1287,26 +1286,24 @@ export const Taskbar = class extends EventEmitter { } if (counter <= 10) { - icon.setNumberOverlay(counter == 10 ? 0 : counter) + icon.setHotkeysNumberOverlayLabel(counter == 10 ? 0 : counter) } else { // No overlay after 10 - icon.setNumberOverlay(-1) + icon.setHotkeysNumberOverlayLabel(-1) } - - icon.updateHotkeyNumberOverlay() }) if ( SETTINGS.get_boolean('hot-keys') && SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS' ) - this.toggleNumberOverlay(true) + this.toggleHotkeysNumberOverlay(true) } - toggleNumberOverlay(activate) { + toggleHotkeysNumberOverlay(activate) { let appIcons = this._getAppIcons() appIcons.forEach(function (icon) { - icon.toggleNumberOverlay(activate) + icon.toggleHotkeysNumberOverlay(activate) }) }