// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Signals = imports.signals; 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 Util = imports.misc.util; const Workspace = imports.ui.workspace; const Me = imports.misc.extensionUtils.getCurrentExtension(); const Docking = Me.imports.docking; const Utils = Me.imports.utils; const AppIcons = Me.imports.appIcons; const Locations = Me.imports.locations; const DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; const DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; const DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; /** * Extend DashItemContainer * * - set label position based on dash orientation * */ let MyDashItemContainer = GObject.registerClass( class DashToDock_MyDashItemContainer extends Dash.DashItemContainer { showLabel() { return AppIcons.itemShowLabel.call(this); } }); /** * 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 */ var MyDashActor = GObject.registerClass( class DashToDock_MyDashActor extends St.Widget { _init() { // a prefix is required to avoid conflicting with the parent class variable this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); this._position = Utils.getPosition(); this._isHorizontal = ((this._position == St.Side.TOP) || (this._position == St.Side.BOTTOM)); let layout = new Clutter.BoxLayout({ orientation: this._isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL }); super._init({ name: 'dash', layout_manager: layout, clip_to_allocation: true }); // Since we are usually visible but not usually changing, make sure // most repaint requests don't actually require us to repaint anything. // This saves significant CPU when repainting the screen. this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); } vfunc_allocate(box, flags) { this.set_allocation(box, flags); let contentBox = box; let availWidth = contentBox.x2 - contentBox.x1; let availHeight = contentBox.y2 - contentBox.y1; let [appIcons, showAppsButton] = this.get_children(); let [showAppsMinHeight, showAppsNatHeight] = showAppsButton.get_preferred_height(availWidth); let [showAppsMinWidth, showAppsNatWidth] = showAppsButton.get_preferred_width(availHeight); let offset_x = this._isHorizontal?showAppsNatWidth:0; let offset_y = this._isHorizontal?0:showAppsNatHeight; let childBox = new Clutter.ActorBox(); let settings = Docking.DockManager.settings; if ((settings.get_boolean('show-apps-at-top') && !this._isHorizontal) || (settings.get_boolean('show-apps-at-top') && !this._rtl) || (!settings.get_boolean('show-apps-at-top') && this._isHorizontal && this._rtl)) { childBox.x1 = contentBox.x1 + offset_x; childBox.y1 = contentBox.y1 + offset_y; 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.y1 + showAppsNatHeight; showAppsButton.allocate(childBox, flags); } else { childBox.x1 = contentBox.x1; childBox.y1 = contentBox.y1; childBox.x2 = contentBox.x2 - offset_x; childBox.y2 = contentBox.y2 - offset_y; appIcons.allocate(childBox, flags); childBox.x2 = contentBox.x2; childBox.y2 = contentBox.y2; childBox.x1 = contentBox.x2 - showAppsNatWidth; childBox.y1 = contentBox.y2 - showAppsNatHeight; showAppsButton.allocate(childBox, flags); } } vfunc_get_preferred_width(forHeight) { // 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), but we only request the showApps // button as the minimum size let [, natWidth] = this.layout_manager.get_preferred_width(this, forHeight); let themeNode = this.get_theme_node(); let [, showAppsButton] = this.get_children(); let [minWidth, ] = showAppsButton.get_preferred_height(forHeight); return [minWidth, natWidth]; } vfunc_get_preferred_height(forWidth) { // 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), but we only request the showApps // button as the minimum size let [, natHeight] = this.layout_manager.get_preferred_height(this, forWidth); let themeNode = this.get_theme_node(); let [, showAppsButton] = this.get_children(); let [minHeight, ] = showAppsButton.get_preferred_height(forWidth); return [minHeight, natHeight]; } }); const baseIconSizes = [16, 22, 24, 32, 48, 64, 96, 128]; /** * 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 * - hide showApps label when the custom menu is shown. * - add scrollview * ensure actor is visible on keyfocus inseid the scrollview * - add 128px icon size, might be usefull for hidpi display * - sync minimization application target position. * - keep running apps ordered. */ var MyDash = GObject.registerClass({ Signals: { 'menu-closed': {}, 'icon-size-changed': {}, } }, class DashToDock_MyDash extends St.Bin { _init(remoteModel, monitorIndex) { // Initialize icon variables and size this._maxHeight = -1; this.iconSize = Docking.DockManager.settings.get_int('dash-max-icon-size'); this._availableIconSizes = baseIconSizes; this._shownInitially = false; this._initializeIconSize(this.iconSize); this._remoteModel = remoteModel; this._monitorIndex = monitorIndex; this._position = Utils.getPosition(); this._isHorizontal = ((this._position == St.Side.TOP) || (this._position == St.Side.BOTTOM)); this._signalsHandler = new Utils.GlobalSignalsHandler(); this._dragPlaceholder = null; this._dragPlaceholderPos = -1; this._animatingPlaceholdersCount = 0; this._showLabelTimeoutId = 0; this._resetHoverTimeoutId = 0; this._ensureAppIconVisibilityTimeoutId = 0; this._labelShowing = false; this._container = new MyDashActor(); this._scrollView = new St.ScrollView({ name: 'dashtodockDashScrollview', hscrollbar_policy: Gtk.PolicyType.NEVER, vscrollbar_policy: Gtk.PolicyType.NEVER, enable_mouse_scrolling: false }); this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this)); this._box = new St.BoxLayout({ vertical: !this._isHorizontal, 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); // Create a wrapper around the real showAppsIcon in order to add a popupMenu. this._showAppsIcon = new AppIcons.MyShowAppsIcon(); this._showAppsIcon.show(); this._showAppsIcon.icon.setIconSize(this.iconSize); this._hookUpLabel(this._showAppsIcon); this._showAppsIcon.connect('menu-state-changed', (_icon, opened) => { this._itemMenuStateChanged(this._showAppsIcon, opened); }); this._container.add_actor(this._showAppsIcon); let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; super._init({ child: this._container, y_align: St.Align.START, x_align: rtl ? St.Align.END : St.Align.START }); if (this._isHorizontal) { this.connect('notify::width', () => { if (this._maxHeight != this.width) this._queueRedisplay(); this._maxHeight = this.width; }); } else { this.connect('notify::height', () => { if (this._maxHeight != this.height) this._queueRedisplay(); this._maxHeight = this.height; }); } // Update minimization animation target position on allocation of the // container and on scrollview change. this._box.connect('notify::allocation', this._updateAppsIconGeometry.bind(this)); let scrollViewAdjustment = this._isHorizontal ? this._scrollView.hscroll.adjustment : this._scrollView.vscroll.adjustment; scrollViewAdjustment.connect('notify::value', this._updateAppsIconGeometry.bind(this)); this._workId = Main.initializeDeferredWork(this._box, this._redisplay.bind(this)); this._shellSettings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); this._appSystem = Shell.AppSystem.get_default(); this._signalsHandler.add([ this._appSystem, 'installed-changed', () => { AppFavorites.getAppFavorites().reload(); this._queueRedisplay(); } ], [ AppFavorites.getAppFavorites(), 'changed', this._queueRedisplay.bind(this) ], [ this._appSystem, 'app-state-changed', this._queueRedisplay.bind(this) ], [ Main.overview, 'item-drag-begin', this._onDragBegin.bind(this) ], [ Main.overview, 'item-drag-end', this._onDragEnd.bind(this) ], [ Main.overview, 'item-drag-cancelled', this._onDragCancelled.bind(this) ]); this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { this._signalsHandler.destroy(); } _onScrollEvent(actor, event) { // If scroll is not used because the icon is resized, let the scroll event propagate. if (!Docking.DockManager.settings.get_boolean('icon-size-fixed')) 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; if (this._isHorizontal) adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); else adjustment = this._scrollView.get_vscroll_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; // Also consider horizontal component, for instance touchpad if (this._isHorizontal) delta += dx * increment; break; } adjustment.set_value(adjustment.get_value() + delta); return Clutter.EVENT_STOP; } _onDragBegin() { this._dragCancelled = false; this._dragMonitor = { dragMotion: this._onDragMotion.bind(this) }; 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() { this._dragCancelled = true; this._endDrag(); } _onDragEnd() { if (this._dragCancelled) return; this._endDrag(); } _endDrag() { this._clearDragPlaceholder(); this._clearEmptyDropTarget(); this._showAppsIcon.setDragApp(null); DND.removeDragMonitor(this._dragMonitor); } _onDragMotion(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(apps) { let ids = {}; for (let i = 0; i < apps.length; i++) ids[apps[i].get_id()] = apps[i]; return ids; } _queueRedisplay() { Main.queueDeferredWork(this._workId); } _hookUpLabel(item, appIcon) { item.child.connect('notify::hover', () => { this._syncLabel(item, appIcon); }); let id = Main.overview.connect('hiding', () => { this._labelShowing = false; item.hideLabel(); }); item.child.connect('destroy', function() { Main.overview.disconnect(id); }); if (appIcon) { appIcon.connect('sync-tooltip', () => { this._syncLabel(item, appIcon); }); } } _createAppItem(app) { let appIcon = new AppIcons.MyAppIcon(this._remoteModel, app, this._monitorIndex, { setSizeManually: true, showLabel: false }); if (appIcon._draggable) { appIcon._draggable.connect('drag-begin', () => { appIcon.actor.opacity = 50; }); appIcon._draggable.connect('drag-end', () => { appIcon.actor.opacity = 255; }); } appIcon.connect('menu-state-changed', (appIcon, opened) => { this._itemMenuStateChanged(item, opened); }); let item = new MyDashItemContainer(); item.setChild(appIcon.actor); appIcon.actor.connect('notify::hover', () => { if (appIcon.actor.hover) { this._ensureAppIconVisibilityTimeoutId = Mainloop.timeout_add(100, () => { 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', (actor) => { ensureActorVisibleInScrollView(this._scrollView, actor); }); appIcon.actor.connect('key-focus-in', (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 dash */ getAppIcons() { // 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; } _updateAppsIconGeometry() { let appIcons = this.getAppIcons(); appIcons.forEach(function(icon) { icon.updateIconGeometry(); }); } _itemMenuStateChanged(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.label.opacity = 0; item.label.hide(); } 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(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, () => { 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, () => { this._labelShowing = false; this._resetHoverTimeoutId = 0; return GLib.SOURCE_REMOVE; }); GLib.Source.set_name_by_id(this._resetHoverTimeoutId, '[gnome-shell] this._labelShowing'); } } } _adjustIconSize() { // 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; }); iconChildren.push(this._showAppsIcon); if (this._maxHeight == -1) return; // Check if the container is present in the stage. This avoids critical // errors when unlocking the screen if (!this._container.get_stage()) return; let themeNode = this._container.get_theme_node(); let maxAllocation = new Clutter.ActorBox({ x1: 0, y1: 0, x2: this._isHorizontal ? this._maxHeight : 42 /* whatever */, y2: this._isHorizontal ? 42 : this._maxHeight }); let maxContent = themeNode.get_content_box(maxAllocation); let availHeight; if (this._isHorizontal) availHeight = maxContent.x2 - maxContent.x1; else availHeight = maxContent.y2 - maxContent.y1; let spacing = themeNode.get_length('spacing'); let firstButton = iconChildren[0].child; let firstIcon = firstButton._delegate.icon; let minHeight, natHeight, minWidth, natWidth; // Enforce the current icon size during the size request firstIcon.setIconSize(this.iconSize); [minHeight, natHeight] = firstButton.get_preferred_height(-1); [minWidth, natWidth] = firstButton.get_preferred_width(-1); let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; let iconSizes = this._availableIconSizes.map(function(s) { return s * scaleFactor; }); // Subtract icon padding and box spacing from the available height if (this._isHorizontal) availHeight -= iconChildren.length * (natWidth - this.iconSize * scaleFactor) + (iconChildren.length - 1) * spacing; else availHeight -= iconChildren.length * (natHeight - this.iconSize * scaleFactor) + (iconChildren.length - 1) * spacing; let availSize = availHeight / iconChildren.length; 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 dash 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); icon.icon.remove_all_transitions(); icon.icon.ease({ width: targetWidth, height: targetHeight, time: DASH_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD }); } } _redisplay() { let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); let running = this._appSystem.get_running(); let settings = Docking.DockManager.settings; if (settings.get_boolean('isolate-workspaces') || settings.get_boolean('isolate-monitors')) { // When using isolation, we filter out apps that have no windows in // the current workspace let monitorIndex = this._monitorIndex; running = running.filter(function(_app) { return AppIcons.getInterestingWindows(_app, monitorIndex).length != 0; }); } let children = this._box.get_children().filter(function(actor) { return actor.child && actor.child._delegate && actor.child._delegate.app; }); // Apps currently in the dash let oldApps = children.map(function(actor) { return actor.child._delegate.app; }); // Apps supposed to be in the dash let newApps = []; if (settings.get_boolean('show-favorites')) { for (let id in favorites) newApps.push(favorites[id]); } // We reorder the running apps so that they don't change position on the // dash with every redisplay() call if (settings.get_boolean('show-running')) { // First: add the apps from the oldApps list that are still running for (let i = 0; i < oldApps.length; i++) { let index = running.indexOf(oldApps[i]); if (index > -1) { let app = running.splice(index, 1)[0]; if (settings.get_boolean('show-favorites') && (app.get_id() in favorites)) continue; newApps.push(app); } } // Second: add the new apps for (let i = 0; i < running.length; i++) { let app = running[i]; if (settings.get_boolean('show-favorites') && (app.get_id() in favorites)) continue; newApps.push(app); } } if (settings.get_boolean('show-mounts')) { if (!this._removables) { this._removables = new Locations.Removables(); this._signalsHandler.addWithLabel('show-mounts', [ this._removables, 'changed', this._queueRedisplay.bind(this) ]); } Array.prototype.push.apply(newApps, this._removables.getApps()); } else if (this._removables) { this._signalsHandler.removeWithLabel('show-mounts'); this._removables.destroy(); this._removables = null; } if (settings.get_boolean('show-trash')) { if (!this._trash) { this._trash = new Locations.Trash(); this._signalsHandler.addWithLabel('show-trash', [ this._trash, 'changed', this._queueRedisplay.bind(this) ]); } newApps.push(this._trash.getApp()); } else if (this._trash) { this._signalsHandler.removeWithLabel('show-trash'); this._trash.destroy(); this._trash = null; } // Figure out the actual changes to the list of items; we iterate // over both the list of items currently in the dash 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] && 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)) { let newItem = this._createAppItem(newApps[newIndex]); addedItems.push({ app: newApps[newIndex], item: newItem, 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]; // Don't animate item removal when the overview is transitioning if (!Main.overview.animationInProgress) item.animateOutAndDestroy(); else item.destroy(); } this._adjustIconSize(); // Skip animations on first run when adding the initial set // of items, to avoid all items zooming in at once let animate = this._shownInitially && !Main.overview.animationInProgress; 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._updateAppsIconGeometry(); // This will update the size, and the corresponding number for each icon this._updateNumberOverlay(); } _updateNumberOverlay() { 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(); }); } toggleNumberOverlay(activate) { let appIcons = this.getAppIcons(); appIcons.forEach(function(icon) { icon.toggleNumberOverlay(activate); }); } _initializeIconSize(max_size) { let max_allowed = baseIconSizes[baseIconSizes.length-1]; max_size = Math.min(max_size, max_allowed); if (Docking.DockManager.settings.get_boolean('icon-size-fixed')) this._availableIconSizes = [max_size]; else { this._availableIconSizes = baseIconSizes.filter(function(val) { return (val { this._animatingPlaceholdersCount--; }); this._dragPlaceholder = null; } this._dragPlaceholderPos = -1; } _clearEmptyDropTarget() { if (this._emptyDropTarget) { this._emptyDropTarget.animateOutAndDestroy(); this._emptyDropTarget = null; } } handleDragOver(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._shellSettings.is_writable('favorite-apps') || !Docking.DockManager.settings.get_boolean('show-favorites')) 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 += this._isHorizontal?children[i].width:children[i].height; // 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._isHorizontal?this._dragPlaceholder.width:this._dragPlaceholder.height; numChildren--; } let pos; if (!this._emptyDropTarget) { pos = Math.floor((this._isHorizontal?x:y) * numChildren / boxHeight); if (pos > numChildren) pos = numChildren; } else pos = 0; // always insert at the top when dash is empty // Take into account childredn position in rtl if (this._isHorizontal && (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 / 2); 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(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._shellSettings.is_writable('favorite-apps') || !Docking.DockManager.settings.get_boolean('show-favorites')) 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, () => { let appFavorites = AppFavorites.getAppFavorites(); if (srcIsFavorite) appFavorites.moveFavoriteToPos(id, favPos); else appFavorites.addFavoriteAtPos(id, favPos); return false; }); return true; } get showAppsButton() { return this._showAppsIcon.toggleButton; } showShowAppsButton() { this.showAppsButton.visible = true this.showAppsButton.set_width(-1) this.showAppsButton.set_height(-1) } hideShowAppsButton() { this.showAppsButton.hide() this.showAppsButton.set_width(0) this.showAppsButton.set_height(0) } }); /** * 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.get_vscroll_bar().get_adjustment(); let hadjustment = scrollView.get_hscroll_bar().get_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) { vadjustment.ease(vvalue, { mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: Util.SCROLL_TIME }); } if (hvalue !== hvalue0) { hadjustment.ease(hvalue, { mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: Util.SCROLL_TIME }); } return [hvalue- hvalue0, vvalue - vvalue0]; }