/* * 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 Utils = Me.imports.utils; const WindowPreview = Me.imports.windowPreview; const AppIcons = Me.imports.appIcons; var 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; var DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; let HFADE_WIDTH = 48; function getPosition() { return Main.layoutManager.panelBox.anchor_y == 0 ? St.Side.TOP : St.Side.BOTTOM; } /** * Extend DashItemContainer * * - set label position based on taskbar orientation * * I can't subclass the original object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973. * thus use this ugly pattern. */ function extendDashItemContainer(dashItemContainer) { dashItemContainer.showLabel = AppIcons.ItemShowLabel; }; /* This class is a fork of the upstream DashActor class (ui.dash.js) * * Summary of changes: * - modified chldBox calculations for when 'show-apps-at-top' option is checked * - handle horizontal dash */ function findIndex(array, predicate) { if (Array.prototype.findIndex) { return array.findIndex(predicate); } for (let i = 0, l = array.length; i < l; ++i) { if (predicate(array[i])) { return i; } } return -1; }; var taskbarActor = new Lang.Class({ Name: 'DashToPanel.TaskbarActor', _init: function() { this._rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; this._position = getPosition(); let layout = new Clutter.BoxLayout({ orientation: Clutter.Orientation.HORIZONTAL }); this.actor = new Shell.GenericContainer({ name: 'dashtopanelTaskbar', layout_manager: layout, clip_to_allocation: true }); this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); this.actor.connect('allocate', Lang.bind(this, this._allocate)); this.actor._delegate = this; }, _allocate: function(actor, box, flags) { this._isHorizontal = true; this._isAppAtLeft = true; let contentBox = box; let availWidth = contentBox.x2 - contentBox.x1; let availHeight = contentBox.y2 - contentBox.y1; let [appIcons, showAppsButton] = actor.get_children(); let [showAppsMinHeight, showAppsNatHeight] = showAppsButton.get_preferred_height(availWidth); let [showAppsMinWidth, showAppsNatWidth] = showAppsButton.get_preferred_width(availHeight); let childBox = new Clutter.ActorBox(); childBox.x1 = contentBox.x1 + showAppsNatWidth; childBox.y1 = contentBox.y1; childBox.x2 = contentBox.x2; childBox.y2 = contentBox.y2; appIcons.allocate(childBox, flags); childBox.y1 = contentBox.y1; childBox.x1 = contentBox.x1; childBox.x2 = contentBox.x1 + showAppsNatWidth; childBox.y2 = contentBox.y2; showAppsButton.allocate(childBox, flags); }, _getPreferredWidth: function(actor, forHeight, alloc) { // We want to request the natural height of all our children // as our natural height, so we chain up to StWidget (which // then calls BoxLayout) let [, natWidth] = this.actor.layout_manager.get_preferred_width(this.actor, forHeight); alloc.min_size = 0; alloc.natural_size = natWidth + HFADE_WIDTH; }, _getPreferredHeight: function(actor, forWidth, alloc) { // We want to request the natural height of all our children // as our natural height, so we chain up to StWidget (which // then calls BoxLayout) let [, natHeight] = this.actor.layout_manager.get_preferred_height(this.actor, forWidth); alloc.min_size = 0; alloc.natural_size = natHeight; } }); /* This class is a fork of the upstream dash class (ui.dash.js) * * Summary of changes: * - disconnect global signals adding a destroy method; * - play animations even when not in overview mode * - set a maximum icon size * - show running and/or favorite applications * - emit a custom signal when an app icon is added * - Add scrollview * Ensure actor is visible on keyfocus inside the scrollview * - add 128px icon size, might be useful for hidpi display * - Sync minimization application target position. */ var taskbar = new Lang.Class({ Name: 'DashToPanel.Taskbar', _init : function(settings) { this._dtpSettings = settings; // start at smallest size due to running indicator drawing area expanding but not shrinking this.iconSize = 16; this._shownInitially = false; this._position = getPosition(); this._signalsHandler = new Utils.GlobalSignalsHandler(); this._dragPlaceholder = null; this._dragPlaceholderPos = -1; this._showLabelTimeoutId = 0; this._resetHoverTimeoutId = 0; this._ensureAppIconVisibilityTimeoutId = 0; this._labelShowing = false; this._containerObject = new taskbarActor(); this._container = this._containerObject.actor; this._scrollView = new St.ScrollView({ name: 'dashtopanelScrollview', hscrollbar_policy: Gtk.PolicyType.NEVER, vscrollbar_policy: Gtk.PolicyType.NEVER, enable_mouse_scrolling: true }); this._scrollView.connect('scroll-event', Lang.bind(this, this._onScrollEvent )); this._box = new St.BoxLayout({ vertical: false, clip_to_allocation: false, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.START }); this._box._delegate = this; this._container.add_actor(this._scrollView); this._scrollView.add_actor(this._box); this._showAppsIcon = new Dash.ShowAppsIcon(); AppIcons.extendShowAppsIcon(this._showAppsIcon, this._dtpSettings); this.showAppsButton = this._showAppsIcon.toggleButton; this.showAppsButton.connect('notify::checked', Lang.bind(this, this._onShowAppsButtonToggled)); this._showAppsIcon.childScale = 1; this._showAppsIcon.childOpacity = 255; this._showAppsIcon.icon.setIconSize(this.iconSize); this._hookUpLabel(this._showAppsIcon); let appsIcon = this._showAppsIcon; appsIcon.connect('menu-state-changed', Lang.bind(this, function(appsIcon, opened) { this._itemMenuStateChanged(appsIcon, opened); })); this._container.add_actor(this._showAppsIcon); if (!this._dtpSettings.get_boolean('show-show-apps-button')) this.hideShowAppsButton(); let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; this.actor = new St.Bin({ child: this._container, y_align: St.Align.START, x_align:rtl?St.Align.END:St.Align.START }); // Update minimization animation target position on allocation of the // container and on scrollview change. this._box.connect('notify::allocation', Lang.bind(this, this._updateAppIconsGeometry)); let scrollViewAdjustment = this._scrollView.hscroll.adjustment; scrollViewAdjustment.connect('notify::value', Lang.bind(this, this._updateAppIconsGeometry)); this._workId = Main.initializeDeferredWork(this._box, Lang.bind(this, this._redisplay)); this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); this._appSystem = Shell.AppSystem.get_default(); this._signalsHandler.add( [ Main.panel.actor, 'notify::height', () => this._queueRedisplay() ], [ Main.panel.actor, 'notify::width', () => this._queueRedisplay() ], [ this._appSystem, 'installed-changed', Lang.bind(this, function() { AppFavorites.getAppFavorites().reload(); this._queueRedisplay(); }) ], [ AppFavorites.getAppFavorites(), 'changed', Lang.bind(this, this._queueRedisplay) ], [ global.window_manager, 'switch-workspace', () => this._connectWorkspaceSignals() ], [ Main.overview, 'item-drag-begin', Lang.bind(this, this._onDragBegin) ], [ Main.overview, 'item-drag-end', Lang.bind(this, this._onDragEnd) ], [ Main.overview, 'item-drag-cancelled', Lang.bind(this, this._onDragCancelled) ], [ // Ensure the ShowAppsButton status is kept in sync Main.overview.viewSelector._showAppsButton, 'notify::checked', Lang.bind(this, this._syncShowAppsButtonToggled) ], [ this._dtpSettings, 'changed::show-window-previews', Lang.bind(this, this._toggleWindowPreview) ] ); this.isGroupApps = this._dtpSettings.get_boolean('group-apps'); this._connectWorkspaceSignals(); this._bindSettingsChanges(); }, destroy: function() { this._signalsHandler.destroy(); this._signalsHandler = 0; this._container.destroy(); this._disconnectWorkspaceSignals(); }, _bindSettingsChanges: function () { this._dtpSettings.connect('changed::show-show-apps-button', Lang.bind(this, function() { if (this._dtpSettings.get_boolean('show-show-apps-button')) this.showShowAppsButton(); else this.hideShowAppsButton(); })); this._dtpSettings.connect('changed::dot-size', Lang.bind(this, this._redisplay)); this._dtpSettings.connect('changed::show-favorites', Lang.bind(this, this._redisplay)); this._dtpSettings.connect('changed::group-apps', Lang.bind(this, function() { this.isGroupApps = this._dtpSettings.get_boolean('group-apps'); this._connectWorkspaceSignals(); this.resetAppIcons(); })); this._dtpSettings.connect('changed::group-apps-use-launchers', Lang.bind(this, function() { this.resetAppIcons(); })); }, _onScrollEvent: function(actor, event) { // Event coordinates are relative to the stage but can be transformed // as the actor will only receive events within his bounds. let stage_x, stage_y, ok, event_x, event_y, actor_w, actor_h; [stage_x, stage_y] = event.get_coords(); [ok, event_x, event_y] = actor.transform_stage_point(stage_x, stage_y); [actor_w, actor_h] = actor.get_size(); // reset timeout to avid conflicts with the mousehover event if (this._ensureAppIconVisibilityTimeoutId>0) { Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId); this._ensureAppIconVisibilityTimeoutId = 0; } // Skip to avoid double events mouse if (event.is_pointer_emulated()) return Clutter.EVENT_STOP; let adjustment, delta; adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); let increment = adjustment.step_increment; switch ( event.get_scroll_direction() ) { case Clutter.ScrollDirection.UP: delta = -increment; break; case Clutter.ScrollDirection.DOWN: delta = +increment; break; case Clutter.ScrollDirection.SMOOTH: let [dx, dy] = event.get_scroll_delta(); delta = dy*increment; delta += dx*increment; break; } adjustment.set_value(adjustment.get_value() + delta); return Clutter.EVENT_STOP; }, _onDragBegin: function() { this._dragCancelled = false; this._dragMonitor = { dragMotion: Lang.bind(this, this._onDragMotion) }; DND.addDragMonitor(this._dragMonitor); if (this._box.get_n_children() == 0) { this._emptyDropTarget = new Dash.EmptyDropTargetItem(); this._box.insert_child_at_index(this._emptyDropTarget, 0); this._emptyDropTarget.show(true); } }, _onDragCancelled: function() { this._dragCancelled = true; this._endDrag(); }, _onDragEnd: function() { if (this._dragCancelled) return; this._endDrag(); }, _endDrag: function() { this._clearDragPlaceholder(); this._clearEmptyDropTarget(); this._showAppsIcon.setDragApp(null); DND.removeDragMonitor(this._dragMonitor); }, _onDragMotion: function(dragEvent) { let app = Dash.getAppFromSource(dragEvent.source); if (app == null) return DND.DragMotionResult.CONTINUE; let showAppsHovered = this._showAppsIcon.contains(dragEvent.targetActor); if (!this._box.contains(dragEvent.targetActor) || showAppsHovered) this._clearDragPlaceholder(); if (showAppsHovered) this._showAppsIcon.setDragApp(app); else this._showAppsIcon.setDragApp(null); return DND.DragMotionResult.CONTINUE; }, _appIdListToHash: function(apps) { let ids = {}; for (let i = 0; i < apps.length; i++) ids[apps[i].get_id()] = apps[i]; return ids; }, handleIsolatedWorkspaceSwitch: function() { if (this.isGroupApps) { this._queueRedisplay(); } else { this.resetAppIcons(); } }, _connectWorkspaceSignals: function() { this._disconnectWorkspaceSignals(); this._lastWorkspace = global.screen.get_active_workspace(); this._workspaceWindowAddedId = this._lastWorkspace.connect('window-added', () => this._queueRedisplay()); this._workspaceWindowRemovedId = this._lastWorkspace.connect('window-removed', () => this._queueRedisplay()); }, _disconnectWorkspaceSignals: function() { if (this._lastWorkspace) { this._lastWorkspace.disconnect(this._workspaceWindowAddedId); this._lastWorkspace.disconnect(this._workspaceWindowRemovedId); this._lastWorkspace = null; } }, _queueRedisplay: function () { Main.queueDeferredWork(this._workId); }, _hookUpLabel: function(item, appIcon) { item.child.connect('notify::hover', Lang.bind(this, function() { this._syncLabel(item, appIcon); })); if (appIcon) { appIcon.connect('sync-tooltip', Lang.bind(this, function() { this._syncLabel(item, appIcon); })); } }, _createAppItem: function(app, window, isLauncher) { let appIcon = new AppIcons.taskbarAppIcon( this._dtpSettings, { app: app, window: window, isLauncher: isLauncher }, { setSizeManually: true, showLabel: false } ); if (appIcon._draggable) { appIcon._draggable.connect('drag-begin', Lang.bind(this, function() { appIcon.actor.opacity = 50; appIcon.windowPreview.requestCloseMenu(); this._disableWindowPreview(); })); appIcon._draggable.connect('drag-end', Lang.bind(this, function() { appIcon.actor.opacity = 255; this._enableWindowPreview(); appIcon.syncWindowPreview(this._getAppIcons()); })); } appIcon.connect('menu-state-changed', Lang.bind(this, function(appIcon, opened) { this._itemMenuStateChanged(item, opened); })); let item = new Dash.DashItemContainer(); extendDashItemContainer(item); item.setChild(appIcon.actor); appIcon._dashItemContainer = item; appIcon.actor.connect('notify::hover', Lang.bind(this, function() { if (appIcon.actor.hover){ this._ensureAppIconVisibilityTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function(){ ensureActorVisibleInScrollView(this._scrollView, appIcon.actor); this._ensureAppIconVisibilityTimeoutId = 0; return GLib.SOURCE_REMOVE; })); } else { if (this._ensureAppIconVisibilityTimeoutId>0) { Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId); this._ensureAppIconVisibilityTimeoutId = 0; } } })); appIcon.actor.connect('clicked', Lang.bind(this, function(actor) { ensureActorVisibleInScrollView(this._scrollView, actor); })); appIcon.actor.connect('key-focus-in', Lang.bind(this, function(actor) { let [x_shift, y_shift] = ensureActorVisibleInScrollView(this._scrollView, actor); // This signal is triggered also by mouse click. The popup menu is opened at the original // coordinates. Thus correct for the shift which is going to be applied to the scrollview. if (appIcon._menu) { appIcon._menu._boxPointer.xOffset = -x_shift; appIcon._menu._boxPointer.yOffset = -y_shift; } })); // Override default AppIcon label_actor, now the // accessible_name is set at DashItemContainer.setLabelText appIcon.actor.label_actor = null; item.setLabelText(app.get_name()); appIcon.icon.setIconSize(this.iconSize); this._hookUpLabel(item, appIcon); return item; }, _toggleWindowPreview: function() { if (this._dtpSettings.get_boolean('show-window-previews')) this._enableWindowPreview(); else this._disableWindowPreview(); }, _enableWindowPreview: function() { let appIcons = this._getAppIcons(); appIcons.filter(appIcon => !appIcon.isLauncher) .forEach(function (appIcon) { appIcon.enableWindowPreview(appIcons); }); }, _disableWindowPreview: function() { let appIcons = this._getAppIcons(); appIcons.forEach(function (appIcon) { appIcon.disableWindowPreview(); }); }, // Return an array with the "proper" appIcons currently in the taskbar _getAppIcons: function() { // Only consider children which are "proper" // icons (i.e. ignoring drag placeholders) and which are not // animating out (which means they will be destroyed at the end of // the animation) return this._getTaskbarIcons().map(function(actor){ return actor.child._delegate; }); }, _getTaskbarIcons: function() { return this._box.get_children().filter(function(actor) { return actor.child && actor.child._delegate && actor.child._delegate.icon && !actor.animatingOut; }); }, _updateAppIconsGeometry: function() { let appIcons = this._getAppIcons(); appIcons.forEach(function(icon){ icon.updateIconGeometry(); }); }, _itemMenuStateChanged: function(item, opened) { // When the menu closes, it calls sync_hover, which means // that the notify::hover handler does everything we need to. if (opened) { if (this._showLabelTimeoutId > 0) { Mainloop.source_remove(this._showLabelTimeoutId); this._showLabelTimeoutId = 0; } item.hideLabel(); } else { // I want to listen from outside when a menu is closed. I used to // add a custom signal to the appIcon, since gnome 3.8 the signal // calling this callback was added upstream. this.emit('menu-closed'); } }, _syncLabel: function (item, appIcon) { let shouldShow = appIcon ? appIcon.shouldShowTooltip() : item.child.get_hover(); if (shouldShow) { if (this._showLabelTimeoutId == 0) { let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT; this._showLabelTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, function() { this._labelShowing = true; item.showLabel(); this._showLabelTimeoutId = 0; return GLib.SOURCE_REMOVE; })); GLib.Source.set_name_by_id(this._showLabelTimeoutId, '[gnome-shell] item.showLabel'); if (this._resetHoverTimeoutId > 0) { Mainloop.source_remove(this._resetHoverTimeoutId); this._resetHoverTimeoutId = 0; } } } else { if (this._showLabelTimeoutId > 0) Mainloop.source_remove(this._showLabelTimeoutId); this._showLabelTimeoutId = 0; item.hideLabel(); if (this._labelShowing) { this._resetHoverTimeoutId = Mainloop.timeout_add(DASH_ITEM_HOVER_TIMEOUT, Lang.bind(this, function() { this._labelShowing = false; this._resetHoverTimeoutId = 0; return GLib.SOURCE_REMOVE; })); GLib.Source.set_name_by_id(this._resetHoverTimeoutId, '[gnome-shell] this._labelShowing'); } } }, _adjustIconSize: function() { // For the icon size, we only consider children which are "proper" // icons (i.e. ignoring drag placeholders) and which are not // animating out (which means they will be destroyed at the end of // the animation) let iconChildren = this._getTaskbarIcons(); iconChildren.push(this._showAppsIcon); let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; // Getting the panel height and making sure that the icon padding is at // least the size of the app running indicator on both the top and bottom. let availSize = (Main.panel.actor.get_height() - (this._dtpSettings.get_int('dot-size') * scaleFactor * 2) - (this._dtpSettings.get_int('appicon-padding') * 2)) / scaleFactor; if (availSize == this.iconSize) return; if (availSize < 1) { availSize = 1; } let oldIconSize = this.iconSize; this.iconSize = availSize; this.emit('icon-size-changed'); let scale = oldIconSize / this.iconSize; for (let i = 0; i < iconChildren.length; i++) { let icon = iconChildren[i].child._delegate.icon; // Set the new size immediately, to keep the icons' sizes // in sync with this.iconSize icon.setIconSize(this.iconSize); // Don't animate the icon size change when the overview // is transitioning, or when initially filling // the taskbar if (Main.overview.animationInProgress || !this._shownInitially) continue; let [targetWidth, targetHeight] = icon.icon.get_size(); // Scale the icon's texture to the previous size and // tween to the new size icon.icon.set_size(icon.icon.width * scale, icon.icon.height * scale); Tweener.addTween(icon.icon, { width: targetWidth, height: targetHeight, time: DASH_ANIMATION_TIME, transition: 'easeOutQuad', }); } }, sortAppsCompareFunction: function(appA, appB) { return getAppStableSequence(appA) - getAppStableSequence(appB); }, sortWindowsCompareFunction: function(windowA, windowB) { return windowA.get_stable_sequence() - windowB.get_stable_sequence(); }, _redisplay: function () { if (!this._signalsHandler) { return; } let showFavorites = this._dtpSettings.get_boolean('show-favorites'); //get the currently displayed appIcons let currentAppIcons = this._getTaskbarIcons(); //get the user's favorite apps let favoriteApps = showFavorites ? AppFavorites.getAppFavorites().getFavorites() : []; //find the apps that should be in the taskbar: the favorites first, then add the running apps // When using isolation, we filter out apps that have no windows in // the current workspace (this check is done in AppIcons.getInterstingWindows) let runningApps = this._getRunningApps().sort(this.sortAppsCompareFunction); let expectedAppInfos; if (!this.isGroupApps && this._dtpSettings.get_boolean('group-apps-use-launchers')) { expectedAppInfos = this._createAppInfos(favoriteApps, [], true) .concat(this._createAppInfos(runningApps) .filter(appInfo => appInfo.windows.length)); } else { expectedAppInfos = this._createAppInfos(favoriteApps.concat(runningApps.filter(app => favoriteApps.indexOf(app) < 0))) .filter(appInfo => appInfo.windows.length || favoriteApps.indexOf(appInfo.app) >= 0); } //remove the appIcons which are not in the expected apps list for (let i = currentAppIcons.length - 1; i > -1; --i) { let appIcon = currentAppIcons[i].child._delegate; let appIndex = findIndex(expectedAppInfos, appInfo => appInfo.app == appIcon.app && appInfo.isLauncher == appIcon.isLauncher); if (appIndex < 0 || (appIcon.window && (this.isGroupApps || expectedAppInfos[appIndex].windows.indexOf(appIcon.window) < 0)) || (!appIcon.window && !appIcon.isLauncher && !this.isGroupApps && expectedAppInfos[appIndex].windows.length)) { currentAppIcons[i].animateOutAndDestroy(); currentAppIcons.splice(i, 1); } } //if needed, reorder the existing appIcons and create the missing ones let currentPosition = 0; for (let i = 0, l = expectedAppInfos.length; i < l; ++i) { let neededAppIcons = this.isGroupApps || !expectedAppInfos[i].windows.length ? [{ app: expectedAppInfos[i].app, window: null, isLauncher: expectedAppInfos[i].isLauncher }] : expectedAppInfos[i].windows.map(window => ({ app: expectedAppInfos[i].app, window: window, isLauncher: false })); for (let j = 0, ll = neededAppIcons.length; j < ll; ++j) { //check if the icon already exists let matchingAppIconIndex = findIndex(currentAppIcons, appIcon => appIcon.child._delegate.app == neededAppIcons[j].app && appIcon.child._delegate.window == neededAppIcons[j].window); if (matchingAppIconIndex > 0 && matchingAppIconIndex != currentPosition) { //moved icon, reposition it this._box.remove_child(currentAppIcons[matchingAppIconIndex]); this._box.insert_child_at_index(currentAppIcons[matchingAppIconIndex], currentPosition); } else if (matchingAppIconIndex < 0) { //the icon doesn't exist yet, create a new one let newAppIcon = this._createAppItem(neededAppIcons[j].app, neededAppIcons[j].window, neededAppIcons[j].isLauncher); this._box.insert_child_at_index(newAppIcon, currentPosition); currentAppIcons.splice(currentPosition, 0, newAppIcon); // Emit a custom signal notifying that a new item has been added this.emit('item-added', newAppIcon); // Skip animations on first run when adding the initial set // of items, to avoid all items zooming in at once newAppIcon.show(this._shownInitially); } ++currentPosition; } } this._adjustIconSize(); // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 // Without it, StBoxLayout may use a stale size cache this._box.queue_relayout(); // This is required for icon reordering when the scrollview is used. this._updateAppIconsGeometry(); // This will update the size, and the corresponding number for each icon this._updateNumberOverlay(); // Connect windows previews to hover events this._toggleWindowPreview(); this._shownInitially = true; }, _getRunningApps: function() { let tracker = Shell.WindowTracker.get_default(); let windows = global.get_window_actors(); let apps = []; for (let i = 0, l = windows.length; i < l; ++i) { let app = tracker.get_window_app(windows[i].metaWindow); if (app && apps.indexOf(app) < 0) { apps.push(app); } } return apps; }, _createAppInfos: function(apps, defaultWindows, defaultIsLauncher) { return apps.map(app => ({ app: app, isLauncher: defaultIsLauncher || false, windows: defaultWindows || AppIcons.getInterestingWindows(app, this._dtpSettings) .sort(this.sortWindowsCompareFunction) })); }, // Reset the displayed apps icon to mantain the correct order resetAppIcons : function() { let children = this._box.get_children().filter(function(actor) { return actor.child && actor.child._delegate && actor.child._delegate.icon; }); for (let i = 0; i < children.length; i++) { let item = children[i]; item.destroy(); } // to avoid ugly animations, just suppress them like when taskbar is first loaded. this._shownInitially = false; this._redisplay(); }, _updateNumberOverlay: function() { let appIcons = this._getAppIcons(); let counter = 1; appIcons.forEach(function(icon) { if (counter < 10){ icon.setNumberOverlay(counter); counter++; } else if (counter == 10) { icon.setNumberOverlay(0); counter++; } else { // No overlay after 10 icon.setNumberOverlay(-1); } icon.updateNumberOverlay(); }); if (this._dtpSettings.get_boolean('hot-keys') && this._dtpSettings.get_string('hotkeys-overlay-combo') === 'ALWAYS') this.toggleNumberOverlay(true); }, toggleNumberOverlay: function(activate) { let appIcons = this._getAppIcons(); appIcons.forEach(function(icon) { icon.toggleNumberOverlay(activate); }); }, _clearDragPlaceholder: function() { if (this._dragPlaceholder) { this._dragPlaceholder.destroy(); this._dragPlaceholder = null; } this._dragPlaceholderPos = -1; }, _clearEmptyDropTarget: function() { if (this._emptyDropTarget) { this._emptyDropTarget.animateOutAndDestroy(); this._emptyDropTarget = null; } }, handleDragOver : function(source, actor, x, y, time) { if (source == Main.xdndHandler) return DND.DragMotionResult.CONTINUE; // Don't allow favoriting of transient apps if (source.app == null || source.app.is_window_backed()) return DND.DragMotionResult.NO_DROP; if (!this._settings.is_writable('favorite-apps')) return DND.DragMotionResult.NO_DROP; let currentAppIcons = this._getAppIcons(); let hoveredAppIcon = currentAppIcons.filter(appIcon => x >= appIcon._dashItemContainer.x && x <= (appIcon._dashItemContainer.x + appIcon._dashItemContainer.width))[0]; if (hoveredAppIcon) { let hoveredIndex = currentAppIcons.indexOf(hoveredAppIcon); let sourceIndex = currentAppIcons.indexOf(source); let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; // Don't allow positioning on non favorites, same app, before or after self if (hoveredAppIcon.app == source.app || sourceIndex == hoveredIndex || sourceIndex + 1 == hoveredIndex || AppFavorites.getAppFavorites().getFavorites().indexOf(hoveredAppIcon.app) < 0 || (!rtl && (hoveredIndex - 1 >= 0 && currentAppIcons[hoveredIndex - 1].app == currentAppIcons[hoveredIndex].app)) || (rtl && (hoveredIndex + 1 < currentAppIcons.length && currentAppIcons[hoveredIndex + 1].app == currentAppIcons[hoveredIndex].app))) { this._clearDragPlaceholder(); } else if (hoveredIndex != this._dragPlaceholderPos) { if (!this._dragPlaceholder) { this._dragPlaceholder = new Dash.DragPlaceholderItem(); this._dragPlaceholder.show(); } else { this._box.remove_child(this._dragPlaceholder); } this._dragPlaceholder.child.set_width(this.iconSize); this._dragPlaceholder.child.set_height(this.iconSize); this._box.insert_child_at_index(this._dragPlaceholder, hoveredIndex); this._dragPlaceholderPos = hoveredIndex; // Ensure the next and previous icon are visible when moving the placeholder // (I assume there's room for both of them) if (this._dragPlaceholderPos > 1) ensureActorVisibleInScrollView(this._scrollView, this._box.get_children()[this._dragPlaceholderPos-1]); if (this._dragPlaceholderPos < this._box.get_children().length-1) ensureActorVisibleInScrollView(this._scrollView, this._box.get_children()[this._dragPlaceholderPos+1]); } } return this._dragPlaceholder ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.CONTINUE; }, // Draggable target interface acceptDrop : function(source, actor, x, y, time) { let app = Dash.getAppFromSource(source); // Don't allow favoriting of transient apps if (app == null || app.is_window_backed()) { return false; } if (!this._settings.is_writable('favorite-apps')) return false; let id = app.get_id(); let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); let seenFavorites = {}; let srcIsFavorite = (id in favorites); let favPos = 0; let children = this._box.get_children(); for (let i = 0; i < this._dragPlaceholderPos; i++) { if (this._dragPlaceholder && children[i] == this._dragPlaceholder) continue; let childId = children[i].child._delegate.app.get_id(); if (childId == id) continue; if (childId in favorites && !seenFavorites[childId]) { seenFavorites[childId] = favPos++; } } // No drag placeholder means we don't wan't to favorite the app // and we are dragging it to its original position if (!this._dragPlaceholder) return true; Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function () { let appFavorites = AppFavorites.getAppFavorites(); if (srcIsFavorite) appFavorites.moveFavoriteToPos(id, favPos); else appFavorites.addFavoriteAtPos(id, favPos); return false; })); return true; }, _onShowAppsButtonToggled: function() { // Sync the status of the default appButtons. Only if the two statuses are // different, that means the user interacted with the extension provided // application button, cutomize the behaviour. Otherwise the shell has changed the // status (due to the _syncShowAppsButtonToggled function below) and it // has already performed the desired action. let animate = this._dtpSettings.get_boolean('animate-show-apps'); let selector = Main.overview.viewSelector; if (selector._showAppsButton.checked !== this.showAppsButton.checked) { // find visible view let visibleView; Main.overview.viewSelector.appDisplay._views.every(function(v, index) { if (v.view.actor.visible) { visibleView = index; return false; } else return true; }); if (this.showAppsButton.checked) { // force spring animation triggering.By default the animation only // runs if we are already inside the overview. if (!Main.overview._shown) { this.forcedOverview = true; let view = Main.overview.viewSelector.appDisplay._views[visibleView].view; let grid = view._grid; if (animate) { // Animate in the the appview, hide the appGrid to avoiud flashing // Go to the appView before entering the overview, skipping the workspaces. // Do this manually avoiding opacity in transitions so that the setting of the opacity // to 0 doesn't get overwritten. Main.overview.viewSelector._activePage.opacity = 0; Main.overview.viewSelector._activePage.hide(); Main.overview.viewSelector._activePage = Main.overview.viewSelector._appsPage; Main.overview.viewSelector._activePage.show(); grid.actor.opacity = 0; // The animation has to be trigered manually because the AppDisplay.animate // method is waiting for an allocation not happening, as we skip the workspace view // and the appgrid could already be allocated from previous shown. // It has to be triggered after the overview is shown as wrong coordinates are obtained // otherwise. let overviewShownId = Main.overview.connect('shown', Lang.bind(this, function() { Main.overview.disconnect(overviewShownId); Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { grid.actor.opacity = 255; grid.animateSpring(IconGrid.AnimationDirection.IN, this.showAppsButton); })); })); } else { Main.overview.viewSelector._activePage = Main.overview.viewSelector._appsPage; Main.overview.viewSelector._activePage.show(); grid.actor.opacity = 255; } } // Finally show the overview selector._showAppsButton.checked = true; Main.overview.show(); } else { if (this.forcedOverview) { // force exiting overview if needed if (animate) { // Manually trigger springout animation without activating the // workspaceView to avoid the zoomout animation. Hide the appPage // onComplete to avoid ugly flashing of original icons. let view = Main.overview.viewSelector.appDisplay._views[visibleView].view; let grid = view._grid; view.animate(IconGrid.AnimationDirection.OUT, Lang.bind(this, function() { Main.overview.viewSelector._appsPage.hide(); Main.overview.hide(); selector._showAppsButton.checked = false; this.forcedOverview = false; })); } else { Main.overview.hide(); this.forcedOverview = false; } } else { selector._showAppsButton.checked = false; this.forcedOverview = false; } } } // whenever the button is unactivated even if not by the user still reset the // forcedOverview flag if (this.showAppsButton.checked == false) this.forcedOverview = false; }, _syncShowAppsButtonToggled: function() { let status = Main.overview.viewSelector._showAppsButton.checked; if (this.showAppsButton.checked !== status) this.showAppsButton.checked = status; }, showShowAppsButton: function() { this.showAppsButton.visible = true; this.showAppsButton.set_width(-1); this.showAppsButton.set_height(-1); }, hideShowAppsButton: function() { this.showAppsButton.hide(); this.showAppsButton.set_width(0); this.showAppsButton.set_height(0); }, popupFocusedAppSecondaryMenu: function() { let appIcons = this._getAppIcons(); for(let i in appIcons) { if(appIcons[i].app == tracker.focus_app) { let appIcon = appIcons[i]; if(appIcon._menu && appIcon._menu.isOpen) appIcon._menu.close(); else appIcons[i].popupMenu(); break; } } } }); Signals.addSignalMethods(taskbar.prototype); function getAppInterestingWindows(app, settings) { let windows = app.get_windows().filter(function(w) { return !w.skip_taskbar; }); return windows; } function getAppStableSequence(app) { let windows = getAppInterestingWindows(app); return windows.reduce((prevWindow, window) => { return Math.min(prevWindow, window.get_stable_sequence()); }, Infinity); } /* * This is a copy of the same function in utils.js, but also adjust horizontal scrolling * and perform few further cheks on the current value to avoid changing the values when * it would be clamp to the current one in any case. * Return the amount of shift applied */ function ensureActorVisibleInScrollView(scrollView, actor) { let adjust_v = true; let adjust_h = true; let vadjustment = scrollView.vscroll.adjustment; let hadjustment = scrollView.hscroll.adjustment; let [vvalue, vlower, vupper, vstepIncrement, vpageIncrement, vpageSize] = vadjustment.get_values(); let [hvalue, hlower, hupper, hstepIncrement, hpageIncrement, hpageSize] = hadjustment.get_values(); let [hvalue0, vvalue0] = [hvalue, vvalue]; let voffset = 0; let hoffset = 0; let fade = scrollView.get_effect("fade"); if (fade){ voffset = fade.vfade_offset; hoffset = fade.hfade_offset; } let box = actor.get_allocation_box(); let y1 = box.y1, y2 = box.y2, x1 = box.x1, x2 = box.x2; let parent = actor.get_parent(); while (parent != scrollView) { if (!parent) throw new Error("actor not in scroll view"); let box = parent.get_allocation_box(); y1 += box.y1; y2 += box.y1; x1 += box.x1; x2 += box.x1; parent = parent.get_parent(); } if (y1 < vvalue + voffset) vvalue = Math.max(0, y1 - voffset); else if (vvalue < vupper - vpageSize && y2 > vvalue + vpageSize - voffset) vvalue = Math.min(vupper -vpageSize, y2 + voffset - vpageSize); if (x1 < hvalue + hoffset) hvalue = Math.max(0, x1 - hoffset); else if (hvalue < hupper - hpageSize && x2 > hvalue + hpageSize - hoffset) hvalue = Math.min(hupper - hpageSize, x2 + hoffset - hpageSize); if (vvalue !== vvalue0) { Tweener.addTween(vadjustment, { value: vvalue, time: Util.SCROLL_TIME, transition: 'easeOutQuad' }); } if (hvalue !== hvalue0) { Tweener.addTween(hadjustment, { value: hvalue, time: Util.SCROLL_TIME, transition: 'easeOutQuad' }); } return [hvalue- hvalue0, vvalue - vvalue0]; }