/* ======================================================================================================== * myThumbnailsBox.js - thumbnailsbox object * -------------------------------------------------------------------------------------------------------- * CREDITS: Part of this code was copied from the gnome-shell-extensions framework * http://git.gnome.org/browse/gnome-shell-extensions/ * ======================================================================================================== */ const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; const Signals = imports.signals; const Background = imports.ui.background; const DND = imports.ui.dnd; const Main = imports.ui.main; const Workspace = imports.ui.workspace; const WorkspacesView = imports.ui.workspacesView; const Config = imports.misc.config; const Me = imports.misc.extensionUtils.getCurrentExtension(); const Convenience = Me.imports.convenience; const ThumbnailCaption = Me.imports.thumbnailCaption; const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); const _ = Gettext.gettext; // The maximum size of a thumbnail is 1/10 the width and height of the screen let MAX_THUMBNAIL_SCALE = 1 / 10.; var RESCALE_ANIMATION_TIME = 200; var SLIDE_ANIMATION_TIME = 200; // When we create workspaces by dragging, we add a "cut" into the top and // bottom of each workspace so that the user doesn't have to hit the // placeholder exactly. var WORKSPACE_CUT_SIZE = 10; var WORKSPACE_KEEP_ALIVE_TIME = 100; var MUTTER_SCHEMA = 'org.gnome.mutter'; /* Return the actual position reverseing left and right in rtl */ function getPosition(settings) { let position = settings.get_enum('dock-position'); if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { if (position == St.Side.LEFT) position = St.Side.RIGHT; else if (position == St.Side.RIGHT) position = St.Side.LEFT; } return position; } /* A layout manager that requests size only for primary_actor, but then allocates all using a fixed layout */ var MyPrimaryActorLayout = GObject.registerClass( class WorkspacesToDock_MyPrimaryActorLayout extends Clutter.FixedLayout { _init(primaryActor) { super._init(); this.primaryActor = primaryActor; } vfunc_get_preferred_width(container, forHeight) { return this.primaryActor.get_preferred_width(forHeight); } vfunc_get_preferred_height(container, forWidth) { return this.primaryActor.get_preferred_height(forWidth); } }); var MyWindowClone = GObject.registerClass({ Signals: { 'drag-begin': {}, 'drag-cancelled': {}, 'drag-end': {}, 'selected': { param_types: [GObject.TYPE_UINT] }, }, }, class WorkspacesToDock_MyWindowClone extends Clutter.Actor { _init(realWindow) { this._mySettings = Convenience.getSettings('org.gnome.shell.extensions.workspaces-to-dock'); let clone = new Clutter.Clone({ source: realWindow }); super._init({ layout_manager: new MyPrimaryActorLayout(clone), reactive: true, }); this._delegate = this; this.add_child(clone); this.realWindow = realWindow; this.metaWindow = realWindow.meta_window; clone._updateId = this.realWindow.connect('notify::position', this._onPositionChanged.bind(this)); clone._destroyId = this.realWindow.connect('destroy', () => { // First destroy the clone and then destroy everything // This will ensure that we never see it in the _disconnectSignals loop clone.destroy(); this.destroy(); }); this._onPositionChanged(); this.connect('destroy', this._onDestroy.bind(this)); this._draggable = DND.makeDraggable(this, { restoreOnSuccess: true, dragActorMaxSize: Workspace.WINDOW_DND_SIZE, dragActorOpacity: Workspace.DRAGGING_WINDOW_OPACITY }); this._draggable.connect('drag-begin', this._onDragBegin.bind(this)); this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this)); this._draggable.connect('drag-end', this._onDragEnd.bind(this)); this.inDrag = false; let iter = win => { let actor = win.get_compositor_private(); if (!actor) return false; if (!win.is_attached_dialog()) return false; this._doAddAttachedDialog(win, actor); win.foreach_transient(iter); return true; }; this.metaWindow.foreach_transient(iter); } // Find the actor just below us, respecting reparenting done // by DND code getActualStackAbove() { if (this._stackAbove == null) return null; if (this.inDrag) { if (this._stackAbove._delegate) return this._stackAbove._delegate.getActualStackAbove(); else return null; } else { return this._stackAbove; } } setStackAbove(actor) { this._stackAbove = actor; // Don't apply the new stacking now, it will be applied // when dragging ends and window are stacked again if (actor.inDrag) return; let parent = this.get_parent(); let actualAbove = this.getActualStackAbove(); if (actualAbove == null) parent.set_child_below_sibling(this, null); else parent.set_child_above_sibling(this, actualAbove); } addAttachedDialog(win) { this._doAddAttachedDialog(win, win.get_compositor_private()); } _doAddAttachedDialog(metaDialog, realDialog) { let clone = new Clutter.Clone({ source: realDialog }); this._updateDialogPosition(realDialog, clone); clone._updateId = realDialog.connect('notify::position', dialog => { this._updateDialogPosition(dialog, clone); }); clone._destroyId = realDialog.connect('destroy', () => { clone.destroy(); }); this.add_child(clone); } _updateDialogPosition(realDialog, cloneDialog) { let metaDialog = realDialog.meta_window; let dialogRect = metaDialog.get_frame_rect(); let rect = this.metaWindow.get_frame_rect(); cloneDialog.set_position(dialogRect.x - rect.x, dialogRect.y - rect.y); } _onPositionChanged() { this.set_position(this.realWindow.x, this.realWindow.y); } _disconnectSignals() { this.get_children().forEach(child => { let realWindow = child.source; realWindow.disconnect(child._updateId); realWindow.disconnect(child._destroyId); }); } _onDestroy() { this._disconnectSignals(); this._delegate = null; if (this.inDrag) { this.emit('drag-end'); this.inDrag = false; } } vfunc_button_press_event() { return Clutter.EVENT_STOP; } vfunc_button_release_event(buttonEvent) { if (this._mySettings.get_boolean('toggle-overview')) { if (buttonEvent.button == 3) { // pass right-click event on allowing it to bubble up to thumbnailsBox return Clutter.EVENT_PROPAGATE; } } this.emit('selected', buttonEvent.time); return Clutter.EVENT_STOP; } vfunc_touch_event(touchEvent) { if (touchEvent.type != Clutter.EventType.TOUCH_END || !global.display.is_pointer_emulating_sequence(touchEvent.sequence)) return Clutter.EVENT_PROPAGATE; this.emit('selected', touchEvent.time); return Clutter.EVENT_STOP; } _onDragBegin(_draggable, _time) { this.inDrag = true; this.emit('drag-begin'); } _onDragCancelled(_draggable, _time) { this.emit('drag-cancelled'); } _onDragEnd(_draggable, _time, _snapback) { this.inDrag = false; // We may not have a parent if DnD completed successfully, in // which case our clone will shortly be destroyed and replaced // with a new one on the target workspace. let parent = this.get_parent(); if (parent !== null) { if (this._stackAbove == null) parent.set_child_below_sibling(this, null); else parent.set_child_above_sibling(this, this._stackAbove); } this.emit('drag-end'); } }); var ThumbnailState = { NEW: 0, ANIMATING_IN: 1, NORMAL: 2, REMOVING: 3, ANIMATING_OUT: 4, ANIMATED_OUT: 5, COLLAPSING: 6, DESTROYED: 7, }; /** * @metaWorkspace: a #Meta.Workspace */ var MyWorkspaceThumbnail = GObject.registerClass({ Properties: { 'collapse-fraction': GObject.ParamSpec.double( 'collapse-fraction', 'collapse-fraction', 'collapse-fraction', GObject.ParamFlags.READWRITE, 0, 1, 0), 'slide-position': GObject.ParamSpec.double( 'slide-position', 'slide-position', 'slide-position', GObject.ParamFlags.READWRITE, 0, 1, 0), }, }, class WorkspacesToDock_MyWorkspaceThumbnail extends St.Widget { _init(metaWorkspace, thumbnailsBox) { super._init({ clip_to_allocation: true, style_class: 'workspace-thumbnail', }); this._delegate = this; this.metaWorkspace = metaWorkspace; this.monitorIndex = Main.layoutManager.primaryIndex; this._thumbnailsBox = thumbnailsBox; this._removed = false; this._contents = new Clutter.Actor(); this.add_child(this._contents); this.connect('destroy', this._onDestroy.bind(this)); this.caption = new ThumbnailCaption.ThumbnailCaption(this); this._createBackground(); let workArea = Main.layoutManager.getWorkAreaForMonitor(this.monitorIndex); this.setPorthole(workArea.x, workArea.y, workArea.width, workArea.height); let windows = global.get_window_actors().filter(actor => { let win = actor.meta_window; return win.located_on_workspace(metaWorkspace); }); // Create clones for windows that should be visible in the Overview this._windows = []; this._allWindows = []; this._minimizedChangedIds = []; for (let i = 0; i < windows.length; i++) { let minimizedChangedId = windows[i].meta_window.connect('notify::minimized', this._updateMinimized.bind(this)); this._allWindows.push(windows[i].meta_window); this._minimizedChangedIds.push(minimizedChangedId); if (this._isMyWindow(windows[i]) && this._isOverviewWindow(windows[i])) this._addWindowClone(windows[i]); } // Track window changes this._windowAddedId = this.metaWorkspace.connect('window-added', this._windowAdded.bind(this)); this._windowRemovedId = this.metaWorkspace.connect('window-removed', this._windowRemoved.bind(this)); this._windowEnteredMonitorId = global.display.connect('window-entered-monitor', this._windowEnteredMonitor.bind(this)); this._windowLeftMonitorId = global.display.connect('window-left-monitor', this._windowLeftMonitor.bind(this)); this.state = ThumbnailState.NORMAL; this._slidePosition = 0; // Fully slid in this._collapseFraction = 0; // Not collapsed } _createBackground() { this._bgManager = new Background.BackgroundManager({ monitorIndex: Main.layoutManager.primaryIndex, container: this._contents, vignette: false }); } setPorthole(x, y, width, height) { this.set_size(width, height); this._contents.set_position(-x, -y); } _lookupIndex(metaWindow) { return this._windows.findIndex(w => w.metaWindow == metaWindow); } syncStacking(stackIndices) { this._windows.sort((a, b) => { let indexA = stackIndices[a.metaWindow.get_stable_sequence()]; let indexB = stackIndices[b.metaWindow.get_stable_sequence()]; return indexA - indexB; }); for (let i = 0; i < this._windows.length; i++) { let clone = this._windows[i]; if (i == 0) { clone.setStackAbove(this._bgManager.backgroundActor); } else { let previousClone = this._windows[i - 1]; clone.setStackAbove(previousClone); } } } // eslint-disable-next-line camelcase set slide_position(slidePosition) { if (this._slidePosition == slidePosition) return; this._slidePosition = slidePosition; this.notify('slide-position'); this.queue_relayout(); } // eslint-disable-next-line camelcase get slide_position() { return this._slidePosition; } // eslint-disable-next-line camelcase set collapse_fraction(collapseFraction) { if (this._collapseFraction == collapseFraction) return; this._collapseFraction = collapseFraction; this.notify('collapse-fraction'); this.queue_relayout(); } // eslint-disable-next-line camelcase get collapse_fraction() { return this._collapseFraction; } refreshWindowClones() { // Disconnect window signals for (let i = 0; i < this._allWindows.length; i++) { this._allWindows[i].disconnect(this._minimizedChangedIds[i]); } // Destroy window clones for (let i = 0; i < this._windows.length; i++) { this._windows[i].destroy(); } // Create clones for windows that should be visible in the Overview this._windows = []; this._allWindows = []; this._minimizedChangedIds = []; let windows = global.get_window_actors().filter(actor => { let win = actor.meta_window; return win.located_on_workspace(this.metaWorkspace); }); for (let i = 0; i < windows.length; i++) { let minimizedChangedId = windows[i].meta_window.connect('notify::minimized', this._updateMinimized.bind(this)); this._allWindows.push(windows[i].meta_window); this._minimizedChangedIds.push(minimizedChangedId); if (this._isMyWindow(windows[i]) && this._isOverviewWindow(windows[i])) { this._addWindowClone(windows[i]); } } } _doRemoveWindow(metaWin) { let clone = this._removeWindowClone(metaWin); if (clone) clone.destroy(); } _doAddWindow(metaWin) { if (this._removed) return; let win = metaWin.get_compositor_private(); if (!win) { // Newly-created windows are added to a workspace before // the compositor finds out about them... let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { if (!this._removed && metaWin.get_compositor_private() && metaWin.get_workspace() == this.metaWorkspace) this._doAddWindow(metaWin); return GLib.SOURCE_REMOVE; }); GLib.Source.set_name_by_id(id, '[gnome-shell] this._doAddWindow'); return; } if (!this._allWindows.includes(metaWin)) { let minimizedChangedId = metaWin.connect('notify::minimized', this._updateMinimized.bind(this)); this._allWindows.push(metaWin); this._minimizedChangedIds.push(minimizedChangedId); } // We might have the window in our list already if it was on all workspaces and // now was moved to this workspace if (this._lookupIndex(metaWin) != -1) return; if (!this._isMyWindow(win)) return; if (this._isOverviewWindow(win)) { // passingthru67 - force thumbnail refresh if window is on all workspaces // note: _addWindowClone checks if metawindow is on all workspaces this._addWindowClone(win); } else if (metaWin.is_attached_dialog()) { let parent = metaWin.get_transient_for(); while (parent.is_attached_dialog()) parent = parent.get_transient_for(); let idx = this._lookupIndex(parent); if (idx < 0) { // parent was not created yet, it will take care // of the dialog when created return; } let clone = this._windows[idx]; clone.addAttachedDialog(metaWin); } } _windowAdded(metaWorkspace, metaWin) { this._doAddWindow(metaWin); } _windowRemoved(metaWorkspace, metaWin) { let index = this._allWindows.indexOf(metaWin); if (index != -1) { metaWin.disconnect(this._minimizedChangedIds[index]); this._allWindows.splice(index, 1); this._minimizedChangedIds.splice(index, 1); } this._doRemoveWindow(metaWin); } _windowEnteredMonitor(metaDisplay, monitorIndex, metaWin) { if (monitorIndex == this.monitorIndex) this._doAddWindow(metaWin); } _windowLeftMonitor(metaDisplay, monitorIndex, metaWin) { if (monitorIndex == this.monitorIndex) this._doRemoveWindow(metaWin); } _updateMinimized(metaWin) { if (metaWin.minimized) this._doRemoveWindow(metaWin); else this._doAddWindow(metaWin); } workspaceRemoved() { if (this._removed) return; this.caption.workspaceRemoved(); this._removed = true; this.metaWorkspace.disconnect(this._windowAddedId); this.metaWorkspace.disconnect(this._windowRemovedId); global.display.disconnect(this._windowEnteredMonitorId); global.display.disconnect(this._windowLeftMonitorId); for (let i = 0; i < this._allWindows.length; i++) this._allWindows[i].disconnect(this._minimizedChangedIds[i]); } _onDestroy() { this.caption.destroy(); this.workspaceRemoved(); if (this._bgManager) { this._bgManager.destroy(); this._bgManager = null; } this._windows = []; } // Tests if @actor belongs to this workspace and monitor _isMyWindow(actor, isMetaWin) { let win; if (isMetaWin) win = actor; else win = actor.meta_window; return win.located_on_workspace(this.metaWorkspace) && (win.get_monitor() == this.monitorIndex); } // Tests if @win should be shown in the Overview _isOverviewWindow(win) { return !win.get_meta_window().skip_taskbar && win.get_meta_window().showing_on_its_workspace(); } // Create a clone of a (non-desktop) window and add it to the window list _addWindowClone(win, refresh) { let clone = new MyWindowClone(win); clone.connect('selected', (clone, time) => { this.activate(time); }); clone.connect('drag-begin', () => { Main.overview.beginWindowDrag(clone.metaWindow); }); clone.connect('drag-cancelled', () => { Main.overview.cancelledWindowDrag(clone.metaWindow); }); clone.connect('drag-end', () => { Main.overview.endWindowDrag(clone.metaWindow); }); clone.connect('destroy', () => { this._removeWindowClone(clone.metaWindow); }); this._contents.add_actor(clone); if (this._windows.length == 0) clone.setStackAbove(this._bgManager.backgroundActor); else clone.setStackAbove(this._windows[this._windows.length - 1]); this._windows.push(clone); return clone; } _removeWindowClone(metaWin) { // find the position of the window in our list let index = this._lookupIndex(metaWin); if (index == -1) return null; return this._windows.splice(index, 1).pop(); } setCaptionReactiveState(state) { if (state == null) return; // Deactivate caption if (this.caption._wsCaption) this.caption._wsCaption.reactive = state; // Deactivate caption tasbar icons if (this.caption._taskBarBox) { let children = this.caption._taskBarBox.get_children(); for (let i = 0; i < children.length; i++) { children[i].reactive = state; } } } setWindowClonesReactiveState(state) { if (state == null) return; // Deactivate window clones for (let i = 0; i < this._windows.length; i++) { let clone = this._windows[i]; clone.reactive = state; } } activate(time) { if (this.state > ThumbnailState.NORMAL) return; // a click on the already current workspace should go back to the main view let workspaceManager = global.workspace_manager; let activeWorkspace = workspaceManager.get_active_workspace(); if (this.metaWorkspace == activeWorkspace) Main.overview.hide(); else this.metaWorkspace.activate(time); } // Draggable target interface used only by ThumbnailsBox handleDragOverInternal(source, actor, time) { if (source == Main.xdndHandler) { this.metaWorkspace.activate(time); return DND.DragMotionResult.CONTINUE; } if (this.state > ThumbnailState.NORMAL) return DND.DragMotionResult.CONTINUE; if (source.realWindow && !this._isMyWindow(source.realWindow)) return DND.DragMotionResult.MOVE_DROP; if (source._caption && !this._isMyWindow(source._metaWin, true)) return DND.DragMotionResult.MOVE_DROP; if (source.app && source.app.can_open_new_window()) return DND.DragMotionResult.COPY_DROP; if (!source.app && source.shellWorkspaceLaunch) return DND.DragMotionResult.COPY_DROP; return DND.DragMotionResult.CONTINUE; } acceptDropInternal(source, actor, time) { if (this.state > ThumbnailState.NORMAL) return false; if (source.realWindow) { let win = source.realWindow; if (this._isMyWindow(win)) return false; let metaWindow = win.get_meta_window(); // We need to move the window before changing the workspace, because // the move itself could cause a workspace change if the window enters // the primary monitor if (metaWindow.get_monitor() != this.monitorIndex) metaWindow.move_to_monitor(this.monitorIndex); metaWindow.change_workspace_by_index(this.metaWorkspace.index(), false); return true; } else if (source._caption) { let metaWindow = source._metaWin; if (this._isMyWindow(metaWindow, true)) return false; // We need to move the window before changing the workspace, because // the move itself could cause a workspace change if the window enters // the primary monitor if (metaWindow.get_monitor() != this.monitorIndex) metaWindow.move_to_monitor(this.monitorIndex); metaWindow.change_workspace_by_index(this.metaWorkspace.index(), false); return true; } else if (source.app && source.app.can_open_new_window()) { if (source.animateLaunchAtPos) source.animateLaunchAtPos(actor.x, actor.y); source.app.open_new_window(this.metaWorkspace.index()); return true; } else if (!source.app && source.shellWorkspaceLaunch) { // While unused in our own drag sources, shellWorkspaceLaunch allows // extensions to define custom actions for their drag sources. source.shellWorkspaceLaunch({ workspace: this.metaWorkspace.index(), timestamp: time }); return true; } return false; } }); var MyThumbnailsBox = GObject.registerClass({ Properties: { 'indicator-x': GObject.ParamSpec.double( 'indicator-x', 'indicator-x', 'indicator-x', GObject.ParamFlags.READWRITE, 0, Infinity, 0), 'indicator-y': GObject.ParamSpec.double( 'indicator-y', 'indicator-y', 'indicator-y', GObject.ParamFlags.READWRITE, 0, Infinity, 0), 'scale': GObject.ParamSpec.double( 'scale', 'scale', 'scale', GObject.ParamFlags.READWRITE, 0, Infinity, 0), }, }, class WorkspacesToDock_MyThumbnailsBox extends St.Widget { _init(scrollAdjustment, dock) { this._dock = dock; this._gsCurrentVersion = Config.PACKAGE_VERSION.split('.'); this._thumbnailsBoxWidth = 0; this._thumbnailsBoxHeight = 0; this._mySettings = Convenience.getSettings('org.gnome.shell.extensions.workspaces-to-dock'); this._position = getPosition(this._mySettings); this._isHorizontal = (this._position == St.Side.TOP || this._position == St.Side.BOTTOM); this._popupCaptionMenuTimeoutId = 0; // Set _centerContainer property if (this._mySettings.get_boolean('customize-height') && this._mySettings.get_boolean('center-thumbnails-on-dock')) { this._centerContainer = true; } else { this._centerContainer = false; } // Set _centerPanelsIndependently property if (this._centerContainer && this._mySettings.get_int('center-thumbnails-option') == 0) { this._centerPanelsIndependently = true; } else { this._centerPanelsIndependently = false; } if (this._isHorizontal) { super._init({ reactive: true, style_class: 'workspace-thumbnails workspacestodock-thumbnails-panel', request_mode: Clutter.RequestMode.HEIGHT_FOR_WIDTH, x_align: (this._centerContainer && this._centerPanelsIndependently) ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START, y_align: (this._centerContainer && this._centerPanelsIndependently) ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START }); } else { super._init({ reactive: true, style_class: 'workspace-thumbnails workspacestodock-thumbnails-panel', request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT, x_align: (this._centerContainer && this._centerPanelsIndependently) ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START, y_align: (this._centerContainer && this._centerPanelsIndependently) ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START }); } this._delegate = this; // Add addtional style class when workspace is fixed and set to full height if (this._mySettings.get_boolean('customize-height') && this._mySettings.get_int('customize-height-option') == 1) { if (this._mySettings.get_double('top-margin') == 0 || this._mySettings.get_double('bottom-margin') == 0) { this.add_style_class_name('workspace-thumbnails-fullheight'); } } let indicator = new St.Bin({ style_class: 'workspace-thumbnail-indicator' }); // We don't want the indicator to affect drag-and-drop Shell.util_set_hidden_from_pick(indicator, true); this._indicator = indicator; this.add_actor(indicator); // The porthole is the part of the screen we're showing in the thumbnails this._porthole = { width: global.stage.width, height: global.stage.height, x: global.stage.x, y: global.stage.y }; this._dropWorkspace = -1; this._dropPlaceholderPos = -1; this._dropPlaceholder = new St.Bin({ style_class: 'placeholder' }); this.add_actor(this._dropPlaceholder); this._spliceIndex = -1; this._targetScale = 0; this._scale = 0; this._pendingScaleUpdate = false; this._stateUpdateQueued = false; this._animatingIndicator = false; this._indicatorY = 0; // only used when _animatingIndicator is true this._indicatorX = 0; // passingthru67 - added for dock position isHorizontal this._stateCounts = {}; for (let key in ThumbnailState) this._stateCounts[ThumbnailState[key]] = 0; this._thumbnails = []; this.connect('destroy', this._onDestroy.bind(this)); //Main.overview.connect('showing', // this._createThumbnails.bind(this)); //Main.overview.connect('hidden', // this._destroyThumbnails.bind(this)); //Main.overview.connect('item-drag-begin', // this._onDragBegin.bind(this)); //Main.overview.connect('item-drag-end', // this._onDragEnd.bind(this)); //Main.overview.connect('item-drag-cancelled', // this._onDragCancelled.bind(this)); //Main.overview.connect('window-drag-begin', // this._onDragBegin.bind(this)); //Main.overview.connect('window-drag-end', // this._onDragEnd.bind(this)); //Main.overview.connect('window-drag-cancelled', // this._onDragCancelled.bind(this)); // Connect global signals let workspaceManager = global.workspace_manager; this._signalHandler = new Convenience.globalSignalHandler(); this._signalHandler.push( [ 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) ], [ Main.overview, 'window-drag-begin', this._onDragBegin.bind(this) ], [ Main.overview, 'window-drag-end', this._onDragEnd.bind(this) ], [ Main.overview, 'window-drag-cancelled', this._onDragCancelled.bind(this) ], [ global.display, 'in-fullscreen-changed', this.refreshThumbnails.bind(this) ], [ global.display, 'workareas-changed', this._updatePorthole.bind(this) ], [ Main.layoutManager, 'monitors-changed', this._rebuildThumbnails.bind(this) ], [ global.window_manager, 'switch-workspace', this._activeWorkspaceChanged.bind(this) ], [ workspaceManager, 'notify::n-workspaces', this._workspacesChanged.bind(this) ], [ workspaceManager, 'workspaces-reordered', this._workspacesReordered.bind(this) ], [ Main.overview, 'windows-restacked', this._syncStacking.bind(this) ] ); this._settings = new Gio.Settings({ schema_id: MUTTER_SCHEMA }); this._settings.connect('changed::dynamic-workspaces', this._updateSwitcherVisibility.bind(this)); //Main.layoutManager.connect('monitors-changed', () => { // this._destroyThumbnails(); // if (Main.overview.visible) // this._createThumbnails(); //}); //global.display.connect('workareas-changed', // this._updatePorthole.bind(this)); this._switchWorkspaceNotifyId = 0; this._nWorkspacesNotifyId = 0; this._syncStackingId = 0; this._workareasChangedId = 0; } _onDestroy() { // Disconnect global signals this._signalHandler.disconnect(); // Destroy thumbnails this._destroyThumbnails(); this._indicator = null; // Disconnect GSettings signals this._settings.run_dispose(); this._mySettings.run_dispose(); } _updateSwitcherVisibility() { let workspaceManager = global.workspace_manager; this.visible = this._settings.get_boolean('dynamic-workspaces') || workspaceManager.n_workspaces > 1; } _activateThumbnailAtPoint(stageX, stageY, time) { let [r_, x, y] = this.transform_stage_point(stageX, stageY); let thumbnail = this._thumbnails.find(t => { let [w, h] = t.get_transformed_size(); if (this._isHorizontal) { return x >= t.x && x <= t.x + w; } else { return y >= t.y && y <= t.y + h; } }); if (thumbnail) thumbnail.activate(time); } _activateCaptionMenuAtPoint(stageX, stageY, time) { let [r_, x, y] = this.transform_stage_point(stageX, stageY); let thumbnail = this._thumbnails.find(t => { let [w, h] = t.get_transformed_size(); if (this._isHorizontal) { return x >= t.x && x <= t.x + w; } else { return y >= t.y && y <= t.y + h; } }); if (thumbnail) { thumbnail.caption.showCaptionMenu(); } } vfunc_button_release_event(buttonEvent) { // ThumbnailsBox click events are passed on to dock handler if conditions are met // Helpful in cases where the 'dock-edge-visible' option is enabled. It provides more // area to click on to show the dock when the window is maximized. // Should we continue processing the button release or pass the event on to the dock handler? // Continue if 'dock-edge-visible' && 'require-click-to-show' are not enabled if (this._mySettings.get_boolean('dock-edge-visible') && this._mySettings.get_boolean('require-click-to-show')) { // Continue if window is not maximized (_hovering only true if window is maximized) if (this._dock._hovering) { // Continue if dock is not in autohide mode for instance because it is shown by intellihide if (this._mySettings.get_boolean('autohide') && this._dock._autohideStatus) { if (this._dock.actor.hover) { // Continue if dock is showing or shown if (this._dock._animStatus.hidden() || this._dock._animStatus.hiding()) { // STOP. Lets not continue but pass the click event on to dock handler return Clutter.EVENT_PROPAGATE; } } } } } let { x, y } = buttonEvent; this._activateThumbnailAtPoint(x, y, buttonEvent.time); if (buttonEvent.button == 3) { //right click if (this._popupCaptionMenuTimeoutId == 0) { let { x, y } = buttonEvent; this._activateCaptionMenuAtPoint(x, y, buttonEvent.time); } } return Clutter.EVENT_STOP; } vfunc_touch_event(touchEvent) { if (touchEvent.type == Clutter.EventType.TOUCH_END && global.display.is_pointer_emulating_sequence(touchEvent.sequence)) { let { x, y } = touchEvent; this._activateThumbnailAtPoint(x, y, touchEvent.time); } return Clutter.EVENT_STOP; } _onDragBegin() { this._dragCancelled = false; this._dragMonitor = { dragMotion: this._onDragMotion.bind(this), }; DND.addDragMonitor(this._dragMonitor); } _onDragEnd() { if (this._dragCancelled) return; this._endDrag(); } _onDragCancelled() { this._dragCancelled = true; this._endDrag(); } _endDrag() { this._clearDragPlaceholder(); DND.removeDragMonitor(this._dragMonitor); } _onDragMotion(dragEvent) { if (!this.contains(dragEvent.targetActor)) this._onLeave(); return DND.DragMotionResult.CONTINUE; } _onLeave() { this._clearDragPlaceholder(); } _clearDragPlaceholder() { if (this._dropPlaceholderPos == -1) return; this._dropPlaceholderPos = -1; this.queue_relayout(); } // Draggable target interface handleDragOver(source, actor, x, y, time) { if (!source._caption && !source.realWindow && (!source.app || !source.app.can_open_new_window()) && (source.app || !source.shellWorkspaceLaunch) && source != Main.xdndHandler) return DND.DragMotionResult.CONTINUE; let canCreateWorkspaces = Meta.prefs_get_dynamic_workspaces(); let spacing = this.get_theme_node().get_length('spacing'); this._dropWorkspace = -1; let placeholderPos = -1; let targetBase; // passingthru67: targetBase depends on horizontal/vertical position if (this._isHorizontal) { if (this._dropPlaceholderPos == 0) targetBase = this._dropPlaceholder.x; else targetBase = this._thumbnails[0].x; } else { if (this._dropPlaceholderPos == 0) targetBase = this._dropPlaceholder.y; else targetBase = this._thumbnails[0].y; } let targetTop = targetBase - spacing - WORKSPACE_CUT_SIZE; let length = this._thumbnails.length; for (let i = 0; i < length; i++) { // Allow the reorder target to have a 10px "cut" into // each side of the thumbnail, to make dragging onto the // placeholder easier let [w, h] = this._thumbnails[i].get_transformed_size(); let targetBottom, nextTargetBase, nextTargetTop; if (this._isHorizontal) { targetBottom = targetBase + WORKSPACE_CUT_SIZE; nextTargetBase = targetBase + w + spacing; nextTargetTop = nextTargetBase - spacing - (i == length - 1 ? 0: WORKSPACE_CUT_SIZE); // Expand the target to include the placeholder, if it exists. if (i == this._dropPlaceholderPos) targetBottom += this._dropPlaceholder.get_width(); if (x > targetTop && x <= targetBottom && source != Main.xdndHandler && canCreateWorkspaces) { placeholderPos = i; break; } else if (x > targetBottom && x <= nextTargetTop) { this._dropWorkspace = i; break } //targetBase = nextTargetBase; //targetTop = nextTargetTop; } else { targetBottom = targetBase + WORKSPACE_CUT_SIZE; nextTargetBase = targetBase + h + spacing; nextTargetTop = nextTargetBase - spacing - (i == length - 1 ? 0 : WORKSPACE_CUT_SIZE); // Expand the target to include the placeholder, if it exists. if (i == this._dropPlaceholderPos) targetBottom += this._dropPlaceholder.get_height(); if (y > targetTop && y <= targetBottom && source != Main.xdndHandler && canCreateWorkspaces) { placeholderPos = i; break; } else if (y > targetBottom && y <= nextTargetTop) { this._dropWorkspace = i; break } //targetBase = nextTargetBase; //targetTop = nextTargetTop; } targetBase = nextTargetBase; targetTop = nextTargetTop; } if (this._dropPlaceholderPos != placeholderPos) { this._dropPlaceholderPos = placeholderPos; this.queue_relayout(); } if (this._dropWorkspace != -1) return this._thumbnails[this._dropWorkspace].handleDragOverInternal(source, actor, time); else if (this._dropPlaceholderPos != -1) return source.realWindow ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.COPY_DROP; else return DND.DragMotionResult.CONTINUE; } acceptDrop(source, actor, x, y, time) { if (this._dropWorkspace != -1) { return this._thumbnails[this._dropWorkspace].acceptDropInternal(source, actor, time); } else if (this._dropPlaceholderPos != -1) { if (!source._caption && !source.realWindow && (!source.app || !source.app.can_open_new_window()) && (source.app || !source.shellWorkspaceLaunch)) return false; let isWindow = !!source.realWindow; let newWorkspaceIndex; [newWorkspaceIndex, this._dropPlaceholderPos] = [this._dropPlaceholderPos, -1]; this._spliceIndex = newWorkspaceIndex; Main.wm.insertWorkspace(newWorkspaceIndex); if (isWindow) { // Move the window to our monitor first if necessary. let thumbMonitor = this._thumbnails[newWorkspaceIndex].monitorIndex; if (source.metaWindow.get_monitor() != thumbMonitor) source.metaWindow.move_to_monitor(thumbMonitor); source.metaWindow.change_workspace_by_index(newWorkspaceIndex, true); } else if (source._caption) { // Move the window to our monitor first if necessary. let thumbMonitor = this._thumbnails[newWorkspaceIndex].monitorIndex; if (source._metaWin.get_monitor() != thumbMonitor) source._metaWin.move_to_monitor(thumbMonitor); source._metaWin.change_workspace_by_index(newWorkspaceIndex, true); } else if (source.app && source.app.can_open_new_window()) { if (source.animateLaunchAtPos) source.animateLaunchAtPos(actor.x, actor.y); source.app.open_new_window(newWorkspaceIndex); } else if (!source.app && source.shellWorkspaceLaunch) { // While unused in our own drag sources, shellWorkspaceLaunch allows // extensions to define custom actions for their drag sources. source.shellWorkspaceLaunch({ workspace: newWorkspaceIndex, timestamp: time }); } if (source.app || (!source.app && source.shellWorkspaceLaunch)) { // This new workspace will be automatically removed if the application fails // to open its first window within some time, as tracked by Shell.WindowTracker. // Here, we only add a very brief timeout to avoid the _immediate_ removal of the // workspace while we wait for the startup sequence to load. let workspaceManager = global.workspace_manager; Main.wm.keepWorkspaceAlive(workspaceManager.get_workspace_by_index(newWorkspaceIndex), WORKSPACE_KEEP_ALIVE_TIME); } // Start the animation on the workspace (which is actually // an old one which just became empty) let thumbnail = this._thumbnails[newWorkspaceIndex]; this._setThumbnailState(thumbnail, ThumbnailState.NEW); thumbnail.slide_position = 1; this._queueUpdateStates(); return true; } else { return false; } } _createThumbnails() { let workspaceManager = global.workspace_manager; this._targetScale = 0; this._scale = 0; this._pendingScaleUpdate = false; this._stateUpdateQueued = false; this._stateCounts = {}; for (let key in ThumbnailState) this._stateCounts[ThumbnailState[key]] = 0; this.addThumbnails(0, workspaceManager.n_workspaces); this._updateSwitcherVisibility(); } _destroyThumbnails() { if (this._thumbnails.length == 0) return; for (let w = 0; w < this._thumbnails.length; w++) this._thumbnails[w].destroy(); this._thumbnails = []; } _workspacesChanged() { let validThumbnails = this._thumbnails.filter(t => t.state <= ThumbnailState.NORMAL); let workspaceManager = global.workspace_manager; let oldNumWorkspaces = validThumbnails.length; let newNumWorkspaces = workspaceManager.n_workspaces; if (newNumWorkspaces > oldNumWorkspaces) { this.addThumbnails(oldNumWorkspaces, newNumWorkspaces - oldNumWorkspaces); } else { let removedIndex; let removedNum = oldNumWorkspaces - newNumWorkspaces; for (let w = 0; w < oldNumWorkspaces; w++) { let metaWorkspace = workspaceManager.get_workspace_by_index(w); if (this._thumbnails[w].metaWorkspace != metaWorkspace) { removedIndex = w; break; } } this.removeThumbnails(removedIndex, removedNum); } this._updateSwitcherVisibility(); } _workspacesReordered() { this._thumbnails.sort((a, b) => { return a.metaWorkspace.index() - b.metaWorkspace.index(); }); this.queue_relayout(); } refreshThumbnails() { for (let i = 0; i < this._thumbnails.length; i++) { this._thumbnails[i].refreshWindowClones(); this._thumbnails[i].caption.activeWorkspaceChanged(); } } _rebuildThumbnails() { this._destroyThumbnails(); this._createThumbnails(); } addThumbnails(start, count) { let workspaceManager = global.workspace_manager; this._updatePorthole(); for (let k = start; k < start + count; k++) { let metaWorkspace = workspaceManager.get_workspace_by_index(k); let thumbnail = new MyWorkspaceThumbnail(metaWorkspace, this); thumbnail.setPorthole(this._porthole.x, this._porthole.y, this._porthole.width, this._porthole.height); this._thumbnails.push(thumbnail); this.add_actor(thumbnail); if (start > 0 && this._spliceIndex == -1) { // not the initial fill, and not splicing via DND thumbnail.state = ThumbnailState.NEW; thumbnail.slide_position = 1; // start slid out this._haveNewThumbnails = true; } else { thumbnail.state = ThumbnailState.NORMAL; } this._stateCounts[thumbnail.state]++; } this._queueUpdateStates(); // The thumbnails indicator actually needs to be on top of the thumbnails this.set_child_above_sibling(this._indicator, null); // Clear the splice index, we got the message this._spliceIndex = -1; } removeThumbnails(start, count) { let currentPos = 0; for (let k = 0; k < this._thumbnails.length; k++) { let thumbnail = this._thumbnails[k]; if (thumbnail.state > ThumbnailState.NORMAL) continue; if (currentPos >= start && currentPos < start + count) { thumbnail.workspaceRemoved(); this._setThumbnailState(thumbnail, ThumbnailState.REMOVING); } currentPos++; } this._queueUpdateStates(); } updateTaskbars(metaWin, action) { for (let i = 0; i < this._thumbnails.length; i++) { this._thumbnails[i].caption.updateTaskbar(metaWin, action); } } setPopupMenuFlag(showing) { this._dock.setPopupMenuFlag(showing); if (!showing) { this._popupCaptionMenuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { this._popupCaptionMenuTimeoutId = 0; return GLib.SOURCE_REMOVE; }); GLib.Source.set_name_by_id(this._popupCaptionMenuTimeoutId, '[gnome-shell] this.setPopupMenuFlag'); } } _updateThumbnailCaption(thumbnail, i, captionHeight, captionBackgroundHeight) { thumbnail.caption.updateCaption(i, captionHeight, captionBackgroundHeight); } _syncStacking(overview, stackIndices) { for (let i = 0; i < this._thumbnails.length; i++) this._thumbnails[i].syncStacking(stackIndices); } set scale(scale) { if (this._scale == scale) return; this._scale = scale; this.notify('scale'); this.queue_relayout(); } get scale() { return this._scale; } // eslint-disable-next-line camelcase set indicator_y(indicatorY) { if (this._indicatorY == indicatorY) return; this._indicatorY = indicatorY; this.notify('indicator-y'); this.queue_relayout(); } // eslint-disable-next-line camelcase get indicator_y() { return this._indicatorY; } // passingthru67 - added set indicatorX for when position isHorizontal set indicator_x(indicatorX) { if (this._indicatorX == indicatorX) return; this._indicatorX = indicatorX; this.notify('indicator-x'); this.queue_relayout(); } // passingthru67 - added get indicatorX for when position isHorizontal get indicator_x() { return this._indicatorX; } _setThumbnailState(thumbnail, state) { this._stateCounts[thumbnail.state]--; thumbnail.state = state; this._stateCounts[thumbnail.state]++; } _iterateStateThumbnails(state, callback) { if (this._stateCounts[state] == 0) return; for (let i = 0; i < this._thumbnails.length; i++) { if (this._thumbnails[i].state == state) callback.call(this, this._thumbnails[i]); } } _updateStates() { this._stateUpdateQueued = false; // If we are animating the indicator, wait if (this._animatingIndicator) return; // Then slide out any thumbnails that have been destroyed this._iterateStateThumbnails(ThumbnailState.REMOVING, thumbnail => { this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_OUT); thumbnail.ease_property('slide-position', 1, { duration: SLIDE_ANIMATION_TIME, mode: Clutter.AnimationMode.LINEAR, onComplete: () => { this._setThumbnailState(thumbnail, ThumbnailState.ANIMATED_OUT); this._queueUpdateStates(); }, }); }); // As long as things are sliding out, don't proceed if (this._stateCounts[ThumbnailState.ANIMATING_OUT] > 0) return; // Once that's complete, we can start scaling to the new size and collapse any removed thumbnails this._iterateStateThumbnails(ThumbnailState.ANIMATED_OUT, thumbnail => { this._setThumbnailState(thumbnail, ThumbnailState.COLLAPSING); thumbnail.ease_property('collapse-fraction', 1, { duration: RESCALE_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { this._stateCounts[thumbnail.state]--; thumbnail.state = ThumbnailState.DESTROYED; let index = this._thumbnails.indexOf(thumbnail); this._thumbnails.splice(index, 1); thumbnail.destroy(); this._queueUpdateStates(); }, }); }); if (this._pendingScaleUpdate) { this.ease_property('scale', this._targetScale, { mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: RESCALE_ANIMATION_TIME, onComplete: () => this._queueUpdateStates(), }); this._pendingScaleUpdate = false; } // Wait until that's done if (this._scale != this._targetScale || this._stateCounts[ThumbnailState.COLLAPSING] > 0) return; // And then slide in any new thumbnails this._iterateStateThumbnails(ThumbnailState.NEW, thumbnail => { this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_IN); thumbnail.ease_property('slide-position', 0, { duration: SLIDE_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { this._setThumbnailState(thumbnail, ThumbnailState.NORMAL); }, }); }); } _queueUpdateStates() { if (this._stateUpdateQueued) return; Meta.later_add(Meta.LaterType.BEFORE_REDRAW, this._updateStates.bind(this)); this._stateUpdateQueued = true; } vfunc_get_preferred_height(forWidth) { // Note that for getPreferredWidth/Height we cheat a bit and skip propagating // the size request to our children because we know how big they are and know // that the actors aren't depending on the virtual functions being called. let workspaceManager = global.workspace_manager; let themeNode = this.get_theme_node(); let spacing = themeNode.get_length('spacing'); let nWorkspaces = workspaceManager.n_workspaces; // passingthru67 - make room for thumbnail captions let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor; let captionBackgroundHeight = 0; if (this._mySettings.get_boolean('workspace-captions')) { captionBackgroundHeight = this._mySettings.get_double('workspace-caption-height') * scale_factor; } if (this._isHorizontal) { let totalSpacing = (nWorkspaces - 1) * spacing; let avail = forWidth - totalSpacing; let scale = (avail / nWorkspaces) / this._porthole.width; if (this._mySettings.get_boolean('customize-thumbnail')) { scale = Math.min(scale, this._mySettings.get_double('thumbnail-size') * scale_factor); } else { scale = Math.min(scale, MAX_THUMBNAIL_SCALE * scale_factor); } let minHeight = Math.round(this._porthole.height * scale); let naturalHeight = minHeight + captionBackgroundHeight; return themeNode.adjust_preferred_height(naturalHeight, naturalHeight); } else { let totalSpacing = (nWorkspaces * captionBackgroundHeight) + ((nWorkspaces - 1) * spacing); let maxScale; if (this._mySettings.get_boolean('customize-thumbnail')) { maxScale = this._mySettings.get_double('thumbnail-size') * scale_factor; } else { maxScale = MAX_THUMBNAIL_SCALE * scale_factor; } let minHeight = totalSpacing + this._porthole.height * maxScale; let naturalHeight = totalSpacing + nWorkspaces * this._porthole.height * maxScale; return themeNode.adjust_preferred_height(minHeight, naturalHeight); } } vfunc_get_preferred_width(forHeight) { let workspaceManager = global.workspace_manager; let themeNode = this.get_theme_node(); forHeight = themeNode.adjust_for_height(forHeight); let spacing = themeNode.get_length('spacing'); let nWorkspaces = workspaceManager.n_workspaces; // passingthru67 - make room for thumbnail captions let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor; let captionBackgroundHeight = 0; if (this._mySettings.get_boolean('workspace-captions')) { captionBackgroundHeight = this._mySettings.get_double('workspace-caption-height') * scale_factor; } if (this._isHorizontal) { let totalSpacing = (nWorkspaces - 1) * spacing; let maxScale; if (this._mySettings.get_boolean('customize-thumbnail')) { maxScale = this._mySettings.get_double('thumbnail-size') * scale_factor; } else { maxScale = MAX_THUMBNAIL_SCALE * scale_factor; } let minWidth = totalSpacing + this._porthole.width * maxScale; let naturalWidth = totalSpacing + nWorkspaces * this._porthole.width * maxScale; return themeNode.adjust_preferred_width(minWidth, naturalWidth); } else { let totalSpacing = (nWorkspaces * captionBackgroundHeight) + ((nWorkspaces - 1) * spacing); let avail = forHeight - totalSpacing; let scale = (avail / nWorkspaces) / this._porthole.height; if (this._mySettings.get_boolean('customize-thumbnail')) { scale = Math.min(scale, this._mySettings.get_double('thumbnail-size') * scale_factor); } else { scale = Math.min(scale, MAX_THUMBNAIL_SCALE * scale_factor); } let width = Math.round(this._porthole.width * scale); return themeNode.adjust_preferred_width(width, width); } } _updatePorthole() { if (!Main.layoutManager.primaryMonitor) { this._porthole = { width: global.stage.width, height: global.stage.height, x: global.stage.x, y: global.stage.y }; } else { this._porthole = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex); } this.queue_relayout(); } vfunc_allocate(box, flags) { this.set_allocation(box, flags); this._thumbnailsBoxWidth = this.width; this._thumbnailsBoxHeight = this.height; // passingthru67: we use this._position instead of rtl // let rtl = (Clutter.get_default_text_direction () == Clutter.TextDirection.RTL); if (this._thumbnails.length == 0) // not visible return; let workspaceManager = global.workspace_manager; let themeNode = this.get_theme_node(); box = themeNode.get_content_box(box); let portholeWidth = this._porthole.width; let portholeHeight = this._porthole.height; let spacing = themeNode.get_length('spacing'); // passingthru67 - Caption area below thumbnail used to display thumbnail labels let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor; let captionHeight = 0; let captionBackgroundHeight = 0; if (this._mySettings.get_boolean('workspace-captions')) { captionBackgroundHeight = this._mySettings.get_double('workspace-caption-height') * scale_factor; let zoomSize = this._mySettings.get_double('workspace-caption-taskbar-icon-size') + ThumbnailCaption.CAPTION_APP_ICON_ZOOM; captionHeight = Math.max(captionBackgroundHeight + 4, zoomSize*scale_factor + 4); // NOTE: +4 needed for padding // This value should actually be gotten from the theme node get_padding } // Compute the scale we'll need once everything is updated let nWorkspaces = workspaceManager.n_workspaces; // passingthru67 - total spacing depends on on caption showing let totalSpacing; if (this._isHorizontal) { totalSpacing = (nWorkspaces - 1) * spacing; } else { totalSpacing = (nWorkspaces * captionBackgroundHeight) + ((nWorkspaces -1) * spacing); } let avail; if (this._isHorizontal) { avail = (box.x2 - box.x1) - totalSpacing; } else { avail = (box.y2 - box.y1) - totalSpacing; } let newScale; if (this._isHorizontal) { newScale = (avail / nWorkspaces) / portholeWidth; } else { newScale = (avail / nWorkspaces) / portholeHeight } if (this._mySettings.get_boolean('customize-thumbnail')) { newScale = Math.min(newScale, this._mySettings.get_double('thumbnail-size') * scale_factor); } else { newScale = Math.min(newScale, MAX_THUMBNAIL_SCALE * scale_factor); } if (newScale != this._targetScale) { if (this._targetScale > 0) { // We don't do the tween immediately because we need to observe the ordering // in queueUpdateStates - if workspaces have been removed we need to slide them // out as the first thing. this._targetScale = newScale; this._pendingScaleUpdate = true; } else { this._targetScale = this._scale = newScale; } this._queueUpdateStates(); } // passingthru67 - roundedVScale used instead of roundedHscale when position isHorizontal let thumbnailHeight, thumbnailWidth, roundedHScale, roundedVScale; if (this._isHorizontal) { thumbnailWidth = portholeWidth * this._scale; thumbnailHeight = Math.round(portholeHeight * this._scale); roundedVScale = thumbnailHeight / portholeHeight; } else { thumbnailHeight = portholeHeight * this._scale; thumbnailWidth = Math.round(portholeWidth * this._scale); roundedHScale = thumbnailWidth / portholeWidth; } let slideOffset; // X offset when thumbnail is fully slid offscreen if (this._position == St.Side.LEFT) slideOffset = - (thumbnailWidth + themeNode.get_padding(St.Side.LEFT)); else if (this._position == St.Side.RIGHT) slideOffset = thumbnailWidth + themeNode.get_padding(St.Side.RIGHT); else if (this._position == St.Side.TOP) slideOffset = - (thumbnailHeight + themeNode.get_padding(St.Side.LEFT)); else if (this._position == St.Side.BOTTOM) slideOffset = thumbnailHeight + themeNode.get_padding(St.Side.RIGHT); // passingthru67 - indicatorX used instead of indicatorY when position isHorizontal let indicatorY1 = this._indicatorY; let indicatorY2; let indicatorX1 = this._indicatorX; let indicatorX2; // when not animating, the workspace position overrides this._indicatorY let activeWorkspace = workspaceManager.get_active_workspace(); let indicatorWorkspace = !this._animatingIndicator ? activeWorkspace : null; let indicatorThemeNode = this._indicator.get_theme_node(); let indicatorTopFullBorder = indicatorThemeNode.get_padding(St.Side.TOP) + indicatorThemeNode.get_border_width(St.Side.TOP); let indicatorBottomFullBorder = indicatorThemeNode.get_padding(St.Side.BOTTOM) + indicatorThemeNode.get_border_width(St.Side.BOTTOM); let indicatorLeftFullBorder = indicatorThemeNode.get_padding(St.Side.LEFT) + indicatorThemeNode.get_border_width(St.Side.LEFT); let indicatorRightFullBorder = indicatorThemeNode.get_padding(St.Side.RIGHT) + indicatorThemeNode.get_border_width(St.Side.RIGHT); // passingthru67 - x used instead of y when position isHorizontal let y = box.y1; let x = box.x1; if (this._dropPlaceholderPos == -1) { Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { this._dropPlaceholder.hide(); }); } let childBox = new Clutter.ActorBox(); if (this._isHorizontal) { for (let i = 0; i < this._thumbnails.length; i++) { let thumbnail = this._thumbnails[i]; if (i > 0) { // x += spacing - Math.round(thumbnail.collapse_fraction * spacing); x += spacing; } let y1, y2; if (this._position == St.Side.TOP) { y1 = box.y1; y2 = y1 + thumbnailHeight + captionBackgroundHeight; } else { y1 = box.y2 - thumbnailHeight - captionBackgroundHeight; y2 = y1 + thumbnailHeight + captionBackgroundHeight; } if (i == this._dropPlaceholderPos) { //Passingthru67: Gnome's placeholder style doesn't take into account horizontal workspaces. //So we'll use the preferred height value instead of the width // let [, placeholderWidth] = this._dropPlaceholder.get_preferred_width(-1); let [, placeholderWidth] = this._dropPlaceholder.get_preferred_height(-1); childBox.y1 = y1; childBox.y2 = y2; childBox.x1 = Math.round(x); childBox.x2 = Math.round(x + placeholderWidth); this._dropPlaceholder.allocate(childBox, flags); Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { this._dropPlaceholder.show(); }); x += placeholderWidth + spacing; } // We might end up with thumbnailHeight being something like 99.33 // pixels. To make this work and not end up with a gap at the bottom, // we need some thumbnails to be 99 pixels and some 100 pixels height; // we compute an actual scale separately for each thumbnail. let x1 = Math.round(x); let x2 = Math.round(x + thumbnailWidth); // passingthru67 - roundedHScale now defined above roundedHScale = (x2 - x1) / portholeWidth; if (thumbnail.metaWorkspace == indicatorWorkspace) { indicatorX1 = x1; indicatorX2 = x2; } // Allocating a scaled actor is funny - x1/y1 correspond to the origin // of the actor, but x2/y2 are increased by the *unscaled* size. childBox.x1 = x1; childBox.x2 = x1 + portholeWidth; childBox.y1 = y1; // passingthru67 - size needs to include caption area childBox.y2 = y1 + portholeHeight + (captionBackgroundHeight/roundedVScale); thumbnail.set_scale(roundedHScale, roundedVScale); thumbnail.allocate(childBox, flags); // passingthru67 - set myWorkspaceThumbnail labels if (this._mySettings.get_boolean('workspace-captions')) this._updateThumbnailCaption(thumbnail, i, captionHeight, captionBackgroundHeight); // We round the collapsing portion so that we don't get thumbnails resizing // during an animation due to differences in rounded, but leave the uncollapsed // portion unrounded so that non-animating we end up with the right total x += thumbnailWidth - Math.round(thumbnailWidth * thumbnail.collapse_fraction); } if (this._position == St.Side.TOP) { childBox.y1 = box.y1; childBox.y2 = box.y1 + thumbnailHeight + captionBackgroundHeight; } else { childBox.y1 = box.y2 - thumbnailHeight - captionBackgroundHeight; childBox.y2 = box.y2; } childBox.y1 -= indicatorTopFullBorder; childBox.y2 += indicatorBottomFullBorder; childBox.x1 = indicatorX1 - indicatorLeftFullBorder; childBox.x2 = (indicatorX2 ? indicatorX2 : (indicatorX1 + thumbnailWidth)) + indicatorRightFullBorder; } else { for (let i = 0; i < this._thumbnails.length; i++) { let thumbnail = this._thumbnails[i]; if (i > 0) y += spacing + captionBackgroundHeight - Math.round(thumbnail.collapse_fraction * spacing); let x1, x2; if (this._position == St.Side.LEFT) { x1 = box.x1 + slideOffset * thumbnail.slide_position; x2 = x1 + thumbnailWidth; } else { x1 = box.x2 - thumbnailWidth + slideOffset * thumbnail.slide_position; x2 = x1 + thumbnailWidth; } if (i == this._dropPlaceholderPos) { let [, placeholderHeight] = this._dropPlaceholder.get_preferred_height(-1); childBox.x1 = x1; childBox.x2 = x2; childBox.y1 = Math.round(y); childBox.y2 = Math.round(y + placeholderHeight); this._dropPlaceholder.allocate(childBox, flags); Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { this._dropPlaceholder.show(); }); y += placeholderHeight + spacing; } // We might end up with thumbnailHeight being something like 99.33 // pixels. To make this work and not end up with a gap at the bottom, // we need some thumbnails to be 99 pixels and some 100 pixels height; // we compute an actual scale separately for each thumbnail. let y1 = Math.round(y); let y2 = Math.round(y + thumbnailHeight); // passingthru67 - roundedVScale now defined above with roundedHScale roundedVScale = (y2 - y1) / portholeHeight; if (thumbnail.metaWorkspace == indicatorWorkspace) { indicatorY1 = y1; indicatorY2 = y2; } // Allocating a scaled actor is funny - x1/y1 correspond to the origin // of the actor, but x2/y2 are increased by the *unscaled* size. childBox.x1 = x1; childBox.x2 = x1 + portholeWidth; childBox.y1 = y1; // passingthru67 - size needs to include caption area childBox.y2 = y1 + portholeHeight + (captionBackgroundHeight/roundedVScale); thumbnail.set_scale(roundedHScale, roundedVScale); thumbnail.allocate(childBox, flags); // passingthru67 - set myWorkspaceThumbnail labels if (this._mySettings.get_boolean('workspace-captions')) this._updateThumbnailCaption(thumbnail, i, captionHeight, captionBackgroundHeight); // We round the collapsing portion so that we don't get thumbnails resizing // during an animation due to differences in rounded, but leave the uncollapsed // portion unrounded so that non-animating we end up with the right total y += thumbnailHeight - Math.round(thumbnailHeight * thumbnail.collapse_fraction); } if (this._position == St.Side.LEFT) { childBox.x1 = box.x1; childBox.x2 = box.x1 + thumbnailWidth; } else { childBox.x1 = box.x2 - thumbnailWidth; childBox.x2 = box.x2; } childBox.x1 -= indicatorLeftFullBorder; childBox.x2 += indicatorRightFullBorder; childBox.y1 = indicatorY1 - indicatorTopFullBorder; // passingthru67 - indicator needs to include caption childBox.y2 = (indicatorY2 ? indicatorY2 + captionBackgroundHeight : (indicatorY1 + thumbnailHeight + captionBackgroundHeight)) + indicatorBottomFullBorder; } this._indicator.allocate(childBox, flags); } _activeWorkspaceChanged(_wm, _from, _to, _direction) { let thumbnail; let workspaceManager = global.workspace_manager; let activeWorkspace = workspaceManager.get_active_workspace(); for (let i = 0; i < this._thumbnails.length; i++) { if (this._thumbnails[i].metaWorkspace == activeWorkspace) { thumbnail = this._thumbnails[i]; break; } } // passingthru67 - needed in case thumbnail is null outside of overview if (thumbnail == null) return this._animatingIndicator = true; let indicatorThemeNode = this._indicator.get_theme_node(); if (this._isHorizontal) { let indicatorLeftFullBorder = indicatorThemeNode.get_padding(St.Side.LEFT) + indicatorThemeNode.get_border_width(St.Side.LEFT); this.indicatorX = this._indicator.allocation.x1 + indicatorLeftFullBorder; this.ease_property('indicator-x', thumbnail.allocation.x1, { progress_mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: WorkspacesView.WORKSPACE_SWITCH_TIME, onComplete: () => { this._animatingIndicator = false; this._queueUpdateStates(); } }); } else { let indicatorTopFullBorder = indicatorThemeNode.get_padding(St.Side.TOP) + indicatorThemeNode.get_border_width(St.Side.TOP); this.indicator_y = this._indicator.allocation.y1 + indicatorTopFullBorder; this.ease_property('indicator-y', thumbnail.allocation.y1, { progress_mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: WorkspacesView.WORKSPACE_SWITCH_TIME, onComplete: () => { this._animatingIndicator = false; this._queueUpdateStates(); } }); } } });