const Cogl = imports.gi.Cogl; const Cairo = imports.cairo; const Clutter = imports.gi.Clutter; const GdkPixbuf = imports.gi.GdkPixbuf const Gio = imports.gi.Gio; const Gtk = imports.gi.Gtk; const Pango = imports.gi.Pango; const Shell = imports.gi.Shell; const St = imports.gi.St; const Util = imports.misc.util; const Me = imports.misc.extensionUtils.getCurrentExtension(); const Docking = Me.imports.docking; const Utils = Me.imports.utils; let tracker = Shell.WindowTracker.get_default(); const RunningIndicatorStyle = { DEFAULT: 0, DOTS: 1, SQUARES: 2, DASHES: 3, SEGMENTED: 4, SOLID: 5, CILIORA: 6, METRO: 7 }; const MAX_WINDOWS_CLASSES = 4; /* * This is the main indicator class to be used. The desired bahviour is * obtained by composing the desired classes below based on the settings. * */ var AppIconIndicator = class DashToDock_AppIconIndicator { constructor(source) { this._indicators = []; // Unity indicators always enabled for now let unityIndicator = new UnityIndicator(source); this._indicators.push(unityIndicator); // Choose the style for the running indicators let runningIndicator = null; let runningIndicatorStyle; let settings = Docking.DockManager.settings; if (settings.get_boolean('apply-custom-theme' )) { runningIndicatorStyle = RunningIndicatorStyle.DOTS; } else { runningIndicatorStyle = settings.get_enum('running-indicator-style'); } switch (runningIndicatorStyle) { case RunningIndicatorStyle.DEFAULT: runningIndicator = new RunningIndicatorDefault(source); break; case RunningIndicatorStyle.DOTS: runningIndicator = new RunningIndicatorDots(source); break; case RunningIndicatorStyle.SQUARES: runningIndicator = new RunningIndicatorSquares(source); break; case RunningIndicatorStyle.DASHES: runningIndicator = new RunningIndicatorDashes(source); break; case RunningIndicatorStyle.SEGMENTED: runningIndicator = new RunningIndicatorSegmented(source); break; case RunningIndicatorStyle.SOLID: runningIndicator = new RunningIndicatorSolid(source); break; case RunningIndicatorStyle.CILIORA: runningIndicator = new RunningIndicatorCiliora(source); break; case RunningIndicatorStyle.METRO: runningIndicator = new RunningIndicatorMetro(source); break; default: runningIndicator = new RunningIndicatorBase(source); } this._indicators.push(runningIndicator); } update() { for (let i=0; i { this._signalsHandler.destroy(); }); } update() { } destroy() { this._source.actor.disconnect(this._sourceDestroyId); this._signalsHandler.destroy(); } }; /* * A base indicator class for running style, from which all other EunningIndicators should derive, * providing some basic methods, variables definitions and their update, css style classes handling. * */ var RunningIndicatorBase = class DashToDock_RunningIndicatorBase extends IndicatorBase { constructor(source) { super(source) this._side = Utils.getPosition(); this._nWindows = 0; this._dominantColorExtractor = new DominantColorExtractor(this._source.app); // These statuses take into account the workspace/monitor isolation this._isFocused = false; this._isRunning = false; } update() { // Limit to 1 to MAX_WINDOWS_CLASSES windows classes this._nWindows = Math.min(this._source.getInterestingWindows().length, MAX_WINDOWS_CLASSES); // We need to check the number of windows, as the focus might be // happening on another monitor if using isolation if (tracker.focus_app == this._source.app && this._nWindows > 0) this._isFocused = true; else this._isFocused = false; // In the case of workspace isolation, we need to hide the dots of apps with // no windows in the current workspace if ((this._source.app.state != Shell.AppState.STOPPED || this._source.isLocation()) && this._nWindows > 0) this._isRunning = true; else this._isRunning = false; this._updateCounterClass(); this._updateFocusClass(); this._updateDefaultDot(); } _updateCounterClass() { for (let i = 1; i <= MAX_WINDOWS_CLASSES; i++) { let className = 'running' + i; if (i != this._nWindows) this._source.actor.remove_style_class_name(className); else this._source.actor.add_style_class_name(className); } } _updateFocusClass() { if (this._isFocused) this._source.actor.add_style_class_name('focused'); else this._source.actor.remove_style_class_name('focused'); } _updateDefaultDot() { if (this._isRunning) this._source._dot.show(); else this._source._dot.hide(); } _hideDefaultDot() { // I use opacity to hide the default dot because the show/hide function // are used by the parent class. this._source._dot.opacity = 0; } _restoreDefaultDot() { this._source._dot.opacity = 255; } _enableBacklight() { let colorPalette = this._dominantColorExtractor._getColorPalette(); // Fallback if (colorPalette === null) { this._source._iconContainer.set_style( 'border-radius: 5px;' + 'background-gradient-direction: vertical;' + 'background-gradient-start: #e0e0e0;' + 'background-gradient-end: darkgray;' ); return; } this._source._iconContainer.set_style( 'border-radius: 5px;' + 'background-gradient-direction: vertical;' + 'background-gradient-start: ' + colorPalette.original + ';' + 'background-gradient-end: ' + colorPalette.darker + ';' ); } _disableBacklight() { this._source._iconContainer.set_style(null); } destroy() { this._disableBacklight(); // Remove glossy background if the children still exists if (this._source._iconContainer.get_children().length > 1) this._source._iconContainer.get_children()[1].set_style(null); this._restoreDefaultDot(); super.destroy(); } }; // We add a css class so third parties themes can limit their indicaor customization // to the case we do nothing var RunningIndicatorDefault = class DashToDock_RunningIndicatorDefault extends RunningIndicatorBase { constructor(source) { super(source); this._source.actor.add_style_class_name('default'); } destory() { this._source.actor.remove_style_class_name('default'); super.destroy(); } }; var RunningIndicatorDots = class DashToDock_RunningIndicatorDots extends RunningIndicatorBase { constructor(source) { super(source) this._hideDefaultDot(); this._area = new St.DrawingArea({x_expand: true, y_expand: true}); // We draw for the bottom case and rotate the canvas for other placements //set center of rotatoins to the center this._area.set_pivot_point(0.5, 0.5); // prepare transformation matrix let m = new Cogl.Matrix(); m.init_identity(); switch (this._side) { case St.Side.TOP: m.xx = -1; m.rotate(180, 0, 0, 1); break case St.Side.BOTTOM: // nothing break; case St.Side.LEFT: m.yy = -1; m.rotate(90, 0, 0, 1); break; case St.Side.RIGHT: m.rotate(-90, 0, 0, 1); break } this._area.set_transform(m); this._area.connect('repaint', this._updateIndicator.bind(this)); this._source._iconContainer.add_child(this._area); let keys = ['custom-theme-running-dots-color', 'custom-theme-running-dots-border-color', 'custom-theme-running-dots-border-width', 'custom-theme-customize-running-dots', 'unity-backlit-items', 'running-indicator-dominant-color']; keys.forEach(function(key) { this._signalsHandler.add([ Docking.DockManager.settings, 'changed::' + key, this.update.bind(this) ]); }, this); // Apply glossy background // TODO: move to enable/disableBacklit to apply itonly to the running apps? // TODO: move to css class for theming support this._glossyBackgroundStyle = 'background-image: url(\'' + Me.path + '/media/glossy.svg\');' + 'background-size: contain;'; } update() { super.update(); // Enable / Disable the backlight of running apps if (!Docking.DockManager.settings.get_boolean('apply-custom-theme') && Docking.DockManager.settings.get_boolean('unity-backlit-items')) { this._source._iconContainer.get_children()[1].set_style(this._glossyBackgroundStyle); if (this._isRunning) this._enableBacklight(); else this._disableBacklight(); } else { this._disableBacklight(); this._source._iconContainer.get_children()[1].set_style(null); } if (this._area) this._area.queue_repaint(); } _computeStyle() { let [width, height] = this._area.get_surface_size(); this._width = height; this._height = width; // By defaut re-use the style - background color, and border width and color - // of the default dot let themeNode = this._source._dot.get_theme_node(); this._borderColor = themeNode.get_border_color(this._side); this._borderWidth = themeNode.get_border_width(this._side); this._bodyColor = themeNode.get_background_color(); let settings = Docking.DockManager.settings; if (!settings.get_boolean('apply-custom-theme')) { // Adjust for the backlit case if (settings.get_boolean('unity-backlit-items')) { // Use dominant color for dots too if the backlit is enables let colorPalette = this._dominantColorExtractor._getColorPalette(); // Slightly adjust the styling this._borderWidth = 2; if (colorPalette !== null) { this._borderColor = Clutter.color_from_string(colorPalette.lighter)[1] ; this._bodyColor = Clutter.color_from_string(colorPalette.darker)[1]; } else { // Fallback this._borderColor = Clutter.color_from_string('white')[1]; this._bodyColor = Clutter.color_from_string('gray')[1]; } } // Apply dominant color if requested if (settings.get_boolean('running-indicator-dominant-color')) { let colorPalette = this._dominantColorExtractor._getColorPalette(); if (colorPalette !== null) { this._bodyColor = Clutter.color_from_string(colorPalette.original)[1]; } } // Finally, use customize style if requested if (settings.get_boolean('custom-theme-customize-running-dots')) { this._borderColor = Clutter.color_from_string(settings.get_string('custom-theme-running-dots-border-color'))[1]; this._borderWidth = settings.get_int('custom-theme-running-dots-border-width'); this._bodyColor = Clutter.color_from_string(settings.get_string('custom-theme-running-dots-color'))[1]; } } // Define the radius as an arbitrary size, but keep large enough to account // for the drawing of the border. this._radius = Math.max(this._width/22, this._borderWidth/2); this._padding = 0; // distance from the margin this._spacing = this._radius + this._borderWidth; // separation between the dots } _updateIndicator() { let area = this._area; let cr = this._area.get_context(); this._computeStyle(); this._drawIndicator(cr); cr.$dispose(); } _drawIndicator(cr) { // Draw the required numbers of dots let n = this._nWindows; cr.setLineWidth(this._borderWidth); Clutter.cairo_set_source_color(cr, this._borderColor); // draw for the bottom case: cr.translate((this._width - (2*n)*this._radius - (n-1)*this._spacing)/2, this._height - this._padding); for (let i = 0; i < n; i++) { cr.newSubPath(); cr.arc((2*i+1)*this._radius + i*this._spacing, -this._radius - this._borderWidth/2, this._radius, 0, 2*Math.PI); } cr.strokePreserve(); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.fill(); } destroy() { this._area.destroy(); super.destroy(); } }; // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel var RunningIndicatorCiliora = class DashToDock_RunningIndicatorCiliora extends RunningIndicatorDots { _drawIndicator(cr) { if (this._isRunning) { let size = Math.max(this._width/20, this._borderWidth); let spacing = size; // separation between the dots let lineLength = this._width - (size*(this._nWindows-1)) - (spacing*(this._nWindows-1)); let padding = this._borderWidth; // For the backlit case here we don't want the outer border visible if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) padding = 0; let yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Clutter.cairo_set_source_color(cr, this._borderColor); cr.translate(0, yOffset); cr.newSubPath(); cr.rectangle(0, 0, lineLength, size); for (let i = 1; i < this._nWindows; i++) { cr.newSubPath(); cr.rectangle(lineLength + (i*spacing) + ((i-1)*size), 0, size, size); } cr.strokePreserve(); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.fill(); } } }; // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel var RunningIndicatorSegmented = class DashToDock_RunningIndicatorSegmented extends RunningIndicatorDots { _drawIndicator(cr) { if (this._isRunning) { let size = Math.max(this._width/20, this._borderWidth); let spacing = Math.ceil(this._width/18); // separation between the dots let dashLength = Math.ceil((this._width - ((this._nWindows-1)*spacing))/this._nWindows); let lineLength = this._width - (size*(this._nWindows-1)) - (spacing*(this._nWindows-1)); let padding = this._borderWidth; // For the backlit case here we don't want the outer border visible if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) padding = 0; let yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Clutter.cairo_set_source_color(cr, this._borderColor); cr.translate(0, yOffset); for (let i = 0; i < this._nWindows; i++) { cr.newSubPath(); cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); } cr.strokePreserve(); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.fill() } } }; // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel var RunningIndicatorSolid = class DashToDock_RunningIndicatorSolid extends RunningIndicatorDots { _drawIndicator(cr) { if (this._isRunning) { let size = Math.max(this._width/20, this._borderWidth); let padding = this._borderWidth; // For the backlit case here we don't want the outer border visible if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) padding = 0; let yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Clutter.cairo_set_source_color(cr, this._borderColor); cr.translate(0, yOffset); cr.newSubPath(); cr.rectangle(0, 0, this._width, size); cr.strokePreserve(); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.fill(); } } }; // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel var RunningIndicatorSquares = class DashToDock_RunningIndicatorSquares extends RunningIndicatorDots { _drawIndicator(cr) { if (this._isRunning) { let size = Math.max(this._width/11, this._borderWidth); let padding = this._borderWidth; let spacing = Math.ceil(this._width/18); // separation between the dots let yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Clutter.cairo_set_source_color(cr, this._borderColor); cr.translate(Math.floor((this._width - this._nWindows*size - (this._nWindows-1)*spacing)/2), yOffset); for (let i = 0; i < this._nWindows; i++) { cr.newSubPath(); cr.rectangle(i*size + i*spacing, 0, size, size); } cr.strokePreserve(); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel var RunningIndicatorDashes = class DashToDock_RunningIndicatorDashes extends RunningIndicatorDots { _drawIndicator(cr) { if (this._isRunning) { let size = Math.max(this._width/20, this._borderWidth); let padding = this._borderWidth; let spacing = Math.ceil(this._width/18); // separation between the dots let dashLength = Math.floor(this._width/4) - spacing; let yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Clutter.cairo_set_source_color(cr, this._borderColor); cr.translate(Math.floor((this._width - this._nWindows*dashLength - (this._nWindows-1)*spacing)/2), yOffset); for (let i = 0; i < this._nWindows; i++) { cr.newSubPath(); cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); } cr.strokePreserve(); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel var RunningIndicatorMetro = class DashToDock_RunningIndicatorMetro extends RunningIndicatorDots { constructor(source) { super(source); this._source.actor.add_style_class_name('metro'); } destroy() { this._source.actor.remove_style_class_name('metro'); super.destroy(); } _drawIndicator(cr) { if (this._isRunning) { let size = Math.max(this._width/20, this._borderWidth); let padding = 0; // For the backlit case here we don't want the outer border visible if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) padding = 0; let yOffset = this._height - padding - size; let n = this._nWindows; if(n <= 1) { cr.translate(0, yOffset); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.newSubPath(); cr.rectangle(0, 0, this._width, size); cr.fill(); } else { let blackenedLength = (1/48)*this._width; // need to scale with the SVG for the stacked highlight let darkenedLength = this._isFocused ? (2/48)*this._width : (10/48)*this._width; let blackenedColor = this._bodyColor.shade(.3); let darkenedColor = this._bodyColor.shade(.7); cr.translate(0, yOffset); Clutter.cairo_set_source_color(cr, this._bodyColor); cr.newSubPath(); cr.rectangle(0, 0, this._width - darkenedLength - blackenedLength, size); cr.fill(); Clutter.cairo_set_source_color(cr, blackenedColor); cr.newSubPath(); cr.rectangle(this._width - darkenedLength - blackenedLength, 0, 1, size); cr.fill(); Clutter.cairo_set_source_color(cr, darkenedColor); cr.newSubPath(); cr.rectangle(this._width - darkenedLength, 0, darkenedLength, size); cr.fill(); } } } } /* * Unity like notification and progress indicators */ var UnityIndicator = class DashToDock_UnityIndicator extends IndicatorBase { constructor(source) { super(source); this._notificationBadgeLabel = new St.Label(); this._notificationBadgeBin = new St.Bin({ child: this._notificationBadgeLabel, x_align: St.Align.END, y_align: St.Align.START, x_expand: true, y_expand: true }); this._notificationBadgeLabel.add_style_class_name('notification-badge'); this._notificationBadgeCount = 0; this._notificationBadgeBin.hide(); this._source._iconContainer.add_child(this._notificationBadgeBin); this._source._iconContainer.connect('allocation-changed', this.updateNotificationBadge.bind(this)); this._remoteEntries = []; this._source.remoteModel.lookupById(this._source.app.id).forEach( (entry) => { this.insertEntryRemote(entry); } ); this._signalsHandler.add([ this._source.remoteModel, 'entry-added', this._onLauncherEntryRemoteAdded.bind(this) ], [ this._source.remoteModel, 'entry-removed', this._onLauncherEntryRemoteRemoved.bind(this) ]) } _onLauncherEntryRemoteAdded(remoteModel, entry) { if (!entry || !entry.appId()) return; if (this._source && this._source.app && this._source.app.id == entry.appId()) { this.insertEntryRemote(entry); } } _onLauncherEntryRemoteRemoved(remoteModel, entry) { if (!entry || !entry.appId()) return; if (this._source && this._source.app && this._source.app.id == entry.appId()) { this.removeEntryRemote(entry); } } updateNotificationBadge() { let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; let [minWidth, natWidth] = this._source._iconContainer.get_preferred_width(-1); let logicalNatWidth = natWidth / scaleFactor; let font_size = Math.max(10, Math.round(logicalNatWidth / 5)); let margin_left = Math.round(logicalNatWidth / 4); this._notificationBadgeLabel.set_style( 'font-size: ' + font_size + 'px;' + 'margin-left: ' + margin_left + 'px;' ); this._notificationBadgeBin.width = Math.round(logicalNatWidth - margin_left); 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 Clutter.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 Clutter.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 = St.ThemeContext.get_for_stage(global.stage).scale_factor; 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.20*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.20, 0.20, 0.20, 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(); } } insertEntryRemote(remote) { if (!remote || this._remoteEntries.indexOf(remote) !== -1) return; this._remoteEntries.push(remote); this._selectEntryRemote(remote); } removeEntryRemote(remote) { if (!remote || this._remoteEntries.indexOf(remote) == -1) return; this._remoteEntries.splice(this._remoteEntries.indexOf(remote), 1); if (this._remoteEntries.length > 0) { this._selectEntryRemote(this._remoteEntries[this._remoteEntries.length-1]); } else { this.setNotificationBadge(0); this.toggleNotificationBadge(false); this.setProgress(0); this.toggleProgressOverlay(false); } } _selectEntryRemote(remote) { if (!remote) return; this._signalsHandler.removeWithLabel('entry-remotes'); this._signalsHandler.addWithLabel('entry-remotes', [ remote, 'count-changed', (remote, value) => { this.setNotificationBadge(value); } ], [ remote, 'count-visible-changed', (remote, value) => { this.toggleNotificationBadge(value); } ], [ remote, 'progress-changed', (remote, value) => { this.setProgress(value); } ], [ remote, 'progress-visible-changed', (remote, value) => { this.toggleProgressOverlay(value); } ]); this.setNotificationBadge(remote.count()); this.toggleNotificationBadge(remote.countVisible()); this.setProgress(remote.progress()); this.toggleProgressOverlay(remote.progressVisible()); } } // We need an icons theme object, this is the only way I managed to get // pixel buffers that can be used for calculating the backlight color let themeLoader = null; // Global icon cache. Used for Unity7 styling. let iconCacheMap = new Map(); // Max number of items to store // We don't expect to ever reach this number, but let's put an hard limit to avoid // even the remote possibility of the cached items to grow indefinitely. const MAX_CACHED_ITEMS = 1000; // When the size exceed it, the oldest 'n' ones are deleted const BATCH_SIZE_TO_DELETE = 50; // The icon size used to extract the dominant color const DOMINANT_COLOR_ICON_SIZE = 64; // Compute dominant color frim the app icon. // The color is cached for efficiency. var DominantColorExtractor = class DashToDock_DominantColorExtractor { constructor(app) { this._app = app; } /** * Try to get the pixel buffer for the current icon, if not fail gracefully */ _getIconPixBuf() { 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() { 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 = Utils.ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255); if (hsv.s > 0.15) hsv.s = 0.65; hsv.v = 0.90; let rgb = Utils.ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v); // Cache the result. let backgroundColor = { lighter: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0.2), original: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0), darker: Utils.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 (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; } };