mirror of
https://github.com/morgan9e/dash-to-panel
synced 2026-04-14 00:04:17 +09:00
702 lines
17 KiB
JavaScript
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
|
|
}
|
|
}
|
|
}
|