/** * V-Shell (Vertical Workspaces) * overviewControls.js * * @author GdH * @copyright 2022 - 2024 * @license GPL-3.0 * */ 'use strict'; import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import St from 'gi://St'; import Shell from 'gi://Shell'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Overview from 'resource:///org/gnome/shell/ui/overview.js'; import * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js'; import * as WorkspacesView from 'resource:///org/gnome/shell/ui/workspacesView.js'; import * as Background from 'resource:///org/gnome/shell/ui/background.js'; import * as Util from 'resource:///org/gnome/shell/misc/util.js'; let Me; let opt; // gettext let _; const ControlsState = OverviewControls.ControlsState; const FitMode = WorkspacesView.FitMode; const STARTUP_ANIMATION_TIME = 500; const ANIMATION_TIME = Overview.ANIMATION_TIME; const DASH_MAX_SIZE_RATIO = 0.35; let _timeouts; export const OverviewControlsModule = class { constructor(me) { Me = me; opt = Me.opt; _ = Me.gettext; this._firstActivation = true; this.moduleEnabled = false; this._overrides = null; } cleanGlobals() { Me = null; opt = null; _ = null; } update(reset) { this._removeTimeouts(); this.moduleEnabled = true; const conflict = false; reset = reset || !this.moduleEnabled || conflict; // don't touch the original code if module disabled if (reset && !this._firstActivation) { this._disableModule(); } else if (!reset) { this._firstActivation = false; this._activateModule(); } if (reset && this._firstActivation) console.debug(' OverviewControlsModule - Keeping untouched'); } _activateModule() { if (!this._overrides) this._overrides = new Me.Util.Overrides(); _timeouts = {}; this._replaceOnSearchChanged(); this._overrides.addOverride('ControlsManager', OverviewControls.ControlsManager.prototype, ControlsManagerCommon); this._overrides.addOverride('ControlsManagerLayoutCommon', Main.overview._overview.controls.layoutManager, ControlsManagerLayoutCommon); if (opt.ORIENTATION === Clutter.Orientation.VERTICAL) this._overrides.addOverride('ControlsManagerLayout', Main.overview._overview.controls.layoutManager, ControlsManagerLayoutVertical); else this._overrides.addOverride('ControlsManagerLayout', Main.overview._overview.controls.layoutManager, ControlsManagerLayoutHorizontal); // Allow user to close the overview by clicking on an empty space on the primary monitor's overview // Secondary monitors are handled in workspacesView this._addClickToCloseOverview(); // Update custom workAreaBox Main.overview._overview.controls.layoutManager._updateWorkAreaBox(); console.debug(' OverviewControlsModule - Activated'); } _disableModule() { if (this._overrides) this._overrides.removeAll(); this._overrides = null; const reset = true; this._replaceOnSearchChanged(reset); Main.overview._overview._controls._appDisplay.opacity = 255; this._addClickToCloseOverview(reset); console.debug(' OverviewControlsModule - Disabled'); } _removeTimeouts() { if (_timeouts) { Object.values(_timeouts).forEach(t => { if (t) GLib.source_remove(t); }); _timeouts = null; } } _replaceOnSearchChanged(reset) { const searchController = Main.overview.searchController; if (reset) { if (this._searchControllerSigId) { searchController.disconnect(this._searchControllerSigId); this._searchControllerSigId = 0; } if (this._originalSearchControllerSigId) { searchController.unblock_signal_handler(this._originalSearchControllerSigId); this._originalSearchControllerSigId = 0; } searchController._searchResults.translation_x = 0; searchController._searchResults.translation_y = 0; Main.overview.searchEntry.visible = true; Main.overview.searchEntry.opacity = 255; } else { // reconnect signal to use custom function (callbacks cannot be overridden in class prototype, they are already in memory as a copy for the given callback) if (!this._originalSearchControllerSigId) { this._originalSearchControllerSigId = GObject.signal_handler_find(searchController, { signalId: 'notify', detail: 'search-active' }); if (this._originalSearchControllerSigId) searchController.block_signal_handler(this._originalSearchControllerSigId); } if (!this._searchControllerSigId) this._searchControllerSigId = searchController.connect('notify::search-active', () => Main.overview._overview.controls._onSearchChanged()); } } _addClickToCloseOverview(reset) { const overview = Main.overview._overview; overview.reactive = false; if (this._clickEmptyConId) { overview.disconnect(this._clickEmptyConId); this._clickEmptyConId = 0; } if (reset || !opt.CLICK_EMPTY_CLOSE) return; overview.reactive = true; this._clickEmptyConId = overview.connect('button-release-event', (actor, event) => { const button = event.get_button(); const overviewState = overview.controls._stateAdjustment.value; const buttonPrimary = button === Clutter.BUTTON_PRIMARY; const buttonSecondary = button === Clutter.BUTTON_SECONDARY; const buttonAny = buttonPrimary || buttonSecondary; if ((overviewState === 1 && buttonAny) || (overviewState === 2 && buttonSecondary)) Main.overview.hide(); }); } }; const ControlsManagerCommon = { // this function is used as a callback by a signal handler, needs to be reconnected after modification as the original callback uses a copy of the original function /* _update: function() { ... }*/ prepareToEnterOverview() { this._searchController.prepareToEnterOverview(); this._workspacesDisplay.prepareToEnterOverview(); // Workaround for thumbnailsBox not re-scaling after switching workspace outside of overview using a trackpad this._thumbnailsBox._updateIndicator(); Main.overview._overview.controls.opacity = 255; // Ensure that overview backgrounds are ready when needed if (!this._bgManagers && (opt.SHOW_BG_IN_OVERVIEW || !opt.SHOW_WS_PREVIEW_BG)) this._setBackground(); else if (this._bgManagers && !(opt.SHOW_BG_IN_OVERVIEW || !opt.SHOW_WS_PREVIEW_BG)) this._setBackground(true); // store pointer X coordinate for OVERVIEW_MODE 1 - to prevent immediate switch to WORKSPACE_MODE 1 if the mouse pointer is steady opt.showingPointerX = global.get_pointer()[0]; }, // this function has duplicate in WorkspaceView so we use one function for both to avoid issues with syncing them _getFitModeForState(state) { return _getFitModeForState(state); }, _updateThumbnailsBox() { const { shouldShow } = this._thumbnailsBox; const thumbnailsBoxVisible = shouldShow; this._thumbnailsBox.visible = thumbnailsBoxVisible; // this call should be directly in _update(), but it's used as a callback function and it would require to reconnect the signal this._updateOverview(); }, // this function is pure addition to the original code and handles wsDisp transition to APP_GRID view _updateOverview() { this._workspacesDisplay.translation_x = 0; this._workspacesDisplay.translation_y = 0; this._workspacesDisplay.scale_x = 1; this._workspacesDisplay.scale_y = 1; const { initialState, finalState, progress, currentState } = this._stateAdjustment.getStateTransitionParams(); const paramsForState = s => { let opacity; switch (s) { case ControlsState.HIDDEN: case ControlsState.WINDOW_PICKER: opacity = 255; break; case ControlsState.APP_GRID: opacity = 0; break; default: opacity = 255; break; } return { opacity }; }; let initialParams = paramsForState(initialState); let finalParams = paramsForState(finalState); let opacity = Math.round(Util.lerp(initialParams.opacity, finalParams.opacity, progress)); let workspacesDisplayVisible = opacity !== 0; // improve transition from search results to desktop if (finalState === 0 && this._searchController._searchResults.visible) this._searchController.hide(); // reset Static Workspace window picker mode if (currentState === 0 && opt.OVERVIEW_MODE && opt.WORKSPACE_MODE) opt.WORKSPACE_MODE = 0; if (!opt.WS_ANIMATION || (!opt.SHOW_WS_TMB && opt.SHOW_WS_PREVIEW_BG)) { this._workspacesDisplay.opacity = opacity; } else if (!opt.SHOW_WS_TMB_BG && opt.SHOW_WS_PREVIEW_BG) { // fade out ws wallpaper during transition to ws switcher if ws switcher background disabled const workspaces = this._workspacesDisplay._workspacesViews[global.display.get_primary_monitor()]?._workspaces; // Speed up the workspace background opacity transition if (opt.WORKSPACE_MAX_SPACING < opt.WS_MAX_SPACING_OFF_SCREEN && workspaces) // If workspacesDisplay max spacing is set so adjacent workspaces could be visible on the screen workspaces.forEach(w => w._background.set_opacity(Math.max(0, opacity - (255 - opacity)))); else if (workspaces) // If adjacent workspaces should not be visible on the screen, set the opacity only for the visible one workspaces[this._workspaceAdjustment.value]?._background.set_opacity(Math.max(0, opacity - (255 - opacity))); } // if ws preview background is disabled, animate tmb box and dash const tmbBox = this._thumbnailsBox; const dash = this.dash; const searchEntryBin = this._searchEntryBin; // this dash transition collides with startup animation and freezes GS for good, needs to be delayed (first Main.overview 'hiding' event enables it) const skipDash = Me.Util.dashNotDefault(); // OVERVIEW_MODE 2 should animate dash and wsTmbBox only if WORKSPACE_MODE === 0 (windows not spread) const animateOverviewMode2 = opt.OVERVIEW_MODE2 && !(finalState === 1 && opt.WORKSPACE_MODE); if (!Main.layoutManager._startingUp && ((!opt.SHOW_WS_PREVIEW_BG && !opt.OVERVIEW_MODE2) || animateOverviewMode2)) { if (!tmbBox._translationOriginal || Math.abs(tmbBox._translationOriginal[0]) > 500) { // swipe gesture can call this calculation before tmbBox is realized, giving nonsense width const [dashTranslationX, dashTranslationY, tmbTranslationX, tmbTranslationY, searchTranslationY] = this._getOverviewTranslations(dash, tmbBox, searchEntryBin); tmbBox._translationOriginal = [tmbTranslationX, tmbTranslationY]; dash._translationOriginal = [dashTranslationX, dashTranslationY]; searchEntryBin._translationOriginal = searchTranslationY; } if (finalState === 0 || initialState === 0) { const prg = Math.abs((finalState === 0 ? 0 : 1) - progress); tmbBox.translation_x = Math.round(prg * tmbBox._translationOriginal[0]); tmbBox.translation_y = Math.round(prg * tmbBox._translationOriginal[1]); if (!skipDash) { dash.translation_x = Math.round(prg * dash._translationOriginal[0]); dash.translation_y = Math.round(prg * dash._translationOriginal[1]); } searchEntryBin.translation_y = Math.round(prg * searchEntryBin._translationOriginal); } if (progress === 1) { tmbBox._translationOriginal = 0; if (!skipDash) dash._translationOriginal = 0; searchEntryBin._translationOriginal = 0; } } else if (!Main.layoutManager._startingUp && (tmbBox.translation_x || tmbBox.translation_y)) { tmbBox.translation_x = 0; tmbBox.translation_y = 0; if (!skipDash) { dash.translation_x = 0; dash.translation_y = 0; } searchEntryBin.translation_y = 0; } if (!Main.layoutManager._startingUp) { if (initialState === ControlsState.HIDDEN && finalState === ControlsState.APP_GRID) this._appDisplay.opacity = Math.round(progress * 255); else this._appDisplay.opacity = 255 - opacity; } if (currentState === ControlsState.APP_GRID) { // in app grid hide workspaces so they're not blocking app grid or ws thumbnails this._workspacesDisplay.scale_x = 0; } else { this._workspacesDisplay.scale_x = 1; } if (opt.LEAVING_SEARCH && currentState <= ControlsState.WINDOW_PICKER) { opt.LEAVING_SEARCH = false; } this._workspacesDisplay.setPrimaryWorkspaceVisible(workspacesDisplayVisible); if (!this.dash._isAbove && progress > 0 && opt.OVERVIEW_MODE2) { // set searchEntry above appDisplay this.set_child_above_sibling(this._searchEntryBin, null); // move dash above wsTmb for case that dash and wsTmb animate from the same side if (!Me.Util.dashNotDefault()) this.set_child_above_sibling(dash, null); this.set_child_below_sibling(this._thumbnailsBox, null); this.set_child_below_sibling(this._workspacesDisplay, null); this.set_child_below_sibling(this._appDisplay, null); } else if (!this.dash._isAbove && progress === 1 && finalState > ControlsState.HIDDEN) { // set dash above workspace in the overview this.set_child_above_sibling(this._thumbnailsBox, null); this.set_child_above_sibling(this._searchEntryBin, null); if (!Me.Util.dashNotDefault()) this.set_child_above_sibling(this.dash, null); if (Main.layoutManager.panelBox.get_parent() === Main.layoutManager.overviewGroup) Main.layoutManager.overviewGroup.set_child_above_sibling(Main.layoutManager.panelBox, Main.overview._overview); this.dash._isAbove = true; } else if (this.dash._isAbove && progress < 1) { // keep dash below for ws transition between the overview and hidden state this.set_child_above_sibling(this._workspacesDisplay, null); if (Main.layoutManager.panelBox.get_parent() === Main.layoutManager.overviewGroup) Main.layoutManager.overviewGroup.set_child_below_sibling(Main.layoutManager.panelBox, Main.overview._overview); this.dash._isAbove = false; } }, // fix for upstream bug - appGrid.visible after transition from APP_GRID to HIDDEN _updateAppDisplayVisibility(stateTransitionParams = null) { if (!stateTransitionParams) stateTransitionParams = this._stateAdjustment.getStateTransitionParams(); const { currentState } = stateTransitionParams; if (this.dash.showAppsButton.checked) this._searchTransition = false; // if !APP_GRID_ANIMATION, appGrid needs to be hidden in WINDOW_PICKER mode (1) // but needs to be visible for transition from HIDDEN (0) to APP_GRID (2) this._appDisplay.visible = currentState > ControlsState.HIDDEN && !this._searchController.searchActive && !(currentState === ControlsState.WINDOW_PICKER && !opt.APP_GRID_ANIMATION) && !this._searchTransition; }, _activateSearchAppGridMode() { if (!this._origAppGridContent) { this._origAppGridContent = { usage: opt.APP_GRID_USAGE, favorites: opt.APP_GRID_EXCLUDE_FAVORITES, running: opt.APP_GRID_EXCLUDE_RUNNING, incompletePages: this._appDisplay._grid.layoutManager.allowIncompletePages, order: opt.APP_GRID_ORDER, }; opt.APP_GRID_ORDER = 3; opt.APP_GRID_USAGE = true; opt.APP_GRID_EXCLUDE_FAVORITES = false; opt.APP_GRID_EXCLUDE_RUNNING = false; this._appDisplay._grid.layoutManager.allowIncompletePages = false; this._appDisplay._redisplay(); } }, _deactivateSearchAppGridMode() { if (this._origAppGridContent) { const icons = this._appDisplay._orderedItems; icons.forEach(icon => { icon.visible = true; }); opt.APP_GRID_ORDER = this._origAppGridContent.order; opt.APP_GRID_USAGE = this._origAppGridContent.usage; opt.APP_GRID_EXCLUDE_FAVORITES = this._origAppGridContent.favorites; opt.APP_GRID_EXCLUDE_RUNNING = this._origAppGridContent.running; this._appDisplay._grid.layoutManager.allowIncompletePages = this._origAppGridContent.incompletePages; this._origAppGridContent = null; this._appDisplay._redisplay(); } }, _onSearchChanged() { // something is somewhere setting the opacity to 0 if V-Shell is rebased while in overview / search this._searchController.opacity = 255; const { finalState, currentState } = this._stateAdjustment.getStateTransitionParams(); const { searchActive } = this._searchController; const SIDE_CONTROLS_ANIMATION_TIME = 250; // OverviewControls.SIDE_CONTROLS_ANIMATION_TIME = Overview.ANIMATION_TIME = 250 const entry = this._searchEntry; if (opt.SHOW_SEARCH_ENTRY) { entry.visible = true; entry.opacity = 255; } else if (!(searchActive && entry.visible)) { entry.visible = true; entry.opacity = searchActive ? 0 : 255; // show search entry only if the user starts typing, and hide it when leaving the search mode entry.ease({ opacity: searchActive ? 255 : 0, duration: SIDE_CONTROLS_ANIMATION_TIME / 2, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { entry.visible = searchActive; }, }); } // if user start typing or activated search provider during overview animation, this switcher will be called again after animation ends if (opt.SEARCH_VIEW_ANIMATION && Main.overview._animationInProgress && finalState !== ControlsState.HIDDEN) return; if (!searchActive) { if (!this.dash.showAppsButton.checked) opt.LEAVING_SEARCH = true; if (this._origAppGridContent) this._deactivateSearchAppGridMode(); this._workspacesDisplay.reactive = true; this._workspacesDisplay.setPrimaryWorkspaceVisible(true); } else { if (opt.SEARCH_APP_GRID_MODE && this.dash.showAppsButton.checked) { this._activateSearchAppGridMode(); return; } if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) this._searchController._searchResults._statusText.add_style_class_name('search-statustext-om2'); else this._searchController._searchResults._statusText.remove_style_class_name('search-statustext-om2'); this._searchController.show(); entry.visible = true; entry.opacity = 255; opt.LEAVING_SEARCH = false; } if (opt.SHOW_BG_IN_OVERVIEW && this._bgManagers) this._updateBackground(this._bgManagers[0]); this._searchTransition = true; this._searchController._searchResults.translation_x = 0; this._searchController._searchResults.translation_y = 0; this._searchController.visible = true; if (opt.SEARCH_VIEW_ANIMATION && ![4, 8].includes(opt.WS_TMB_POSITION)) { this._updateAppDisplayVisibility(); this._searchController._searchResults._statusBin.opacity = 1; let translationX = 0; let translationY = 0; const geometry = global.display.get_monitor_geometry(global.display.get_primary_monitor()); switch (opt.SEARCH_VIEW_ANIMATION) { case 1: // make it longer to cover the delay before results appears translationX = geometry.width; translationY = 0; break; case 2: translationX = -geometry.width; translationY = 0; break; case 3: translationX = 0; translationY = geometry.height; break; case 5: translationX = 0; translationY = -geometry.height; break; } if (searchActive) { this._searchController._searchResults.translation_x = translationX; this._searchController._searchResults.translation_y = translationY; } else { this._searchController._searchResults.translation_x = 0; this._searchController._searchResults.translation_y = 0; } this._searchController._searchResults.ease({ delay: 150, // wait for results opacity: searchActive ? 255 : 0, translation_x: searchActive ? 0 : translationX, translation_y: searchActive ? 0 : translationY, duration: SIDE_CONTROLS_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { this._searchController.visible = searchActive; this._searchTransition = false; this._searchController._searchResults._statusBin.opacity = 255; }, }); this._workspacesDisplay.opacity = 255; } else { this._appDisplay.ease({ opacity: searchActive || currentState < 2 ? 0 : 255, duration: SIDE_CONTROLS_ANIMATION_TIME / 2, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { this._updateAppDisplayVisibility(); }, }); this._workspacesDisplay.setPrimaryWorkspaceVisible(true); this._searchController._searchResults.ease({ opacity: searchActive ? 255 : 0, duration: searchActive ? SIDE_CONTROLS_ANIMATION_TIME / 2 : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => (this._searchController.visible = searchActive), }); } // reuse already tuned overview transition, just replace APP_GRID with the search view if (!(opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) && !Main.overview._animationInProgress && finalState !== ControlsState.HIDDEN && !this.dash.showAppsButton.checked) { this._searchController._searchResults._content.remove_style_class_name('search-section-content-bg-om2'); this._searchEntry.remove_style_class_name('search-entry-om2'); const duration = opt.SEARCH_VIEW_ANIMATION ? 140 : 0; this._stateAdjustment.ease(searchActive ? ControlsState.APP_GRID : ControlsState.WINDOW_PICKER, { // shorter animation time when entering search view can avoid stuttering in transition // collecting search results take some time and the problematic part is the realization of the object on the screen // if the ws animation ends before this event, the whole transition is smoother // removing the ws transition (duration: 0) seems like the best solution here duration: searchActive ? duration : SIDE_CONTROLS_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { this._workspacesDisplay.setPrimaryWorkspaceVisible(!searchActive); // Set the delay before processing a new search entry to 150 on deactivation, so search providers can't make make the workspace animation stuttering // set it back to 0 after in-animation, so the search can be snappy opt.SEARCH_DELAY = searchActive || !opt.SEARCH_VIEW_ANIMATION ? 0 : 150; }, }); } else if (opt.OVERVIEW_MODE2 && !(opt.WORKSPACE_MODE || this.dash.showAppsButton.checked)) { // add background to search results and make searchEntry border thicker for better visibility this._searchController._searchResults._content.add_style_class_name('search-section-content-bg-om2'); this._searchEntry.add_style_class_name('search-entry-om2'); } else { this._searchController._searchResults._content.remove_style_class_name('search-section-content-bg-om2'); this._searchEntry.remove_style_class_name('search-entry-om2'); } }, async runStartupAnimation(callback) { this._ignoreShowAppsButtonToggle = true; this.prepareToEnterOverview(); this._stateAdjustment.value = ControlsState.HIDDEN; this._stateAdjustment.ease(ControlsState.WINDOW_PICKER, { duration: ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); this.dash.showAppsButton.checked = false; this._ignoreShowAppsButtonToggle = false; // Set the opacity here to avoid a 1-frame flicker this.opacity = 1; this._appDisplay.opacity = 1; // We can't run the animation before the first allocation happens await this.layout_manager.ensureAllocation(); this._setBackground(); Me.Modules.panelModule.update(); Main.panel.opacity = 255; // Opacity this.ease({ opacity: opt.STARTUP_STATE === 1 ? 0 : 255, duration: STARTUP_ANIMATION_TIME, mode: Clutter.AnimationMode.LINEAR, }); const dash = this.dash; const tmbBox = this._thumbnailsBox; // Set the opacity here to avoid a 1-frame flicker dash.opacity = 0; for (const view of this._workspacesDisplay._workspacesViews) { if (view._monitorIndex !== global.display.get_primary_monitor()) view._thumbnails.opacity = 0; } const searchEntryBin = this._searchEntryBin; const [dashTranslationX, dashTranslationY, tmbTranslationX, tmbTranslationY, searchTranslationY] = this._getOverviewTranslations(dash, tmbBox, searchEntryBin); const onComplete = function () { // running init callback again causes issues (multiple connections) if (!Main.overview._startupInitComplete) callback(); const appDisplayModule = Me.Modules.appDisplayModule; if (!appDisplayModule.moduleEnabled) this._finishStartupSequence(); else this._realizeAppDisplayAndFinishSequence(); Main.overview._startupInitComplete = true; }.bind(this); if (dash.visible && !Me.Util.dashNotDefault()) { dash.translation_x = dashTranslationX; dash.translation_y = dashTranslationY; dash.opacity = 255; dash.ease({ translation_x: 0, translation_y: 0, delay: STARTUP_ANIMATION_TIME / 2, duration: STARTUP_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete, }); } else { // set dash opacity to make it visible if user enable it later dash.opacity = 255; // if dash is hidden, substitute the ease timeout with GLib.timeout _timeouts.startupAnim2 = GLib.timeout_add( GLib.PRIORITY_DEFAULT, // delay + animation time STARTUP_ANIMATION_TIME * 2 * St.Settings.get().slow_down_factor, () => { onComplete(); _timeouts.startupAnim2 = 0; return GLib.SOURCE_REMOVE; } ); } if (searchEntryBin.visible) { searchEntryBin.translation_y = searchTranslationY; searchEntryBin.ease({ translation_y: 0, delay: STARTUP_ANIMATION_TIME / 2, duration: STARTUP_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } if (tmbBox.visible) { tmbBox.translation_x = tmbTranslationX; tmbBox.translation_y = tmbTranslationY; tmbBox.ease({ translation_x: 0, translation_y: 0, delay: STARTUP_ANIMATION_TIME / 2, duration: STARTUP_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } // upstream bug - following animation will be cancelled, don't know where // needs further investigation const workspacesViews = this._workspacesDisplay._workspacesViews; if (workspacesViews.length > 1) { for (const view of workspacesViews) { if (view._monitorIndex !== global.display.get_primary_monitor() && view._thumbnails.visible) { const secTmbBox = view._thumbnails; if (opt.SEC_WS_TMB_LEFT) secTmbBox.translation_x = -(secTmbBox.width + 12); // compensate for padding else if (opt.SEC_WS_TMB_RIGHT) secTmbBox.translation_x = secTmbBox.width + 12; else if (opt.SEC_WS_TMB_TOP) secTmbBox.translation_y = -(secTmbBox.height + 12); else if (opt.SEC_WS_TMB_BOTTOM) secTmbBox.translation_y = secTmbBox.height + 12; secTmbBox.opacity = 255; secTmbBox.ease({ translation_y: 0, delay: STARTUP_ANIMATION_TIME / 2, duration: STARTUP_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } } } }, _realizeAppDisplayAndFinishSequence() { const appDisplayModule = Me.Modules.appDisplayModule; // realize app grid for smoother first animation appDisplayModule._repopulateAppDisplay(false, this._finishStartupSequence.bind(this)); }, _finishStartupSequence() { if (!this._bgManagers) this._setBackground(); _timeouts.finishStartup = GLib.idle_add( GLib.PRIORITY_LOW, () => { this._appDisplay.opacity = 255; if (opt.STARTUP_STATE === 1) { Main.overview.hide(); } else if (opt.STARTUP_STATE === 2) { Main.overview.show(2); // just because of DtD, because we skipped startup animation this.dash.showAppsButton.checked = true; } else if (!opt.STARTUP_STATE && Me.Util.dashNotDefault()) { Main.overview.show(); } _timeouts.finishStartup = 0; return GLib.SOURCE_REMOVE; } ); }, setInitialTranslations() { const dash = this.dash; const tmbBox = this._thumbnailsBox; const searchEntryBin = this._searchEntryBin; const [dashTranslationX, dashTranslationY, tmbTranslationX, tmbTranslationY, searchTranslationY] = this._getOverviewTranslations(dash, tmbBox, searchEntryBin); if (!Me.Util.dashNotDefault()) { dash.translation_x = dashTranslationX; dash.translation_y = dashTranslationY; } tmbBox.translation_x = tmbTranslationX; tmbBox.translation_y = tmbTranslationY; searchEntryBin.translation_y = searchTranslationY; }, _getOverviewTranslations(dash, tmbBox, searchEntryBin) { const animationsDisabled = !St.Settings.get().enable_animations || ((opt.SHOW_WS_PREVIEW_BG && !opt.OVERVIEW_MODE2) && !Main.layoutManager._startingUp); if (animationsDisabled) return [0, 0, 0, 0, 0]; let searchTranslationY = 0; if (searchEntryBin.visible) { const offset = (dash.visible && (!opt.DASH_VERTICAL ? dash.height + 12 : 0)) + (opt.WS_TMB_TOP ? tmbBox.height + 12 : 0); searchTranslationY = -searchEntryBin.height - offset - 30; } let tmbTranslationX = 0; let tmbTranslationY = 0; let offset; if (tmbBox.visible) { const tmbWidth = tmbBox.width === Infinity ? 0 : tmbBox.width; const tmbHeight = tmbBox.height === Infinity ? 0 : tmbBox.height; switch (opt.WS_TMB_POSITION) { case 3: // left offset = 10 + (dash?.visible && opt.DASH_LEFT ? dash.width : 0); tmbTranslationX = -tmbWidth - offset; tmbTranslationY = 0; break; case 1: // right offset = 10 + (dash?.visible && opt.DASH_RIGHT ? dash.width : 0); tmbTranslationX = tmbWidth + offset; tmbTranslationY = 0; break; case 0: // top offset = 10 + (dash?.visible && opt.DASH_TOP ? dash.height : 0) + Main.panel.height; tmbTranslationX = 0; tmbTranslationY = -tmbHeight - offset; break; case 2: // bottom offset = 10 + (dash?.visible && opt.DASH_BOTTOM ? dash.height : 0) + Main.panel.height; // just for case the panel is at bottom tmbTranslationX = 0; tmbTranslationY = tmbHeight + offset; break; } } let dashTranslationX = 0; let dashTranslationY = 0; let position = opt.DASH_POSITION; // if DtD replaced the original Dash, read its position if (Me.Util.dashIsDashToDock()) position = dash._position; if (dash?.visible) { const dashWidth = dash.width === Infinity ? 0 : dash.width; const dashHeight = dash.height === Infinity ? 0 : dash.height; switch (position) { case 0: // top dashTranslationX = 0; dashTranslationY = -dashHeight - dash.margin_bottom - Main.panel.height; break; case 1: // right dashTranslationX = dashWidth; dashTranslationY = 0; break; case 2: // bottom dashTranslationX = 0; dashTranslationY = dashHeight + dash.margin_bottom + Main.panel.height; break; case 3: // left dashTranslationX = -dashWidth; dashTranslationY = 0; break; } } return [dashTranslationX, dashTranslationY, tmbTranslationX, tmbTranslationY, searchTranslationY]; }, animateToOverview(state, callback) { this._ignoreShowAppsButtonToggle = true; this._searchTransition = false; this._stateAdjustment.value = ControlsState.HIDDEN; // building window thumbnails takes some time and with many windows on the workspace // the time can be close to or longer than ANIMATION_TIME // in which case the the animation is greatly delayed, stuttering, or even skipped // for user it is more acceptable to watch delayed smooth animation, // even if it takes little more time, than jumping frames let delay = 0; if (opt.DELAY_OVERVIEW_ANIMATION) delay = global.display.get_tab_list(0, global.workspace_manager.get_active_workspace()).length * 3; this._stateAdjustment.ease(state, { delay, duration: 250, // Overview.ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onStopped: () => { if (callback) callback(); }, }); this.dash.showAppsButton.checked = state === ControlsState.APP_GRID; this._ignoreShowAppsButtonToggle = false; }, _setBackground(reset = false) { if (this._bgManagers) { this._bgManagers.forEach(bg => { this._stateAdjustment.disconnect(bg._fadeSignal); bg.destroy(); }); } // if (!SHOW_BG_IN_OVERVIEW && !SHOW_WS_PREVIEW_BG) the background is used for static transition from wallpaper to empty bg in the overview if (reset || (!opt.SHOW_BG_IN_OVERVIEW && opt.SHOW_WS_PREVIEW_BG)) { delete this._bgManagers; return; } this._bgManagers = []; for (const monitor of Main.layoutManager.monitors) { const bgManager = new Background.BackgroundManager({ monitorIndex: monitor.index, container: Main.layoutManager.overviewGroup, vignette: true, }); bgManager.backgroundActor.content.vignette_sharpness = 0; bgManager.backgroundActor.content.brightness = 1; bgManager._fadeSignal = this._stateAdjustment.connect('notify::value', v => { this._updateBackground(bgManager, v.value, v); }); if (monitor.index === global.display.get_primary_monitor()) { bgManager._primary = true; this._bgManagers.unshift(bgManager); // primary monitor first } else { bgManager._primary = false; this._bgManagers.push(bgManager); } } }, _updateBackground(bgManager, stateValue = 2, stateAdjustment = null) { // Just in case something destroys our background (like older versions of Blur My Shell) if (this._bgManagers[0] && !Main.layoutManager.overviewGroup.get_children().includes(this._bgManagers[0].backgroundActor)) { console.error(`[${Me.metadata.name}]`, 'Error: The overview background has been destroyed, possibly by another incompatible extension'); // remove and disconnect our destroyed backgrounds to avoid further errors this._setBackground(true); return; } const finalState = stateAdjustment?.getStateTransitionParams().finalState; if (!opt.SHOW_BG_IN_OVERVIEW && !opt.SHOW_WS_PREVIEW_BG) { // if no bg shown in the overview, fade out the wallpaper if (bgManager.backgroundActor.get_effect('blur')) bgManager.backgroundActor.remove_effect_by_name('blur'); if (!(opt.OVERVIEW_MODE2 && opt.WORKSPACE_MODE && finalState === 1)) bgManager.backgroundActor.opacity = Util.lerp(255, 0, Math.min(stateValue, 1)); } else { bgManager.backgroundActor.opacity = 255; let VIGNETTE, BRIGHTNESS, bgValue; if (opt.OVERVIEW_MODE2 && stateValue <= 1 && !opt.WORKSPACE_MODE) { VIGNETTE = 0; BRIGHTNESS = 1; bgValue = stateValue; } else { VIGNETTE = 0.2; BRIGHTNESS = opt.OVERVIEW_BG_BRIGHTNESS; if (opt.OVERVIEW_MODE2 && stateValue > 1 && !opt.WORKSPACE_MODE) bgValue = stateValue - 1; else bgValue = stateValue; } let blurEffect = bgManager.backgroundActor.get_effect('blur'); if (!blurEffect) { blurEffect = new Shell.BlurEffect({ brightness: 1, mode: Shell.BlurMode.ACTOR, }); bgManager.backgroundActor.add_effect_with_name('blur', blurEffect); } // In GNOME 46 the "sigma" property has been renamed to "radius" const radius = blurEffect.sigma !== undefined ? 'sigma' : 'radius'; const searchActive = this._searchController.searchActive; if (searchActive) BRIGHTNESS = opt.SEARCH_BG_BRIGHTNESS; bgManager.backgroundActor.content.vignette_sharpness = VIGNETTE; bgManager.backgroundActor.content.brightness = BRIGHTNESS; let vignetteInit, brightnessInit;// , sigmaInit; if (opt.SHOW_BG_IN_OVERVIEW && opt.SHOW_WS_PREVIEW_BG) { vignetteInit = VIGNETTE; brightnessInit = BRIGHTNESS; // sigmaInit = opt.OVERVIEW_BG_BLUR_SIGMA; } else { vignetteInit = 0; brightnessInit = 1; // sigmaInit = 0; } if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) { bgManager.backgroundActor.content.vignette_sharpness = Util.lerp(vignetteInit, VIGNETTE, bgValue); bgManager.backgroundActor.content.brightness = Util.lerp(brightnessInit, BRIGHTNESS, bgValue); } else { bgManager.backgroundActor.content.vignette_sharpness = Util.lerp(vignetteInit, VIGNETTE, Math.min(stateValue, 1)); bgManager.backgroundActor.content.brightness = Util.lerp(brightnessInit, BRIGHTNESS, Math.min(stateValue, 1)); } if (opt.OVERVIEW_BG_BLUR_SIGMA || opt.APP_GRID_BG_BLUR_SIGMA) { // reduce number of steps of blur transition to improve performance const step = opt.SMOOTH_BLUR_TRANSITIONS ? 0.05 : 0.2; const progress = stateValue - (stateValue % step); if (opt.SHOW_WS_PREVIEW_BG && stateValue < 1 && !searchActive) { // no need to animate transition, unless appGrid state is involved, static bg is covered by the ws preview bg if (blurEffect[radius] !== opt.OVERVIEW_BG_BLUR_SIGMA) blurEffect[radius] = opt.OVERVIEW_BG_BLUR_SIGMA; } else if (stateValue < 1 && !searchActive && !(opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE)) { const sigma = Math.round(Util.lerp(0, opt.OVERVIEW_BG_BLUR_SIGMA, progress)); if (sigma !== blurEffect[radius]) blurEffect[radius] = sigma; } else if (stateValue < 1 && !searchActive && (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && blurEffect[radius])) { const sigma = Math.round(Util.lerp(0, opt.OVERVIEW_BG_BLUR_SIGMA, progress)); if (sigma !== blurEffect[radius]) blurEffect[radius] = sigma; } else if (stateValue > 1 && !searchActive && (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && finalState === 1)) { const sigma = Math.round(Util.lerp(0, opt.OVERVIEW_BG_BLUR_SIGMA, progress % 1)); if (sigma !== blurEffect[radius]) blurEffect[radius] = sigma; } else if ((stateValue > 1 && bgManager._primary) || searchActive) { const sigma = Math.round(Util.lerp(opt.OVERVIEW_BG_BLUR_SIGMA, opt.APP_GRID_BG_BLUR_SIGMA, progress % 1)); if (sigma !== blurEffect[radius]) blurEffect[radius] = sigma; } else if (stateValue === 1 && !(opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE)) { blurEffect[radius] = opt.OVERVIEW_BG_BLUR_SIGMA; } else if (stateValue === 0 || (stateValue === 1 && (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE))) { blurEffect[radius] = 0; } } } }, }; const ControlsManagerLayoutCommon = { after__updateWorkAreaBox() { const workArea = this._workAreaBox.copy(); // opt.PANEL_OVERVIEW_ONLY removes affectsStruts panel property if (opt.get('panelModule') && opt.PANEL_OVERVIEW_ONLY) { let offsetY = 0; let reduction = 0; reduction = Main.panel.height; offsetY = opt.PANEL_POSITION_TOP ? reduction : 0; const startX = workArea.x1; const startY = workArea.y1 + offsetY; const width = workArea.get_width(); const height = workArea.get_height() - reduction; workArea.set_origin(startX, startY); workArea.set_size(width, height); } this._workAreaBoxForVShellConfig = workArea; }, _updatePositionFromDashToDock() { // update variables that cannot be processed within settings const dash = Main.overview.dash; opt.DASH_POSITION = dash._position; opt.DASH_TOP = opt.DASH_POSITION === 0; opt.DASH_RIGHT = opt.DASH_POSITION === 1; opt.DASH_BOTTOM = opt.DASH_POSITION === 2; opt.DASH_LEFT = opt.DASH_POSITION === 3; opt.DASH_VERTICAL = opt.DASH_LEFT || opt.DASH_RIGHT; }, _dashToDockAffectsWorkArea() { const dash = Main.overview.dash; const dtd = dash.get_parent()?.get_parent()?.get_parent(); const layoutManager = Main.layoutManager; const index = layoutManager._findActor(dtd); const data = index > -1 ? layoutManager._trackedActors[index] : null; const affectsStruts = data?.affectsStruts; return !!affectsStruts; }, }; const ControlsManagerLayoutVertical = { _computeWorkspacesBoxForState(state, box, wsTmbWidth, wsTmbHeight, leftBoxOffset, rightBoxOffset, topBoxOffset, bottomBoxOffset, centeredBoxOffset) { const workspaceBox = box.copy(); let [width, height] = this._workAreaBoxForVShellConfig.get_size(); const startX = this._workAreaBoxForVShellConfig.x1; const startY = this._workAreaBoxForVShellConfig.y1; let wsBoxWidth, wsBoxHeight, wsBoxY, wsBoxX; switch (state) { case ControlsState.HIDDEN: workspaceBox.set_origin(...this._workAreaBox.get_origin()); workspaceBox.set_size(...this._workAreaBox.get_size()); break; case ControlsState.WINDOW_PICKER: case ControlsState.APP_GRID: if (opt.WS_ANIMATION && opt.SHOW_WS_TMB && state === ControlsState.APP_GRID) { workspaceBox.set_origin(...this._workspacesThumbnails.get_position()); workspaceBox.set_size(wsTmbWidth, wsTmbHeight); } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) { workspaceBox.set_origin(...this._workAreaBox.get_origin()); workspaceBox.set_size(...this._workAreaBox.get_size()); } else { wsBoxWidth = width - leftBoxOffset - rightBoxOffset; wsBoxHeight = height - topBoxOffset - bottomBoxOffset; const ratio = width / height; let wRatio = wsBoxWidth / wsBoxHeight; let scale = ratio / wRatio; if (scale > 1) { wsBoxHeight /= scale; wsBoxWidth = wsBoxHeight * ratio; } else { wsBoxWidth *= scale; wsBoxHeight = wsBoxWidth / ratio; } // height decides the actual size, ratio is given by the workArea wsBoxHeight = Math.round(wsBoxHeight * opt.WS_PREVIEW_SCALE); wsBoxWidth = Math.round(wsBoxWidth * opt.WS_PREVIEW_SCALE); let xOffset = 0; let yOffset = 0; const yAvailableSpace = Math.round((height - topBoxOffset - wsBoxHeight - bottomBoxOffset) / 2); yOffset = topBoxOffset + yAvailableSpace; const centeredBoxX = Math.round((width - wsBoxWidth) / 2); this._xAlignCenter = false; if (centeredBoxX < centeredBoxOffset) { xOffset = Math.round(leftBoxOffset + (width - leftBoxOffset - wsBoxWidth - rightBoxOffset) / 2); } else { xOffset = centeredBoxX; this._xAlignCenter = true; } wsBoxX = startX + xOffset; wsBoxY = startY + yOffset; workspaceBox.set_origin(wsBoxX, wsBoxY); workspaceBox.set_size(wsBoxWidth, wsBoxHeight); } } return workspaceBox; }, _getAppDisplayBoxForState(state, box, leftBoxOffset, rightBoxOffset, topBoxOffset, bottomBoxOffset) { const appDisplayBox = new Clutter.ActorBox(); const startX = this._workAreaBoxForVShellConfig.x1; const startY = this._workAreaBoxForVShellConfig.y1; let [width, height] = this._workAreaBoxForVShellConfig.get_size(); const centeredBoxOffset = Math.max(leftBoxOffset, rightBoxOffset); const adWidth = opt.CENTER_APP_GRID ? width - 2 * centeredBoxOffset : width - leftBoxOffset - rightBoxOffset; const adHeight = height - topBoxOffset - bottomBoxOffset; const appDisplayX = startX + (opt.CENTER_APP_GRID ? Math.round((width - adWidth) / 2) : leftBoxOffset ); const appDisplayY = startY + topBoxOffset; switch (state) { case ControlsState.HIDDEN: case ControlsState.WINDOW_PICKER: // 1 - left, 2 - right, 3 - bottom, 5 - top switch (opt.APP_GRID_ANIMATION) { case 0: appDisplayBox.set_origin(appDisplayX, appDisplayY); break; case 1: appDisplayBox.set_origin(startX + width, appDisplayY); break; case 2: appDisplayBox.set_origin(box.x1 - adWidth, appDisplayY); break; case 3: appDisplayBox.set_origin(appDisplayX, box.y2); break; case 5: appDisplayBox.set_origin(appDisplayX, box.y1 - adHeight); break; } break; case ControlsState.APP_GRID: appDisplayBox.set_origin(appDisplayX, appDisplayY); break; } appDisplayBox.set_size(adWidth, adHeight); return appDisplayBox; }, vfunc_allocate(container, box) { const childBox = new Clutter.ActorBox(); const startX = this._workAreaBoxForVShellConfig.x1; const startY = this._workAreaBoxForVShellConfig.y1; let [width, height] = this._workAreaBoxForVShellConfig.get_size(); const transitionParams = this._stateAdjustment.getStateTransitionParams(); const spacing = opt.SPACING; // Dash const maxDashHeight = Math.round(box.get_height() * DASH_MAX_SIZE_RATIO); const maxDashWidth = Math.round(maxDashHeight * 0.8); let dashHeight = 0; let dashWidth = 0; // dash cloud be overridden by the Dash to Dock clone if (Me.Util.dashIsDashToDock()) { this._updatePositionFromDashToDock(); // If DtD affects workArea, dash size needs to be 0 + spacing const dash = Main.overview.dash; if (this._dashToDockAffectsWorkArea()) { if (opt.DASH_VERTICAL) dashWidth = spacing; else dashHeight = spacing; } else { dashHeight = dash.height; dashWidth = dash.width; if (opt.DASH_VERTICAL) dashWidth += spacing; else dashHeight += spacing; } } else if (this._dash.visible) { // default dock if (opt.DASH_VERTICAL) { this._dash.setMaxSize(maxDashWidth, height); [, dashWidth] = this._dash.get_preferred_width(height); [, dashHeight] = this._dash.get_preferred_height(dashWidth); dashWidth = Math.min(dashWidth, maxDashWidth); dashHeight = Math.min(dashHeight, height); } else if (!opt.WS_TMB_FULL) { this._dash.setMaxSize(width, maxDashHeight); [, dashHeight] = this._dash.get_preferred_height(width); [, dashWidth] = this._dash.get_preferred_width(dashHeight); dashHeight = Math.min(dashHeight, maxDashHeight); dashWidth = Math.min(dashWidth, width); } } // Workspace Thumbnails let wsTmbWidth = 0; let wsTmbHeight = 0; if (opt.SHOW_WS_TMB) { const searchActive = this._searchController.searchActive; let maxWsTmbScale = this._dash.showAppsButton.checked && !(searchActive && !opt.SEARCH_APP_GRID_MODE) ? opt.MAX_THUMBNAIL_SCALE_APPGRID : opt.MAX_THUMBNAIL_SCALE; if (transitionParams.currentState % 1 && !opt.MAX_THUMBNAIL_SCALE_STABLE && !searchActive && !opt.LEAVING_SEARCH) { const initState = transitionParams.initialState === ControlsState.APP_GRID ? opt.MAX_THUMBNAIL_SCALE_APPGRID : opt.MAX_THUMBNAIL_SCALE; const finalState = transitionParams.finalState === ControlsState.APP_GRID ? opt.MAX_THUMBNAIL_SCALE_APPGRID : opt.MAX_THUMBNAIL_SCALE; maxWsTmbScale = Util.lerp(initState, finalState, transitionParams.progress); } wsTmbWidth = Math.round(width * maxWsTmbScale); let totalTmbSpacing; [totalTmbSpacing, wsTmbHeight] = this._workspacesThumbnails.get_preferred_height(wsTmbWidth); wsTmbHeight += totalTmbSpacing; const wstTopOffset = !opt.WS_TMB_FULL && opt.DASH_TOP ? dashHeight : spacing; const wstBottomOffset = !opt.WS_TMB_FULL && opt.DASH_BOTTOM ? dashHeight : spacing; const wstLeftOffset = opt.DASH_LEFT ? dashWidth : spacing; const wstRightOffset = opt.DASH_RIGHT ? dashWidth : spacing; const wsTmbHeightMax = height - wstTopOffset - wstBottomOffset; // Reduce size to fit wsTmb to the screen if (wsTmbHeight > wsTmbHeightMax) { wsTmbHeight = wsTmbHeightMax; wsTmbWidth = this._workspacesThumbnails.get_preferred_width(wsTmbHeight)[1]; } let wsTmbX = opt.WS_TMB_LEFT ? startX + wstLeftOffset : startX + width - wstRightOffset - wsTmbWidth; let offset = (height - wstTopOffset - wsTmbHeight - wstBottomOffset) / 2; offset = Math.round(offset - (opt.WS_TMB_POSITION_ADJUSTMENT * offset)); const wsTmbY = startY + wstTopOffset + offset; childBox.set_origin(wsTmbX, wsTmbY); childBox.set_size(Math.max(wsTmbWidth, 1), Math.max(wsTmbHeight, 1)); this._workspacesThumbnails.allocate(childBox); } if (this._dash.visible) { const wMaxWidth = width - spacing - wsTmbWidth - 2 * spacing - (opt.DASH_VERTICAL ? dashWidth + spacing : 0); if (opt.WS_TMB_FULL && !opt.DASH_VERTICAL) { this._dash.setMaxSize(wMaxWidth, maxDashHeight); [, dashHeight] = this._dash.get_preferred_height(wMaxWidth); [, dashWidth] = this._dash.get_preferred_width(dashHeight); dashHeight = Math.min(dashHeight, maxDashHeight); dashWidth = Math.min(dashWidth, wMaxWidth); } let dashX = opt.DASH_RIGHT ? width - dashWidth : 0; let dashY = opt.DASH_TOP ? startY : startY + height - dashHeight; if (!opt.DASH_VERTICAL) { const dashLeftOffset = (opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && opt.WS_TMB_LEFT ? wsTmbWidth + spacing : 0; const dashRightOffset = (opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && opt.WS_TMB_RIGHT ? wsTmbWidth + spacing : 0; let offset = (width - dashWidth - (opt.CENTER_DASH_WS && !this._xAlignCenter ? dashLeftOffset + dashRightOffset : 0)) / 2; offset -= opt.DASH_POSITION_ADJUSTMENT * (offset - spacing); dashX = startX + (opt.CENTER_DASH_WS ? dashLeftOffset : 0) + offset; if (opt.WS_TMB_FULL) // Limit the adjustment while keeping the center of adjustment on the screen center dashX = Math.clamp(startX + dashLeftOffset + spacing, dashX, startX + width - dashRightOffset - spacing - dashWidth); } else { const offset = (height - dashHeight) / 2; dashY = startY + (offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing)); } dashY = Math.round(dashY); childBox.set_origin(startX + dashX, dashY); childBox.set_size(dashWidth, dashHeight); this._dash.allocate(childBox); } // View box offsets const leftBoxOffset = (opt.DASH_LEFT ? dashWidth : spacing) + (opt.WS_TMB_LEFT ? wsTmbWidth + spacing : 0); const rightBoxOffset = (opt.DASH_RIGHT ? dashWidth : spacing) + (opt.WS_TMB_RIGHT ? wsTmbWidth + spacing : 0); let topBoxOffset = (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0); const bottomBoxOffset = (opt.DASH_BOTTOM ? dashHeight : spacing) + (opt.WS_TMB_BOTTOM ? wsTmbHeight + spacing : 0); const centeredBoxOffset = Math.max(leftBoxOffset, rightBoxOffset); // App grid needs to be calculated for the max wsTmbWidth in app grid, independently on the current wsTmb scale const wsTmbWidthAppGrid = Math.round(width * opt.MAX_THUMBNAIL_SCALE_APPGRID); const leftBoxOffsetAppGrid = (opt.DASH_LEFT ? dashWidth : spacing) + (opt.WS_TMB_LEFT ? wsTmbWidthAppGrid + spacing : 0); const rightBoxOffsetAppGrid = (opt.DASH_RIGHT ? dashWidth : spacing) + (opt.WS_TMB_RIGHT ? wsTmbWidthAppGrid + spacing : 0); // searchEntry const [searchEntryHeight] = this._searchEntry.get_preferred_height(width - wsTmbWidth); const searchEntryY = startY + topBoxOffset; const searchX = startX + (opt.CENTER_SEARCH_VIEW || this._xAlignCenter ? centeredBoxOffset : leftBoxOffset); // xAlignCenter is set by wsBox const searchWidth = width - (opt.CENTER_SEARCH_VIEW || this._xAlignCenter ? 2 * centeredBoxOffset : leftBoxOffset + rightBoxOffset); childBox.set_origin(searchX, searchEntryY); childBox.set_size(searchWidth, searchEntryHeight); this._searchEntry.allocate(childBox); // searchResults const searchY = startY + topBoxOffset + searchEntryHeight + spacing; const searchHeight = height - topBoxOffset - bottomBoxOffset - searchEntryHeight - 2 * spacing; childBox.set_origin(searchX, searchY); childBox.set_size(searchWidth, searchHeight); this._searchController.allocate(childBox); // Add searchEntry height if needed topBoxOffset += opt.SHOW_SEARCH_ENTRY ? searchEntryHeight + spacing : 0; // workspace let params = [box, wsTmbWidth, wsTmbHeight, leftBoxOffset, rightBoxOffset, topBoxOffset, bottomBoxOffset, centeredBoxOffset]; // Update cached boxes for (const state of Object.values(ControlsState)) { this._cachedWorkspaceBoxes.set( state, this._computeWorkspacesBoxForState(state, ...params)); } let workspacesBox; if (!transitionParams.transitioning) workspacesBox = this._cachedWorkspaceBoxes.get(transitionParams.currentState); if (!workspacesBox) { const initialBox = this._cachedWorkspaceBoxes.get(transitionParams.initialState); const finalBox = this._cachedWorkspaceBoxes.get(transitionParams.finalState); workspacesBox = initialBox.interpolate(finalBox, transitionParams.progress); } this._workspacesDisplay.allocate(workspacesBox); // appDisplay params = [ box, leftBoxOffsetAppGrid, rightBoxOffsetAppGrid, topBoxOffset, bottomBoxOffset, ]; let appDisplayBox; if (!transitionParams.transitioning) { appDisplayBox = this._getAppDisplayBoxForState(transitionParams.currentState, ...params); } else { const initialBox = this._getAppDisplayBoxForState(transitionParams.initialState, ...params); const finalBox = this._getAppDisplayBoxForState(transitionParams.finalState, ...params); appDisplayBox = initialBox.interpolate(finalBox, transitionParams.progress); } this._appDisplay.allocate(appDisplayBox); this._runPostAllocation(); }, }; const ControlsManagerLayoutHorizontal = { _computeWorkspacesBoxForState: ControlsManagerLayoutVertical._computeWorkspacesBoxForState, _getAppDisplayBoxForState: ControlsManagerLayoutVertical._getAppDisplayBoxForState, vfunc_allocate(container, box) { const childBox = new Clutter.ActorBox(); const startX = this._workAreaBoxForVShellConfig.x1; const startY = this._workAreaBoxForVShellConfig.y1; let [width, height] = this._workAreaBoxForVShellConfig.get_size(); const transitionParams = this._stateAdjustment.getStateTransitionParams(); const spacing = opt.SPACING; // Dash const maxDashHeight = Math.round(box.get_height() * DASH_MAX_SIZE_RATIO); const maxDashWidth = Math.round(maxDashHeight * 0.8); let dashHeight = 0; let dashWidth = 0; // dash cloud be overridden by the Dash to Dock clone if (Me.Util.dashIsDashToDock()) { this._updatePositionFromDashToDock(); // If DtD affects workArea, dash size needs to be 0 const dash = Main.overview.dash; if (this._dashToDockAffectsWorkArea()) { if (opt.DASH_VERTICAL) dashWidth = spacing; else dashHeight = spacing; } else { dashHeight = dash.height; dashWidth = dash.width; if (opt.DASH_VERTICAL) dashWidth += spacing; else dashHeight += spacing; } } else if (this._dash.visible) { // default dock if (!opt.DASH_VERTICAL) { this._dash.setMaxSize(width, maxDashHeight); [, dashHeight] = this._dash.get_preferred_height(width); [, dashWidth] = this._dash.get_preferred_width(dashHeight); dashHeight = Math.min(dashHeight, maxDashHeight); dashWidth = Math.min(dashWidth, width - spacing); } else if (!opt.WS_TMB_FULL) { this._dash.setMaxSize(maxDashWidth, height); [, dashWidth] = this._dash.get_preferred_width(height); [, dashHeight] = this._dash.get_preferred_height(dashWidth); dashHeight = Math.min(dashHeight, height - spacing); dashWidth = Math.min(dashWidth, width); } } const [searchEntryHeight] = this._searchEntry.get_preferred_height(width); // Workspace Thumbnails let wsTmbWidth = 0; let wsTmbHeight = 0; if (opt.SHOW_WS_TMB) { const searchActive = this._searchController.searchActive; let maxWsTmbScale = this._dash.showAppsButton.checked && !(searchActive && !opt.SEARCH_APP_GRID_MODE) ? opt.MAX_THUMBNAIL_SCALE_APPGRID : opt.MAX_THUMBNAIL_SCALE; if (transitionParams.currentState % 1 && !opt.MAX_THUMBNAIL_SCALE_STABLE && !searchActive && !opt.LEAVING_SEARCH) { const initState = transitionParams.initialState === ControlsState.APP_GRID ? opt.MAX_THUMBNAIL_SCALE_APPGRID : opt.MAX_THUMBNAIL_SCALE; const finalState = transitionParams.finalState === ControlsState.APP_GRID ? opt.MAX_THUMBNAIL_SCALE_APPGRID : opt.MAX_THUMBNAIL_SCALE; maxWsTmbScale = Util.lerp(initState, finalState, transitionParams.progress); } wsTmbHeight = Math.round(height * maxWsTmbScale); let totalTmbSpacing; [totalTmbSpacing, wsTmbWidth] = this._workspacesThumbnails.get_preferred_width(wsTmbHeight); wsTmbWidth += totalTmbSpacing; const wstLeftOffset = !opt.WS_TMB_FULL && opt.DASH_LEFT ? dashWidth : spacing; const wstRightOffset = !opt.WS_TMB_FULL && opt.DASH_RIGHT ? dashWidth : spacing; const wstTopOffset = opt.DASH_TOP ? dashHeight : spacing; const wstBottomOffset = opt.DASH_BOTTOM ? dashHeight : spacing; const wsTmbWidthMax = width - wstLeftOffset - wstRightOffset; // Reduce size to fit wsTmb to the screen if (wsTmbWidth > wsTmbWidthMax) { wsTmbWidth = wsTmbWidthMax; wsTmbHeight = this._workspacesThumbnails.get_preferred_height(wsTmbWidth)[1]; } let wsTmbY = opt.WS_TMB_TOP ? startY + wstTopOffset : startY + height - wstBottomOffset - wsTmbHeight; let offset = (width - wstLeftOffset - wsTmbWidth - wstRightOffset) / 2; offset = Math.round(offset - (opt.WS_TMB_POSITION_ADJUSTMENT * offset)); const wsTmbX = startX + wstLeftOffset + offset; childBox.set_origin(wsTmbX, wsTmbY); childBox.set_size(Math.max(wsTmbWidth, 1), Math.max(wsTmbHeight, 1)); this._workspacesThumbnails.allocate(childBox); } if (this._dash.visible) { if (opt.WS_TMB_FULL && opt.DASH_VERTICAL) { const wMaxHeight = height - spacing - wsTmbHeight; this._dash.setMaxSize(maxDashWidth, wMaxHeight); [, dashWidth] = this._dash.get_preferred_width(wMaxHeight); [, dashHeight] = this._dash.get_preferred_height(dashWidth); dashWidth = Math.min(dashWidth, maxDashWidth); dashHeight = Math.min(dashHeight, wMaxHeight); } let dashX = opt.DASH_RIGHT ? width - dashWidth : 0; let dashY = opt.DASH_TOP ? startY : startY + height - dashHeight; if (opt.DASH_VERTICAL) { const dashTopOffset = (opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0; const dashBottomOffset = (opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && opt.WS_TMB_BOTTOM ? wsTmbHeight + spacing : 0; let offset = (height - dashHeight - (opt.CENTER_DASH_WS ? dashTopOffset + dashBottomOffset : 0)) / 2; offset -= opt.DASH_POSITION_ADJUSTMENT * (offset - spacing); dashY = startY + (opt.CENTER_DASH_WS ? dashTopOffset : 0) + offset; if (opt.WS_TMB_FULL) // Limit the adjustment while keeping the center of adjustment on the screen center dashY = Math.clamp(startY + dashTopOffset + spacing, dashY, startY + height - dashBottomOffset - spacing - dashHeight); } else { const offset = (width - dashWidth) / 2; dashX = startX + (offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing)); } dashX = Math.round(dashX); childBox.set_origin(startX + dashX, dashY); childBox.set_size(dashWidth, dashHeight); this._dash.allocate(childBox); } // Main view offsets const leftBoxOffset = opt.DASH_LEFT ? dashWidth : spacing; const rightBoxOffset = opt.DASH_RIGHT ? dashWidth : spacing; let topBoxOffset = (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0); const bottomBoxOffset = (opt.DASH_BOTTOM ? dashHeight : spacing) + (opt.WS_TMB_BOTTOM ? wsTmbHeight + spacing : 0); const centeredBoxOffset = Math.max(leftBoxOffset, rightBoxOffset); // App grid needs to be calculated for the max wsTmbWidth in app grid, independently on the current wsTmb scale const wsTmbHeightAppGrid = Math.round(height * opt.MAX_THUMBNAIL_SCALE_APPGRID); const topBoxOffsetAppGrid = (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeightAppGrid + spacing : 0) + (opt.SHOW_SEARCH_ENTRY ? searchEntryHeight + spacing : 0); const bottomBoxOffsetAppGrid = (opt.DASH_BOTTOM ? dashHeight : spacing) + (opt.WS_TMB_BOTTOM ? wsTmbHeightAppGrid + spacing : 0); // searchEntry const searchEntryY = startY + topBoxOffset; const searchX = startX + (opt.CENTER_SEARCH_VIEW || this._xAlignCenter ? centeredBoxOffset : leftBoxOffset); // xAlignCenter is set by wsBox const searchWidth = width - (opt.CENTER_SEARCH_VIEW || this._xAlignCenter ? 2 * centeredBoxOffset : leftBoxOffset + rightBoxOffset); childBox.set_origin(searchX, searchEntryY); childBox.set_size(searchWidth, searchEntryHeight); this._searchEntry.allocate(childBox); // searchResults const searchY = startY + topBoxOffset + searchEntryHeight + spacing; const searchHeight = height - topBoxOffset - bottomBoxOffset - searchEntryHeight - 2 * spacing; childBox.set_origin(searchX, searchY); childBox.set_size(searchWidth, searchHeight); this._searchController.allocate(childBox); // Add searchEntry height if needed topBoxOffset += opt.SHOW_SEARCH_ENTRY ? searchEntryHeight + spacing : 0; // Workspace let params = [ box, wsTmbWidth, wsTmbHeight, leftBoxOffset, rightBoxOffset, topBoxOffset, bottomBoxOffset, centeredBoxOffset, ]; // Update cached boxes for (const state of Object.values(ControlsState)) { this._cachedWorkspaceBoxes.set( state, this._computeWorkspacesBoxForState(state, ...params)); } let workspacesBox; if (!transitionParams.transitioning) workspacesBox = this._cachedWorkspaceBoxes.get(transitionParams.currentState); if (!workspacesBox) { const initialBox = this._cachedWorkspaceBoxes.get(transitionParams.initialState); const finalBox = this._cachedWorkspaceBoxes.get(transitionParams.finalState); workspacesBox = initialBox.interpolate(finalBox, transitionParams.progress); } this._workspacesDisplay.allocate(workspacesBox); // appDisplay params = [ box, leftBoxOffset === spacing ? 0 : leftBoxOffset, rightBoxOffset === spacing ? 0 : rightBoxOffset, topBoxOffsetAppGrid, bottomBoxOffsetAppGrid, ]; let appDisplayBox; if (!transitionParams.transitioning) { appDisplayBox = this._getAppDisplayBoxForState(transitionParams.currentState, ...params); } else { const initialBox = this._getAppDisplayBoxForState(transitionParams.initialState, ...params); const finalBox = this._getAppDisplayBoxForState(transitionParams.finalState, ...params); appDisplayBox = initialBox.interpolate(finalBox, transitionParams.progress); } this._appDisplay.allocate(appDisplayBox); this._runPostAllocation(); }, }; // same copy of this function should be available in OverviewControls and WorkspacesView function _getFitModeForState(state) { switch (state) { case ControlsState.HIDDEN: case ControlsState.WINDOW_PICKER: return FitMode.SINGLE; case ControlsState.APP_GRID: if (opt.WS_ANIMATION && opt.SHOW_WS_TMB) return FitMode.ALL; else return FitMode.SINGLE; default: return FitMode.SINGLE; } }