/* * Taskbar: A taskbar extension for the Gnome panel. * Copyright (C) 2016 Zorin OS * * 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. * 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; 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 RUNNING_INDICATOR_SIZE = 3; 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. */ // 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; }; /* 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 */ const taskbarActor = new Lang.Class({ Name: '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: 'taskbar', 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) { let contentBox = box; let [appIcons] = actor.get_children(); let childBox = new Clutter.ActorBox(); childBox.x1 = contentBox.x1; childBox.y1 = contentBox.y1; childBox.x2 = contentBox.x2; childBox.y2 = contentBox.y2; appIcons.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. */ const baseIconSizes = [ 16, 22, 24, 32, 48, 64, 96, 128 ]; const taskbar = new Lang.Class({ Name: 'taskbar.taskbar', _init : function() { this._maxHeight = -1; this.iconSize = 32; this._availableIconSizes = baseIconSizes; this._shownInitially = false; this._position = getPosition(); this._signalsHandler = new Convenience.GlobalSignalsHandler(); this._dragPlaceholder = null; this._dragPlaceholderPos = -1; this._animatingPlaceholdersCount = 0; 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: 'taskbarScrollview', 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); 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 }); Main.panel.actor.connect('notify::height', Lang.bind(this, function() { this._queueRedisplay(); })); this.actor.connect('notify::width', Lang.bind(this, function() { if (this._maxHeight != this.actor.width) this._queueRedisplay(); this._maxHeight = this.actor.width; })); // 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( [ this._appSystem, 'installed-changed', Lang.bind(this, function() { AppFavorites.getAppFavorites().reload(); this._queueRedisplay(); }) ], [ AppFavorites.getAppFavorites(), 'changed', Lang.bind(this, this._queueRedisplay) ], [ this._appSystem, 'app-state-changed', Lang.bind(this, this._queueRedisplay) ], [ 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) ] ); }, destroy: function() { this._signalsHandler.destroy(); }, _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(); // If the scroll event is within a 1px margin from // the relevant edge of the actor, let the event propagate. if ((this._position == St.Side.TOP && event_y <= 1) || (this._position == St.Side.BOTTOM && event_y >= actor_h - 2)) return Clutter.EVENT_PROPAGATE; // 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(); DND.removeDragMonitor(this._dragMonitor); }, _onDragMotion: function(dragEvent) { let app = Dash.getAppFromSource(dragEvent.source); if (app == null) return DND.DragMotionResult.CONTINUE; if (!this._box.contains(dragEvent.targetActor)) this._clearDragPlaceholder(); 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; }, _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) { let appIcon = new taskbarAppIcon(app, { setSizeManually: true, showLabel: false }); if (appIcon._draggable) { appIcon._draggable.connect('drag-begin', Lang.bind(this, function() { appIcon.actor.opacity = 50; })); appIcon._draggable.connect('drag-end', Lang.bind(this, function() { appIcon.actor.opacity = 255; })); } 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.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; }, // 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) let iconChildren = this._box.get_children().filter(function(actor) { return actor.child && actor.child._delegate && actor.child._delegate.icon && !actor.animatingOut; }); let appIcons = iconChildren.map(function(actor){ return actor.child._delegate; }); return appIcons; }, _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._box.get_children().filter(function(actor) { return actor.child && actor.child._delegate && actor.child._delegate.icon && !actor.animatingOut; }); if (this._maxHeight == -1) return; let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; let iconSizes = this._availableIconSizes.map(function(s) { return s * scaleFactor; }); // 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() - (RUNNING_INDICATOR_SIZE * 2); let newIconSize = this._availableIconSizes[0]; for (let i = 0; i < iconSizes.length ; i++) { if (iconSizes[i] < availSize) { newIconSize = this._availableIconSizes[i]; } } if (newIconSize == this.iconSize) return; let oldIconSize = this.iconSize; this.iconSize = newIconSize; this.emit('icon-size-changed'); let scale = oldIconSize / newIconSize; 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) { let windowA = getAppInterestingWindows(appA)[0]; let windowB = getAppInterestingWindows(appB)[0]; return windowA.get_stable_sequence() > windowB.get_stable_sequence(); }, _redisplay: function () { let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); let running = this._appSystem.get_running().sort(this.sortAppsCompareFunction); let children = this._box.get_children().filter(function(actor) { return actor.child && actor.child._delegate && actor.child._delegate.app; }); // Apps currently in the taskbar let oldApps = children.map(function(actor) { return actor.child._delegate.app; }); // Apps supposed to be in the taskbar let newApps = []; // Adding favorites for (let id in favorites) newApps.push(favorites[id]); // Adding running apps for (let i = 0; i < running.length; i++) { let app = running[i]; if (app.get_id() in favorites) continue; newApps.push(app); } // Figure out the actual changes to the list of items; we iterate // over both the list of items currently in the taskbar and the list // of items expected there, and collect additions and removals. // Moves are both an addition and a removal, where the order of // the operations depends on whether we encounter the position // where the item has been added first or the one from where it // was removed. // There is an assumption that only one item is moved at a given // time; when moving several items at once, everything will still // end up at the right position, but there might be additional // additions/removals (e.g. it might remove all the launchers // and add them back in the new order even if a smaller set of // additions and removals is possible). // If above assumptions turns out to be a problem, we might need // to use a more sophisticated algorithm, e.g. Longest Common // Subsequence as used by diff. let addedItems = []; let removedActors = []; let newIndex = 0; let oldIndex = 0; while (newIndex < newApps.length || oldIndex < oldApps.length) { // No change at oldIndex/newIndex if (oldApps[oldIndex] == newApps[newIndex]) { oldIndex++; newIndex++; continue; } // App removed at oldIndex if (oldApps[oldIndex] && newApps.indexOf(oldApps[oldIndex]) == -1) { removedActors.push(children[oldIndex]); oldIndex++; continue; } // App added at newIndex if (newApps[newIndex] && oldApps.indexOf(newApps[newIndex]) == -1) { addedItems.push({ app: newApps[newIndex], item: this._createAppItem(newApps[newIndex]), pos: newIndex }); newIndex++; continue; } // App moved let insertHere = newApps[newIndex + 1] && newApps[newIndex + 1] == oldApps[oldIndex]; let alreadyRemoved = removedActors.reduce(function(result, actor) { let removedApp = actor.child._delegate.app; return result || removedApp == newApps[newIndex]; }, false); if (insertHere || alreadyRemoved) { let newItem = this._createAppItem(newApps[newIndex]); addedItems.push({ app: newApps[newIndex], item: newItem, pos: newIndex + removedActors.length }); newIndex++; } else { removedActors.push(children[oldIndex]); oldIndex++; } } for (let i = 0; i < addedItems.length; i++) this._box.insert_child_at_index(addedItems[i].item, addedItems[i].pos); for (let i = 0; i < removedActors.length; i++) { let item = removedActors[i]; item.animateOutAndDestroy(); } this._adjustIconSize(); for (let i = 0; i < addedItems.length; i++){ // Emit a custom signal notifying that a new item has been added this.emit('item-added', addedItems[i]); } // Skip animations on first run when adding the initial set // of items, to avoid all items zooming in at once let animate = this._shownInitially; if (!this._shownInitially) this._shownInitially = true; for (let i = 0; i < addedItems.length; i++) { addedItems[i].item.show(animate); } // 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(); }, // 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(); }, _clearDragPlaceholder: function() { if (this._dragPlaceholder) { this._animatingPlaceholdersCount++; this._dragPlaceholder.animateOutAndDestroy(); this._dragPlaceholder.connect('destroy', Lang.bind(this, function() { this._animatingPlaceholdersCount--; })); this._dragPlaceholder = null; } this._dragPlaceholderPos = -1; }, _clearEmptyDropTarget: function() { if (this._emptyDropTarget) { this._emptyDropTarget.animateOutAndDestroy(); this._emptyDropTarget = null; } }, handleDragOver : 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 DND.DragMotionResult.NO_DROP; if (!this._settings.is_writable('favorite-apps')) return DND.DragMotionResult.NO_DROP; let favorites = AppFavorites.getAppFavorites().getFavorites(); let numFavorites = favorites.length; let favPos = favorites.indexOf(app); let children = this._box.get_children(); let numChildren = children.length; let boxHeight = 0; for (let i = 0; i < numChildren; i++) { boxHeight += children[i].width; } // Keep the placeholder out of the index calculation; assuming that // the remove target has the same size as "normal" items, we don't // need to do the same adjustment there. if (this._dragPlaceholder) { boxHeight -= this._dragPlaceholder.width; numChildren--; } let pos; if (!this._emptyDropTarget){ pos = Math.floor(x * numChildren / boxHeight); if (pos > numChildren) pos = numChildren; } else pos = 0; // always insert at the top when taskbar is empty /* Take into account childredn position in rtl*/ if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) pos = numChildren - pos; if (pos != this._dragPlaceholderPos && pos <= numFavorites && this._animatingPlaceholdersCount == 0) { this._dragPlaceholderPos = pos; // Don't allow positioning before or after self if (favPos != -1 && (pos == favPos || pos == favPos + 1)) { this._clearDragPlaceholder(); return DND.DragMotionResult.CONTINUE; } // If the placeholder already exists, we just move // it, but if we are adding it, expand its size in // an animation let fadeIn; if (this._dragPlaceholder) { this._dragPlaceholder.destroy(); fadeIn = false; } else { fadeIn = true; } this._dragPlaceholder = new Dash.DragPlaceholderItem(); this._dragPlaceholder.child.set_width(this.iconSize); this._dragPlaceholder.child.set_height(this.iconSize); this._box.insert_child_at_index(this._dragPlaceholder, this._dragPlaceholderPos); this._dragPlaceholder.show(fadeIn); // 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]); } // Remove the drag placeholder if we are not in the // "favorites zone" if (pos > numFavorites) this._clearDragPlaceholder(); if (!this._dragPlaceholder) return DND.DragMotionResult.NO_DROP; let srcIsFavorite = (favPos != -1); if (srcIsFavorite) return DND.DragMotionResult.MOVE_DROP; return DND.DragMotionResult.COPY_DROP; }, // 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 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) 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; } }); 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: 'taskbar.AppIcon', Extends: AppDisplay.AppIcon, _init: function(app, iconParams, onActivateOverride) { 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._dots = null; this._showDots(); // 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._windowPreview.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { if (!isPoppedUp) this._onMenuPoppedDown(); })); this._menuManagerWindowPreview.addMenu(this._windowPreview); }, shouldShowTooltip: function() { let windows = getAppInterestingWindows(this.app); if (windows.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._updateRunningStyle(); 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._dots) { this._updateCounterClass(); return; } this._dots = new St.DrawingArea({x_expand: true, y_expand: true}); this._dots.connect('repaint', Lang.bind(this, function() { this._drawCircles(this._dots); this._onFocusAppChanged(); })); this._iconContainer.add_child(this._dots); this._updateCounterClass(); }, _updateRunningStyle: function() { this._updateCounterClass(); }, popupMenu: function() { this._removeMenuTimeout(); this.actor.fake_release(); this._draggable.fakeRelease(); if (!this._menu) { this._menu = new SecondaryMenu.taskbarSecondaryMenu(this); this._menu.connect('activate-window', Lang.bind(this, function (menu, window) { this.activateWindow(window); })); 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.shouldOpen = false; this._windowPreview.close(); this.actor.set_hover(true); this._menu.actor.add_style_class_name('taskbarSecondaryMenu'); this._menu.popup(); this._menuManager.ignoreRelease(); this.emit('sync-tooltip'); return false; }, _onFocusAppChanged: function() { if(tracker.focus_app == this.app) { this._dot.opacity = 255; Tweener.addTween(this._dot, { width: this._iconContainer.get_width(), height: RUNNING_INDICATOR_SIZE, time: DASH_ANIMATION_TIME, transition: 'easeInOutCubic', }); Tweener.addTween(this._dots, { opacity: 0, time: DASH_ANIMATION_TIME, transition: 'easeInOutCubic', }); } else { this._dot.opacity = 255; Tweener.addTween(this._dot, { width: 0, height: RUNNING_INDICATOR_SIZE, time: DASH_ANIMATION_TIME, transition: 'easeInOutCubic', }); Tweener.addTween(this._dots, { opacity: 255, time: DASH_ANIMATION_TIME, transition: 'easeInOutCubic', }); } }, activate: function(button) { this._windowPreview.shouldOpen = false; this._windowPreview.requestCloseMenu(); let event = Clutter.get_current_event(); let modifiers = event ? event.get_state() : 0; let openNewWindow = modifiers & Clutter.ModifierType.CONTROL_MASK && this.app.state == Shell.AppState.RUNNING || button && button == 2; let focusedApp = tracker.focus_app; if (this.app.state == Shell.AppState.STOPPED || openNewWindow) this.animateLaunch(); if (button && button == 1 && this.app.state == Shell.AppState.RUNNING) { 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.animateLaunch(); this.app.open_new_window(-1); return; } else if (this.app == focusedApp && !Main.overview._shown){ minimizeWindow(this.app, true); } else { // If click minimizes all, then one expects all windows to be reshown activateAllWindows(this.app); } } else { // Default behaviour if (openNewWindow) this.app.open_new_window(-1); else this.app.activate(); } Main.overview.hide(); }, _updateCounterClass: function() { let maxN = 4; this._nWindows = Math.min(getAppInterestingWindows(this.app).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); } if (this._dots) this._dots.queue_repaint(); }, _drawCircles: function(area) { // Re-use the style - background color, and border width and color - // of the default dot let themeNode = this._dot.get_theme_node(); let bodyColor = themeNode.get_background_color(); let [width, height] = area.get_surface_size(); let cr = area.get_context(); // Draw the required numbers of dots let radius = RUNNING_INDICATOR_SIZE/2; let padding = 0; // distance from the margin let spacing = width/22; // separation between the dots let n = this._nWindows; Clutter.cairo_set_source_color(cr, bodyColor); cr.translate((width - (2*n)*radius - (n-1)*spacing)/2, height- padding- 2*radius); 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(); cr.$dispose(); } }); function minimizeWindow(app, param){ // Param true make all app windows minimize let windows = getAppInterestingWindows(app); 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){ // First activate first window so workspace is switched if needed, // then activate all other app windows in the current workspace. let windows = getAppInterestingWindows(app); 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 getAppInterestingWindows(app) { // Filter out unnecessary windows, for instance // nautilus desktop window. let windows = app.get_windows().filter(function(w) { return !w.skip_taskbar; }); 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 * 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]; }