From bf213af4c23dcd09ad0eb36e5d9355be67ef9149 Mon Sep 17 00:00:00 2001 From: Jason DeRose Date: Sat, 25 Jan 2020 13:22:19 -0500 Subject: [PATCH] Initial implementation of dbus progress and badge counts --- Makefile | 2 +- appIcons.js | 3 + panel.js | 5 + progress.js | 571 +++++++++++++++++++++++++++++++++++++++++++++++++ stylesheet.css | 19 +- utils.js | 33 +++ 6 files changed, 631 insertions(+), 2 deletions(-) create mode 100644 progress.js diff --git a/Makefile b/Makefile index ac39741..dbc134b 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ UUID = dash-to-panel@jderose9.github.com BASE_MODULES = extension.js stylesheet.css metadata.json COPYING README.md -EXTRA_MODULES = appIcons.js convenience.js panel.js panelManager.js proximity.js intellihide.js panelStyle.js overview.js taskbar.js transparency.js windowPreview.js prefs.js update.js utils.js Settings.ui +EXTRA_MODULES = appIcons.js convenience.js panel.js panelManager.js proximity.js intellihide.js progress.js panelStyle.js overview.js taskbar.js transparency.js windowPreview.js prefs.js update.js utils.js Settings.ui EXTRA_IMAGES = highlight_stacked_bg.svg highlight_stacked_bg_2.svg highlight_stacked_bg_3.svg TOLOCALIZE = prefs.js appIcons.js update.js diff --git a/appIcons.js b/appIcons.js index 6d43d0c..2dda4d8 100644 --- a/appIcons.js +++ b/appIcons.js @@ -49,6 +49,7 @@ const Me = imports.misc.extensionUtils.getCurrentExtension(); const Utils = Me.imports.utils; const Panel = Me.imports.panel; const Taskbar = Me.imports.taskbar; +const Progress = Me.imports.progress; const _ = imports.gettext.domain(Utils.TRANSLATION_DOMAIN).gettext; let LABEL_GAP = 5; @@ -240,6 +241,8 @@ var taskbarAppIcon = Utils.defineClass({ this.forcedOverview = false; this._numberOverlay(); + + this._progressIndicator = new Progress.ProgressIndicator(this, panel.progressManager); }, shouldShowTooltip: function() { diff --git a/panel.js b/panel.js index b5fea83..cb5bc1f 100644 --- a/panel.js +++ b/panel.js @@ -54,6 +54,7 @@ const ViewSelector = imports.ui.viewSelector; const DateMenu = imports.ui.dateMenu; const Tweener = imports.ui.tweener; const Volume = imports.ui.status.volume; +const Progress = Me.imports.progress; const Intellihide = Me.imports.intellihide; const Transparency = Me.imports.transparency; @@ -411,6 +412,8 @@ var dtpPanel = Utils.defineClass({ // 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.progressManager = new Progress.ProgressManager(); }, disable: function () { @@ -434,6 +437,8 @@ var dtpPanel = Utils.defineClass({ this.dynamicTransparency.destroy(); } + this.progressManager.destroy(); + this.taskbar.destroy(); // reset stored icon size to the default dash diff --git a/progress.js b/progress.js new file mode 100644 index 0000000..c1d25ba --- /dev/null +++ b/progress.js @@ -0,0 +1,571 @@ +/* + * 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 + */ + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Gio = imports.gi.Gio; +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const Pango = imports.gi.Pango; +const St = imports.gi.St; +const Signals = imports.signals; +const Utils = Me.imports.utils; + + +var ProgressManager = Utils.defineClass({ + Name: 'DashToPanel.ProgressManager', + + _init: function() { + + 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: function() { + 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: function() { + return Object.keys(this._entriesByDBusName).length; + }, + + lookupByDBusName: function(dbusName) { + return this._entriesByDBusName.hasOwnProperty(dbusName) ? this._entriesByDBusName[dbusName] : null; + }, + + lookupById: function(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: function(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: function(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: function() { + if (this._unity_bus_id) { + Gio.DBus.session.unown_name(this._unity_bus_id); + this._unity_bus_id = 0; + } + }, + + _onEntrySignalReceived: function(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: function(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: function(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); + } + } +}); +Signals.addSignalMethods(ProgressManager.prototype); + +var AppProgress = Utils.defineClass({ + Name: 'DashToPanel.AppProgress', + + _init: function(dbusName, appId, properties) { + this._dbusName = dbusName; + this._appId = appId; + this._count = 0; + this._countVisible = false; + this._progress = 0.0; + this._progressVisible = false; + this.update(properties); + }, + + appId: function() { + return this._appId; + }, + + dbusName: function() { + return this._dbusName; + }, + + count: function() { + return this._count; + }, + + setCount: function(count) { + if (this._count != count) { + this._count = count; + this.emit('count-changed', this._count); + } + }, + + countVisible: function() { + return this._countVisible; + }, + + setCountVisible: function(countVisible) { + if (this._countVisible != countVisible) { + this._countVisible = countVisible; + this.emit('count-visible-changed', this._countVisible); + } + }, + + progress: function() { + return this._progress; + }, + + setProgress: function(progress) { + if (this._progress != progress) { + this._progress = progress; + this.emit('progress-changed', this._progress); + } + }, + + progressVisible: function() { + return this._progressVisible; + }, + + setProgressVisible: function(progressVisible) { + if (this._progressVisible != progressVisible) { + this._progressVisible = progressVisible; + this.emit('progress-visible-changed', this._progressVisible); + } + }, + + setDBusName: function(dbusName) { + if (this._dbusName != dbusName) { + let oldName = this._dbusName; + this._dbusName = dbusName; + this.emit('dbus-name-changed', oldName); + } + }, + + update: function(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()) + } 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(other[property].get_boolean()); + } if (property == 'progress') { + this.setProgress(other[property].get_double()); + } else if (property == 'progress-visible') { + this.setProgressVisible(other[property].get_boolean()); + } else { + // Not implemented yet + } + } + } + } + } +}); +Signals.addSignalMethods(AppProgress.prototype); + + +var ProgressIndicator = Utils.defineClass({ + Name: 'DashToPanel.ProgressIndicator', + + _init: function(source, progressManager) { + this._source = source; + this._progressManager = progressManager; + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._sourceDestroyId = this._source.actor.connect('destroy', () => { + this._signalsHandler.destroy(); + }); + + this._notificationBadgeLabel = new St.Label(); + this._notificationBadgeBin = new St.Bin({ + child: this._notificationBadgeLabel, + x_align: St.Align.END, y_align: St.Align.START, + x_expand: true, y_expand: true + }); + this._notificationBadgeLabel.add_style_class_name('notification-badge'); + this._notificationBadgeCount = 0; + this._notificationBadgeBin.hide(); + + this._source._iconContainer.add_child(this._notificationBadgeBin); + this._source._iconContainer.connect('allocation-changed', 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: function() { + this._source.actor.disconnect(this._sourceDestroyId); + this._signalsHandler.destroy(); + }, + + _onEntryAdded: function(appProgress, entry) { + if (!entry || !entry.appId()) + return; + if (this._source && this._source.app && this._source.app.id == entry.appId()) { + this.insertEntry(entry); + } + }, + + _onEntryRemoved: function(appProgress, entry) { + if (!entry || !entry.appId()) + return; + + if (this._source && this._source.app && this._source.app.id == entry.appId()) { + this.removeEntry(entry); + } + }, + + updateNotificationBadge: function() { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let [minWidth, natWidth] = this._source._iconContainer.get_preferred_width(-1); + let logicalNatWidth = natWidth / scaleFactor; + let font_size = Math.max(10, Math.round(logicalNatWidth / 5)); + let margin_left = Math.round(logicalNatWidth / 4); + + this._notificationBadgeLabel.set_style( + 'font-size: ' + font_size + 'px;' + + 'margin-left: ' + margin_left + 'px;' + ); + + this._notificationBadgeBin.width = Math.round(logicalNatWidth - margin_left); + this._notificationBadgeLabel.clutter_text.ellipsize = Pango.EllipsizeMode.MIDDLE; + }, + + _notificationBadgeCountToText: function(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: function(count) { + this._notificationBadgeCount = count; + let text = this._notificationBadgeCountToText(count); + this._notificationBadgeLabel.set_text(text); + }, + + toggleNotificationBadge: function(activate) { + if (activate && this._notificationBadgeCount > 0) { + this.updateNotificationBadge(); + this._notificationBadgeBin.show(); + } + else + this._notificationBadgeBin.hide(); + }, + + _showProgressOverlay: function() { + 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: function() { + if (this._progressOverlayArea) + this._progressOverlayArea.destroy(); + + this._progressOverlayArea = null; + this._progressbar_background = null; + this._progressbar_border = null; + }, + + _updateProgressOverlay: function() { + + if (this._progressOverlayArea) { + this._progressOverlayArea.queue_repaint(); + } + }, + + _drawProgressOverlay: function(area) { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + 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: function(progress) { + this._progress = Math.min(Math.max(progress, 0.0), 1.0); + this._updateProgressOverlay(); + }, + + toggleProgressOverlay: function(activate) { + if (activate) { + this._showProgressOverlay(); + } + else { + this._hideProgressOverlay(); + } + }, + + insertEntry: function(appProgress) { + if (!appProgress || this._progressManagerEntries.indexOf(appProgress) !== -1) + return; + + this._progressManagerEntries.push(appProgress); + this._selectEntry(appProgress); + }, + + removeEntry: function(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); + } + }, + + _selectEntry: function(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); + } + ]); + + this.setNotificationBadge(appProgress.count()); + this.toggleNotificationBadge(appProgress.countVisible()); + this.setProgress(appProgress.progress()); + this.toggleProgressOverlay(appProgress.progressVisible()); + + } +}); diff --git a/stylesheet.css b/stylesheet.css index f0cbb0e..d894ee1 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -59,6 +59,23 @@ margin-bottom: 0; } +#dashtopanelScrollview .notification-badge { + color: rgba(255,255,255,1); + background-color: rgba(255,0,0,1.0); + padding: 0.2em 0.5em; + border-radius: 1em; + font-weight: bold; + text-align: center; + margin: 2px; +} + +#dashtopanelScrollview .progress-bar { + /* Customization of the progress bar style, e.g.: + -progress-bar-background: rgba(0.8, 0.8, 0.8, 1); + -progress-bar-border: rgba(0.9, 0.9, 0.9, 1); + */ +} + #dashtopanelTaskbar .scrollview-fade { background-gradient-end: rgba(0, 0, 0, 0); } @@ -127,4 +144,4 @@ background-color: rgba(0,0,0,0.8); color: rgba(256, 256, 256, 1); text-align: center; -} \ No newline at end of file +} diff --git a/utils.js b/utils.js index ea9c824..9d6a35d 100644 --- a/utils.js +++ b/utils.js @@ -835,3 +835,36 @@ var DominantColorExtractor = defineClass({ } }); + +var drawRoundedLine = function(cr, x, y, width, height, isRoundLeft, isRoundRight, stroke, fill) { + if (height > width) { + y += Math.floor((height - width) / 2.0); + height = width; + } + + height = 2.0 * Math.floor(height / 2.0); + + var leftRadius = isRoundLeft ? height / 2.0 : 0.0; + var rightRadius = isRoundRight ? height / 2.0 : 0.0; + + cr.moveTo(x + width - rightRadius, y); + cr.lineTo(x + leftRadius, y); + if (isRoundLeft) + cr.arcNegative(x + leftRadius, y + leftRadius, leftRadius, -Math.PI/2, Math.PI/2); + else + cr.lineTo(x, y + height); + cr.lineTo(x + width - rightRadius, y + height); + if (isRoundRight) + cr.arcNegative(x + width - rightRadius, y + rightRadius, rightRadius, Math.PI/2, -Math.PI/2); + else + cr.lineTo(x + width, y); + cr.closePath(); + + if (fill != null) { + cr.setSource(fill); + cr.fillPreserve(); + } + if (stroke != null) + cr.setSource(stroke); + cr.stroke(); +} \ No newline at end of file