/* * 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 this._entriesByDBusName.hasOwnProperty(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, user_data, ) { 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, user_data, ) { if (!parameters || !this.size()) return let [name, before, after] = parameters.deep_unpack() if (!after) { if (this._entriesByDBusName.hasOwnProperty(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 (other.hasOwnProperty(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 } } }