/* * 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 Clutter.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 Clutter.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.20*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.20, 0.20, 0.20, 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; } } };