diff --git a/Makefile b/Makefile index a91d6cb..8ada6a2 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ UUID = dash-to-panel@jderose9.github.com BASE_MODULES = extension.js stylesheet.css metadata.json COPYING README.md -EXTRA_MODULES = convenience.js panel.js panelStyle.js overview.js taskbar.js secondaryMenu.js windowPreview.js prefs.js Settings.ui +EXTRA_MODULES = appIcons.js convenience.js panel.js panelStyle.js overview.js taskbar.js secondaryMenu.js windowPreview.js prefs.js Settings.ui EXTRA_IMAGES = highlight_bg.svg highlight_stacked_bg.svg TOLOCALIZE = prefs.js MSGSRC = $(wildcard po/*.po) diff --git a/appIcons.js b/appIcons.js new file mode 100644 index 0000000..b2a54fe --- /dev/null +++ b/appIcons.js @@ -0,0 +1,900 @@ +/* + * 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 Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Signals = imports.signals; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; +const SecondaryMenu = Me.imports.secondaryMenu; +const WindowPreview = Me.imports.windowPreview; +const Taskbar = Me.imports.taskbar; + +let DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; +let DASH_ITEM_LABEL_SHOW_TIME = Dash.DASH_ITEM_LABEL_SHOW_TIME; +let DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; +let DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; +let LABEL_GAP = 5; + +let DOT_STYLE = { + DOTS: "DOTS", + SQUARES: "SQUARES", + DASHES: "DASHES", + SEGMENTED: "SEGMENTED", + CILIORA: "CILIORA", + METRO: "METRO", + SOLID: "SOLID" +} + +let DOT_POSITION = { + TOP: "TOP", + BOTTOM: "BOTTOM" +} + +let recentlyClickedAppLoopId = 0; +let recentlyClickedApp = null; +let recentlyClickedAppWindows = null; +let recentlyClickedAppIndex = 0; + +let tracker = Shell.WindowTracker.get_default(); + +/** + * Extend AppIcon + * + * - Apply a css class based on the number of windows of each application (#N); + * - Draw a dot for each window of the application based on the default "dot" style which is hidden (#N); + * a class of the form "running#N" is applied to the AppWellIcon actor. + * like the original .running one. + * - add a .focused style to the focused app + * - Customize click actions. + * - Update minimization animation target + * + */ + +const taskbarAppIcon = new Lang.Class({ + Name: 'DashToPanel.TaskbarAppIcon', + Extends: AppDisplay.AppIcon, + + _init: function(settings, app, iconParams, onActivateOverride) { + + // a prefix is required to avoid conflicting with the parent class variable + this._dtpSettings = settings; + this._nWindows = 0; + + this.parent(app, iconParams, onActivateOverride); + + this._dot.set_width(0); + this._focused = tracker.focus_app == this.app; + + // Monitor windows-changes instead of app state. + // Keep using the same Id and function callback (that is extended) + if(this._stateChangedId > 0) { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + this._stateChangedId = this.app.connect('windows-changed', + Lang.bind(this, this.onWindowsChanged)); + this._focuseAppChangeId = tracker.connect('notify::focus-app', + Lang.bind(this, this._onFocusAppChanged)); + + this._focusedDots = null; + this._unfocusedDots = null; + + this._showDots(); + + this._dtpSettings.connect('changed::dot-position', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-size', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-style-focused', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-style-unfocused', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-override', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-1', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-2', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-3', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-4', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-unfocused-different', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-unfocused-1', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-unfocused-2', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-unfocused-3', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::dot-color-unfocused-4', Lang.bind(this, this._settingsChangeRefresh)); + this._dtpSettings.connect('changed::focus-highlight', Lang.bind(this, this._settingsChangeRefresh)); + + this._dtpSettings.connect('changed::appicon-margin', Lang.bind(this, this._setIconStyle)); + + // Creating a new menu manager for window previews as adding it to the + // using the secondary menu's menu manager (which uses the "ignoreRelease" + // function) caused the extension to crash. + this.menuManagerWindowPreview = new PopupMenu.PopupMenuManager(this); + + this.windowPreview = new WindowPreview.thumbnailPreviewMenu(this, this._dtpSettings, this.menuManagerWindowPreview); + + this.windowPreview.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + })); + this.menuManagerWindowPreview.addMenu(this.windowPreview); + + // grabHelper.grab() is usually called when the menu is opened. However, there seems to be a bug in the + // underlying gnome-shell that causes all window contents to freeze if the grab and ungrab occur + // in quick succession in timeouts from the Mainloop (for example, clicking the icon as the preview window is opening) + // So, instead wait until the mouse is leaving the icon (and might be moving toward the open window) to trigger the grab + // in windowPreview.js + let windowPreviewMenuData = this.menuManagerWindowPreview._menus[this.menuManagerWindowPreview._findMenu(this.windowPreview)]; + this.windowPreview.disconnect(windowPreviewMenuData.openStateChangeId); + windowPreviewMenuData.openStateChangeId = this.windowPreview.connect('open-state-changed', Lang.bind(this.menuManagerWindowPreview, function(menu, open) { + if (open) { + if (this.activeMenu) + this.activeMenu.close(BoxPointer.PopupAnimation.FADE); + + // don't grab here, we are grabbing in onLeave in windowPreview.js + //this._grabHelper.grab({ actor: menu.actor, focus: menu.sourceActor, onUngrab: Lang.bind(this, this._closeMenu, menu) }); + } else { + this._grabHelper.ungrab({ actor: menu.actor }); + } + })); + + this.forcedOverview = false; + + this._numberOverlay(); + }, + + shouldShowTooltip: function() { + if (this._dtpSettings.get_boolean("show-window-previews") && + getInterestingWindows(this.app, this._dtpSettings).length > 0) { + return false; + } else { + return this.actor.hover && (!this._menu || !this._menu.isOpen) && (!this.windowPreview || !this.windowPreview.isOpen); + } + }, + + _onDestroy: function() { + this.parent(); + + // Disconect global signals + // stateChangedId is already handled by parent) + if(this._focusAppId>0) + tracker.disconnect(this._focusAppId); + }, + + onWindowsChanged: function() { + this._updateCounterClass(); + this.updateIconGeometry(); + }, + + // Update taraget for minimization animation + updateIconGeometry: function() { + + // If (for unknown reason) the actor is not on the stage the reported size + // and position are random values, which might exceeds the integer range + // resulting in an error when assigned to the a rect. This is a more like + // a workaround to prevent flooding the system with errors. + if (this.actor.get_stage() == null) + return + + let rect = new Meta.Rectangle(); + + [rect.x, rect.y] = this.actor.get_transformed_position(); + [rect.width, rect.height] = this.actor.get_transformed_size(); + + let windows = this.app.get_windows(); + windows.forEach(function(w) { + w.set_icon_geometry(rect); + }); + + }, + + _showDots: function() { + // Just update style if dots already exist + if (this._focusedDots && this._unfocusedDots) { + this._updateCounterClass(); + return; + } + + this._focusedDots = new St.DrawingArea({width:1, y_expand: true}); + this._unfocusedDots = new St.DrawingArea({width:1, y_expand: true}); + + this._focusedDots.connect('repaint', Lang.bind(this, function() { + if(this._dashItemContainer.animatingIn || this._dashItemContainer.animatingOut) { + // don't draw and trigger more animations if the icon is in the middle of + // being added to the panel + return; + } + this._drawRunningIndicator(this._focusedDots, this._dtpSettings.get_string('dot-style-focused'), true); + this._displayProperIndicator(); + })); + + this._unfocusedDots.connect('repaint', Lang.bind(this, function() { + if(this._dashItemContainer.animatingIn || this._dashItemContainer.animatingOut) { + // don't draw and trigger more animations if the icon is in the middle of + // being added to the panel + return; + } + this._drawRunningIndicator(this._unfocusedDots, this._dtpSettings.get_string('dot-style-unfocused'), false); + this._displayProperIndicator(); + })); + + + this._iconContainer.add_child(this._focusedDots); + this._iconContainer.add_child(this._unfocusedDots); + + this._updateCounterClass(); + }, + + _settingsChangeRefresh: function() { + this._updateCounterClass(); + this._focusedDots.queue_repaint(); + this._unfocusedDots.queue_repaint(); + this._displayProperIndicator(true); + }, + + _setIconStyle: function() { + let margin = this._dtpSettings.get_int('appicon-margin'); + let inlineStyle = 'margin: 0 ' + margin + 'px;'; + + if(this._dtpSettings.get_boolean('focus-highlight') && tracker.focus_app == this.app && !this._isThemeProvidingIndicator()) { + let containerWidth = this._iconContainer.get_width() / St.ThemeContext.get_for_stage(global.stage).scale_factor; + let focusedDotStyle = this._dtpSettings.get_string('dot-style-focused'); + let isWide = this._isWideDotStyle(focusedDotStyle); + let pos = this._dtpSettings.get_string('dot-position'); + let highlightMargin = isWide ? this._dtpSettings.get_int('dot-size') : 0; + + if(focusedDotStyle == DOT_STYLE.CILIORA || focusedDotStyle == DOT_STYLE.SEGMENTED) + highlightMargin += 1; + + inlineStyle += "background-image: url('" + + Me.path + "/img/highlight_" + + ((this._nWindows > 1 && focusedDotStyle == DOT_STYLE.METRO) ? "stacked_" : "") + + "bg.svg'); background-position: 0 " + + (pos == DOT_POSITION.TOP ? highlightMargin : 0) + + "px; background-size: " + + containerWidth + "px " + + (containerWidth - (pos == DOT_POSITION.BOTTOM ? highlightMargin : 0)) + "px;"; + } + + // graphical glitches if i dont set this on a timeout + if(this.actor.get_style() != inlineStyle) + Mainloop.timeout_add(0, Lang.bind(this, function() { this.actor.set_style(inlineStyle); })); + }, + + popupMenu: function() { + this._removeMenuTimeout(); + this.actor.fake_release(); + this._draggable.fakeRelease(); + + if (!this._menu) { + this._menu = new SecondaryMenu.taskbarSecondaryMenu(this, this._dtpSettings); + this._menu.connect('activate-window', Lang.bind(this, function (menu, window) { + this.activateWindow(window, this._dtpSettings); + })); + this._menu.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + })); + let id = Main.overview.connect('hiding', Lang.bind(this, function () { this._menu.close(); })); + this._menu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + this._menuManager.addMenu(this._menu); + } + + this.emit('menu-state-changed', true); + + this.windowPreview.close(); + + this.actor.set_hover(true); + this._menu.actor.add_style_class_name('dashtopanelSecondaryMenu'); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + }, + + _onFocusAppChanged: function(windowTracker) { + this._displayProperIndicator(); + }, + + _displayProperIndicator: function (force) { + let containerWidth = this._iconContainer.get_width(); + let isFocused = (tracker.focus_app == this.app); + let focusedDotStyle = this._dtpSettings.get_string('dot-style-focused'); + let unfocusedDotStyle = this._dtpSettings.get_string('dot-style-unfocused'); + let focusedIsWide = this._isWideDotStyle(focusedDotStyle); + let unfocusedIsWide = this._isWideDotStyle(unfocusedDotStyle); + + this._setIconStyle(); + + let newFocusedDotsWidth = 0; + let newFocusedDotsOpacity = 0; + let newUnfocusedDotsWidth = 0; + let newUnfocusedDotsOpacity = 0; + + + if(isFocused) + this.actor.add_style_class_name('focused'); + else + this.actor.remove_style_class_name('focused'); + + if(focusedIsWide) { + newFocusedDotsWidth = (isFocused && this._nWindows > 0) ? containerWidth : 0; + newFocusedDotsOpacity = 255; + } else { + newFocusedDotsWidth = containerWidth; + newFocusedDotsOpacity = (isFocused && this._nWindows > 0) ? 255 : 0; + } + + if(unfocusedIsWide) { + newUnfocusedDotsWidth = (!isFocused && this._nWindows > 0) ? containerWidth : 0; + newUnfocusedDotsOpacity = 255; + } else { + newUnfocusedDotsWidth = containerWidth; + newUnfocusedDotsOpacity = (!isFocused && this._nWindows > 0) ? 255 : 0; + } + + // Only animate if... + // animation is enabled in settings + // AND (going from a wide style to a narrow style indicator or vice-versa + // OR going from an open app to a closed app or vice versa) + if(this._dtpSettings.get_boolean('animate-app-switch') && + ((focusedIsWide != unfocusedIsWide) || + (this._focusedDots.width != newUnfocusedDotsWidth || this._unfocusedDots.width != newFocusedDotsWidth))) { + this._animateDotDisplay(this._focusedDots, newFocusedDotsWidth, this._unfocusedDots, newUnfocusedDotsOpacity, force); + this._animateDotDisplay(this._unfocusedDots, newUnfocusedDotsWidth, this._focusedDots, newFocusedDotsOpacity, force); + } + else { + this._focusedDots.opacity = newFocusedDotsOpacity; + this._unfocusedDots.opacity = newUnfocusedDotsOpacity; + this._focusedDots.width = newFocusedDotsWidth; + this._unfocusedDots.width = newUnfocusedDotsWidth; + } + }, + + _animateDotDisplay: function (dots, newWidth, otherDots, newOtherOpacity, force) { + if((dots.width != newWidth && dots._tweeningToWidth !== newWidth) || force) { + dots._tweeningToWidth = newWidth; + Tweener.addTween(dots, + { width: newWidth, + time: DASH_ANIMATION_TIME, + transition: 'easeInOutCubic', + onStart: Lang.bind(this, function() { + if(newOtherOpacity == 0) + otherDots.opacity = newOtherOpacity; + }), + onComplete: Lang.bind(this, function() { + if(newOtherOpacity > 0) + otherDots.opacity = newOtherOpacity; + dots._tweeningToWidth = null; + }) + }); + } + }, + + _isWideDotStyle: function(dotStyle) { + return dotStyle == DOT_STYLE.SEGMENTED || + dotStyle == DOT_STYLE.CILIORA || + dotStyle == DOT_STYLE.METRO || + dotStyle == DOT_STYLE.SOLID; + }, + + _isThemeProvidingIndicator: function () { + // This is an attempt to determine if the theme is providing their own + // running indicator by way of a border image on the icon, for example in + // the theme Ciliora + return (this.icon.actor.get_stage() && + this.icon.actor.get_theme_node().get_border_image()); + }, + + activate: function(button) { + this.windowPreview.requestCloseMenu(); + + let event = Clutter.get_current_event(); + let modifiers = event ? event.get_state() : 0; + let focusedApp = tracker.focus_app; + + // Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.) + modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK); + + // We don't change the CTRL-click behaviour: in such case we just chain + // up the parent method and return. + if (modifiers & Clutter.ModifierType.CONTROL_MASK) { + // Keep default behaviour: launch new window + // By calling the parent method I make it compatible + // with other extensions tweaking ctrl + click + this.parent(button); + return; + } + + // We check what type of click we have and if the modifier SHIFT is + // being used. We then define what buttonAction should be for this + // event. + let buttonAction = 0; + if (button && button == 2 ) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = this._dtpSettings.get_string('shift-middle-click-action'); + else + buttonAction = this._dtpSettings.get_string('middle-click-action'); + } + else if (button && button == 1) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = this._dtpSettings.get_string('shift-click-action'); + else + buttonAction = this._dtpSettings.get_string('click-action'); + } + + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation, + let appIsRunning = this.app.state == Shell.AppState.RUNNING + && getInterestingWindows(this.app, this._dtpSettings).length > 0 + + // We customize the action only when the application is already running + if (appIsRunning) { + switch (buttonAction) { + case "RAISE": + activateAllWindows(this.app, this._dtpSettings); + break; + + case "LAUNCH": + if(this._dtpSettings.get_boolean('animate-window-launch')) + this.animateLaunch(); + this.app.open_new_window(-1); + break; + + case "MINIMIZE": + // In overview just activate the app, unless the acion is explicitely + // requested with a keyboard modifier + if (!Main.overview._shown || modifiers){ + // If we have button=2 or a modifier, allow minimization even if + // the app is not focused + if (this.app == focusedApp || button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK) { + // minimize all windows on double click and always in the case of primary click without + // additional modifiers + let click_count = 0; + if (Clutter.EventType.CLUTTER_BUTTON_PRESS) + click_count = event.get_click_count(); + let all_windows = (button == 1 && ! modifiers) || click_count > 1; + minimizeWindow(this.app, all_windows, this._dtpSettings); + } + else + activateAllWindows(this.app, this._dtpSettings); + } + else + this.app.activate(); + break; + + case "CYCLE": + if (!Main.overview._shown){ + if (this.app == focusedApp) + cycleThroughWindows(this.app, this._dtpSettings, false, false); + else { + activateFirstWindow(this.app, this._dtpSettings); + } + } + else + this.app.activate(); + break; + case "CYCLE-MIN": + if (!Main.overview._shown){ + if (this.app == focusedApp || + (recentlyClickedApp == this.app && recentlyClickedAppWindows[recentlyClickedAppIndex % recentlyClickedAppWindows.length] == "MINIMIZE")) + cycleThroughWindows(this.app, this._dtpSettings, false, true); + else { + activateFirstWindow(this.app, this._dtpSettings); + } + } + else + this.app.activate(); + break; + + case "QUIT": + closeAllWindows(this.app, this._dtpSettings); + break; + } + } + else { + if(this._dtpSettings.get_boolean('animate-window-launch')) + this.animateLaunch(); + this.app.open_new_window(-1); + } + + Main.overview.hide(); + }, + + _updateCounterClass: function() { + let maxN = 4; + this._nWindows = Math.min(getInterestingWindows(this.app, this._dtpSettings).length, maxN); + + for (let i = 1; i <= maxN; i++){ + let className = 'running'+i; + if(i != this._nWindows) + this.actor.remove_style_class_name(className); + else + this.actor.add_style_class_name(className); + } + }, + + _drawRunningIndicator: function(area, type, isFocused) { + let bodyColor; + if(this._dtpSettings.get_boolean('dot-color-override')) { + let dotColorSettingPrefix = 'dot-color-'; + if(!isFocused && this._dtpSettings.get_boolean('dot-color-unfocused-different')) + dotColorSettingPrefix = 'dot-color-unfocused-'; + bodyColor = Clutter.color_from_string(this._dtpSettings.get_string(dotColorSettingPrefix + (this._nWindows > 0 ? this._nWindows : 1)))[1]; + } else { + // Re-use the style - background color, and border width and color - + // of the default dot + let themeNode = this._dot.get_theme_node(); + bodyColor = themeNode.get_background_color(); + if(bodyColor.alpha == 0) // theme didn't provide one, use a default + bodyColor = new Clutter.Color({ red: 82, green: 148, blue: 226, alpha: 255 }); + } + + let [width, height] = area.get_surface_size(); + let cr = area.get_context(); + let n = this._nWindows; + let size = this._dtpSettings.get_int('dot-size') * St.ThemeContext.get_for_stage(global.stage).scale_factor; + let padding = 0; // distance from the margin + let yOffset = this._dtpSettings.get_string('dot-position') == DOT_POSITION.TOP ? 0 : (height - padding - size); + + if(type == DOT_STYLE.DOTS) { + // Draw the required numbers of dots + let radius = size/2; + let spacing = Math.ceil(width/18); // separation between the dots + + cr.translate((width - (2*n)*radius - (n-1)*spacing)/2, yOffset); + + Clutter.cairo_set_source_color(cr, bodyColor); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.arc((2*i+1)*radius + i*spacing, radius, radius, 0, 2*Math.PI); + } + cr.fill(); + } else if(type == DOT_STYLE.SQUARES) { + let spacing = Math.ceil(width/18); // separation between the dots + + cr.translate(Math.floor((width - n*size - (n-1)*spacing)/2), yOffset); + + Clutter.cairo_set_source_color(cr, bodyColor); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.rectangle(i*size + i*spacing, 0, size, size); + } + cr.fill(); + } else if(type == DOT_STYLE.DASHES) { + let spacing = Math.ceil(width/18); // separation between the dots + let dashLength = Math.floor(width/4) - spacing; + + cr.translate(Math.floor((width - n*dashLength - (n-1)*spacing)/2), yOffset); + + Clutter.cairo_set_source_color(cr, bodyColor); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + cr.fill(); + } else if(type == DOT_STYLE.SEGMENTED) { + let spacing = Math.ceil(width/18); // separation between the dots + let dashLength = Math.ceil((width - ((n-1)*spacing))/n); + + cr.translate(0, yOffset); + + Clutter.cairo_set_source_color(cr, bodyColor); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + cr.fill(); + } else if (type == DOT_STYLE.CILIORA) { + let spacing = size; // separation between the dots + let lineLength = width - (size*(n-1)) - (spacing*(n-1)); + + cr.translate(0, yOffset); + + Clutter.cairo_set_source_color(cr, bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, lineLength, size); + for (let i = 1; i < n; i++) { + cr.newSubPath(); + cr.rectangle(lineLength + (i*spacing) + ((i-1)*size), 0, size, size); + } + cr.fill(); + } else if (type == DOT_STYLE.METRO) { + if(n <= 1) { + cr.translate(0, yOffset); + Clutter.cairo_set_source_color(cr, bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, width, size); + cr.fill(); + } else { + let blackenedLength = (1/48)*width; // need to scale with the SVG for the stacked highlight + let darkenedLength = isFocused ? (2/48)*width : (10/48)*width; + let blackenedColor = bodyColor.shade(.3); + let darkenedColor = bodyColor.shade(.7); + + cr.translate(0, yOffset); + + Clutter.cairo_set_source_color(cr, bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, width - darkenedLength - blackenedLength, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, blackenedColor); + cr.newSubPath(); + cr.rectangle(width - darkenedLength - blackenedLength, 0, 1, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, darkenedColor); + cr.newSubPath(); + cr.rectangle(width - darkenedLength, 0, darkenedLength, size); + cr.fill(); + } + } else { // solid + cr.translate(0, yOffset); + Clutter.cairo_set_source_color(cr, bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, width, size); + cr.fill(); + } + + cr.$dispose(); + }, + + _numberOverlay: function() { + // Add label for a Hot-Key visual aid + this._numberOverlayLabel = new St.Label(); + this._numberOverlayBin = new St.Bin({ + child: this._numberOverlayLabel, + x_align: St.Align.START, y_align: St.Align.START, + x_expand: true, y_expand: true + }); + this._numberOverlayStyle = 'background-color: rgba(0,0,0,0.8);' + this._numberOverlayOrder = -1; + this._numberOverlayBin.hide(); + + this._iconContainer.add_child(this._numberOverlayBin); + + }, + + updateNumberOverlay: function() { + // Set the font size to something smaller than the whole icon so it is + // still visible. The border radius is large to make the shape circular + let [minWidth, natWidth] = this._iconContainer.get_preferred_width(-1); + let font_size = Math.round(Math.max(12, 0.3*natWidth)); + let size = Math.round(font_size*1.2); + this._numberOverlayLabel.set_style( + this._numberOverlayStyle + + 'font-size: ' + font_size + 'px;' + + 'text-align: center;' + + 'border-radius: ' + this.icon.iconSize + 'px;' + + 'width: ' + size + 'px; height: ' + size +'px;' + ); + }, + + setNumberOverlay: function(number) { + this._numberOverlayOrder = number; + this._numberOverlayLabel.set_text(number.toString()); + }, + + toggleNumberOverlay: function(activate) { + if (activate && this._numberOverlayOrder > -1) + this._numberOverlayBin.show(); + else + this._numberOverlayBin.hide(); + } + +}); + +function minimizeWindow(app, param, settings){ + // Param true make all app windows minimize + let windows = getInterestingWindows(app, settings); + let current_workspace = global.screen.get_active_workspace(); + for (let i = 0; i < windows.length; i++) { + let w = windows[i]; + if (w.get_workspace() == current_workspace && w.showing_on_its_workspace()){ + w.minimize(); + // Just minimize one window. By specification it should be the + // focused window on the current workspace. + if(!param) + break; + } + } +} + +/* + * By default only non minimized windows are activated. + * This activates all windows in the current workspace. + */ +function activateAllWindows(app, settings){ + + // First activate first window so workspace is switched if needed, + // then activate all other app windows in the current workspace. + let windows = getInterestingWindows(app, settings); + let w = windows[0]; + Main.activateWindow(w); + let activeWorkspace = global.screen.get_active_workspace_index(); + + if (windows.length <= 0) + return; + + let activatedWindows = 0; + + for (let i = windows.length - 1; i >= 0; i--){ + if (windows[i].get_workspace().index() == activeWorkspace){ + Main.activateWindow(windows[i]); + activatedWindows++; + } + } +} + +function activateFirstWindow(app, settings){ + + let windows = getInterestingWindows(app, settings); + Main.activateWindow(windows[0]); +} + +function cycleThroughWindows(app, settings, reversed, shouldMinimize) { + // Store for a little amount of time last clicked app and its windows + // since the order changes upon window interaction + let MEMORY_TIME=3000; + + let app_windows = getInterestingWindows(app, settings); + + if(shouldMinimize) + app_windows.push("MINIMIZE"); + + if (recentlyClickedAppLoopId > 0) + Mainloop.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId = Mainloop.timeout_add(MEMORY_TIME, resetRecentlyClickedApp); + + // If there isn't already a list of windows for the current app, + // or the stored list is outdated, use the current windows list. + if (!recentlyClickedApp || + recentlyClickedApp.get_id() != app.get_id() || + recentlyClickedAppWindows.length != app_windows.length) { + recentlyClickedApp = app; + recentlyClickedAppWindows = app_windows; + recentlyClickedAppIndex = 0; + } + + if (reversed) { + recentlyClickedAppIndex--; + if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1; + } else { + recentlyClickedAppIndex++; + } + let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length; + + if(recentlyClickedAppWindows[index] === "MINIMIZE") + minimizeWindow(app, true, settings); + else + Main.activateWindow(recentlyClickedAppWindows[index]); +} + +function resetRecentlyClickedApp() { + if (recentlyClickedAppLoopId > 0) + Mainloop.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId=0; + recentlyClickedApp =null; + recentlyClickedAppWindows = null; + recentlyClickedAppIndex = 0; + + return false; +} + +function closeAllWindows(app, settings) { + let windows = getInterestingWindows(app, settings); + for (let i = 0; i < windows.length; i++) + windows[i].delete(global.get_current_time()); +} + +// Filter out unnecessary windows, for instance +// nautilus desktop window. +function getInterestingWindows(app, settings) { + let windows = app.get_windows().filter(function(w) { + return !w.skip_taskbar; + }); + + // When using workspace isolation, we filter out windows + // that are not in the current workspace + if (settings.get_boolean('isolate-workspaces')) + windows = windows.filter(function(w) { + return w.get_workspace().index() == global.screen.get_active_workspace_index(); + }); + + return windows; +} + + +// define first this function to use it in extendDashItemContainer +function ItemShowLabel() { + if (!this._labelText) + return; + + this.label.set_text(this._labelText); + this.label.opacity = 0; + this.label.show(); + + let [stageX, stageY] = this.get_transformed_position(); + let node = this.label.get_theme_node(); + + let itemWidth = this.allocation.x2 - this.allocation.x1; + let itemHeight = this.allocation.y2 - this.allocation.y1; + + let labelWidth = this.label.get_width(); + let labelHeight = this.label.get_height(); + + let x, y, xOffset, yOffset; + + let position = Taskbar.getPosition(); + let labelOffset = node.get_length('-x-offset'); + + switch(position) { + case St.Side.TOP: + y = stageY + labelOffset + itemHeight; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + case St.Side.BOTTOM: + yOffset = labelOffset; + y = stageY - labelHeight - yOffset; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + } + + // keep the label inside the screen border + // Only needed for the x coordinate. + + // Leave a few pixel gap + let gap = LABEL_GAP; + let monitor = Main.layoutManager.findMonitorForActor(this); + if ( x - monitor.x < gap) + x += monitor.x - x + labelOffset; + else if ( x + labelWidth > monitor.x + monitor.width - gap) + x -= x + labelWidth -( monitor.x + monitor.width) + gap; + + this.label.set_position(x, y); + Tweener.addTween(this.label, + { opacity: 255, + time: DASH_ITEM_LABEL_SHOW_TIME, + transition: 'easeOutQuad', + }); +}; \ No newline at end of file diff --git a/secondaryMenu.js b/secondaryMenu.js index e956e52..dd4d9f8 100644 --- a/secondaryMenu.js +++ b/secondaryMenu.js @@ -31,6 +31,8 @@ const RemoteMenu = imports.ui.remoteMenu; const PopupMenu = imports.ui.popupMenu; const Shell = imports.gi.Shell; const AppFavorites = imports.ui.appFavorites; +const Convenience = Me.imports.convenience; +const AppIcons = Me.imports.appIcons; /** * Extend AppIconMenu @@ -235,7 +237,7 @@ _redisplay: function() { // quit menu let app = this._source.app; - let count = Taskbar.getInterestingWindows(app, this._dtpSettings).length; + let count = AppIcons.getInterestingWindows(app, this._dtpSettings).length; if ( count > 0) { this._appendSeparator(); let quitFromTaskbarMenuText = ""; diff --git a/taskbar.js b/taskbar.js index c84e98d..cb0b758 100644 --- a/taskbar.js +++ b/taskbar.js @@ -48,38 +48,17 @@ const Me = imports.misc.extensionUtils.getCurrentExtension(); const Convenience = Me.imports.convenience; const SecondaryMenu = Me.imports.secondaryMenu; const WindowPreview = Me.imports.windowPreview; +const AppIcons = Me.imports.appIcons; let DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; let DASH_ITEM_LABEL_SHOW_TIME = Dash.DASH_ITEM_LABEL_SHOW_TIME; let DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; let DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; -let LABEL_GAP = 5; let HFADE_WIDTH = 48; -let DOT_STYLE = { - DOTS: "DOTS", - SQUARES: "SQUARES", - DASHES: "DASHES", - SEGMENTED: "SEGMENTED", - CILIORA: "CILIORA", - METRO: "METRO", - SOLID: "SOLID" -} - -let DOT_POSITION = { - TOP: "TOP", - BOTTOM: "BOTTOM" -} - -let recentlyClickedAppLoopId = 0; -let recentlyClickedApp = null; -let recentlyClickedAppWindows = null; -let recentlyClickedAppIndex = 0; - function getPosition() { return Main.layoutManager.panelBox.anchor_y == 0 ? St.Side.TOP : St.Side.BOTTOM; } - /** * Extend DashItemContainer * @@ -89,64 +68,8 @@ function getPosition() { * thus use this ugly pattern. */ -// define first this function to use it in extendDashItemContainer -function ItemShowLabel() { - if (!this._labelText) - return; - - this.label.set_text(this._labelText); - this.label.opacity = 0; - this.label.show(); - - let [stageX, stageY] = this.get_transformed_position(); - let node = this.label.get_theme_node(); - - let itemWidth = this.allocation.x2 - this.allocation.x1; - let itemHeight = this.allocation.y2 - this.allocation.y1; - - let labelWidth = this.label.get_width(); - let labelHeight = this.label.get_height(); - - let x, y, xOffset, yOffset; - - let position = getPosition(); - let labelOffset = node.get_length('-x-offset'); - - switch(position) { - case St.Side.TOP: - y = stageY + labelOffset + itemHeight; - xOffset = Math.floor((itemWidth - labelWidth) / 2); - x = stageX + xOffset; - break; - case St.Side.BOTTOM: - yOffset = labelOffset; - y = stageY - labelHeight - yOffset; - xOffset = Math.floor((itemWidth - labelWidth) / 2); - x = stageX + xOffset; - break; - } - - // keep the label inside the screen border - // Only needed for the x coordinate. - - // Leave a few pixel gap - let gap = LABEL_GAP; - let monitor = Main.layoutManager.findMonitorForActor(this); - if ( x - monitor.x < gap) - x += monitor.x - x + labelOffset; - else if ( x + labelWidth > monitor.x + monitor.width - gap) - x -= x + labelWidth -( monitor.x + monitor.width) + gap; - - this.label.set_position(x, y); - Tweener.addTween(this.label, - { opacity: 255, - time: DASH_ITEM_LABEL_SHOW_TIME, - transition: 'easeOutQuad', - }); -}; - function extendDashItemContainer(dashItemContainer) { - dashItemContainer.showLabel = ItemShowLabel; + dashItemContainer.showLabel = AppIcons.ItemShowLabel; // override show so we know when an animation is occurring to suppress indicator animations dashItemContainer.show = Lang.bind(dashItemContainer, function(animate) { @@ -295,7 +218,7 @@ const taskbar = new Lang.Class({ this._scrollView.add_actor(this._box); this._showAppsIcon = new Dash.ShowAppsIcon(); - this._showAppsIcon.showLabel = ItemShowLabel; + this._showAppsIcon.showLabel = AppIcons.ItemShowLabel; this.showAppsButton = this._showAppsIcon.toggleButton; this._showAppsIcon.actor = this.showAppsButton; @@ -514,7 +437,7 @@ const taskbar = new Lang.Class({ }, _createAppItem: function(app) { - let appIcon = new taskbarAppIcon(this._dtpSettings, app, + let appIcon = new AppIcons.taskbarAppIcon(this._dtpSettings, app, { setSizeManually: true, showLabel: false }); @@ -777,7 +700,7 @@ const taskbar = new Lang.Class({ // the current workspace let settings = this._dtpSettings; running = running.filter(function(_app) { - return getInterestingWindows(_app, settings).length != 0; + return AppIcons.getInterestingWindows(_app, settings).length != 0; }); } @@ -1255,756 +1178,6 @@ const taskbar = new Lang.Class({ Signals.addSignalMethods(taskbar.prototype); - -/** - * Extend AppIcon - * - * - Apply a css class based on the number of windows of each application (#N); - * - Draw a dot for each window of the application based on the default "dot" style which is hidden (#N); - * a class of the form "running#N" is applied to the AppWellIcon actor. - * like the original .running one. - * - add a .focused style to the focused app - * - Customize click actions. - * - Update minimization animation target - * - */ - -let tracker = Shell.WindowTracker.get_default(); - -const taskbarAppIcon = new Lang.Class({ - Name: 'DashToPanel.TaskbarAppIcon', - Extends: AppDisplay.AppIcon, - - _init: function(settings, app, iconParams, onActivateOverride) { - - // a prefix is required to avoid conflicting with the parent class variable - this._dtpSettings = settings; - this._nWindows = 0; - - this.parent(app, iconParams, onActivateOverride); - - this._dot.set_width(0); - this._focused = tracker.focus_app == this.app; - - // Monitor windows-changes instead of app state. - // Keep using the same Id and function callback (that is extended) - if(this._stateChangedId > 0) { - this.app.disconnect(this._stateChangedId); - this._stateChangedId = 0; - } - - this._stateChangedId = this.app.connect('windows-changed', - Lang.bind(this, this.onWindowsChanged)); - this._focuseAppChangeId = tracker.connect('notify::focus-app', - Lang.bind(this, this._onFocusAppChanged)); - - this._focusedDots = null; - this._unfocusedDots = null; - - this._showDots(); - - this._dtpSettings.connect('changed::dot-position', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-size', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-style-focused', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-style-unfocused', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-override', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-1', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-2', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-3', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-4', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-unfocused-different', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-unfocused-1', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-unfocused-2', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-unfocused-3', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::dot-color-unfocused-4', Lang.bind(this, this._settingsChangeRefresh)); - this._dtpSettings.connect('changed::focus-highlight', Lang.bind(this, this._settingsChangeRefresh)); - - this._dtpSettings.connect('changed::appicon-margin', Lang.bind(this, this._setIconStyle)); - - // Creating a new menu manager for window previews as adding it to the - // using the secondary menu's menu manager (which uses the "ignoreRelease" - // function) caused the extension to crash. - this.menuManagerWindowPreview = new PopupMenu.PopupMenuManager(this); - - this.windowPreview = new WindowPreview.thumbnailPreviewMenu(this, this._dtpSettings, this.menuManagerWindowPreview); - - this.windowPreview.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { - if (!isPoppedUp) - this._onMenuPoppedDown(); - })); - this.menuManagerWindowPreview.addMenu(this.windowPreview); - - // grabHelper.grab() is usually called when the menu is opened. However, there seems to be a bug in the - // underlying gnome-shell that causes all window contents to freeze if the grab and ungrab occur - // in quick succession in timeouts from the Mainloop (for example, clicking the icon as the preview window is opening) - // So, instead wait until the mouse is leaving the icon (and might be moving toward the open window) to trigger the grab - // in windowPreview.js - let windowPreviewMenuData = this.menuManagerWindowPreview._menus[this.menuManagerWindowPreview._findMenu(this.windowPreview)]; - this.windowPreview.disconnect(windowPreviewMenuData.openStateChangeId); - windowPreviewMenuData.openStateChangeId = this.windowPreview.connect('open-state-changed', Lang.bind(this.menuManagerWindowPreview, function(menu, open) { - if (open) { - if (this.activeMenu) - this.activeMenu.close(BoxPointer.PopupAnimation.FADE); - - // don't grab here, we are grabbing in onLeave in windowPreview.js - //this._grabHelper.grab({ actor: menu.actor, focus: menu.sourceActor, onUngrab: Lang.bind(this, this._closeMenu, menu) }); - } else { - this._grabHelper.ungrab({ actor: menu.actor }); - } - })); - - this.forcedOverview = false; - - this._numberOverlay(); - }, - - shouldShowTooltip: function() { - if (this._dtpSettings.get_boolean("show-window-previews") && - getInterestingWindows(this.app, this._dtpSettings).length > 0) { - return false; - } else { - return this.actor.hover && (!this._menu || !this._menu.isOpen) && (!this.windowPreview || !this.windowPreview.isOpen); - } - }, - - _onDestroy: function() { - this.parent(); - - // Disconect global signals - // stateChangedId is already handled by parent) - if(this._focusAppId>0) - tracker.disconnect(this._focusAppId); - }, - - onWindowsChanged: function() { - this._updateCounterClass(); - this.updateIconGeometry(); - }, - - // Update taraget for minimization animation - updateIconGeometry: function() { - - // If (for unknown reason) the actor is not on the stage the reported size - // and position are random values, which might exceeds the integer range - // resulting in an error when assigned to the a rect. This is a more like - // a workaround to prevent flooding the system with errors. - if (this.actor.get_stage() == null) - return - - let rect = new Meta.Rectangle(); - - [rect.x, rect.y] = this.actor.get_transformed_position(); - [rect.width, rect.height] = this.actor.get_transformed_size(); - - let windows = this.app.get_windows(); - windows.forEach(function(w) { - w.set_icon_geometry(rect); - }); - - }, - - _showDots: function() { - // Just update style if dots already exist - if (this._focusedDots && this._unfocusedDots) { - this._updateCounterClass(); - return; - } - - this._focusedDots = new St.DrawingArea({width:1, y_expand: true}); - this._unfocusedDots = new St.DrawingArea({width:1, y_expand: true}); - - this._focusedDots.connect('repaint', Lang.bind(this, function() { - if(this._dashItemContainer.animatingIn || this._dashItemContainer.animatingOut) { - // don't draw and trigger more animations if the icon is in the middle of - // being added to the panel - return; - } - this._drawRunningIndicator(this._focusedDots, this._dtpSettings.get_string('dot-style-focused'), true); - this._displayProperIndicator(); - })); - - this._unfocusedDots.connect('repaint', Lang.bind(this, function() { - if(this._dashItemContainer.animatingIn || this._dashItemContainer.animatingOut) { - // don't draw and trigger more animations if the icon is in the middle of - // being added to the panel - return; - } - this._drawRunningIndicator(this._unfocusedDots, this._dtpSettings.get_string('dot-style-unfocused'), false); - this._displayProperIndicator(); - })); - - - this._iconContainer.add_child(this._focusedDots); - this._iconContainer.add_child(this._unfocusedDots); - - this._updateCounterClass(); - }, - - _settingsChangeRefresh: function() { - this._updateCounterClass(); - this._focusedDots.queue_repaint(); - this._unfocusedDots.queue_repaint(); - this._displayProperIndicator(true); - }, - - _setIconStyle: function() { - let margin = this._dtpSettings.get_int('appicon-margin'); - let inlineStyle = 'margin: 0 ' + margin + 'px;'; - - if(this._dtpSettings.get_boolean('focus-highlight') && tracker.focus_app == this.app && !this._isThemeProvidingIndicator()) { - let containerWidth = this._iconContainer.get_width() / St.ThemeContext.get_for_stage(global.stage).scale_factor; - let focusedDotStyle = this._dtpSettings.get_string('dot-style-focused'); - let isWide = this._isWideDotStyle(focusedDotStyle); - let pos = this._dtpSettings.get_string('dot-position'); - let highlightMargin = isWide ? this._dtpSettings.get_int('dot-size') : 0; - - if(focusedDotStyle == DOT_STYLE.CILIORA || focusedDotStyle == DOT_STYLE.SEGMENTED) - highlightMargin += 1; - - inlineStyle += "background-image: url('" + - Me.path + "/img/highlight_" + - ((this._nWindows > 1 && focusedDotStyle == DOT_STYLE.METRO) ? "stacked_" : "") + - "bg.svg'); background-position: 0 " + - (pos == DOT_POSITION.TOP ? highlightMargin : 0) + - "px; background-size: " + - containerWidth + "px " + - (containerWidth - (pos == DOT_POSITION.BOTTOM ? highlightMargin : 0)) + "px;"; - } - - // graphical glitches if i dont set this on a timeout - if(this.actor.get_style() != inlineStyle) - Mainloop.timeout_add(0, Lang.bind(this, function() { this.actor.set_style(inlineStyle); })); - }, - - popupMenu: function() { - this._removeMenuTimeout(); - this.actor.fake_release(); - this._draggable.fakeRelease(); - - if (!this._menu) { - this._menu = new SecondaryMenu.taskbarSecondaryMenu(this, this._dtpSettings); - this._menu.connect('activate-window', Lang.bind(this, function (menu, window) { - this.activateWindow(window, this._dtpSettings); - })); - this._menu.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { - if (!isPoppedUp) - this._onMenuPoppedDown(); - })); - let id = Main.overview.connect('hiding', Lang.bind(this, function () { this._menu.close(); })); - this._menu.actor.connect('destroy', function() { - Main.overview.disconnect(id); - }); - - this._menuManager.addMenu(this._menu); - } - - this.emit('menu-state-changed', true); - - this.windowPreview.close(); - - this.actor.set_hover(true); - this._menu.actor.add_style_class_name('dashtopanelSecondaryMenu'); - this._menu.popup(); - this._menuManager.ignoreRelease(); - this.emit('sync-tooltip'); - - return false; - }, - - _onFocusAppChanged: function(windowTracker) { - this._displayProperIndicator(); - }, - - _displayProperIndicator: function (force) { - let containerWidth = this._iconContainer.get_width(); - let isFocused = (tracker.focus_app == this.app); - let focusedDotStyle = this._dtpSettings.get_string('dot-style-focused'); - let unfocusedDotStyle = this._dtpSettings.get_string('dot-style-unfocused'); - let focusedIsWide = this._isWideDotStyle(focusedDotStyle); - let unfocusedIsWide = this._isWideDotStyle(unfocusedDotStyle); - - this._setIconStyle(); - - let newFocusedDotsWidth = 0; - let newFocusedDotsOpacity = 0; - let newUnfocusedDotsWidth = 0; - let newUnfocusedDotsOpacity = 0; - - - if(isFocused) - this.actor.add_style_class_name('focused'); - else - this.actor.remove_style_class_name('focused'); - - if(focusedIsWide) { - newFocusedDotsWidth = (isFocused && this._nWindows > 0) ? containerWidth : 0; - newFocusedDotsOpacity = 255; - } else { - newFocusedDotsWidth = containerWidth; - newFocusedDotsOpacity = (isFocused && this._nWindows > 0) ? 255 : 0; - } - - if(unfocusedIsWide) { - newUnfocusedDotsWidth = (!isFocused && this._nWindows > 0) ? containerWidth : 0; - newUnfocusedDotsOpacity = 255; - } else { - newUnfocusedDotsWidth = containerWidth; - newUnfocusedDotsOpacity = (!isFocused && this._nWindows > 0) ? 255 : 0; - } - - // Only animate if... - // animation is enabled in settings - // AND (going from a wide style to a narrow style indicator or vice-versa - // OR going from an open app to a closed app or vice versa) - if(this._dtpSettings.get_boolean('animate-app-switch') && - ((focusedIsWide != unfocusedIsWide) || - (this._focusedDots.width != newUnfocusedDotsWidth || this._unfocusedDots.width != newFocusedDotsWidth))) { - this._animateDotDisplay(this._focusedDots, newFocusedDotsWidth, this._unfocusedDots, newUnfocusedDotsOpacity, force); - this._animateDotDisplay(this._unfocusedDots, newUnfocusedDotsWidth, this._focusedDots, newFocusedDotsOpacity, force); - } - else { - this._focusedDots.opacity = newFocusedDotsOpacity; - this._unfocusedDots.opacity = newUnfocusedDotsOpacity; - this._focusedDots.width = newFocusedDotsWidth; - this._unfocusedDots.width = newUnfocusedDotsWidth; - } - }, - - _animateDotDisplay: function (dots, newWidth, otherDots, newOtherOpacity, force) { - if((dots.width != newWidth && dots._tweeningToWidth !== newWidth) || force) { - dots._tweeningToWidth = newWidth; - Tweener.addTween(dots, - { width: newWidth, - time: DASH_ANIMATION_TIME, - transition: 'easeInOutCubic', - onStart: Lang.bind(this, function() { - if(newOtherOpacity == 0) - otherDots.opacity = newOtherOpacity; - }), - onComplete: Lang.bind(this, function() { - if(newOtherOpacity > 0) - otherDots.opacity = newOtherOpacity; - dots._tweeningToWidth = null; - }) - }); - } - }, - - _isWideDotStyle: function(dotStyle) { - return dotStyle == DOT_STYLE.SEGMENTED || - dotStyle == DOT_STYLE.CILIORA || - dotStyle == DOT_STYLE.METRO || - dotStyle == DOT_STYLE.SOLID; - }, - - _isThemeProvidingIndicator: function () { - // This is an attempt to determine if the theme is providing their own - // running indicator by way of a border image on the icon, for example in - // the theme Ciliora - return (this.icon.actor.get_stage() && - this.icon.actor.get_theme_node().get_border_image()); - }, - - activate: function(button) { - this.windowPreview.requestCloseMenu(); - - let event = Clutter.get_current_event(); - let modifiers = event ? event.get_state() : 0; - let focusedApp = tracker.focus_app; - - // Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.) - modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK); - - // We don't change the CTRL-click behaviour: in such case we just chain - // up the parent method and return. - if (modifiers & Clutter.ModifierType.CONTROL_MASK) { - // Keep default behaviour: launch new window - // By calling the parent method I make it compatible - // with other extensions tweaking ctrl + click - this.parent(button); - return; - } - - // We check what type of click we have and if the modifier SHIFT is - // being used. We then define what buttonAction should be for this - // event. - let buttonAction = 0; - if (button && button == 2 ) { - if (modifiers & Clutter.ModifierType.SHIFT_MASK) - buttonAction = this._dtpSettings.get_string('shift-middle-click-action'); - else - buttonAction = this._dtpSettings.get_string('middle-click-action'); - } - else if (button && button == 1) { - if (modifiers & Clutter.ModifierType.SHIFT_MASK) - buttonAction = this._dtpSettings.get_string('shift-click-action'); - else - buttonAction = this._dtpSettings.get_string('click-action'); - } - - // We check if the app is running, and that the # of windows is > 0 in - // case we use workspace isolation, - let appIsRunning = this.app.state == Shell.AppState.RUNNING - && getInterestingWindows(this.app, this._dtpSettings).length > 0 - - // We customize the action only when the application is already running - if (appIsRunning) { - switch (buttonAction) { - case "RAISE": - activateAllWindows(this.app, this._dtpSettings); - break; - - case "LAUNCH": - if(this._dtpSettings.get_boolean('animate-window-launch')) - this.animateLaunch(); - this.app.open_new_window(-1); - break; - - case "MINIMIZE": - // In overview just activate the app, unless the acion is explicitely - // requested with a keyboard modifier - if (!Main.overview._shown || modifiers){ - // If we have button=2 or a modifier, allow minimization even if - // the app is not focused - if (this.app == focusedApp || button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK) { - // minimize all windows on double click and always in the case of primary click without - // additional modifiers - let click_count = 0; - if (Clutter.EventType.CLUTTER_BUTTON_PRESS) - click_count = event.get_click_count(); - let all_windows = (button == 1 && ! modifiers) || click_count > 1; - minimizeWindow(this.app, all_windows, this._dtpSettings); - } - else - activateAllWindows(this.app, this._dtpSettings); - } - else - this.app.activate(); - break; - - case "CYCLE": - if (!Main.overview._shown){ - if (this.app == focusedApp) - cycleThroughWindows(this.app, this._dtpSettings, false, false); - else { - activateFirstWindow(this.app, this._dtpSettings); - } - } - else - this.app.activate(); - break; - case "CYCLE-MIN": - if (!Main.overview._shown){ - if (this.app == focusedApp || - (recentlyClickedApp == this.app && recentlyClickedAppWindows[recentlyClickedAppIndex % recentlyClickedAppWindows.length] == "MINIMIZE")) - cycleThroughWindows(this.app, this._dtpSettings, false, true); - else { - activateFirstWindow(this.app, this._dtpSettings); - } - } - else - this.app.activate(); - break; - - case "QUIT": - closeAllWindows(this.app, this._dtpSettings); - break; - } - } - else { - if(this._dtpSettings.get_boolean('animate-window-launch')) - this.animateLaunch(); - this.app.open_new_window(-1); - } - - Main.overview.hide(); - }, - - _updateCounterClass: function() { - let maxN = 4; - this._nWindows = Math.min(getInterestingWindows(this.app, this._dtpSettings).length, maxN); - - for (let i = 1; i <= maxN; i++){ - let className = 'running'+i; - if(i != this._nWindows) - this.actor.remove_style_class_name(className); - else - this.actor.add_style_class_name(className); - } - }, - - _drawRunningIndicator: function(area, type, isFocused) { - let bodyColor; - if(this._dtpSettings.get_boolean('dot-color-override')) { - let dotColorSettingPrefix = 'dot-color-'; - if(!isFocused && this._dtpSettings.get_boolean('dot-color-unfocused-different')) - dotColorSettingPrefix = 'dot-color-unfocused-'; - bodyColor = Clutter.color_from_string(this._dtpSettings.get_string(dotColorSettingPrefix + (this._nWindows > 0 ? this._nWindows : 1)))[1]; - } else { - // Re-use the style - background color, and border width and color - - // of the default dot - let themeNode = this._dot.get_theme_node(); - bodyColor = themeNode.get_background_color(); - if(bodyColor.alpha == 0) // theme didn't provide one, use a default - bodyColor = new Clutter.Color({ red: 82, green: 148, blue: 226, alpha: 255 }); - } - - let [width, height] = area.get_surface_size(); - let cr = area.get_context(); - let n = this._nWindows; - let size = this._dtpSettings.get_int('dot-size') * St.ThemeContext.get_for_stage(global.stage).scale_factor; - let padding = 0; // distance from the margin - let yOffset = this._dtpSettings.get_string('dot-position') == DOT_POSITION.TOP ? 0 : (height - padding - size); - - if(type == DOT_STYLE.DOTS) { - // Draw the required numbers of dots - let radius = size/2; - let spacing = Math.ceil(width/18); // separation between the dots - - cr.translate((width - (2*n)*radius - (n-1)*spacing)/2, yOffset); - - Clutter.cairo_set_source_color(cr, bodyColor); - for (let i = 0; i < n; i++) { - cr.newSubPath(); - cr.arc((2*i+1)*radius + i*spacing, radius, radius, 0, 2*Math.PI); - } - cr.fill(); - } else if(type == DOT_STYLE.SQUARES) { - let spacing = Math.ceil(width/18); // separation between the dots - - cr.translate(Math.floor((width - n*size - (n-1)*spacing)/2), yOffset); - - Clutter.cairo_set_source_color(cr, bodyColor); - for (let i = 0; i < n; i++) { - cr.newSubPath(); - cr.rectangle(i*size + i*spacing, 0, size, size); - } - cr.fill(); - } else if(type == DOT_STYLE.DASHES) { - let spacing = Math.ceil(width/18); // separation between the dots - let dashLength = Math.floor(width/4) - spacing; - - cr.translate(Math.floor((width - n*dashLength - (n-1)*spacing)/2), yOffset); - - Clutter.cairo_set_source_color(cr, bodyColor); - for (let i = 0; i < n; i++) { - cr.newSubPath(); - cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); - } - cr.fill(); - } else if(type == DOT_STYLE.SEGMENTED) { - let spacing = Math.ceil(width/18); // separation between the dots - let dashLength = Math.ceil((width - ((n-1)*spacing))/n); - - cr.translate(0, yOffset); - - Clutter.cairo_set_source_color(cr, bodyColor); - for (let i = 0; i < n; i++) { - cr.newSubPath(); - cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); - } - cr.fill(); - } else if (type == DOT_STYLE.CILIORA) { - let spacing = size; // separation between the dots - let lineLength = width - (size*(n-1)) - (spacing*(n-1)); - - cr.translate(0, yOffset); - - Clutter.cairo_set_source_color(cr, bodyColor); - cr.newSubPath(); - cr.rectangle(0, 0, lineLength, size); - for (let i = 1; i < n; i++) { - cr.newSubPath(); - cr.rectangle(lineLength + (i*spacing) + ((i-1)*size), 0, size, size); - } - cr.fill(); - } else if (type == DOT_STYLE.METRO) { - if(n <= 1) { - cr.translate(0, yOffset); - Clutter.cairo_set_source_color(cr, bodyColor); - cr.newSubPath(); - cr.rectangle(0, 0, width, size); - cr.fill(); - } else { - let blackenedLength = (1/48)*width; // need to scale with the SVG for the stacked highlight - let darkenedLength = isFocused ? (2/48)*width : (10/48)*width; - let blackenedColor = bodyColor.shade(.3); - let darkenedColor = bodyColor.shade(.7); - - cr.translate(0, yOffset); - - Clutter.cairo_set_source_color(cr, bodyColor); - cr.newSubPath(); - cr.rectangle(0, 0, width - darkenedLength - blackenedLength, size); - cr.fill(); - Clutter.cairo_set_source_color(cr, blackenedColor); - cr.newSubPath(); - cr.rectangle(width - darkenedLength - blackenedLength, 0, 1, size); - cr.fill(); - Clutter.cairo_set_source_color(cr, darkenedColor); - cr.newSubPath(); - cr.rectangle(width - darkenedLength, 0, darkenedLength, size); - cr.fill(); - } - } else { // solid - cr.translate(0, yOffset); - Clutter.cairo_set_source_color(cr, bodyColor); - cr.newSubPath(); - cr.rectangle(0, 0, width, size); - cr.fill(); - } - - cr.$dispose(); - }, - - _numberOverlay: function() { - // Add label for a Hot-Key visual aid - this._numberOverlayLabel = new St.Label(); - this._numberOverlayBin = new St.Bin({ - child: this._numberOverlayLabel, - x_align: St.Align.START, y_align: St.Align.START, - x_expand: true, y_expand: true - }); - this._numberOverlayStyle = 'background-color: rgba(0,0,0,0.8);' - this._numberOverlayOrder = -1; - this._numberOverlayBin.hide(); - - this._iconContainer.add_child(this._numberOverlayBin); - - }, - - updateNumberOverlay: function() { - // Set the font size to something smaller than the whole icon so it is - // still visible. The border radius is large to make the shape circular - let [minWidth, natWidth] = this._iconContainer.get_preferred_width(-1); - let font_size = Math.round(Math.max(12, 0.3*natWidth)); - let size = Math.round(font_size*1.2); - this._numberOverlayLabel.set_style( - this._numberOverlayStyle + - 'font-size: ' + font_size + 'px;' + - 'text-align: center;' + - 'border-radius: ' + this.icon.iconSize + 'px;' + - 'width: ' + size + 'px; height: ' + size +'px;' - ); - }, - - setNumberOverlay: function(number) { - this._numberOverlayOrder = number; - this._numberOverlayLabel.set_text(number.toString()); - }, - - toggleNumberOverlay: function(activate) { - if (activate && this._numberOverlayOrder > -1) - this._numberOverlayBin.show(); - else - this._numberOverlayBin.hide(); - } - -}); - -function minimizeWindow(app, param, settings){ - // Param true make all app windows minimize - let windows = getInterestingWindows(app, settings); - let current_workspace = global.screen.get_active_workspace(); - for (let i = 0; i < windows.length; i++) { - let w = windows[i]; - if (w.get_workspace() == current_workspace && w.showing_on_its_workspace()){ - w.minimize(); - // Just minimize one window. By specification it should be the - // focused window on the current workspace. - if(!param) - break; - } - } -} - -/* - * By default only non minimized windows are activated. - * This activates all windows in the current workspace. - */ -function activateAllWindows(app, settings){ - - // First activate first window so workspace is switched if needed, - // then activate all other app windows in the current workspace. - let windows = getInterestingWindows(app, settings); - let w = windows[0]; - Main.activateWindow(w); - let activeWorkspace = global.screen.get_active_workspace_index(); - - if (windows.length <= 0) - return; - - let activatedWindows = 0; - - for (let i = windows.length - 1; i >= 0; i--){ - if (windows[i].get_workspace().index() == activeWorkspace){ - Main.activateWindow(windows[i]); - activatedWindows++; - } - } -} - -function activateFirstWindow(app, settings){ - - let windows = getInterestingWindows(app, settings); - Main.activateWindow(windows[0]); -} - -function cycleThroughWindows(app, settings, reversed, shouldMinimize) { - // Store for a little amount of time last clicked app and its windows - // since the order changes upon window interaction - let MEMORY_TIME=3000; - - let app_windows = getInterestingWindows(app, settings); - - if(shouldMinimize) - app_windows.push("MINIMIZE"); - - if (recentlyClickedAppLoopId > 0) - Mainloop.source_remove(recentlyClickedAppLoopId); - recentlyClickedAppLoopId = Mainloop.timeout_add(MEMORY_TIME, resetRecentlyClickedApp); - - // If there isn't already a list of windows for the current app, - // or the stored list is outdated, use the current windows list. - if (!recentlyClickedApp || - recentlyClickedApp.get_id() != app.get_id() || - recentlyClickedAppWindows.length != app_windows.length) { - recentlyClickedApp = app; - recentlyClickedAppWindows = app_windows; - recentlyClickedAppIndex = 0; - } - - if (reversed) { - recentlyClickedAppIndex--; - if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1; - } else { - recentlyClickedAppIndex++; - } - let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length; - - if(recentlyClickedAppWindows[index] === "MINIMIZE") - minimizeWindow(app, true, settings); - else - Main.activateWindow(recentlyClickedAppWindows[index]); -} - -function resetRecentlyClickedApp() { - if (recentlyClickedAppLoopId > 0) - Mainloop.source_remove(recentlyClickedAppLoopId); - recentlyClickedAppLoopId=0; - recentlyClickedApp =null; - recentlyClickedAppWindows = null; - recentlyClickedAppIndex = 0; - - return false; -} - -function closeAllWindows(app, settings) { - let windows = getInterestingWindows(app, settings); - for (let i = 0; i < windows.length; i++) - windows[i].delete(global.get_current_time()); -} - function getAppInterestingWindows(app, settings) { let windows = app.get_windows().filter(function(w) { return !w.skip_taskbar; @@ -2013,23 +1186,6 @@ function getAppInterestingWindows(app, settings) { return windows; } -// Filter out unnecessary windows, for instance -// nautilus desktop window. -function getInterestingWindows(app, settings) { - let windows = app.get_windows().filter(function(w) { - return !w.skip_taskbar; - }); - - // When using workspace isolation, we filter out windows - // that are not in the current workspace - if (settings.get_boolean('isolate-workspaces')) - windows = windows.filter(function(w) { - return w.get_workspace().index() == global.screen.get_active_workspace_index(); - }); - - return windows; -} - /* * 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 diff --git a/windowPreview.js b/windowPreview.js index 6ce5ce0..97e79d4 100644 --- a/windowPreview.js +++ b/windowPreview.js @@ -36,6 +36,8 @@ const Workspace = imports.ui.workspace; const Me = imports.misc.extensionUtils.getCurrentExtension(); const Taskbar = Me.imports.taskbar; +const Convenience = Me.imports.convenience; +const AppIcons = Me.imports.appIcons; let DEFAULT_THUMBNAIL_WIDTH = 350; let DEFAULT_THUMBNAIL_HEIGHT = 200; @@ -96,7 +98,7 @@ const thumbnailPreviewMenu = new Lang.Class({ }, popup: function() { - let windows = Taskbar.getInterestingWindows(this._app, this._dtpSettings); + let windows = AppIcons.getInterestingWindows(this._app, this._dtpSettings); if (windows.length > 0) { this._redisplay(); this.open(); @@ -592,7 +594,7 @@ const thumbnailPreviewList = new Lang.Class({ }, _redisplay: function () { - let windows = Taskbar.getInterestingWindows(this.app, this._dtpSettings).sort(this.sortWindowsCompareFunction); + let windows = AppIcons.getInterestingWindows(this.app, this._dtpSettings).sort(this.sortWindowsCompareFunction); let children = this.box.get_children().filter(function(actor) { return actor._delegate.window && actor._delegate.preview; });