1040 lines
30 KiB
JavaScript
1040 lines
30 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
|
|
* and code from the Taskbar extension by Zorin OS
|
|
* Some code was also adapted from the upstream Gnome Shell source code.
|
|
*/
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Config = imports.misc.config;
|
|
const GdkPixbuf = imports.gi.GdkPixbuf;
|
|
const Gi = imports._gi;
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const GObject = imports.gi.GObject;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Meta = imports.gi.Meta;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Mainloop = imports.mainloop;
|
|
const Main = imports.ui.main;
|
|
const MessageTray = imports.ui.messageTray;
|
|
const Util = imports.misc.util;
|
|
|
|
var TRANSLATION_DOMAIN = imports.misc.extensionUtils.getCurrentExtension().metadata['gettext-domain'];
|
|
var SCROLL_TIME = Util.SCROLL_TIME / (Util.SCROLL_TIME > 1 ? 1000 : 1);
|
|
|
|
//Clutter implicit animations are available since 3.34
|
|
//prefer those over Tweener if available
|
|
if (Config.PACKAGE_VERSION < '3.34') {
|
|
var Tweener = imports.ui.tweener;
|
|
}
|
|
|
|
var defineClass = function (classDef) {
|
|
let parentProto = classDef.Extends ? classDef.Extends.prototype : null;
|
|
|
|
if (Config.PACKAGE_VERSION < '3.31.9') {
|
|
if (parentProto && (classDef.Extends.name || classDef.Extends.toString()).indexOf('DashToPanel.') < 0) {
|
|
classDef.callParent = function() {
|
|
let args = Array.prototype.slice.call(arguments);
|
|
let func = args.shift();
|
|
|
|
classDef.Extends.prototype[func].apply(this, args);
|
|
};
|
|
}
|
|
|
|
return new imports.lang.Class(classDef);
|
|
}
|
|
|
|
let isGObject = parentProto instanceof GObject.Object;
|
|
let needsSuper = parentProto && !isGObject;
|
|
let getParentArgs = function(args) {
|
|
let parentArgs = [];
|
|
|
|
(classDef.ParentConstrParams || parentArgs).forEach(p => {
|
|
if (p.constructor === Array) {
|
|
let param = args[p[0]];
|
|
|
|
parentArgs.push(p[1] ? param[p[1]] : param);
|
|
} else {
|
|
parentArgs.push(p);
|
|
}
|
|
});
|
|
|
|
return parentArgs;
|
|
};
|
|
|
|
let C = eval(
|
|
'(class C ' + (needsSuper ? 'extends Object' : '') + ' { ' +
|
|
' constructor(...args) { ' +
|
|
(needsSuper ? 'super(...getParentArgs(args));' : '') +
|
|
(needsSuper || !parentProto ? 'this._init(...args);' : '') +
|
|
' }' +
|
|
' callParent(...args) { ' +
|
|
' let func = args.shift(); ' +
|
|
' if (!(func === \'_init\' && needsSuper))' +
|
|
' super[func](...args); ' +
|
|
' }' +
|
|
'})'
|
|
);
|
|
|
|
if (parentProto) {
|
|
Object.setPrototypeOf(C.prototype, parentProto);
|
|
Object.setPrototypeOf(C, classDef.Extends);
|
|
}
|
|
|
|
Object.defineProperty(C, 'name', { value: classDef.Name });
|
|
Object.keys(classDef)
|
|
.filter(k => classDef.hasOwnProperty(k) && classDef[k] instanceof Function)
|
|
.forEach(k => C.prototype[k] = classDef[k]);
|
|
|
|
if (isGObject) {
|
|
C = GObject.registerClass({ Signals: classDef.Signals || {} }, C);
|
|
}
|
|
|
|
return C;
|
|
};
|
|
|
|
// simplify global signals and function injections handling
|
|
// abstract class
|
|
var BasicHandler = defineClass({
|
|
Name: 'DashToPanel.BasicHandler',
|
|
|
|
_init: function(){
|
|
this._storage = new Object();
|
|
},
|
|
|
|
add: function(/*unlimited 3-long array arguments*/){
|
|
|
|
// convert arguments object to array, concatenate with generic
|
|
let args = [].concat('generic', [].slice.call(arguments));
|
|
// call addWithLabel with ags as if they were passed arguments
|
|
this.addWithLabel.apply(this, args);
|
|
},
|
|
|
|
destroy: function() {
|
|
for( let label in this._storage )
|
|
this.removeWithLabel(label);
|
|
},
|
|
|
|
addWithLabel: function( label /* plus unlimited 3-long array arguments*/) {
|
|
|
|
if(this._storage[label] == undefined)
|
|
this._storage[label] = new Array();
|
|
|
|
// skip first element of the arguments
|
|
for( let i = 1; i < arguments.length; i++ ) {
|
|
let item = this._storage[label];
|
|
let handlers = this._create(arguments[i]);
|
|
|
|
for (let j = 0, l = handlers.length; j < l; ++j) {
|
|
item.push(handlers[j]);
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
removeWithLabel: function(label){
|
|
|
|
if(this._storage[label]) {
|
|
for( let i = 0; i < this._storage[label].length; i++ ) {
|
|
this._remove(this._storage[label][i]);
|
|
}
|
|
|
|
delete this._storage[label];
|
|
}
|
|
},
|
|
|
|
/* Virtual methods to be implemented by subclass */
|
|
// create single element to be stored in the storage structure
|
|
_create: function(item){
|
|
throw new Error('no implementation of _create in ' + this);
|
|
},
|
|
|
|
// correctly delete single element
|
|
_remove: function(item){
|
|
throw new Error('no implementation of _remove in ' + this);
|
|
}
|
|
});
|
|
|
|
// Manage global signals
|
|
var GlobalSignalsHandler = defineClass({
|
|
Name: 'DashToPanel.GlobalSignalsHandler',
|
|
Extends: BasicHandler,
|
|
|
|
_create: function(item) {
|
|
let handlers = [];
|
|
|
|
item[1] = [].concat(item[1]);
|
|
|
|
for (let i = 0, l = item[1].length; i < l; ++i) {
|
|
let object = item[0];
|
|
let event = item[1][i];
|
|
let callback = item[2]
|
|
try {
|
|
let id = object.connect(event, callback);
|
|
|
|
handlers.push([object, id]);
|
|
} catch (e)
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
return handlers;
|
|
},
|
|
|
|
_remove: function(item){
|
|
item[0].disconnect(item[1]);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Manage function injection: both instances and prototype can be overridden
|
|
* and restored
|
|
*/
|
|
var InjectionsHandler = defineClass({
|
|
Name: 'DashToPanel.InjectionsHandler',
|
|
Extends: BasicHandler,
|
|
|
|
_create: function(item) {
|
|
let object = item[0];
|
|
let name = item[1];
|
|
let injectedFunction = item[2];
|
|
let original = object[name];
|
|
|
|
object[name] = injectedFunction;
|
|
return [[object, name, injectedFunction, original]];
|
|
},
|
|
|
|
_remove: function(item) {
|
|
let object = item[0];
|
|
let name = item[1];
|
|
let original = item[3];
|
|
object[name] = original;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Manage timeouts: the added timeouts have their id reset on completion
|
|
*/
|
|
var TimeoutsHandler = defineClass({
|
|
Name: 'DashToPanel.TimeoutsHandler',
|
|
Extends: BasicHandler,
|
|
|
|
_create: function(item) {
|
|
let name = item[0];
|
|
let delay = item[1];
|
|
let timeoutHandler = item[2];
|
|
|
|
this._remove(item);
|
|
|
|
this[name] = Mainloop.timeout_add(delay, () => {
|
|
this[name] = 0;
|
|
timeoutHandler();
|
|
});
|
|
|
|
return [[name]];
|
|
},
|
|
|
|
remove: function(name) {
|
|
this._remove([name])
|
|
},
|
|
|
|
_remove: function(item) {
|
|
let name = item[0];
|
|
|
|
if (this[name]) {
|
|
Mainloop.source_remove(this[name]);
|
|
this[name] = 0;
|
|
}
|
|
},
|
|
|
|
getId: function(name) {
|
|
return this[name] ? this[name] : 0;
|
|
}
|
|
});
|
|
|
|
// This is wrapper to maintain compatibility with GNOME-Shell 3.30+ as well as
|
|
// previous versions.
|
|
var DisplayWrapper = {
|
|
getScreen: function() {
|
|
return global.screen || global.display;
|
|
},
|
|
|
|
getWorkspaceManager: function() {
|
|
return global.screen || global.workspace_manager;
|
|
},
|
|
|
|
getMonitorManager: function() {
|
|
return global.screen || Meta.MonitorManager.get();
|
|
}
|
|
};
|
|
|
|
var getCurrentWorkspace = function() {
|
|
return DisplayWrapper.getWorkspaceManager().get_active_workspace();
|
|
};
|
|
|
|
var getWorkspaceByIndex = function(index) {
|
|
return DisplayWrapper.getWorkspaceManager().get_workspace_by_index(index);
|
|
};
|
|
|
|
var getWorkspaceCount = function() {
|
|
return DisplayWrapper.getWorkspaceManager().n_workspaces;
|
|
};
|
|
|
|
var getStageTheme = function() {
|
|
return St.ThemeContext.get_for_stage(global.stage);
|
|
};
|
|
|
|
var getScaleFactor = function() {
|
|
return getStageTheme().scale_factor || 1;
|
|
};
|
|
|
|
var getAppDisplayViews = function() {
|
|
//gnome-shell 3.38 only has one view and it is now the appDisplay
|
|
return imports.ui.appDisplay._views || [{ view: imports.ui.appDisplay }];
|
|
};
|
|
|
|
var findIndex = function(array, predicate) {
|
|
if (Array.prototype.findIndex) {
|
|
return array.findIndex(predicate);
|
|
}
|
|
|
|
for (let i = 0, l = array.length; i < l; ++i) {
|
|
if (predicate(array[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
var find = function(array, predicate) {
|
|
let index = findIndex(array, predicate);
|
|
|
|
if (index > -1) {
|
|
return array[index];
|
|
}
|
|
};
|
|
|
|
var mergeObjects = function(main, bck) {
|
|
for (var prop in bck) {
|
|
if (!main.hasOwnProperty(prop) && bck.hasOwnProperty(prop)) {
|
|
main[prop] = bck[prop];
|
|
}
|
|
}
|
|
|
|
return main;
|
|
};
|
|
|
|
var hookVfunc = function(proto, symbol, func) {
|
|
if (Gi.hook_up_vfunc_symbol && func) {
|
|
//gjs > 1.53.3
|
|
proto[Gi.hook_up_vfunc_symbol](symbol, func);
|
|
} else {
|
|
//On older gjs, this is how to hook vfunc. It is buggy and can't be used reliably to replace
|
|
//already hooked functions. Since it's our only use for it, disabled for now (and probably forever)
|
|
//Gi.hook_up_vfunc(proto, symbol, func);
|
|
}
|
|
};
|
|
|
|
var wrapActor = function(actor) {
|
|
if (actor) {
|
|
Object.defineProperty(actor, 'actor', {
|
|
value: actor instanceof Clutter.Actor ? actor : actor.actor
|
|
});
|
|
}
|
|
};
|
|
|
|
var getTransformedAllocation = function(actor) {
|
|
if (Config.PACKAGE_VERSION < '3.37') {
|
|
return Shell.util_get_transformed_allocation(actor);
|
|
}
|
|
|
|
let extents = actor.get_transformed_extents();
|
|
let topLeft = extents.get_top_left();
|
|
let bottomRight = extents.get_bottom_right();
|
|
|
|
return { x1: topLeft.x, x2: bottomRight.x, y1: topLeft.y, y2: bottomRight.y };
|
|
};
|
|
|
|
var allocate = function(actor, box, flags, useParent) {
|
|
let allocateObj = useParent ? actor.__proto__ : actor;
|
|
|
|
allocateObj.allocate.apply(actor, getAllocationParams(box, flags));
|
|
};
|
|
|
|
var setAllocation = function(actor, box, flags) {
|
|
actor.set_allocation.apply(actor, getAllocationParams(box, flags));
|
|
};
|
|
|
|
var getAllocationParams = function(box, flags) {
|
|
let params = [box];
|
|
|
|
if (Config.PACKAGE_VERSION < '3.37') {
|
|
params.push(flags);
|
|
}
|
|
|
|
return params;
|
|
};
|
|
|
|
var setClip = function(actor, x, y, width, height) {
|
|
actor.set_clip(0, 0, width, height);
|
|
actor.set_position(x, y);
|
|
actor.set_size(width, height);
|
|
};
|
|
|
|
var addKeybinding = function(key, settings, handler, modes) {
|
|
if (!Main.wm._allowedKeybindings[key]) {
|
|
Main.wm.addKeybinding(
|
|
key,
|
|
settings,
|
|
Meta.KeyBindingFlags.NONE,
|
|
modes || (Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW),
|
|
handler
|
|
);
|
|
}
|
|
};
|
|
|
|
var removeKeybinding = function(key) {
|
|
if (Main.wm._allowedKeybindings[key]) {
|
|
Main.wm.removeKeybinding(key);
|
|
}
|
|
};
|
|
|
|
var getrgbColor = function(color) {
|
|
color = typeof color === 'string' ? Clutter.color_from_string(color)[1] : color;
|
|
|
|
return { red: color.red, green: color.green, blue: color.blue };
|
|
};
|
|
|
|
var getrgbaColor = function(color, alpha, offset) {
|
|
if (alpha <= 0) {
|
|
return 'transparent; ';
|
|
}
|
|
|
|
let rgb = getrgbColor(color);
|
|
|
|
if (offset) {
|
|
['red', 'green', 'blue'].forEach(k => {
|
|
rgb[k] = Math.min(255, Math.max(0, rgb[k] + offset));
|
|
|
|
if (rgb[k] == color[k]) {
|
|
rgb[k] = Math.min(255, Math.max(0, rgb[k] - offset));
|
|
}
|
|
});
|
|
}
|
|
|
|
return 'rgba(' + rgb.red + ',' + rgb.green + ',' + rgb.blue + ',' + (Math.floor(alpha * 100) * 0.01) + '); ' ;
|
|
};
|
|
|
|
var checkIfColorIsBright = function(color) {
|
|
let rgb = getrgbColor(color);
|
|
let brightness = 0.2126 * rgb.red + 0.7152 * rgb.green + 0.0722 * rgb.blue;
|
|
|
|
return brightness > 128;
|
|
};
|
|
|
|
var getMouseScrollDirection = function(event) {
|
|
let direction;
|
|
|
|
switch (event.get_scroll_direction()) {
|
|
case Clutter.ScrollDirection.UP:
|
|
case Clutter.ScrollDirection.LEFT:
|
|
direction = 'up';
|
|
break;
|
|
case Clutter.ScrollDirection.DOWN:
|
|
case Clutter.ScrollDirection.RIGHT:
|
|
direction = 'down';
|
|
break;
|
|
}
|
|
|
|
return direction;
|
|
};
|
|
|
|
var checkIfWindowHasTransient = function(window) {
|
|
let hasTransient;
|
|
|
|
window.foreach_transient(t => !(hasTransient = true));
|
|
|
|
return hasTransient;
|
|
};
|
|
|
|
var activateSiblingWindow = function(windows, direction, startWindow) {
|
|
let windowIndex = windows.indexOf(global.display.focus_window);
|
|
let nextWindowIndex = windowIndex < 0 ?
|
|
startWindow ? windows.indexOf(startWindow) : 0 :
|
|
windowIndex + (direction == 'up' ? 1 : -1);
|
|
|
|
if (nextWindowIndex == windows.length) {
|
|
nextWindowIndex = 0;
|
|
} else if (nextWindowIndex < 0) {
|
|
nextWindowIndex = windows.length - 1;
|
|
}
|
|
|
|
if (windowIndex != nextWindowIndex) {
|
|
Main.activateWindow(windows[nextWindowIndex]);
|
|
}
|
|
};
|
|
|
|
var animateWindowOpacity = function(window, tweenOpts) {
|
|
//there currently is a mutter bug with the windowactor opacity, starting with 3.34
|
|
//https://gitlab.gnome.org/GNOME/mutter/issues/836
|
|
|
|
if (Config.PACKAGE_VERSION > '3.35') {
|
|
//on 3.36, a workaround is to use the windowactor's child for the fade animation
|
|
//this leaves a "shadow" on the desktop, so the windowactor needs to be hidden
|
|
//when the animation is complete
|
|
let visible = tweenOpts.opacity > 0;
|
|
let windowActor = window;
|
|
|
|
window = windowActor.get_first_child() || windowActor;
|
|
|
|
if (!windowActor.visible && visible) {
|
|
window.opacity = 0;
|
|
windowActor.visible = visible;
|
|
}
|
|
|
|
if (!visible) {
|
|
let initialOpacity = window.opacity;
|
|
|
|
tweenOpts.onComplete = () => {
|
|
windowActor.visible = visible;
|
|
window.opacity = initialOpacity;
|
|
};
|
|
}
|
|
} else if (Config.PACKAGE_VERSION > '3.33') {
|
|
//the workaround only works on 3.35+, so on 3.34, let's just hide the
|
|
//window without animation
|
|
return window.visible = (tweenOpts.opacity == 255);
|
|
}
|
|
|
|
animate(window, tweenOpts);
|
|
};
|
|
|
|
var animate = function(actor, options) {
|
|
if (Tweener) {
|
|
return Tweener.addTween(actor, options);
|
|
}
|
|
|
|
//to support both Tweener and Clutter animations, we use Tweener "time"
|
|
//and "delay" properties defined in seconds, as opposed to Clutter animations
|
|
//"duration" and "delay" which are defined in milliseconds
|
|
if (options.delay) {
|
|
options.delay = options.delay * 1000;
|
|
}
|
|
|
|
options.duration = options.time * 1000;
|
|
delete options.time;
|
|
|
|
if (options.transition) {
|
|
//map Tweener easing equations to Clutter animation modes
|
|
options.mode = {
|
|
'easeInCubic': Clutter.AnimationMode.EASE_IN_CUBIC,
|
|
'easeInOutCubic': Clutter.AnimationMode.EASE_IN_OUT_CUBIC,
|
|
'easeInOutQuad': Clutter.AnimationMode.EASE_IN_OUT_QUAD,
|
|
'easeOutQuad': Clutter.AnimationMode.EASE_OUT_QUAD
|
|
}[options.transition] || Clutter.AnimationMode.LINEAR;
|
|
|
|
delete options.transition;
|
|
}
|
|
|
|
let params = [options];
|
|
|
|
if ('value' in options && actor instanceof St.Adjustment) {
|
|
params.unshift(options.value);
|
|
delete options.value;
|
|
}
|
|
|
|
actor.ease.apply(actor, params);
|
|
}
|
|
|
|
var isAnimating = function(actor, prop) {
|
|
if (Tweener) {
|
|
return Tweener.isTweening(actor);
|
|
}
|
|
|
|
return !!actor.get_transition(prop);
|
|
}
|
|
|
|
var stopAnimations = function(actor) {
|
|
if (Tweener) {
|
|
return Tweener.removeTweens(actor);
|
|
}
|
|
|
|
actor.remove_all_transitions();
|
|
}
|
|
|
|
var getIndicators = function(delegate) {
|
|
if (delegate instanceof St.BoxLayout) {
|
|
return delegate;
|
|
}
|
|
|
|
return delegate.indicators;
|
|
}
|
|
|
|
var getPoint = function(coords) {
|
|
if (Config.PACKAGE_VERSION > '3.35.1') {
|
|
return new imports.gi.Graphene.Point(coords);
|
|
}
|
|
|
|
return new Clutter.Point(coords);
|
|
}
|
|
|
|
var getPanelGhost = function() {
|
|
if (!Main.overview._panelGhost) {
|
|
return Main.overview._overview.get_first_child();
|
|
}
|
|
|
|
return Main.overview._panelGhost;
|
|
}
|
|
|
|
var notify = function(text, iconName, action, isTransient) {
|
|
let source = new MessageTray.SystemNotificationSource();
|
|
let notification = new MessageTray.Notification(source, 'Dash to Panel', text);
|
|
let notifyFunc = source.showNotification || source.notify;
|
|
|
|
if (iconName) {
|
|
source.createIcon = function() {
|
|
return new St.Icon({ icon_name: iconName });
|
|
};
|
|
}
|
|
|
|
if (action) {
|
|
if (!(action instanceof Array)) {
|
|
action = [action];
|
|
}
|
|
|
|
action.forEach(a => notification.addAction(a.text, a.func));
|
|
}
|
|
|
|
Main.messageTray.add(source);
|
|
|
|
notification.setTransient(isTransient);
|
|
notifyFunc.call(source, notification);
|
|
};
|
|
|
|
/*
|
|
* This is a copy of the same function in utils.js, but also adjust horizontal scrolling
|
|
* and perform few further cheks on the current value to avoid changing the values when
|
|
* it would be clamp to the current one in any case.
|
|
* Return the amount of shift applied
|
|
*/
|
|
var ensureActorVisibleInScrollView = function(scrollView, actor, fadeSize, onComplete) {
|
|
let vadjustment = scrollView.vscroll.adjustment;
|
|
let hadjustment = scrollView.hscroll.adjustment;
|
|
let [vvalue, vlower, vupper, vstepIncrement, vpageIncrement, vpageSize] = vadjustment.get_values();
|
|
let [hvalue, hlower, hupper, hstepIncrement, hpageIncrement, hpageSize] = hadjustment.get_values();
|
|
|
|
let [hvalue0, vvalue0] = [hvalue, vvalue];
|
|
|
|
let voffset = fadeSize;
|
|
let hoffset = fadeSize;
|
|
|
|
let box = actor.get_allocation_box();
|
|
let y1 = box.y1, y2 = box.y2, x1 = box.x1, x2 = box.x2;
|
|
|
|
let parent = actor.get_parent();
|
|
while (parent != scrollView) {
|
|
if (!parent)
|
|
throw new Error("actor not in scroll view");
|
|
|
|
let box = parent.get_allocation_box();
|
|
y1 += box.y1;
|
|
y2 += box.y1;
|
|
x1 += box.x1;
|
|
x2 += box.x1;
|
|
parent = parent.get_parent();
|
|
}
|
|
|
|
if (y1 < vvalue + voffset)
|
|
vvalue = Math.max(0, y1 - voffset);
|
|
else if (vvalue < vupper - vpageSize && y2 > vvalue + vpageSize - voffset)
|
|
vvalue = Math.min(vupper -vpageSize, y2 + voffset - vpageSize);
|
|
|
|
if (x1 < hvalue + hoffset)
|
|
hvalue = Math.max(0, x1 - hoffset);
|
|
else if (hvalue < hupper - hpageSize && x2 > hvalue + hpageSize - hoffset)
|
|
hvalue = Math.min(hupper - hpageSize, x2 + hoffset - hpageSize);
|
|
|
|
let tweenOpts = {
|
|
time: SCROLL_TIME,
|
|
onComplete: onComplete || (() => {}),
|
|
transition: 'easeOutQuad'
|
|
};
|
|
|
|
if (vvalue !== vvalue0) {
|
|
animate(vadjustment, mergeObjects(tweenOpts, { value: vvalue }));
|
|
}
|
|
|
|
if (hvalue !== hvalue0) {
|
|
animate(hadjustment, mergeObjects(tweenOpts, { value: hvalue }));
|
|
}
|
|
|
|
return [hvalue- hvalue0, vvalue - vvalue0];
|
|
}
|
|
|
|
/**
|
|
* ColorUtils is adapted from https://github.com/micheleg/dash-to-dock
|
|
*/
|
|
var ColorUtils = {
|
|
colorLuminance: function(r, g, b, dlum) {
|
|
// Darken or brighten color by a fraction dlum
|
|
// Each rgb value is modified by the same fraction.
|
|
// Return "#rrggbb" strin
|
|
|
|
let rgbString = '#';
|
|
|
|
rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(r*(1+dlum), 0), 255)), 2);
|
|
rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(g*(1+dlum), 0), 255)), 2);
|
|
rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(b*(1+dlum), 0), 255)), 2);
|
|
|
|
return rgbString;
|
|
},
|
|
|
|
_decimalToHex: function(d, padding) {
|
|
// Convert decimal to an hexadecimal string adding the desired padding
|
|
|
|
let hex = d.toString(16);
|
|
while (hex.length < padding)
|
|
hex = '0'+ hex;
|
|
return hex;
|
|
},
|
|
|
|
HSVtoRGB: function(h, s, v) {
|
|
// Convert hsv ([0-1, 0-1, 0-1]) to rgb ([0-255, 0-255, 0-255]).
|
|
// Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV
|
|
// here with h = [0,1] instead of [0, 360]
|
|
// Accept either (h,s,v) independently or {h:h, s:s, v:v} object.
|
|
// Return {r:r, g:g, b:b} object.
|
|
|
|
if (arguments.length === 1) {
|
|
s = h.s;
|
|
v = h.v;
|
|
h = h.h;
|
|
}
|
|
|
|
let r,g,b;
|
|
let c = v*s;
|
|
let h1 = h*6;
|
|
let x = c*(1 - Math.abs(h1 % 2 - 1));
|
|
let m = v - c;
|
|
|
|
if (h1 <=1)
|
|
r = c + m, g = x + m, b = m;
|
|
else if (h1 <=2)
|
|
r = x + m, g = c + m, b = m;
|
|
else if (h1 <=3)
|
|
r = m, g = c + m, b = x + m;
|
|
else if (h1 <=4)
|
|
r = m, g = x + m, b = c + m;
|
|
else if (h1 <=5)
|
|
r = x + m, g = m, b = c + m;
|
|
else
|
|
r = c + m, g = m, b = x + m;
|
|
|
|
return {
|
|
r: Math.round(r * 255),
|
|
g: Math.round(g * 255),
|
|
b: Math.round(b * 255)
|
|
};
|
|
},
|
|
|
|
RGBtoHSV: function(r, g, b) {
|
|
// Convert rgb ([0-255, 0-255, 0-255]) to hsv ([0-1, 0-1, 0-1]).
|
|
// Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV
|
|
// here with h = [0,1] instead of [0, 360]
|
|
// Accept either (r,g,b) independently or {r:r, g:g, b:b} object.
|
|
// Return {h:h, s:s, v:v} object.
|
|
|
|
if (arguments.length === 1) {
|
|
r = r.r;
|
|
g = r.g;
|
|
b = r.b;
|
|
}
|
|
|
|
let h,s,v;
|
|
|
|
let M = Math.max(r, g, b);
|
|
let m = Math.min(r, g, b);
|
|
let c = M - m;
|
|
|
|
if (c == 0)
|
|
h = 0;
|
|
else if (M == r)
|
|
h = ((g-b)/c) % 6;
|
|
else if (M == g)
|
|
h = (b-r)/c + 2;
|
|
else
|
|
h = (r-g)/c + 4;
|
|
|
|
h = h/6;
|
|
v = M/255;
|
|
if (M !== 0)
|
|
s = c/M;
|
|
else
|
|
s = 0;
|
|
|
|
return {h: h, s: s, v: v};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* DominantColorExtractor is adapted from https://github.com/micheleg/dash-to-dock
|
|
*/
|
|
let themeLoader = null;
|
|
let iconCacheMap = new Map();
|
|
const MAX_CACHED_ITEMS = 1000;
|
|
const BATCH_SIZE_TO_DELETE = 50;
|
|
const DOMINANT_COLOR_ICON_SIZE = 64;
|
|
|
|
var DominantColorExtractor = defineClass({
|
|
Name: 'DashToPanel.DominantColorExtractor',
|
|
|
|
_init: function(app){
|
|
this._app = app;
|
|
},
|
|
|
|
/**
|
|
* Try to get the pixel buffer for the current icon, if not fail gracefully
|
|
*/
|
|
_getIconPixBuf: function() {
|
|
let iconTexture = this._app.create_icon_texture(16);
|
|
|
|
if (themeLoader === null) {
|
|
let ifaceSettings = new Gio.Settings({ schema: "org.gnome.desktop.interface" });
|
|
|
|
themeLoader = new Gtk.IconTheme(),
|
|
themeLoader.set_custom_theme(ifaceSettings.get_string('icon-theme')); // Make sure the correct theme is loaded
|
|
}
|
|
|
|
// Unable to load the icon texture, use fallback
|
|
if (iconTexture instanceof St.Icon === false) {
|
|
return null;
|
|
}
|
|
|
|
iconTexture = iconTexture.get_gicon();
|
|
|
|
// Unable to load the icon texture, use fallback
|
|
if (iconTexture === null) {
|
|
return null;
|
|
}
|
|
|
|
if (iconTexture instanceof Gio.FileIcon) {
|
|
// Use GdkPixBuf to load the pixel buffer from the provided file path
|
|
return GdkPixbuf.Pixbuf.new_from_file(iconTexture.get_file().get_path());
|
|
}
|
|
|
|
// Get the pixel buffer from the icon theme
|
|
let icon_info = themeLoader.lookup_icon(iconTexture.get_names()[0], DOMINANT_COLOR_ICON_SIZE, 0);
|
|
if (icon_info !== null)
|
|
return icon_info.load_icon();
|
|
else
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* The backlight color choosing algorithm was mostly ported to javascript from the
|
|
* Unity7 C++ source of Canonicals:
|
|
* https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp
|
|
* so it more or less works the same way.
|
|
*/
|
|
_getColorPalette: function() {
|
|
if (iconCacheMap.get(this._app.get_id())) {
|
|
// We already know the answer
|
|
return iconCacheMap.get(this._app.get_id());
|
|
}
|
|
|
|
let pixBuf = this._getIconPixBuf();
|
|
if (pixBuf == null)
|
|
return null;
|
|
|
|
let pixels = pixBuf.get_pixels(),
|
|
offset = 0;
|
|
|
|
let total = 0,
|
|
rTotal = 0,
|
|
gTotal = 0,
|
|
bTotal = 0;
|
|
|
|
let resample_y = 1,
|
|
resample_x = 1;
|
|
|
|
// Resampling of large icons
|
|
// We resample icons larger than twice the desired size, as the resampling
|
|
// to a size s
|
|
// DOMINANT_COLOR_ICON_SIZE < s < 2*DOMINANT_COLOR_ICON_SIZE,
|
|
// most of the case exactly DOMINANT_COLOR_ICON_SIZE as the icon size is tipycally
|
|
// a multiple of it.
|
|
let width = pixBuf.get_width();
|
|
let height = pixBuf.get_height();
|
|
|
|
// Resample
|
|
if (height >= 2* DOMINANT_COLOR_ICON_SIZE)
|
|
resample_y = Math.floor(height/DOMINANT_COLOR_ICON_SIZE);
|
|
|
|
if (width >= 2* DOMINANT_COLOR_ICON_SIZE)
|
|
resample_x = Math.floor(width/DOMINANT_COLOR_ICON_SIZE);
|
|
|
|
if (resample_x !==1 || resample_y !== 1)
|
|
pixels = this._resamplePixels(pixels, resample_x, resample_y);
|
|
|
|
// computing the limit outside the for (where it would be repeated at each iteration)
|
|
// for performance reasons
|
|
let limit = pixels.length;
|
|
for (let offset = 0; offset < limit; offset+=4) {
|
|
let r = pixels[offset],
|
|
g = pixels[offset + 1],
|
|
b = pixels[offset + 2],
|
|
a = pixels[offset + 3];
|
|
|
|
let saturation = (Math.max(r,g, b) - Math.min(r,g, b));
|
|
let relevance = 0.1 * 255 * 255 + 0.9 * a * saturation;
|
|
|
|
rTotal += r * relevance;
|
|
gTotal += g * relevance;
|
|
bTotal += b * relevance;
|
|
|
|
total += relevance;
|
|
}
|
|
|
|
total = total * 255;
|
|
|
|
let r = rTotal / total,
|
|
g = gTotal / total,
|
|
b = bTotal / total;
|
|
|
|
let hsv = ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255);
|
|
|
|
if (hsv.s > 0.15)
|
|
hsv.s = 0.65;
|
|
hsv.v = 0.90;
|
|
|
|
let rgb = ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v);
|
|
|
|
// Cache the result.
|
|
let backgroundColor = {
|
|
lighter: ColorUtils.colorLuminance(rgb.r, rgb.g, rgb.b, 0.2),
|
|
original: ColorUtils.colorLuminance(rgb.r, rgb.g, rgb.b, 0),
|
|
darker: ColorUtils.colorLuminance(rgb.r, rgb.g, rgb.b, -0.5)
|
|
};
|
|
|
|
if (iconCacheMap.size >= MAX_CACHED_ITEMS) {
|
|
//delete oldest cached values (which are in order of insertions)
|
|
let ctr=0;
|
|
for (let key of iconCacheMap.keys()) {
|
|
if (++ctr > BATCH_SIZE_TO_DELETE)
|
|
break;
|
|
iconCacheMap.delete(key);
|
|
}
|
|
}
|
|
|
|
iconCacheMap.set(this._app.get_id(), backgroundColor);
|
|
|
|
return backgroundColor;
|
|
},
|
|
|
|
/**
|
|
* Downsample large icons before scanning for the backlight color to
|
|
* improve performance.
|
|
*
|
|
* @param pixBuf
|
|
* @param pixels
|
|
* @param resampleX
|
|
* @param resampleY
|
|
*
|
|
* @return [];
|
|
*/
|
|
_resamplePixels: function (pixels, resampleX, resampleY) {
|
|
let resampledPixels = [];
|
|
// computing the limit outside the for (where it would be repeated at each iteration)
|
|
// for performance reasons
|
|
let limit = pixels.length / (resampleX * resampleY) / 4;
|
|
for (let i = 0; i < limit; i++) {
|
|
let pixel = i * resampleX * resampleY;
|
|
|
|
resampledPixels.push(pixels[pixel * 4]);
|
|
resampledPixels.push(pixels[pixel * 4 + 1]);
|
|
resampledPixels.push(pixels[pixel * 4 + 2]);
|
|
resampledPixels.push(pixels[pixel * 4 + 3]);
|
|
}
|
|
|
|
return resampledPixels;
|
|
}
|
|
|
|
});
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Check if an app exists in the system.
|
|
*/
|
|
var checkedCommandsMap = new Map();
|
|
|
|
function checkIfCommandExists(app) {
|
|
let answer = checkedCommandsMap.get(app);
|
|
if (answer === undefined) {
|
|
// Command is a shell built in, use shell to call it.
|
|
// Quotes around app value are important. They let command operate
|
|
// on the whole value, instead of having shell interpret it.
|
|
let cmd = "sh -c 'command -v \"" + app + "\"'";
|
|
try {
|
|
let out = GLib.spawn_command_line_sync(cmd);
|
|
// out contains 1: stdout, 2: stderr, 3: exit code
|
|
answer = out[3] == 0;
|
|
} catch (ex) {
|
|
answer = false;
|
|
}
|
|
|
|
checkedCommandsMap.set(app, answer);
|
|
}
|
|
return answer;
|
|
}
|