diff options
Diffstat (limited to 'js/ui/workspacesView.js')
-rw-r--r-- | js/ui/workspacesView.js | 1156 |
1 files changed, 1156 insertions, 0 deletions
diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js new file mode 100644 index 0000000..660fcf6 --- /dev/null +++ b/js/ui/workspacesView.js @@ -0,0 +1,1156 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported WorkspacesView, WorkspacesDisplay */ + +const { Clutter, Gio, GObject, Meta, Shell, St } = imports.gi; + +const Layout = imports.ui.layout; +const Main = imports.ui.main; +const OverviewControls = imports.ui.overviewControls; +const SwipeTracker = imports.ui.swipeTracker; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; +const { ThumbnailsBox, MAX_THUMBNAIL_SCALE } = imports.ui.workspaceThumbnail; + +var WORKSPACE_SWITCH_TIME = 250; + +const MUTTER_SCHEMA = 'org.gnome.mutter'; + +const WORKSPACE_MIN_SPACING = 24; +const WORKSPACE_MAX_SPACING = 80; + +const WORKSPACE_INACTIVE_SCALE = 0.94; + +const SECONDARY_WORKSPACE_SCALE = 0.80; + +var WorkspacesViewBase = GObject.registerClass({ + GTypeFlags: GObject.TypeFlags.ABSTRACT, +}, class WorkspacesViewBase extends St.Widget { + _init(monitorIndex, overviewAdjustment) { + super._init({ + style_class: 'workspaces-view', + x_expand: true, + y_expand: true, + }); + this.connect('destroy', this._onDestroy.bind(this)); + global.focus_manager.add_group(this); + + this._monitorIndex = monitorIndex; + + this._inDrag = false; + Main.overview.connectObject( + 'window-drag-begin', this._dragBegin.bind(this), + 'window-drag-end', this._dragEnd.bind(this), this); + + this._overviewAdjustment = overviewAdjustment; + overviewAdjustment.connectObject('notify::value', + () => this._updateWorkspaceMode(), this); + } + + _onDestroy() { + this._dragEnd(); + } + + _dragBegin() { + this._inDrag = true; + } + + _dragEnd() { + this._inDrag = false; + } + + _updateWorkspaceMode() { + } + + vfunc_allocate(box) { + this.set_allocation(box); + + for (const child of this) + child.allocate_available_size(0, 0, box.get_width(), box.get_height()); + } + + vfunc_get_preferred_width() { + return [0, 0]; + } + + vfunc_get_preferred_height() { + return [0, 0]; + } +}); + +var FitMode = { + SINGLE: 0, + ALL: 1, +}; + +var WorkspacesView = GObject.registerClass( +class WorkspacesView extends WorkspacesViewBase { + _init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) { + let workspaceManager = global.workspace_manager; + + super._init(monitorIndex, overviewAdjustment); + + this._controls = controls; + this._fitModeAdjustment = fitModeAdjustment; + this._fitModeAdjustment.connectObject('notify::value', () => { + this._updateVisibility(); + this._updateWorkspacesState(); + this.queue_relayout(); + }, this); + + this._animating = false; // tweening + this._gestureActive = false; // touch(pad) gestures + + this._scrollAdjustment = scrollAdjustment; + this._scrollAdjustment.connectObject('notify::value', + this._onScrollAdjustmentChanged.bind(this), this); + + this._workspaces = []; + this._updateWorkspaces(); + workspaceManager.connectObject( + 'notify::n-workspaces', this._updateWorkspaces.bind(this), + 'workspaces-reordered', () => { + this._workspaces.sort((a, b) => { + return a.metaWorkspace.index() - b.metaWorkspace.index(); + }); + this._workspaces.forEach( + (ws, i) => this.set_child_at_index(ws, i)); + }, this); + + global.window_manager.connectObject('switch-workspace', + this._activeWorkspaceChanged.bind(this), this); + } + + _getFirstFitAllWorkspaceBox(box, spacing, vertical) { + const { nWorkspaces } = global.workspaceManager; + const [width, height] = box.get_size(); + const [workspace] = this._workspaces; + + const fitAllBox = new Clutter.ActorBox(); + + let [x1, y1] = box.get_origin(); + + // Spacing here is not only the space between workspaces, but also the + // space before the first workspace, and after the last one. This prevents + // workspaces from touching the edges of the allocation box. + if (vertical) { + const availableHeight = height - spacing * (nWorkspaces + 1); + let workspaceHeight = availableHeight / nWorkspaces; + let [, workspaceWidth] = + workspace.get_preferred_width(workspaceHeight); + + y1 = spacing; + if (workspaceWidth > width) { + [, workspaceHeight] = workspace.get_preferred_height(width); + y1 += Math.max((availableHeight - workspaceHeight * nWorkspaces) / 2, 0); + } + + fitAllBox.set_size(width, workspaceHeight); + } else { + const availableWidth = width - spacing * (nWorkspaces + 1); + let workspaceWidth = availableWidth / nWorkspaces; + let [, workspaceHeight] = + workspace.get_preferred_height(workspaceWidth); + + x1 = spacing; + if (workspaceHeight > height) { + [, workspaceWidth] = workspace.get_preferred_width(height); + x1 += Math.max((availableWidth - workspaceWidth * nWorkspaces) / 2, 0); + } + + fitAllBox.set_size(workspaceWidth, height); + } + + fitAllBox.set_origin(x1, y1); + + return fitAllBox; + } + + _getFirstFitSingleWorkspaceBox(box, spacing, vertical) { + const [width, height] = box.get_size(); + const [workspace] = this._workspaces; + + const rtl = this.text_direction === Clutter.TextDirection.RTL; + const adj = this._scrollAdjustment; + const currentWorkspace = vertical || !rtl + ? adj.value : adj.upper - adj.value - 1; + + // Single fit mode implies centered too + let [x1, y1] = box.get_origin(); + if (vertical) { + const [, workspaceHeight] = workspace.get_preferred_height(width); + y1 += (height - workspaceHeight) / 2; + y1 -= currentWorkspace * (workspaceHeight + spacing); + } else { + const [, workspaceWidth] = workspace.get_preferred_width(height); + x1 += (width - workspaceWidth) / 2; + x1 -= currentWorkspace * (workspaceWidth + spacing); + } + + const fitSingleBox = new Clutter.ActorBox({ x1, y1 }); + + if (vertical) { + const [, workspaceHeight] = workspace.get_preferred_height(width); + fitSingleBox.set_size(width, workspaceHeight); + } else { + const [, workspaceWidth] = workspace.get_preferred_width(height); + fitSingleBox.set_size(workspaceWidth, height); + } + + return fitSingleBox; + } + + _getSpacing(box, fitMode, vertical) { + const [width, height] = box.get_size(); + const [workspace] = this._workspaces; + + let availableSpace; + let workspaceSize; + if (vertical) { + [, workspaceSize] = workspace.get_preferred_height(width); + availableSpace = (height - workspaceSize) / 2; + } else { + [, workspaceSize] = workspace.get_preferred_width(height); + availableSpace = (width - workspaceSize) / 2; + } + + const spacing = (availableSpace - workspaceSize * 0.4) * (1 - fitMode); + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + + return Math.clamp(spacing, WORKSPACE_MIN_SPACING * scaleFactor, + WORKSPACE_MAX_SPACING * scaleFactor); + } + + _getWorkspaceModeForOverviewState(state) { + const { ControlsState } = OverviewControls; + + switch (state) { + case ControlsState.HIDDEN: + return 0; + case ControlsState.WINDOW_PICKER: + return 1; + case ControlsState.APP_GRID: + return 0; + } + + return 0; + } + + _updateWorkspacesState() { + const adj = this._scrollAdjustment; + const fitMode = this._fitModeAdjustment.value; + + const { initialState, finalState, progress } = + this._overviewAdjustment.getStateTransitionParams(); + + const workspaceMode = (1 - fitMode) * Util.lerp( + this._getWorkspaceModeForOverviewState(initialState), + this._getWorkspaceModeForOverviewState(finalState), + progress); + + // Fade and scale inactive workspaces + this._workspaces.forEach((w, index) => { + w.stateAdjustment.value = workspaceMode; + + const distanceToCurrentWorkspace = Math.abs(adj.value - index); + + const scaleProgress = 1 - Math.clamp(distanceToCurrentWorkspace, 0, 1); + + const scale = Util.lerp(WORKSPACE_INACTIVE_SCALE, 1, scaleProgress); + w.set_scale(scale, scale); + }); + } + + _getFitModeForState(state) { + const { ControlsState } = OverviewControls; + + switch (state) { + case ControlsState.HIDDEN: + case ControlsState.WINDOW_PICKER: + return FitMode.SINGLE; + case ControlsState.APP_GRID: + return FitMode.ALL; + default: + return FitMode.SINGLE; + } + } + + _getInitialBoxes(box) { + const offsetBox = new Clutter.ActorBox(); + offsetBox.set_size(...box.get_size()); + + let fitSingleBox = offsetBox; + let fitAllBox = offsetBox; + + const { transitioning, initialState, finalState } = + this._overviewAdjustment.getStateTransitionParams(); + + const isPrimary = Main.layoutManager.primaryIndex === this._monitorIndex; + + if (isPrimary && transitioning) { + const initialFitMode = this._getFitModeForState(initialState); + const finalFitMode = this._getFitModeForState(finalState); + + // Only use the relative boxes when the overview is in a state + // transition, and the corresponding fit modes are different. + if (initialFitMode !== finalFitMode) { + const initialBox = + this._controls.getWorkspacesBoxForState(initialState).copy(); + const finalBox = + this._controls.getWorkspacesBoxForState(finalState).copy(); + + // Boxes are relative to ControlsManager, transform them; + // this.apply_relative_transform_to_point(controls, + // new Graphene.Point3D()); + // would be more correct, but also more expensive + const [parentOffsetX, parentOffsetY] = + this.get_parent().allocation.get_origin(); + [initialBox, finalBox].forEach(b => { + b.set_origin(b.x1 - parentOffsetX, b.y1 - parentOffsetY); + }); + + if (initialFitMode === FitMode.SINGLE) + [fitSingleBox, fitAllBox] = [initialBox, finalBox]; + else + [fitAllBox, fitSingleBox] = [initialBox, finalBox]; + } + } + + return [fitSingleBox, fitAllBox]; + } + + _updateWorkspaceMode() { + this._updateWorkspacesState(); + } + + vfunc_allocate(box) { + this.set_allocation(box); + + if (this.get_n_children() === 0) + return; + + const vertical = global.workspaceManager.layout_rows === -1; + const rtl = this.text_direction === Clutter.TextDirection.RTL; + + const fitMode = this._fitModeAdjustment.value; + + let [fitSingleBox, fitAllBox] = this._getInitialBoxes(box); + const fitSingleSpacing = + this._getSpacing(fitSingleBox, FitMode.SINGLE, vertical); + fitSingleBox = + this._getFirstFitSingleWorkspaceBox(fitSingleBox, fitSingleSpacing, vertical); + + const fitAllSpacing = + this._getSpacing(fitAllBox, FitMode.ALL, vertical); + fitAllBox = + this._getFirstFitAllWorkspaceBox(fitAllBox, fitAllSpacing, vertical); + + // Account for RTL locales by reversing the list + const workspaces = this._workspaces.slice(); + if (rtl) + workspaces.reverse(); + + const [fitSingleX1, fitSingleY1] = fitSingleBox.get_origin(); + const [fitSingleWidth, fitSingleHeight] = fitSingleBox.get_size(); + const [fitAllX1, fitAllY1] = fitAllBox.get_origin(); + const [fitAllWidth, fitAllHeight] = fitAllBox.get_size(); + + workspaces.forEach(child => { + if (fitMode === FitMode.SINGLE) + box = fitSingleBox; + else if (fitMode === FitMode.ALL) + box = fitAllBox; + else + box = fitSingleBox.interpolate(fitAllBox, fitMode); + + child.allocate_align_fill(box, 0.5, 0.5, false, false); + + if (vertical) { + fitSingleBox.set_origin( + fitSingleX1, + fitSingleBox.y1 + fitSingleHeight + fitSingleSpacing); + fitAllBox.set_origin( + fitAllX1, + fitAllBox.y1 + fitAllHeight + fitAllSpacing); + } else { + fitSingleBox.set_origin( + fitSingleBox.x1 + fitSingleWidth + fitSingleSpacing, + fitSingleY1); + fitAllBox.set_origin( + fitAllBox.x1 + fitAllWidth + fitAllSpacing, + fitAllY1); + } + }); + } + + getActiveWorkspace() { + let workspaceManager = global.workspace_manager; + let active = workspaceManager.get_active_workspace_index(); + return this._workspaces[active]; + } + + prepareToLeaveOverview() { + for (let w = 0; w < this._workspaces.length; w++) + this._workspaces[w].prepareToLeaveOverview(); + } + + syncStacking(stackIndices) { + for (let i = 0; i < this._workspaces.length; i++) + this._workspaces[i].syncStacking(stackIndices); + } + + _scrollToActive() { + const { workspaceManager } = global; + const active = workspaceManager.get_active_workspace_index(); + + this._animating = true; + this._updateVisibility(); + + this._scrollAdjustment.remove_transition('value'); + this._scrollAdjustment.ease(active, { + duration: WORKSPACE_SWITCH_TIME, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + onComplete: () => { + this._animating = false; + this._updateVisibility(); + }, + }); + } + + _updateVisibility() { + let workspaceManager = global.workspace_manager; + let active = workspaceManager.get_active_workspace_index(); + + const fitMode = this._fitModeAdjustment.value; + const singleFitMode = fitMode === FitMode.SINGLE; + + for (let w = 0; w < this._workspaces.length; w++) { + let workspace = this._workspaces[w]; + + if (this._animating || this._gestureActive || !singleFitMode) + workspace.show(); + else + workspace.visible = Math.abs(w - active) <= 1; + } + } + + _updateWorkspaces() { + let workspaceManager = global.workspace_manager; + let newNumWorkspaces = workspaceManager.n_workspaces; + + for (let j = 0; j < newNumWorkspaces; j++) { + let metaWorkspace = workspaceManager.get_workspace_by_index(j); + let workspace; + + if (j >= this._workspaces.length) { /* added */ + workspace = new Workspace.Workspace( + metaWorkspace, + this._monitorIndex, + this._overviewAdjustment); + this.add_actor(workspace); + this._workspaces[j] = workspace; + } else { + workspace = this._workspaces[j]; + + if (workspace.metaWorkspace != metaWorkspace) { /* removed */ + workspace.destroy(); + this._workspaces.splice(j, 1); + } /* else kept */ + } + } + + for (let j = this._workspaces.length - 1; j >= newNumWorkspaces; j--) { + this._workspaces[j].destroy(); + this._workspaces.splice(j, 1); + } + + this._updateWorkspacesState(); + this._updateVisibility(); + } + + _activeWorkspaceChanged(_wm, _from, _to, _direction) { + this._scrollToActive(); + } + + _onDestroy() { + super._onDestroy(); + + this._workspaces = []; + } + + startTouchGesture() { + this._gestureActive = true; + + this._updateVisibility(); + } + + endTouchGesture() { + this._gestureActive = false; + + // Make sure title captions etc are shown as necessary + this._scrollToActive(); + this._updateVisibility(); + } + + // sync the workspaces' positions to the value of the scroll adjustment + // and change the active workspace if appropriate + _onScrollAdjustmentChanged() { + if (!this.has_allocation()) + return; + + const adj = this._scrollAdjustment; + const allowSwitch = + adj.get_transition('value') === null && !this._gestureActive; + + let workspaceManager = global.workspace_manager; + let active = workspaceManager.get_active_workspace_index(); + let current = Math.round(adj.value); + + if (allowSwitch && active !== current) { + if (!this._workspaces[current]) { + // The current workspace was destroyed. This could happen + // when you are on the last empty workspace, and consolidate + // windows using the thumbnail bar. + // In that case, the intended behavior is to stay on the empty + // workspace, which is the last one, so pick it. + current = this._workspaces.length - 1; + } + + let metaWorkspace = this._workspaces[current].metaWorkspace; + metaWorkspace.activate(global.get_current_time()); + } + + this._updateWorkspacesState(); + this.queue_relayout(); + } +}); + +var ExtraWorkspaceView = GObject.registerClass( +class ExtraWorkspaceView extends WorkspacesViewBase { + _init(monitorIndex, overviewAdjustment) { + super._init(monitorIndex, overviewAdjustment); + this._workspace = + new Workspace.Workspace(null, monitorIndex, overviewAdjustment); + this.add_actor(this._workspace); + } + + _updateWorkspaceMode() { + const overviewState = this._overviewAdjustment.value; + + const progress = Math.clamp(overviewState, + OverviewControls.ControlsState.HIDDEN, + OverviewControls.ControlsState.WINDOW_PICKER); + + this._workspace.stateAdjustment.value = progress; + } + + vfunc_allocate(box) { + this.set_allocation(box); + + const [width, height] = box.get_size(); + const [, childWidth] = this._workspace.get_preferred_width(height); + + const childBox = new Clutter.ActorBox(); + childBox.set_origin(Math.round((width - childWidth) / 2), 0); + childBox.set_size(childWidth, height); + this._workspace.allocate(childBox); + } + + getActiveWorkspace() { + return this._workspace; + } + + prepareToLeaveOverview() { + this._workspace.prepareToLeaveOverview(); + } + + syncStacking(stackIndices) { + this._workspace.syncStacking(stackIndices); + } + + startTouchGesture() { + } + + endTouchGesture() { + } +}); + +const SecondaryMonitorDisplay = GObject.registerClass( +class SecondaryMonitorDisplay extends St.Widget { + _init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) { + this._monitorIndex = monitorIndex; + this._controls = controls; + this._scrollAdjustment = scrollAdjustment; + this._fitModeAdjustment = fitModeAdjustment; + this._overviewAdjustment = overviewAdjustment; + + super._init({ + style_class: 'secondary-monitor-workspaces', + constraints: new Layout.MonitorConstraint({ + index: this._monitorIndex, + work_area: true, + }), + clip_to_allocation: true, + }); + + this.connect('destroy', () => this._onDestroy()); + + this._thumbnails = new ThumbnailsBox( + this._scrollAdjustment, monitorIndex); + this.add_child(this._thumbnails); + + this._thumbnails.connect('notify::should-show', + () => this._updateThumbnailVisibility()); + + this._overviewAdjustment.connectObject('notify::value', () => { + this._updateThumbnailParams(); + this.queue_relayout(); + }, this); + + this._settings = new Gio.Settings({ schema_id: MUTTER_SCHEMA }); + this._settings.connect('changed::workspaces-only-on-primary', + () => this._workspacesOnPrimaryChanged()); + this._workspacesOnPrimaryChanged(); + } + + _getThumbnailParamsForState(state) { + const { ControlsState } = OverviewControls; + + let opacity, scale; + switch (state) { + case ControlsState.HIDDEN: + case ControlsState.WINDOW_PICKER: + opacity = 255; + scale = 1; + break; + case ControlsState.APP_GRID: + opacity = 0; + scale = 0.5; + break; + default: + opacity = 255; + scale = 1; + break; + } + + return { opacity, scale }; + } + + _getThumbnailsHeight(box) { + if (!this._thumbnails.visible) + return 0; + + const [width, height] = box.get_size(); + const { expandFraction } = this._thumbnails; + const [thumbnailsHeight] = this._thumbnails.get_preferred_height(width); + return Math.min( + thumbnailsHeight * expandFraction, + height * MAX_THUMBNAIL_SCALE); + } + + _getWorkspacesBoxForState(state, box, padding, thumbnailsHeight, spacing) { + const { ControlsState } = OverviewControls; + const workspaceBox = box.copy(); + const [width, height] = workspaceBox.get_size(); + + switch (state) { + case ControlsState.HIDDEN: + break; + case ControlsState.WINDOW_PICKER: + workspaceBox.set_origin(0, padding + thumbnailsHeight + spacing); + workspaceBox.set_size( + width, + height - 2 * padding - thumbnailsHeight - spacing); + break; + case ControlsState.APP_GRID: + workspaceBox.set_origin(0, padding); + workspaceBox.set_size( + width, + height - 2 * padding); + break; + } + + return workspaceBox; + } + + vfunc_allocate(box) { + this.set_allocation(box); + + const themeNode = this.get_theme_node(); + const contentBox = themeNode.get_content_box(box); + const [width, height] = contentBox.get_size(); + const { expandFraction } = this._thumbnails; + const spacing = themeNode.get_length('spacing') * expandFraction; + const padding = + Math.round((1 - SECONDARY_WORKSPACE_SCALE) * height / 2); + + const thumbnailsHeight = this._getThumbnailsHeight(contentBox); + + if (this._thumbnails.visible) { + const childBox = new Clutter.ActorBox(); + childBox.set_origin(0, padding); + childBox.set_size(width, thumbnailsHeight); + this._thumbnails.allocate(childBox); + } + + const { + currentState, initialState, finalState, transitioning, progress, + } = this._overviewAdjustment.getStateTransitionParams(); + + let workspacesBox; + const workspaceParams = [contentBox, padding, thumbnailsHeight, spacing]; + if (!transitioning) { + workspacesBox = + this._getWorkspacesBoxForState(currentState, ...workspaceParams); + } else { + const initialBox = + this._getWorkspacesBoxForState(initialState, ...workspaceParams); + const finalBox = + this._getWorkspacesBoxForState(finalState, ...workspaceParams); + workspacesBox = initialBox.interpolate(finalBox, progress); + } + this._workspacesView.allocate(workspacesBox); + } + + _onDestroy() { + if (this._settings) + this._settings.run_dispose(); + this._settings = null; + } + + _workspacesOnPrimaryChanged() { + this._updateWorkspacesView(); + this._updateThumbnailVisibility(); + } + + _updateWorkspacesView() { + if (this._workspacesView) + this._workspacesView.destroy(); + + if (this._settings.get_boolean('workspaces-only-on-primary')) { + this._workspacesView = new ExtraWorkspaceView( + this._monitorIndex, + this._overviewAdjustment); + } else { + this._workspacesView = new WorkspacesView( + this._monitorIndex, + this._controls, + this._scrollAdjustment, + this._fitModeAdjustment, + this._overviewAdjustment); + } + this.add_child(this._workspacesView); + } + + _updateThumbnailVisibility() { + const visible = + this._thumbnails.should_show && + !this._settings.get_boolean('workspaces-only-on-primary'); + + if (this._thumbnails.visible === visible) + return; + + this._thumbnails.show(); + this._updateThumbnailParams(); + this._thumbnails.ease_property('expand-fraction', visible ? 1 : 0, { + duration: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => (this._thumbnails.visible = visible), + }); + } + + _updateThumbnailParams() { + if (!this._thumbnails.visible) + return; + + const { initialState, finalState, progress } = + this._overviewAdjustment.getStateTransitionParams(); + + const initialParams = this._getThumbnailParamsForState(initialState); + const finalParams = this._getThumbnailParamsForState(finalState); + + const opacity = + Util.lerp(initialParams.opacity, finalParams.opacity, progress); + const scale = + Util.lerp(initialParams.scale, finalParams.scale, progress); + + this._thumbnails.set({ + opacity, + scale_x: scale, + scale_y: scale, + }); + } + + getActiveWorkspace() { + return this._workspacesView.getActiveWorkspace(); + } + + prepareToLeaveOverview() { + this._workspacesView.prepareToLeaveOverview(); + } + + syncStacking(stackIndices) { + this._workspacesView.syncStacking(stackIndices); + } + + startTouchGesture() { + this._workspacesView.startTouchGesture(); + } + + endTouchGesture() { + this._workspacesView.endTouchGesture(); + } +}); + +var WorkspacesDisplay = GObject.registerClass( +class WorkspacesDisplay extends St.Widget { + _init(controls, scrollAdjustment, overviewAdjustment) { + super._init({ + layout_manager: new Clutter.BinLayout(), + reactive: true, + }); + + this._controls = controls; + this._overviewAdjustment = overviewAdjustment; + this._fitModeAdjustment = new St.Adjustment({ + actor: this, + value: FitMode.SINGLE, + lower: FitMode.SINGLE, + upper: FitMode.ALL, + }); + + let workspaceManager = global.workspace_manager; + this._scrollAdjustment = scrollAdjustment; + + global.window_manager.connectObject('switch-workspace', + this._activeWorkspaceChanged.bind(this), this); + + this._swipeTracker = new SwipeTracker.SwipeTracker( + Main.layoutManager.overviewGroup, + Clutter.Orientation.HORIZONTAL, + Shell.ActionMode.OVERVIEW, + { allowDrag: false }); + this._swipeTracker.allowLongSwipes = true; + this._swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this)); + this._swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this)); + this._swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this)); + this.connect('notify::mapped', this._updateSwipeTracker.bind(this)); + + workspaceManager.connectObject( + 'workspaces-reordered', this._workspacesReordered.bind(this), + 'notify::layout-rows', this._updateTrackerOrientation.bind(this), this); + this._updateTrackerOrientation(); + + Main.overview.connectObject( + 'window-drag-begin', this._windowDragBegin.bind(this), + 'window-drag-end', this._windowDragEnd.bind(this), this); + + this._primaryVisible = true; + this._primaryIndex = Main.layoutManager.primaryIndex; + this._workspacesViews = []; + + this._settings = new Gio.Settings({ schema_id: MUTTER_SCHEMA }); + + this._inWindowDrag = false; + this._leavingOverview = false; + + this._gestureActive = false; // touch(pad) gestures + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _onDestroy() { + if (this._parentSetLater) { + Meta.later_remove(this._parentSetLater); + this._parentSetLater = 0; + } + } + + _windowDragBegin() { + this._inWindowDrag = true; + this._updateSwipeTracker(); + } + + _windowDragEnd() { + this._inWindowDrag = false; + this._updateSwipeTracker(); + } + + _updateSwipeTracker() { + this._swipeTracker.enabled = + this.mapped && + !this._inWindowDrag && + !this._leavingOverview; + } + + _workspacesReordered() { + let workspaceManager = global.workspace_manager; + + this._scrollAdjustment.value = + workspaceManager.get_active_workspace_index(); + } + + _activeWorkspaceChanged(_wm, _from, to, _direction) { + if (this._gestureActive) + return; + + this._scrollAdjustment.ease(to, { + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + duration: WORKSPACE_SWITCH_TIME, + }); + } + + _updateTrackerOrientation() { + const { layoutRows } = global.workspace_manager; + this._swipeTracker.orientation = layoutRows !== -1 + ? Clutter.Orientation.HORIZONTAL + : Clutter.Orientation.VERTICAL; + } + + _directionForProgress(progress) { + if (global.workspace_manager.layout_rows === -1) { + return progress > 0 + ? Meta.MotionDirection.DOWN + : Meta.MotionDirection.UP; + } else if (this.text_direction === Clutter.TextDirection.RTL) { + return progress > 0 + ? Meta.MotionDirection.LEFT + : Meta.MotionDirection.RIGHT; + } else { + return progress > 0 + ? Meta.MotionDirection.RIGHT + : Meta.MotionDirection.LEFT; + } + } + + _switchWorkspaceBegin(tracker, monitor) { + if (this._workspacesOnlyOnPrimary && monitor !== this._primaryIndex) + return; + + let workspaceManager = global.workspace_manager; + let adjustment = this._scrollAdjustment; + if (this._gestureActive) + adjustment.remove_transition('value'); + + const distance = global.workspace_manager.layout_rows === -1 + ? this.height : this.width; + + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].startTouchGesture(); + + let progress = adjustment.value / adjustment.page_size; + let points = Array.from( + { length: workspaceManager.n_workspaces }, (v, i) => i); + + tracker.confirmSwipe(distance, points, progress, Math.round(progress)); + + this._gestureActive = true; + } + + _switchWorkspaceUpdate(tracker, progress) { + let adjustment = this._scrollAdjustment; + adjustment.value = progress * adjustment.page_size; + } + + _switchWorkspaceEnd(tracker, duration, endProgress) { + let workspaceManager = global.workspace_manager; + let newWs = workspaceManager.get_workspace_by_index(endProgress); + + this._scrollAdjustment.ease(endProgress, { + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + duration, + onComplete: () => { + if (!newWs.active) + newWs.activate(global.get_current_time()); + this._endTouchGesture(); + }, + }); + } + + _endTouchGesture() { + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].endTouchGesture(); + this._gestureActive = false; + } + + vfunc_navigate_focus(from, direction) { + return this._getPrimaryView()?.navigate_focus(from, direction, false); + } + + setPrimaryWorkspaceVisible(visible) { + if (this._primaryVisible === visible) + return; + + this._primaryVisible = visible; + + const primaryIndex = Main.layoutManager.primaryIndex; + const primaryWorkspace = this._workspacesViews[primaryIndex]; + if (primaryWorkspace) + primaryWorkspace.visible = visible; + } + + prepareToEnterOverview() { + this.show(); + this._updateWorkspacesViews(); + + Main.overview.connectObject( + 'windows-restacked', this._onRestacked.bind(this), + 'scroll-event', this._onScrollEvent.bind(this), this); + + global.stage.connectObject( + 'key-press-event', this._onKeyPressEvent.bind(this), this); + } + + prepareToLeaveOverview() { + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].prepareToLeaveOverview(); + + this._leavingOverview = true; + this._updateSwipeTracker(); + } + + vfunc_hide() { + Main.overview.disconnectObject(this); + global.stage.disconnectObject(this); + + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].destroy(); + this._workspacesViews = []; + + this._leavingOverview = false; + + super.vfunc_hide(); + } + + _updateWorkspacesViews() { + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].destroy(); + + this._primaryIndex = Main.layoutManager.primaryIndex; + this._workspacesViews = []; + let monitors = Main.layoutManager.monitors; + for (let i = 0; i < monitors.length; i++) { + let view; + if (i === this._primaryIndex) { + view = new WorkspacesView(i, + this._controls, + this._scrollAdjustment, + this._fitModeAdjustment, + this._overviewAdjustment); + + view.visible = this._primaryVisible; + this.bind_property('opacity', view, 'opacity', GObject.BindingFlags.SYNC_CREATE); + this.add_child(view); + } else { + view = new SecondaryMonitorDisplay(i, + this._controls, + this._scrollAdjustment, + this._fitModeAdjustment, + this._overviewAdjustment); + Main.layoutManager.overviewGroup.add_actor(view); + } + + this._workspacesViews.push(view); + } + } + + _getMonitorIndexForEvent(event) { + let [x, y] = event.get_coords(); + let rect = new Meta.Rectangle({ x, y, width: 1, height: 1 }); + return global.display.get_monitor_index_for_rect(rect); + } + + _getPrimaryView() { + if (!this._workspacesViews.length) + return null; + return this._workspacesViews[this._primaryIndex]; + } + + activeWorkspaceHasMaximizedWindows() { + const primaryView = this._getPrimaryView(); + return primaryView + ? primaryView.getActiveWorkspace().hasMaximizedWindows() + : false; + } + + _onRestacked(overview, stackIndices) { + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].syncStacking(stackIndices); + } + + _onScrollEvent(actor, event) { + if (this._swipeTracker.canHandleScrollEvent(event)) + return Clutter.EVENT_PROPAGATE; + + if (!this.mapped) + return Clutter.EVENT_PROPAGATE; + + if (this._workspacesOnlyOnPrimary && + this._getMonitorIndexForEvent(event) != this._primaryIndex) + return Clutter.EVENT_PROPAGATE; + + return Main.wm.handleWorkspaceScroll(event); + } + + _onKeyPressEvent(actor, event) { + const { ControlsState } = OverviewControls; + if (this._overviewAdjustment.value !== ControlsState.WINDOW_PICKER) + return Clutter.EVENT_PROPAGATE; + + if (!this.reactive) + return Clutter.EVENT_PROPAGATE; + + const { workspaceManager } = global; + const vertical = workspaceManager.layout_rows === -1; + const rtl = this.get_text_direction() === Clutter.TextDirection.RTL; + + let which; + switch (event.get_key_symbol()) { + case Clutter.KEY_Page_Up: + if (vertical) + which = Meta.MotionDirection.UP; + else if (rtl) + which = Meta.MotionDirection.RIGHT; + else + which = Meta.MotionDirection.LEFT; + break; + case Clutter.KEY_Page_Down: + if (vertical) + which = Meta.MotionDirection.DOWN; + else if (rtl) + which = Meta.MotionDirection.LEFT; + else + which = Meta.MotionDirection.RIGHT; + break; + case Clutter.KEY_Home: + which = 0; + break; + case Clutter.KEY_End: + which = workspaceManager.n_workspaces - 1; + break; + default: + return Clutter.EVENT_PROPAGATE; + } + + let ws; + if (which < 0) + // Negative workspace numbers are directions + // with respect to the current workspace + ws = workspaceManager.get_active_workspace().get_neighbor(which); + else + // Otherwise it is a workspace index + ws = workspaceManager.get_workspace_by_index(which); + + if (ws) + Main.wm.actionMoveWorkspace(ws); + + return Clutter.EVENT_STOP; + } + + get _workspacesOnlyOnPrimary() { + return this._settings.get_boolean('workspaces-only-on-primary'); + } + + get fitModeAdjustment() { + return this._fitModeAdjustment; + } +}); |