Files
dash-to-panel/progress.js
Charles Gagnon d04104ff8a Format code
2025-01-31 11:49:22 -05:00

702 lines
17 KiB
JavaScript

/*
* 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 <http://www.gnu.org/licenses/>.
*
*
* 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
}
}
}