%PDF- %PDF-
Direktori : /usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/ |
Current File : //usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/appIconIndicators.js |
import { Clutter, GdkPixbuf, Gio, GObject, Pango, St, } from './dependencies/gi.js'; import {Main} from './dependencies/shell/ui.js'; import { Docking, Utils, } from './imports.js'; const {cairo: Cairo} = imports; const RunningIndicatorStyle = Object.freeze({ DEFAULT: 0, DOTS: 1, SQUARES: 2, DASHES: 3, SEGMENTED: 4, SOLID: 5, CILIORA: 6, METRO: 7, BINARY: 8, }); 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. * */ export class AppIconIndicator { constructor(source) { this._indicators = []; // Choose the style for the running indicators let runningIndicator = null; let runningIndicatorStyle; const {settings} = Docking.DockManager; if (settings.applyCustomTheme) runningIndicatorStyle = RunningIndicatorStyle.DOTS; else ({runningIndicatorStyle} = settings); if (settings.showIconsEmblems && !Docking.DockManager.getDefault().notificationsMonitor.dndMode) { const unityIndicator = new UnityIndicator(source); this._indicators.push(unityIndicator); } 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; case RunningIndicatorStyle.BINARY: runningIndicator = new RunningIndicatorBinary(source); break; default: runningIndicator = new RunningIndicatorBase(source); } this._indicators.push(runningIndicator); } update() { for (let i = 0; i < this._indicators.length; i++) { const indicator = this._indicators[i]; indicator.update(); } } destroy() { for (let i = 0; i < this._indicators.length; i++) { const indicator = this._indicators[i]; indicator.destroy(); } } } /* * Base class to be inherited by all indicators of any kind */ class IndicatorBase { constructor(source) { this._source = source; this._signalsHandler = new Utils.GlobalSignalsHandler(this._source); } update() { } destroy() { this._source = null; this._signalsHandler.destroy(); this._signalsHandler = null; } } /* * 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. * */ class RunningIndicatorBase extends IndicatorBase { constructor(source) { super(source); this._side = Utils.getPosition(); this._dominantColorExtractor = new DominantColorExtractor(this._source.app); this._signalsHandler.add(this._source, 'notify::running', () => this.update()); this._signalsHandler.add(this._source, 'notify::focused', () => this.update()); this._signalsHandler.add(this._source, 'notify::windows-count', () => this._updateCounterClass()); this.update(); } get _number() { return Math.min(this._source.windowsCount, MAX_WINDOWS_CLASSES); } update() { this._updateCounterClass(); this._updateDefaultDot(); } _updateCounterClass() { for (let i = 1; i <= MAX_WINDOWS_CLASSES; i++) { const className = `running${i}`; if (i !== this._number) this._source.remove_style_class_name(className); else this._source.add_style_class_name(className); } } _updateDefaultDot() { if (this._source.running) 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() { const colorPalette = this._dominantColorExtractor._getColorPalette(); // Fallback if (!colorPalette) { 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 class RunningIndicatorDefault extends RunningIndicatorBase { constructor(source) { super(source); this._source.add_style_class_name('default'); } destroy() { this._source.remove_style_class_name('default'); super.destroy(); } } const IndicatorDrawingArea = GObject.registerClass( class IndicatorDrawingArea extends St.DrawingArea { vfunc_allocate(box) { if (box.x1 !== 0 || box.y1 !== 0) return super.vfunc_allocate(box); // We assume that the are is a rectangle in the operations below: const size = Math.min(box.get_width(), box.get_height()); box.x2 = size; box.y2 = size; this.set_allocation(box); return super.vfunc_allocate(box); } }); class RunningIndicatorDots extends RunningIndicatorBase { constructor(source) { super(source); this._hideDefaultDot(); this._area = new IndicatorDrawingArea({ 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); switch (this._side) { case St.Side.TOP: this._area.rotation_angle_z = 180; break; case St.Side.BOTTOM: // nothing break; case St.Side.LEFT: this._area.rotation_angle_z = 90; break; case St.Side.RIGHT: this._area.rotation_angle_z = -90; break; } this._area.connect('repaint', this._updateIndicator.bind(this)); this._source._iconContainer.add_child(this._area); const 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', 'apply-glossy-effect', '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 const {extension} = Docking.DockManager; this._glossyBackgroundStyle = `background-image: url('${extension.path}/media/glossy.svg');` + 'background-size: contain;'; } update() { super.update(); // Enable / Disable the backlight of running apps if (!Docking.DockManager.settings.applyCustomTheme && Docking.DockManager.settings.unityBacklitItems) { const [icon] = this._source._iconContainer.get_children(); icon.set_style( Docking.DockManager.settings.applyGlossyEffect ? this._glossyBackgroundStyle : null); if (this._source.running) 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() { const [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 const 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(); const {settings} = Docking.DockManager; if (!settings.applyCustomTheme) { // Adjust for the backlit case if (settings.unityBacklitItems) { // Use dominant color for dots too if the backlit is enables const colorPalette = this._dominantColorExtractor._getColorPalette(); // Slightly adjust the styling this._borderWidth = 2; if (colorPalette) { [, this._borderColor] = Clutter.color_from_string(colorPalette.lighter); [, this._bodyColor] = Clutter.color_from_string(colorPalette.darker); } else { // Fallback [, this._borderColor] = Clutter.color_from_string('white'); [, this._bodyColor] = Clutter.color_from_string('gray'); } } // Apply dominant color if requested if (settings.runningIndicatorDominantColor) { const colorPalette = this._dominantColorExtractor._getColorPalette(); if (colorPalette) [, this._bodyColor] = Clutter.color_from_string(colorPalette.original); else // Fallback [, this._bodyColor] = Clutter.color_from_string(settings.customThemeRunningDotsColor); } // Finally, use customize style if requested if (settings.customThemeCustomizeRunningDots) { [, this._borderColor] = Clutter.color_from_string(settings.customThemeRunningDotsBorderColor); this._borderWidth = settings.customThemeRunningDotsBorderWidth; [, this._bodyColor] = Clutter.color_from_string(settings.customThemeRunningDotsColor); } } // 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() { const cr = this._area.get_context(); this._computeStyle(); this._drawIndicator(cr); cr.$dispose(); } _drawIndicator(cr) { // Draw the required numbers of dots const n = this._number; cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(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(); Utils.cairoSetSourceColor(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 class RunningIndicatorCiliora extends RunningIndicatorDots { _drawIndicator(cr) { if (this._source.running) { const size = Math.max(this._width / 20, this._borderWidth); const spacing = size; // separation between the dots const lineLength = this._width - (size * (this._number - 1)) - (spacing * (this._number - 1)); let padding = this._borderWidth; // For the backlit case here we don't want the outer border visible if (Docking.DockManager.settings.unityBacklitItems && !Docking.DockManager.settings.customThemeCustomizeRunningDots) padding = 0; const yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(cr, this._borderColor); cr.translate(0, yOffset); cr.newSubPath(); cr.rectangle(0, 0, lineLength, size); for (let i = 1; i < this._number; i++) { cr.newSubPath(); cr.rectangle(lineLength + (i * spacing) + ((i - 1) * size), 0, size, size); } cr.strokePreserve(); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel class RunningIndicatorSegmented extends RunningIndicatorDots { _drawIndicator(cr) { if (this._source.running) { const size = Math.max(this._width / 20, this._borderWidth); const spacing = Math.ceil(this._width / 18); // separation between the dots const dashLength = Math.ceil((this._width - ((this._number - 1) * spacing)) / this._number); let padding = this._borderWidth; // For the backlit case here we don't want the outer border visible if (Docking.DockManager.settings.unityBacklitItems && !Docking.DockManager.settings.customThemeCustomizeRunningDots) padding = 0; const yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(cr, this._borderColor); cr.translate(0, yOffset); for (let i = 0; i < this._number; i++) { cr.newSubPath(); cr.rectangle(i * dashLength + i * spacing, 0, dashLength, size); } cr.strokePreserve(); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel class RunningIndicatorSolid extends RunningIndicatorDots { _drawIndicator(cr) { if (this._source.running) { const 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.unityBacklitItems && !Docking.DockManager.settings.customThemeCustomizeRunningDots) padding = 0; const yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(cr, this._borderColor); cr.translate(0, yOffset); cr.newSubPath(); cr.rectangle(0, 0, this._width, size); cr.strokePreserve(); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel class RunningIndicatorSquares extends RunningIndicatorDots { _drawIndicator(cr) { if (this._source.running) { const size = Math.max(this._width / 11, this._borderWidth); const padding = this._borderWidth; const spacing = Math.ceil(this._width / 18); // separation between the dots const yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(cr, this._borderColor); cr.translate( Math.floor((this._width - this._number * size - (this._number - 1) * spacing) / 2), yOffset); for (let i = 0; i < this._number; i++) { cr.newSubPath(); cr.rectangle(i * size + i * spacing, 0, size, size); } cr.strokePreserve(); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel class RunningIndicatorDashes extends RunningIndicatorDots { _drawIndicator(cr) { if (this._source.running) { const size = Math.max(this._width / 20, this._borderWidth); const padding = this._borderWidth; const spacing = Math.ceil(this._width / 18); // separation between the dots const dashLength = Math.floor(this._width / 4) - spacing; const yOffset = this._height - padding - size; cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(cr, this._borderColor); cr.translate( Math.floor((this._width - this._number * dashLength - (this._number - 1) * spacing) / 2), yOffset); for (let i = 0; i < this._number; i++) { cr.newSubPath(); cr.rectangle(i * dashLength + i * spacing, 0, dashLength, size); } cr.strokePreserve(); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.fill(); } } } // Adapted from dash-to-panel by Jason DeRose // https://github.com/jderose9/dash-to-panel class RunningIndicatorMetro extends RunningIndicatorDots { constructor(source) { super(source); this._source.add_style_class_name('metro'); } destroy() { this._source.remove_style_class_name('metro'); super.destroy(); } _drawIndicator(cr) { if (this._source.running) { const 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.unityBacklitItems && !Docking.DockManager.settings.customThemeCustomizeRunningDots) padding = 0; const yOffset = this._height - padding - size; const n = this._number; if (n <= 1) { cr.translate(0, yOffset); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.newSubPath(); cr.rectangle(0, 0, this._width, size); cr.fill(); } else { // need to scale with the SVG for the stacked highlight const blackenedLength = (1 / 48) * this._width; const darkenedLength = this._source.focused ? (2 / 48) * this._width : (10 / 48) * this._width; const blackenedColor = this._bodyColor.shade(.3); const darkenedColor = this._bodyColor.shade(.7); cr.translate(0, yOffset); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.newSubPath(); cr.rectangle(0, 0, this._width - darkenedLength - blackenedLength, size); cr.fill(); Utils.cairoSetSourceColor(cr, blackenedColor); cr.newSubPath(); cr.rectangle(this._width - darkenedLength - blackenedLength, 0, 1, size); cr.fill(); Utils.cairoSetSourceColor(cr, darkenedColor); cr.newSubPath(); cr.rectangle(this._width - darkenedLength, 0, darkenedLength, size); cr.fill(); } } } } class RunningIndicatorBinary extends RunningIndicatorDots { _drawIndicator(cr) { // Draw the required numbers of dots const n = Math.min(15, this._source.windowsCount); if (this._source.running) { const size = Math.max(this._width / 11, this._borderWidth); const spacing = Math.ceil(this._width / 18); const yOffset = this._height - size; const binaryValue = String(`0000${(n >>> 0).toString(2)}`).slice(-4); cr.setLineWidth(this._borderWidth); Utils.cairoSetSourceColor(cr, this._borderColor); cr.translate(Math.floor((this._width - 4 * size - (4 - 1) * spacing) / 2), yOffset); for (let i = 0; i < binaryValue.length; i++) { if (binaryValue[i] === '1') { cr.newSubPath(); cr.arc((2 * i + 1) * this._radius + i * spacing, -this._radius - this._borderWidth / 2, this._radius, 0, 2 * Math.PI); } else { cr.newSubPath(); cr.rectangle(i * size + i * spacing, -this._radius - this._borderWidth / 2 - size / 5, size, size / 3); } } cr.strokePreserve(); Utils.cairoSetSourceColor(cr, this._bodyColor); cr.fill(); } } } /* * Unity like notification and progress indicators */ class UnityIndicator extends IndicatorBase { static defaultProgressBar = { // default values for the progress bar itself background: { colorStart: {red: 204, green: 204, blue: 204, alpha: 255}, colorEnd: null, }, border: { colorStart: {red: 230, green: 230, blue: 230, alpha: 255}, colorEnd: null, }, }; static defaultProgressBarTrack = { // default values for the progress bar track background: { colorStart: {red: 64, green: 64, blue: 64, alpha: 255}, colorEnd: {red: 89, green: 89, blue: 89, alpha: 255}, offsetStart: 0.4, offsetEnd: 0.9, }, border: { colorStart: {red: 128, green: 128, blue: 128, alpha: 26}, colorEnd: {red: 204, green: 204, blue: 204, alpha: 102}, offsetStart: 0.5, offsetEnd: 0.9, }, }; constructor(source) { super(source); this._notificationBadgeLabel = new St.Label(); this._notificationBadgeBin = new St.Bin({ child: this._notificationBadgeLabel, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.START, x_expand: true, y_expand: true, }); this._notificationBadgeLabel.add_style_class_name('notification-badge'); this._notificationBadgeLabel.clutter_text.ellipsize = Pango.EllipsizeMode.MIDDLE; this._notificationBadgeBin.hide(); this._source._iconContainer.add_child(this._notificationBadgeBin); this.updateNotificationBadgeStyle(); const {remoteModel, notificationsMonitor} = Docking.DockManager.getDefault(); const remoteEntry = remoteModel.lookupById(this._source.app.id); this._remoteEntry = remoteEntry; this._signalsHandler.add([ remoteEntry, ['count-changed', 'count-visible-changed'], () => this._updateNotificationsCount(), ], [ remoteEntry, ['progress-changed', 'progress-visible-changed'], (sender, {progress, progress_visible: progressVisible}) => this.setProgress(progressVisible ? progress : -1), ], [ remoteEntry, 'urgent-changed', (sender, {urgent}) => this.setUrgent(urgent), ], [ notificationsMonitor, 'changed', () => this._updateNotificationsCount(), ], [ St.ThemeContext.get_for_stage(global.stage), 'changed', this.updateNotificationBadgeStyle.bind(this), ], [ this._source._iconContainer, 'notify::size', this.updateNotificationBadgeStyle.bind(this), ]); } destroy() { this._notificationBadgeBin.destroy(); this._notificationBadgeBin = null; this._hideProgressOverlay(); this.setUrgent(false); this._remoteEntry = null; super.destroy(); } updateNotificationBadgeStyle() { const themeContext = St.ThemeContext.get_for_stage(global.stage); const fontDesc = themeContext.get_font(); const defaultFontSize = fontDesc.get_size() / 1024; let fontSize = defaultFontSize * 0.9; const {iconSize} = Main.overview.dash; const defaultIconSize = Docking.DockManager.settings.get_default_value( 'dash-max-icon-size').unpack(); if (!fontDesc.get_size_is_absolute()) { // fontSize was exprimed in points, so convert to pixel fontSize /= 0.75; } let sizeMultiplier; if (iconSize < defaultIconSize) { sizeMultiplier = Math.max(24, Math.min(iconSize + iconSize * 0.3, defaultIconSize)) / defaultIconSize; } else { sizeMultiplier = iconSize / defaultIconSize; } fontSize = Math.round(sizeMultiplier * fontSize); const leftMargin = Math.round(sizeMultiplier * 3); this._notificationBadgeLabel.set_style( `font-size: ${fontSize}px;` + `margin-left: ${leftMargin}px` ); } _notificationBadgeCountToText(count) { if (count <= 9999) { return count.toString(); } else if (count < 1e5) { const thousands = count / 1e3; return `${thousands.toFixed(1).toString()}k`; } else if (count < 1e6) { const thousands = count / 1e3; return `${thousands.toFixed(0).toString()}k`; } else if (count < 1e8) { const millions = count / 1e6; return `${millions.toFixed(1).toString()}M`; } else if (count < 1e9) { const millions = count / 1e6; return `${millions.toFixed(0).toString()}M`; } else { const billions = count / 1e9; return `${billions.toFixed(1).toString()}B`; } } _updateNotificationsCount() { const remoteCount = this._remoteEntry['count-visible'] ? this._remoteEntry.count ?? 0 : 0; if (remoteCount > 0 && Docking.DockManager.settings.applicationCounterOverridesNotifications) { this.setNotificationCount(remoteCount); return; } const {notificationsMonitor} = Docking.DockManager.getDefault(); const notificationsCount = notificationsMonitor.getAppNotificationsCount( this._source.app.id); this.setNotificationCount(remoteCount + notificationsCount); } setNotificationCount(count) { if (count > 0) { const text = this._notificationBadgeCountToText(count); this._notificationBadgeLabel.set_text(text); 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); this._updateProgressOverlay(); } _hideProgressOverlay() { this._progressOverlayArea?.destroy(); this._progressOverlayArea = null; } _updateProgressOverlay() { this._progressOverlayArea?.queue_repaint(); } _readGradientData(node, elementName, defaultValues) { const output = { colorStart: defaultValues.colorStart, colorEnd: defaultValues.colorEnd, offsetStart: defaultValues.offsetStart ?? 0.0, offsetEnd: defaultValues.offsetEnd ?? 1.0, }; const [hasElementName, elementNameValue] = node.lookup_color(elementName, false); if (hasElementName) { output.colorStart = elementNameValue; output.colorEnd = null; } else { const [hasColorStart, colorStartValue] = node.lookup_color(`${elementName}-color-start`, false); const [hasColorEnd, colorEndValue] = node.lookup_color(`${elementName}-color-end`, false); if (hasColorStart && hasColorEnd) { output.colorStart = colorStartValue; output.colorEnd = colorEndValue; } } const [hasOffsetStart, offsetStartvalue] = node.lookup_color(`${elementName}-offset-start`, false); if (hasOffsetStart) output.offsetStart = offsetStartvalue; const [hasOffsetEnd, offsetEndValue] = node.lookup_color(`${elementName}-offset-end`, false); if (hasOffsetEnd) output.offsetEnd = offsetEndValue; return output; } _readElementData(node, elementName, defaultValues) { const defaultLineWidth = defaultValues.lineWidth ?? 1.0; const [hasValue, lineWidth] = node.lookup_double(`${elementName}-line-width`, false); return { background: this._readGradientData(node, `${elementName}-background`, defaultValues.background), border: this._readGradientData(node, `${elementName}-border`, defaultValues.border), lineWidth: hasValue ? lineWidth : defaultLineWidth, }; } _createGradient(values, x0, y0, x1, y1) { if (values.colorEnd) { const gradient = new Cairo.LinearGradient(x0, y0, x1, y1); gradient.addColorStopRGBA(values.offsetStart, values.colorStart.red / 255, values.colorStart.green / 255, values.colorStart.blue / 255, values.colorStart.alpha / 255); gradient.addColorStopRGBA(values.offsetEnd, values.colorEnd.red / 255, values.colorEnd.green / 255, values.colorEnd.blue / 255, values.colorEnd.alpha / 255); return gradient; } else { const gradient = Cairo.SolidPattern.createRGBA(values.colorStart.red / 255, values.colorStart.green / 255, values.colorStart.blue / 255, values.colorStart.alpha / 255); return gradient; } } _drawProgressOverlay(area) { const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); const [surfaceWidth, surfaceHeight] = area.get_surface_size(); const cr = area.get_context(); const iconSize = this._source.icon.iconSize * scaleFactor; let x = Math.floor((surfaceWidth - iconSize) / 2); let y = Math.floor((surfaceHeight - iconSize) / 2); const baseLineWidth = Math.floor(Number(scaleFactor)); const 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; const node = this._progressOverlayArea.get_theme_node(); const progressBarTrack = this._readElementData(node, '-progress-bar-track', UnityIndicator.defaultProgressBarTrack); const progressBar = this._readElementData(node, '-progress-bar', UnityIndicator.defaultProgressBar); // Draw the track let lineWidth = baseLineWidth * progressBarTrack.lineWidth; cr.setLineWidth(lineWidth); x += lineWidth; y += lineWidth; width -= 2.0 * lineWidth; height -= 2.0 * lineWidth; let fill = this._createGradient(progressBarTrack.background, 0, y, 0, y + height); let stroke = this._createGradient(progressBarTrack.border, 0, y, 0, y + height); Utils.drawRoundedLine(cr, x + lineWidth / 2.0, y + lineWidth / 2.0, width, height, true, true, stroke, fill); // Draw the finished bar lineWidth = baseLineWidth * progressBar.lineWidth; cr.setLineWidth(lineWidth); x += lineWidth; y += lineWidth; width -= 2.0 * lineWidth; height -= 2.0 * lineWidth; const finishedWidth = Math.ceil(this._progress * width); fill = this._createGradient(progressBar.background, 0, y, 0, y + height); stroke = this._createGradient(progressBar.border, 0, y, 0, y + height); 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) { if (progress < 0) { this._hideProgressOverlay(); } else { this._progress = Math.min(progress, 1.0); this._showProgressOverlay(); } } setUrgent(urgent) { if (urgent || this._isUrgent !== undefined) this._source.urgent = urgent; if (urgent) this._isUrgent = urgent; else delete this._isUrgent; } } // Global icon cache. Used for Unity7 styling. const 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. class 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); const themeLoader = Docking.DockManager.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) 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()); } else if (iconTexture instanceof Gio.ThemedIcon) { // Get the first pixel buffer available in the icon theme const iconNames = iconTexture.get_names(); const iconInfo = themeLoader.choose_icon(iconNames, DOMINANT_COLOR_ICON_SIZE, 0); if (iconInfo) return iconInfo.load_icon(); else return null; } // Use GdkPixBuf to load the pixel buffer from memory // iconTexture.load is available unless iconTexture is not an instance of Gio.LoadableIcon // this means that iconTexture is an instance of Gio.EmblemedIcon, // which may be converted to a normal icon via iconTexture.get_icon? const [iconBuffer] = iconTexture.load(DOMINANT_COLOR_ICON_SIZE, null); return GdkPixbuf.Pixbuf.new_from_stream(iconBuffer, 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()); } const pixBuf = this._getIconPixBuf(); if (!pixBuf) return null; let pixels = pixBuf.get_pixels(); let total = 0, rTotal = 0, gTotal = 0, bTotal = 0; let resampleX = 1; let resampleY = 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. const width = pixBuf.get_width(); const height = pixBuf.get_height(); // Resample if (height >= 2 * DOMINANT_COLOR_ICON_SIZE) resampleY = Math.floor(height / DOMINANT_COLOR_ICON_SIZE); if (width >= 2 * DOMINANT_COLOR_ICON_SIZE) resampleX = Math.floor(width / DOMINANT_COLOR_ICON_SIZE); if (resampleX !== 1 || resampleY !== 1) pixels = this._resamplePixels(pixels, resampleX, resampleY); // computing the limit outside the for (where it would be repeated at each iteration) // for performance reasons const limit = pixels.length; for (let offset = 0; offset < limit; offset += 4) { const r = pixels[offset], g = pixels[offset + 1], b = pixels[offset + 2], a = pixels[offset + 3]; const saturation = Math.max(r, g, b) - Math.min(r, g, b); const relevance = 0.1 * 255 * 255 + 0.9 * a * saturation; rTotal += r * relevance; gTotal += g * relevance; bTotal += b * relevance; total += relevance; } total *= 255; const r = rTotal / total, g = gTotal / total, b = bTotal / total; const hsv = Utils.ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255); if (hsv.s > 0.15) hsv.s = 0.65; hsv.v = 0.90; const rgb = Utils.ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v); // Cache the result. const 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 (const 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 * * @returns []; */ _resamplePixels(pixels, resampleX, resampleY) { const resampledPixels = []; // computing the limit outside the for (where it would be repeated at each iteration) // for performance reasons const limit = pixels.length / (resampleX * resampleY) / 4; for (let i = 0; i < limit; i++) { const 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; } }