Files
dash-to-panel/utils.js

890 lines
25 KiB
JavaScript
Raw Normal View History

/*
* 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.
*/
import Clutter from 'gi://Clutter';
2024-09-19 21:01:37 -04:00
import Cogl from 'gi://Cogl';
import GdkPixbuf from 'gi://GdkPixbuf';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
2023-08-14 01:02:59 +02:00
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
2023-09-21 23:10:07 -07:00
const SCROLL_TIME = Util.SCROLL_TIME / (Util.SCROLL_TIME > 1 ? 1000 : 1);
2019-03-10 11:11:43 -04:00
// simplify global signals and function injections handling
// abstract class
export const BasicHandler = class {
2022-04-01 22:32:15 -04:00
constructor() {
this._storage = new Object();
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
add(/*unlimited 3-long array arguments*/){
// convert arguments object to array, concatenate with generic
2020-02-15 14:06:56 -05:00
let args = [].concat('generic', [].slice.call(arguments));
// call addWithLabel with ags as if they were passed arguments
this.addWithLabel.apply(this, args);
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
destroy() {
for( let label in this._storage )
this.removeWithLabel(label);
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
addWithLabel( 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]);
}
}
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
removeWithLabel(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];
}
2022-04-01 22:32:15 -04:00
}
/* Virtual methods to be implemented by subclass */
// create single element to be stored in the storage structure
2022-04-01 22:32:15 -04:00
_create(item){
throw new Error('no implementation of _create in ' + this);
2022-04-01 22:32:15 -04:00
}
// correctly delete single element
2022-04-01 22:32:15 -04:00
_remove(item){
throw new Error('no implementation of _remove in ' + this);
}
2022-04-01 22:32:15 -04:00
}
// Manage global signals
export const GlobalSignalsHandler = class extends BasicHandler {
2022-04-01 22:32:15 -04:00
_create(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]
Update to Gnome 40 (#1303) * 1 * renamed ui.viewSelector to ui.searchController * add arrowIcon null check * add object null check * add _showAppsButton null check * fixed _workspacesDisplay * removed shadow-type properties * removed packing from ui * renamed margin_left/right to margin_start/end * renamed GtkRadioButton to GtkToggleButton, removed draw_indicator and image_position * removed xalign from buttons, stock -> icon_name, GtkFileChooserButton -> BtkButton (todo connect a GtkFileChooserNative), removed format-value (todo connect GtkScaleFormatValueFunc callback) * removed events, relief * comment arrowIcon.set_icon_name and _hbox.get_last_child out * called gtk4-builder-tool simplify --3to4 on Settings.ui * fix _workspaceDisplay * revert Settings.ui back to old gtk3 version * removed _builder.connect_signals_full and added a BuilderScope instead, use class specific add functions, removed show_all for widgets, updated margins* for GtkGrid * add try catch aroung object.connect * fixed _searchEntry path * disabled _newUpdateWorkspacesViews temporarily, because it is very buggy right now * fixed _searchEntry path (this time for real?) * dialog.show_all() -> dialog.show(), widget.get_toplevel() -> widget.get_root(), fixed adjustScrollableHeight (no longer crashes, still does not work correctly) * updated GtkGrid.attach calls * added Gtk.FileChooserNative for show_applications_icon_file_filebutton * added packing again to ui (commented) and also added layout row/column/width/height in exchange for top_attach/left_attach/colspan/rowspan * renamed colspan/rowspan to column-span/row-span * removed all packaing left_attach/top_attach * updated adjustScrollableHeight so the dialog will have a reasonable size again * limit shell-version to 40 since it is not backwards compatible right now * called _updateBackgrounds in layoutManager ,fixed overview <-> _overview typo? * pass Settings instead of Settings._settings to BuilderScope * workaround for showAppsIcon (_showAppsButton seems to be no longer be available) * replaced newlines in the ui file with the newline unicode symbol, so code block collapsing works peroperly * moved the scrolled window directly into each notebook tab, so the tab headers are always visible * fixed taskbarListBox children loops * commented non working elements out (panel on left/right, import/export settings, update) * renamed Settings to Preferences to avoid confusion with dconf settings object * updated this.widget to this.notebook * fixed dialogs size and expand * fixed window preview options having too many columns * removed all packing with expand/fill/position (already uncommented) * removed menu arrows since they are no longer recommended for gnome40 * removed adjustScrollableHeight, default sizes are acceptable * updated ui required version to gtk 4.0 * updated path to dash.showAppsButton * fixes for showAppsButton * fixed clickToExit * fixed import/export settings * added disable show overview on startup option * removed old show apps animation * fixed panel on left/right side * fixed scroll on volume icon no longer will scrolling on the volume icon switch workspaces as well * commented some setFocusedMonitor support out, caused issues on Xorg * removed oldDash * updated hide overview on startup, no longer closes overview after suspend etc * removed _newOverviewRelayout * removed _newUpdateWorkspacesViews * commented setFocusedMonitor out completly, because show overview on non primary monitor does not work properly for now * fixed typo * fixed merging error * check if func is valid in hookVfunc * updated ui to gtk4, fixed merging error * fixed layout for apply changes to all monitors * fix behaviour tab and app icon margin/padding grid * fix animate launching new windows grid row * fixed about tab Co-authored-by: Cole Gerdemann <corvettecole@gmail.com>
2021-05-08 15:07:25 +02:00
try {
let id = object.connect(event, callback);
Update to Gnome 40 (#1303) * 1 * renamed ui.viewSelector to ui.searchController * add arrowIcon null check * add object null check * add _showAppsButton null check * fixed _workspacesDisplay * removed shadow-type properties * removed packing from ui * renamed margin_left/right to margin_start/end * renamed GtkRadioButton to GtkToggleButton, removed draw_indicator and image_position * removed xalign from buttons, stock -> icon_name, GtkFileChooserButton -> BtkButton (todo connect a GtkFileChooserNative), removed format-value (todo connect GtkScaleFormatValueFunc callback) * removed events, relief * comment arrowIcon.set_icon_name and _hbox.get_last_child out * called gtk4-builder-tool simplify --3to4 on Settings.ui * fix _workspaceDisplay * revert Settings.ui back to old gtk3 version * removed _builder.connect_signals_full and added a BuilderScope instead, use class specific add functions, removed show_all for widgets, updated margins* for GtkGrid * add try catch aroung object.connect * fixed _searchEntry path * disabled _newUpdateWorkspacesViews temporarily, because it is very buggy right now * fixed _searchEntry path (this time for real?) * dialog.show_all() -> dialog.show(), widget.get_toplevel() -> widget.get_root(), fixed adjustScrollableHeight (no longer crashes, still does not work correctly) * updated GtkGrid.attach calls * added Gtk.FileChooserNative for show_applications_icon_file_filebutton * added packing again to ui (commented) and also added layout row/column/width/height in exchange for top_attach/left_attach/colspan/rowspan * renamed colspan/rowspan to column-span/row-span * removed all packaing left_attach/top_attach * updated adjustScrollableHeight so the dialog will have a reasonable size again * limit shell-version to 40 since it is not backwards compatible right now * called _updateBackgrounds in layoutManager ,fixed overview <-> _overview typo? * pass Settings instead of Settings._settings to BuilderScope * workaround for showAppsIcon (_showAppsButton seems to be no longer be available) * replaced newlines in the ui file with the newline unicode symbol, so code block collapsing works peroperly * moved the scrolled window directly into each notebook tab, so the tab headers are always visible * fixed taskbarListBox children loops * commented non working elements out (panel on left/right, import/export settings, update) * renamed Settings to Preferences to avoid confusion with dconf settings object * updated this.widget to this.notebook * fixed dialogs size and expand * fixed window preview options having too many columns * removed all packing with expand/fill/position (already uncommented) * removed menu arrows since they are no longer recommended for gnome40 * removed adjustScrollableHeight, default sizes are acceptable * updated ui required version to gtk 4.0 * updated path to dash.showAppsButton * fixes for showAppsButton * fixed clickToExit * fixed import/export settings * added disable show overview on startup option * removed old show apps animation * fixed panel on left/right side * fixed scroll on volume icon no longer will scrolling on the volume icon switch workspaces as well * commented some setFocusedMonitor support out, caused issues on Xorg * removed oldDash * updated hide overview on startup, no longer closes overview after suspend etc * removed _newOverviewRelayout * removed _newUpdateWorkspacesViews * commented setFocusedMonitor out completly, because show overview on non primary monitor does not work properly for now * fixed typo * fixed merging error * check if func is valid in hookVfunc * updated ui to gtk4, fixed merging error * fixed layout for apply changes to all monitors * fix behaviour tab and app icon margin/padding grid * fix animate launching new windows grid row * fixed about tab Co-authored-by: Cole Gerdemann <corvettecole@gmail.com>
2021-05-08 15:07:25 +02:00
handlers.push([object, id]);
} catch (e)
{
}
}
return handlers;
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
_remove(item){
item[0].disconnect(item[1]);
}
2022-04-01 22:32:15 -04:00
};
/**
* Manage function injection: both instances and prototype can be overridden
* and restored
*/
export const InjectionsHandler = class extends BasicHandler {
2022-04-01 22:32:15 -04:00
_create(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]];
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
_remove(item) {
let object = item[0];
let name = item[1];
let original = item[3];
object[name] = original;
}
2022-04-01 22:32:15 -04:00
};
/**
* Manage timeouts: the added timeouts have their id reset on completion
*/
export const TimeoutsHandler = class extends BasicHandler {
2022-04-01 22:32:15 -04:00
_create(item) {
let name = item[0];
let delay = item[1];
let timeoutHandler = item[2];
this._remove(item);
2023-09-21 19:22:31 -07:00
this[name] = GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => {
this[name] = 0;
timeoutHandler();
2023-09-21 19:22:31 -07:00
return GLib.SOURCE_REMOVE;
});
return [[name]];
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
remove(name) {
this._remove([name])
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
_remove(item) {
let name = item[0];
if (this[name]) {
2023-09-21 19:22:31 -07:00
GLib.Source.remove(this[name]);
this[name] = 0;
}
2022-04-01 22:32:15 -04:00
}
2022-04-01 22:32:15 -04:00
getId(name) {
return this[name] ? this[name] : 0;
}
2022-04-01 22:32:15 -04:00
};
// This is wrapper to maintain compatibility with GNOME-Shell 3.30+ as well as
// previous versions.
export const DisplayWrapper = {
2022-04-01 22:32:15 -04:00
getScreen() {
return global.screen || global.display;
},
2022-04-01 22:32:15 -04:00
getWorkspaceManager() {
return global.screen || global.workspace_manager;
},
2022-04-01 22:32:15 -04:00
getMonitorManager() {
2023-02-18 13:55:39 +01:00
return global.screen || global.backend.get_monitor_manager();
}
2019-01-14 00:09:28 -05:00
};
let unredirectEnabled = true
export const setDisplayUnredirect = (enable) => {
if (enable && !unredirectEnabled)
Meta.enable_unredirect_for_display(global.display);
else if (!enable && unredirectEnabled)
Meta.disable_unredirect_for_display(global.display);
unredirectEnabled = enable;
};
export const getSystemMenuInfo = function() {
2022-08-25 08:33:22 -04:00
return {
name: 'quickSettings',
2023-08-20 19:16:50 +02:00
constructor: Main.panel.statusArea.quickSettings.constructor
2022-08-25 08:33:22 -04:00
};
}
export const getCurrentWorkspace = function() {
2019-05-25 11:08:46 -04:00
return DisplayWrapper.getWorkspaceManager().get_active_workspace();
};
export const getWorkspaceByIndex = function(index) {
2019-05-25 11:08:46 -04:00
return DisplayWrapper.getWorkspaceManager().get_workspace_by_index(index);
};
export const getWorkspaceCount = function() {
2019-05-25 11:08:46 -04:00
return DisplayWrapper.getWorkspaceManager().n_workspaces;
};
export const getStageTheme = function() {
2020-05-03 21:27:40 -04:00
return St.ThemeContext.get_for_stage(global.stage);
};
export const getScaleFactor = function() {
2020-05-03 21:27:40 -04:00
return getStageTheme().scale_factor || 1;
};
export const findIndex = function(array, predicate) {
2022-04-03 11:40:22 -04:00
if (array) {
if (Array.prototype.findIndex) {
return array.findIndex(predicate);
}
2019-05-19 10:42:22 -04:00
2022-04-03 11:40:22 -04:00
for (let i = 0, l = array.length; i < l; ++i) {
if (predicate(array[i])) {
return i;
}
2019-05-19 10:42:22 -04:00
}
}
return -1;
};
export const find = function(array, predicate) {
let index = findIndex(array, predicate);
if (index > -1) {
return array[index];
}
};
export const mergeObjects = function(main, bck) {
2023-09-21 23:10:07 -07:00
for (const prop in bck) {
2019-05-14 20:13:01 -04:00
if (!main.hasOwnProperty(prop) && bck.hasOwnProperty(prop)) {
main[prop] = bck[prop];
}
}
return main;
};
export const getTrackedActorData = (actor) => {
let trackedIndex = Main.layoutManager._findActor(actor);
if (trackedIndex >= 0)
return Main.layoutManager._trackedActors[trackedIndex]
}
export const getTransformedAllocation = function(actor) {
2020-08-22 10:18:31 -04:00
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 };
};
export const setClip = function(actor, x, y, width, height) {
2019-06-23 23:56:49 -04:00
actor.set_clip(0, 0, width, height);
actor.set_position(x, y);
actor.set_size(width, height);
};
export const 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
);
}
};
export const removeKeybinding = function(key) {
if (Main.wm._allowedKeybindings[key]) {
Main.wm.removeKeybinding(key);
}
};
2019-05-19 17:51:07 -04:00
export const getrgbColor = function(color) {
2024-09-19 21:01:37 -04:00
color = typeof color === 'string' ? ColorUtils.color_from_string(color)[1] : color;
2020-08-02 10:49:43 -04:00
return { red: color.red, green: color.green, blue: color.blue };
};
export const getrgbaColor = function(color, alpha, offset) {
2019-05-19 17:51:07 -04:00
if (alpha <= 0) {
return 'transparent; ';
}
2020-08-02 10:49:43 -04:00
let rgb = getrgbColor(color);
2019-05-19 17:51:07 -04:00
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) + '); ' ;
};
2019-06-07 20:02:51 -04:00
export const checkIfColorIsBright = function(color) {
2020-08-02 10:49:43 -04:00
let rgb = getrgbColor(color);
let brightness = 0.2126 * rgb.red + 0.7152 * rgb.green + 0.0722 * rgb.blue;
return brightness > 128;
};
export const 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;
};
export const checkIfWindowHasTransient = function(window) {
let hasTransient;
window.foreach_transient(t => !(hasTransient = true));
return hasTransient;
};
export const 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]);
}
2019-08-28 22:07:02 -04:00
};
export const animateWindowOpacity = function(window, tweenOpts) {
2020-04-30 00:30:52 -04:00
//there currently is a mutter bug with the windowactor opacity, starting with 3.34
//https://gitlab.gnome.org/GNOME/mutter/issues/836
2022-04-13 22:29:57 -04:00
//since 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;
2022-04-24 09:48:57 -04:00
let initialOpacity = window.opacity;
2022-04-13 22:29:57 -04:00
window = windowActor.get_first_child() || windowActor;
if (!windowActor.visible && visible) {
2022-04-13 22:29:57 -04:00
window.opacity = 0;
windowActor.visible = visible;
2022-04-24 09:48:57 -04:00
tweenOpts.opacity = Math.min(initialOpacity, tweenOpts.opacity);
2022-04-13 22:29:57 -04:00
}
if (!visible) {
tweenOpts.onComplete = () => {
windowActor.visible = visible;
window.opacity = initialOpacity;
};
}
animate(window, tweenOpts);
};
export const animate = function(actor, options) {
2022-04-13 22:29:57 -04:00
//the original animations used Tweener instead of Clutter animations, so we
//use "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);
}
export const isAnimating = function(actor, prop) {
return !!actor.get_transition(prop);
}
export const stopAnimations = function(actor) {
actor.remove_all_transitions();
}
export const getIndicators = function(delegate) {
if (delegate instanceof St.BoxLayout) {
return delegate;
}
return delegate.indicators;
}
export const getPoint = function(coords) {
2023-08-14 01:02:59 +02:00
return new Graphene.Point(coords);
}
export const notify = function(text, iconName, action, isTransient) {
2019-08-28 22:07:02 -04:00
let source = new MessageTray.SystemNotificationSource();
2019-08-29 22:07:35 -04:00
let notification = new MessageTray.Notification(source, 'Dash to Panel', text);
2020-04-05 12:03:31 -04:00
let notifyFunc = source.showNotification || source.notify;
2019-08-28 22:07:02 -04:00
if (iconName) {
source.createIcon = function() {
return new St.Icon({ icon_name: iconName });
};
}
if (action) {
2019-08-29 22:07:35 -04:00
if (!(action instanceof Array)) {
action = [action];
}
action.forEach(a => notification.addAction(a.text, a.func));
2019-08-28 22:07:02 -04:00
}
Main.messageTray.add(source);
2019-08-29 22:07:35 -04:00
notification.setTransient(isTransient);
2020-04-05 12:03:31 -04:00
notifyFunc.call(source, notification);
2019-08-28 22:07:02 -04:00
};
2019-06-07 20:02:51 -04:00
/*
* 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
*/
export const ensureActorVisibleInScrollView = function(scrollView, actor, fadeSize, onComplete) {
const vadjustment = scrollView.vadjustment;
const hadjustment = scrollView.hadjustment;
2019-06-07 20:02:51 -04:00
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 = {
2019-10-12 08:29:14 -04:00
time: SCROLL_TIME,
onComplete: onComplete || (() => {}),
transition: 'easeOutQuad'
};
2019-06-07 20:02:51 -04:00
if (vvalue !== vvalue0) {
animate(vadjustment, mergeObjects(tweenOpts, { value: vvalue }));
2019-06-07 20:02:51 -04:00
}
if (hvalue !== hvalue0) {
animate(hadjustment, mergeObjects(tweenOpts, { value: hvalue }));
2019-06-07 20:02:51 -04:00
}
return [hvalue- hvalue0, vvalue - vvalue0];
}
/**
* ColorUtils is adapted from https://github.com/micheleg/dash-to-dock
*/
2024-09-19 21:01:37 -04:00
let colorNs = Clutter.Color ? Clutter : Cogl
export const ColorUtils = {
2024-09-19 21:01:37 -04:00
color_from_string: colorNs.color_from_string,
Color: colorNs.Color,
2022-04-01 22:32:15 -04:00
colorLuminance(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;
},
2022-04-01 22:32:15 -04:00
_decimalToHex(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;
},
2022-04-01 22:32:15 -04:00
HSVtoRGB(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)
};
},
2022-04-01 22:32:15 -04:00
RGBtoHSV(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;
export const DominantColorExtractor = class {
2022-04-01 22:32:15 -04:00
constructor(app){
this._app = app;
2022-04-01 22:32:15 -04:00
}
/**
* Try to get the pixel buffer for the current icon, if not fail gracefully
*/
2022-04-01 22:32:15 -04:00
_getIconPixBuf() {
let iconTexture = this._app.create_icon_texture(16);
if (themeLoader === null) {
themeLoader = new St.IconTheme();
}
// 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
if (iconTexture instanceof Gio.ThemedIcon) {
let icon_info = themeLoader.lookup_icon(iconTexture.get_names()[0],
DOMINANT_COLOR_ICON_SIZE, 0);
2023-03-15 23:08:45 -04:00
if (icon_info !== null) {
return icon_info.load_icon();
2023-03-15 23:08:45 -04:00
}
}
return null;
2022-04-01 22:32:15 -04:00
}
/**
* 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.
*/
2022-04-01 22:32:15 -04:00
_getColorPalette() {
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;
2022-04-01 22:32:15 -04:00
}
/**
* Downsample large icons before scanning for the backlight color to
* improve performance.
*
* @param pixBuf
* @param pixels
* @param resampleX
* @param resampleY
*
* @return [];
*/
2022-04-01 22:32:15 -04:00
_resamplePixels(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;
}
2022-04-01 22:32:15 -04:00
};
export const 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);
2023-09-21 23:10:07 -07:00
const leftRadius = isRoundLeft ? height / 2.0 : 0.0;
const 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();
}