/* * This file is part of the Dash-To-Panel extension for Gnome 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * * Credits: * This file is based on code from the Dash to Dock extension by micheleg * 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; }