summaryrefslogtreecommitdiffstats
path: root/extensions/47/vertical-workspaces/lib
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/47/vertical-workspaces/lib')
-rw-r--r--extensions/47/vertical-workspaces/lib/appDisplay.js2014
-rw-r--r--extensions/47/vertical-workspaces/lib/appFavorites.js79
-rw-r--r--extensions/47/vertical-workspaces/lib/dash.js1307
-rw-r--r--extensions/47/vertical-workspaces/lib/iconGrid.js429
-rw-r--r--extensions/47/vertical-workspaces/lib/layout.js473
-rw-r--r--extensions/47/vertical-workspaces/lib/messageTray.js91
-rw-r--r--extensions/47/vertical-workspaces/lib/optionsFactory.js496
-rw-r--r--extensions/47/vertical-workspaces/lib/osdWindow.js118
-rw-r--r--extensions/47/vertical-workspaces/lib/overlayKey.js170
-rw-r--r--extensions/47/vertical-workspaces/lib/overview.js162
-rw-r--r--extensions/47/vertical-workspaces/lib/overviewControls.js1633
-rw-r--r--extensions/47/vertical-workspaces/lib/panel.js253
-rw-r--r--extensions/47/vertical-workspaces/lib/recentFilesSearchProvider.js316
-rw-r--r--extensions/47/vertical-workspaces/lib/search.js474
-rw-r--r--extensions/47/vertical-workspaces/lib/searchController.js94
-rw-r--r--extensions/47/vertical-workspaces/lib/settings.js539
-rw-r--r--extensions/47/vertical-workspaces/lib/swipeTracker.js116
-rw-r--r--extensions/47/vertical-workspaces/lib/util.js445
-rw-r--r--extensions/47/vertical-workspaces/lib/windowAttentionHandler.js185
-rw-r--r--extensions/47/vertical-workspaces/lib/windowManager.js380
-rw-r--r--extensions/47/vertical-workspaces/lib/windowPreview.js629
-rw-r--r--extensions/47/vertical-workspaces/lib/workspace.js472
-rw-r--r--extensions/47/vertical-workspaces/lib/workspaceAnimation.js262
-rw-r--r--extensions/47/vertical-workspaces/lib/workspaceSwitcherPopup.js107
-rw-r--r--extensions/47/vertical-workspaces/lib/workspaceThumbnail.js1261
-rw-r--r--extensions/47/vertical-workspaces/lib/workspacesView.js1013
26 files changed, 13518 insertions, 0 deletions
diff --git a/extensions/47/vertical-workspaces/lib/appDisplay.js b/extensions/47/vertical-workspaces/lib/appDisplay.js
new file mode 100644
index 0000000..d94f7df
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/appDisplay.js
@@ -0,0 +1,2014 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * appDisplay.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
+import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
+import * as PageIndicators from 'resource:///org/gnome/shell/ui/pageIndicators.js';
+
+import { IconSize } from './iconGrid.js';
+
+let Me;
+let opt;
+// gettext
+let _;
+
+let _appDisplay;
+let _timeouts;
+
+const APP_ICON_TITLE_EXPAND_TIME = 200;
+const APP_ICON_TITLE_COLLAPSE_TIME = 100;
+
+const shellVersion46 = !Clutter.Container; // Container has been removed in 46
+
+function _getCategories(info) {
+ let categoriesStr = info.get_categories();
+ if (!categoriesStr)
+ return [];
+ return categoriesStr.split(';');
+}
+
+function _listsIntersect(a, b) {
+ for (let itemA of a) {
+ if (b.includes(itemA))
+ return true;
+ }
+ return false;
+}
+
+export const AppDisplayModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+ _ = Me.gettext;
+
+ _appDisplay = Main.overview._overview.controls._appDisplay;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+
+ this._appSystemStateConId = 0;
+ this._appGridLayoutConId = 0;
+ this._origAppViewItemAcceptDrop = null;
+ this._updateFolderIcons = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ _ = null;
+ _appDisplay = null;
+ }
+
+ update(reset) {
+ this._removeTimeouts();
+ this.moduleEnabled = opt.get('appDisplayModule');
+ const conflict = false;
+
+ reset = reset || !this.moduleEnabled || conflict;
+
+ // don't touch the original code if module disabled
+ if (reset && !this._firstActivation) {
+ this._disableModule();
+ this.moduleEnabled = false;
+ } else if (!reset) {
+ this._firstActivation = false;
+ this._activateModule();
+ }
+ if (reset && this._firstActivation) {
+ this.moduleEnabled = false;
+ console.debug(' AppDisplayModule - Keeping untouched');
+ }
+ }
+
+ _activateModule() {
+ Me.Modules.iconGridModule.update();
+
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ _timeouts = {};
+
+ this._applyOverrides();
+ this._updateAppDisplay();
+ _appDisplay.add_style_class_name('app-display-46');
+
+ console.debug(' AppDisplayModule - Activated');
+ }
+
+ _disableModule() {
+ Me.Modules.iconGridModule.update(true);
+
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ const reset = true;
+ this._updateAppDisplay(reset);
+ this._restoreOverviewGroup();
+
+ _appDisplay.remove_style_class_name('app-display-46');
+
+ console.debug(' AppDisplayModule - Disabled');
+ }
+
+ _removeTimeouts() {
+ if (_timeouts) {
+ Object.values(_timeouts).forEach(t => {
+ if (t)
+ GLib.source_remove(t);
+ });
+ _timeouts = null;
+ }
+ }
+
+ _applyOverrides() {
+ // Common/appDisplay
+ // this._overrides.addOverride('BaseAppViewCommon', AppDisplay.BaseAppView.prototype, BaseAppViewCommon);
+ // instead of overriding inaccessible BaseAppView class, we override its subclasses - AppDisplay and FolderView
+ this._overrides.addOverride('BaseAppViewCommonApp', AppDisplay.AppDisplay.prototype, BaseAppViewCommon);
+ this._overrides.addOverride('AppDisplay', AppDisplay.AppDisplay.prototype, AppDisplayCommon);
+ this._overrides.addOverride('AppViewItem', AppDisplay.AppViewItem.prototype, AppViewItemCommon);
+ this._overrides.addOverride('AppGridCommon', AppDisplay.AppGrid.prototype, AppGridCommon);
+ this._overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIcon);
+ if (opt.ORIENTATION) {
+ this._overrides.removeOverride('AppGridLayoutHorizontal');
+ this._overrides.addOverride('AppGridLayoutVertical', _appDisplay._appGridLayout, BaseAppViewGridLayoutVertical);
+ } else {
+ this._overrides.removeOverride('AppGridLayoutVertical');
+ this._overrides.addOverride('AppGridLayoutHorizontal', _appDisplay._appGridLayout, BaseAppViewGridLayoutHorizontal);
+ }
+
+ // Custom folders
+ this._overrides.addOverride('BaseAppViewCommonFolder', AppDisplay.FolderView.prototype, BaseAppViewCommon);
+ this._overrides.addOverride('FolderView', AppDisplay.FolderView.prototype, FolderView);
+ this._overrides.addOverride('AppFolderDialog', AppDisplay.AppFolderDialog.prototype, AppFolderDialog);
+ this._overrides.addOverride('FolderIcon', AppDisplay.FolderIcon.prototype, FolderIcon);
+
+ // Prevent changing grid page size when showing/hiding _pageIndicators
+ this._overrides.addOverride('PageIndicators', PageIndicators.PageIndicators.prototype, PageIndicatorsCommon);
+ }
+
+ _updateAppDisplay(reset) {
+ const orientation = reset ? Clutter.Orientation.HORIZONTAL : opt.ORIENTATION;
+ BaseAppViewCommon._adaptForOrientation.bind(_appDisplay)(orientation);
+
+ this._updateFavoritesConnection(reset);
+
+ _appDisplay.visible = true;
+ if (reset) {
+ _appDisplay._grid.layoutManager.fixedIconSize = -1;
+ _appDisplay._grid.layoutManager.allow_incomplete_pages = true;
+ _appDisplay._grid._currentMode = -1;
+ _appDisplay._grid.setGridModes();
+ _appDisplay._grid.set_style('');
+ _appDisplay._prevPageArrow.set_scale(1, 1);
+ _appDisplay._nextPageArrow.set_scale(1, 1);
+ if (this._appGridLayoutConId) {
+ global.settings.disconnect(this._appGridLayoutConId);
+ this._appGridLayoutConId = 0;
+ }
+ this._repopulateAppDisplay(reset);
+ } else {
+ _appDisplay._grid._currentMode = -1;
+ // update grid on layout reset
+ if (!this._appGridLayoutConId)
+ this._appGridLayoutConId = global.settings.connect('changed::app-picker-layout', this._updateLayout.bind(this));
+
+ // avoid resetting appDisplay before startup animation
+ // x11 shell restart skips startup animation
+ if (!Main.layoutManager._startingUp) {
+ this._repopulateAppDisplay();
+ } else if (Main.layoutManager._startingUp && Meta.is_restart()) {
+ _timeouts.three = GLib.idle_add(GLib.PRIORITY_LOW, () => {
+ this._repopulateAppDisplay();
+ _timeouts.three = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ }
+ }
+
+ _updateFavoritesConnection(reset) {
+ if (!reset) {
+ if (!this._appSystemStateConId && opt.APP_GRID_INCLUDE_DASH >= 3) {
+ this._appSystemStateConId = Shell.AppSystem.get_default().connect(
+ 'app-state-changed',
+ () => {
+ this._updateFolderIcons = true;
+ _appDisplay._redisplay();
+ }
+ );
+ }
+ } else if (this._appSystemStateConId) {
+ Shell.AppSystem.get_default().disconnect(this._appSystemStateConId);
+ this._appSystemStateConId = 0;
+ }
+ }
+
+ _restoreOverviewGroup() {
+ Main.overview.dash.showAppsButton.checked = false;
+ Main.layoutManager.overviewGroup.opacity = 255;
+ Main.layoutManager.overviewGroup.scale_x = 1;
+ Main.layoutManager.overviewGroup.scale_y = 1;
+ Main.layoutManager.overviewGroup.hide();
+ _appDisplay.translation_x = 0;
+ _appDisplay.translation_y = 0;
+ _appDisplay.visible = true;
+ _appDisplay.opacity = 255;
+ }
+
+ _updateLayout(settings, key) {
+ // Reset the app grid only if the user layout has been completely removed
+ if (!settings.get_value(key).deep_unpack().length) {
+ this._repopulateAppDisplay();
+ }
+ }
+
+ _repopulateAppDisplay(reset = false, callback) {
+ // Remove all icons so they can be re-created with the current configuration
+ // Updating appGrid content while rebasing extensions when session is locked makes no sense (relevant for GS version < 46)
+ if (!Main.sessionMode.isLocked)
+ AppDisplayCommon.removeAllItems.bind(_appDisplay)();
+
+ // appDisplay disabled
+ if (reset) {
+ _appDisplay._redisplay();
+ return;
+ }
+
+ _appDisplay._readyToRedisplay = true;
+ _appDisplay._redisplay();
+
+ // Setting OffscreenRedirect should improve performance when opacity transitions are used
+ _appDisplay.offscreen_redirect = Clutter.OffscreenRedirect.ALWAYS;
+
+ if (opt.APP_GRID_PERFORMANCE)
+ this._realizeAppDisplay(callback);
+ else if (callback)
+ callback();
+ }
+
+ _realizeAppDisplay(callback) {
+ // Workaround - silently realize appDisplay
+ // The realization takes some time and affects animations during the first use
+ // If we do it invisibly before the user needs the app grid, it can improve the user's experience
+ _appDisplay.opacity = 1;
+
+ this._exposeAppGrid();
+ _appDisplay._redisplay();
+ this._exposeAppFolders();
+
+ // Let the main loop process our changes before we continue
+ _timeouts.updateAppGrid = GLib.idle_add(GLib.PRIORITY_LOW, () => {
+ this._restoreAppGrid();
+ Me._resetInProgress = false;
+
+ if (callback)
+ callback();
+
+ _timeouts.updateAppGrid = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _exposeAppGrid() {
+ const overviewGroup = Main.layoutManager.overviewGroup;
+ if (!overviewGroup.visible) {
+ // scale down the overviewGroup so it don't cover uiGroup
+ overviewGroup.scale_y = 0.001;
+ // make it invisible to the eye, but visible for the renderer
+ overviewGroup.opacity = 1;
+ // if overview is hidden, show it
+ overviewGroup.visible = true;
+ }
+ }
+
+ _restoreAppGrid() {
+ if (opt.APP_GRID_PERFORMANCE)
+ this._hideAppFolders();
+
+ const overviewGroup = Main.layoutManager.overviewGroup;
+ if (!Main.overview._shown)
+ overviewGroup.hide();
+ overviewGroup.scale_y = 1;
+ overviewGroup.opacity = 255;
+ _appDisplay.opacity = 0;
+ _appDisplay.visible = false;
+ }
+
+ _exposeAppFolders() {
+ _appDisplay._folderIcons.forEach(d => {
+ d._ensureFolderDialog();
+ d._dialog.scale_y = 0.0001;
+ d._dialog.show();
+ d._dialog._updateFolderSize();
+ });
+ }
+
+ _hideAppFolders() {
+ _appDisplay._folderIcons.forEach(d => {
+ if (d._dialog) {
+ d._dialog.hide();
+ d._dialog.scale_y = 1;
+ }
+ });
+ }
+};
+
+function _getViewFromIcon(icon) {
+ icon = icon._sourceItem ? icon._sourceItem : icon;
+ for (let parent = icon.get_parent(); parent; parent = parent.get_parent()) {
+ if (parent instanceof AppDisplay.AppDisplay || parent instanceof AppDisplay.FolderView) {
+ return parent;
+ }
+ }
+ return null;
+}
+
+const AppDisplayCommon = {
+ _ensureDefaultFolders() {
+ // disable creation of default folders if user deleted them
+ },
+
+ removeAllItems() {
+ this._orderedItems.slice().forEach(item => {
+ if (item._dialog)
+ Main.layoutManager.overviewGroup.remove_child(item._dialog);
+
+ this._removeItem(item);
+ item.destroy();
+ });
+ this._folderIcons = [];
+ },
+
+ // apps load adapted for custom sorting and including dash items
+ _loadApps() {
+ let appIcons = [];
+ const runningApps = Shell.AppSystem.get_default().get_running().map(a => a.id);
+
+ this._appInfoList = Shell.AppSystem.get_default().get_installed().filter(appInfo => {
+ try {
+ appInfo.get_id(); // catch invalid file encodings
+ } catch (e) {
+ return false;
+ }
+
+ const appIsRunning = runningApps.includes(appInfo.get_id());
+ const appIsFavorite = this._appFavorites.isFavorite(appInfo.get_id());
+ const excludeApp = (opt.APP_GRID_EXCLUDE_RUNNING && appIsRunning) || (opt.APP_GRID_EXCLUDE_FAVORITES && appIsFavorite);
+
+ return this._parentalControlsManager.shouldShowApp(appInfo) && !excludeApp;
+ });
+
+ let apps = this._appInfoList.map(app => app.get_id());
+
+ let appSys = Shell.AppSystem.get_default();
+
+ const appsInsideFolders = new Set();
+ this._folderIcons = [];
+ if (!opt.APP_GRID_USAGE) {
+ let folders = this._folderSettings.get_strv('folder-children');
+ folders.forEach(id => {
+ let path = `${this._folderSettings.path}folders/${id}/`;
+ let icon = this._items.get(id);
+ if (!icon) {
+ icon = new AppDisplay.FolderIcon(id, path, this);
+ icon.connect('apps-changed', () => {
+ this._redisplay();
+ this._savePages();
+ });
+ icon.connect('notify::pressed', () => {
+ if (icon.pressed)
+ this.updateDragFocus(icon);
+ });
+ } else if (this._updateFolderIcons && opt.APP_GRID_EXCLUDE_RUNNING) {
+ // if any app changed its running state, update folder icon
+ icon.icon.update();
+ }
+
+ // remove empty folder icons
+ if (!icon.visible) {
+ icon.destroy();
+ return;
+ }
+
+ appIcons.push(icon);
+ this._folderIcons.push(icon);
+
+ icon.getAppIds().forEach(appId => appsInsideFolders.add(appId));
+ });
+ }
+
+ // reset request to update active icon
+ this._updateFolderIcons = false;
+
+ // Allow dragging of the icon only if the Dash would accept a drop to
+ // change favorite-apps. There are no other possible drop targets from
+ // the app picker, so there's no other need for a drag to start,
+ // at least on single-monitor setups.
+ // This also disables drag-to-launch on multi-monitor setups,
+ // but we hope that is not used much.
+ const isDraggable =
+ global.settings.is_writable('favorite-apps') ||
+ global.settings.is_writable('app-picker-layout');
+
+ apps.forEach(appId => {
+ if (!opt.APP_GRID_USAGE && appsInsideFolders.has(appId))
+ return;
+
+ let icon = this._items.get(appId);
+ if (!icon) {
+ let app = appSys.lookup_app(appId);
+ icon = new AppDisplay.AppIcon(app, { isDraggable });
+ icon.connect('notify::pressed', () => {
+ if (icon.pressed)
+ this.updateDragFocus(icon);
+ });
+ }
+
+ appIcons.push(icon);
+ });
+
+ // At last, if there's a placeholder available, add it
+ if (this._placeholder)
+ appIcons.push(this._placeholder);
+
+ return appIcons;
+ },
+
+ _onDragBegin(overview, source) {
+ // let sourceId;
+ // support active preview icons
+ if (source._sourceItem) {
+ // sourceId = source._sourceFolder._id;
+ source = source._sourceItem;
+ } /* else {
+ sourceId = source.id;
+ }*/
+ // Prevent switching page when an item on another page is selected
+ // by removing the focus from all icons
+ // This is an upstream bug
+ // this.selectApp(sourceId);
+ this.grab_key_focus();
+
+ this._dragMonitor = {
+ dragMotion: this._onDragMotion.bind(this),
+ dragDrop: this._onDragDrop.bind(this),
+ };
+ DND.addDragMonitor(this._dragMonitor);
+
+ this._appGridLayout.showPageIndicators();
+ this._dragFocus = null;
+ this._swipeTracker.enabled = false;
+
+ // When dragging from a folder dialog, the dragged app icon doesn't
+ // exist in AppDisplay. We work around that by adding a placeholder
+ // icon that is either destroyed on cancel, or becomes the effective
+ // new icon when dropped.
+ if (/* AppDisplay.*/_getViewFromIcon(source) instanceof AppDisplay.FolderView ||
+ (opt.APP_GRID_EXCLUDE_FAVORITES && this._appFavorites.isFavorite(source.id)))
+ this._ensurePlaceholder(source);
+ },
+
+ _ensurePlaceholder(source) {
+ if (this._placeholder)
+ return;
+
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+ const appSys = Shell.AppSystem.get_default();
+ const app = appSys.lookup_app(source.id);
+
+ const isDraggable =
+ global.settings.is_writable('favorite-apps') ||
+ global.settings.is_writable('app-picker-layout');
+
+ this._placeholder = new AppDisplay.AppIcon(app, { isDraggable });
+ this._placeholder.connect('notify::pressed', () => {
+ if (this._placeholder?.pressed)
+ this.updateDragFocus(this._placeholder);
+ });
+ this._placeholder.scaleAndFade();
+ this._redisplay();
+ },
+
+ // accept source from active folder preview
+ acceptDrop(source) {
+ if (opt.APP_GRID_USAGE)
+ return false;
+ if (source._sourceItem)
+ source = source._sourceItem;
+ if (!this._acceptDropCommon(source))
+ return false;
+
+ this._savePages();
+
+ const view = /* AppDisplay.*/_getViewFromIcon(source);
+ if (view instanceof AppDisplay.FolderView)
+ view.removeApp(source.app);
+
+ if (this._currentDialog)
+ this._currentDialog.popdown();
+
+ if (opt.APP_GRID_EXCLUDE_FAVORITES && this._appFavorites.isFavorite(source.id))
+ this._appFavorites.removeFavorite(source.id);
+ return true;
+ },
+
+ _savePages() {
+ // Skip saving pages when search app grid mode is active
+ // and the grid is showing search results
+ if (Main.overview._overview.controls._origAppGridContent)
+ return;
+
+ const pages = [];
+
+ for (let i = 0; i < this._grid.nPages; i++) {
+ const pageItems =
+ this._grid.getItemsAtPage(i).filter(c => c.visible);
+ const pageData = {};
+
+ pageItems.forEach((item, index) => {
+ pageData[item.id] = {
+ position: GLib.Variant.new_int32(index),
+ };
+ });
+ pages.push(pageData);
+ }
+
+ this._pageManager.pages = pages;
+ },
+};
+
+const BaseAppViewCommon = {
+ after__init() {
+ // Only folders can run this init
+ this._isFolder = true;
+
+ this._adaptForOrientation(opt.ORIENTATION, true);
+
+ // Because the original class prototype is not exported, we need to inject every instance
+ const overrides = new Me.Util.Overrides();
+ if (opt.ORIENTATION) {
+ overrides.addOverride('FolderGridLayoutVertical', this._appGridLayout, BaseAppViewGridLayoutVertical);
+ this._pageIndicators.set_style('margin-right: 12px;');
+ } else {
+ overrides.addOverride('FolderGridLayoutHorizontal', this._appGridLayout, BaseAppViewGridLayoutHorizontal);
+ this._pageIndicators.set_style('margin-bottom: 12px;');
+ }
+ },
+
+ _adaptForOrientation(orientation, folder) {
+ const vertical = !!orientation;
+
+ this._grid.layoutManager.fixedIconSize = folder ? opt.APP_GRID_FOLDER_ICON_SIZE : opt.APP_GRID_ICON_SIZE;
+ this._grid.layoutManager._orientation = orientation;
+ this._orientation = orientation;
+ this._swipeTracker.orientation = orientation;
+ this._swipeTracker._reset();
+
+ this._adjustment = vertical
+ ? this._scrollView.get_vscroll_bar().adjustment
+ : this._scrollView.get_hscroll_bar().adjustment;
+
+ this._prevPageArrow.pivot_point = new Graphene.Point({ x: 0.5, y: 0.5 });
+ this._prevPageArrow.rotation_angle_z = vertical ? 90 : 0;
+
+ this._nextPageArrow.pivot_point = new Graphene.Point({ x: 0.5, y: 0.5 });
+ this._nextPageArrow.rotation_angle_z = vertical ? 90 : 0;
+
+ const pageIndicators = this._pageIndicators;
+ pageIndicators.vertical = vertical;
+ this._box.vertical = !vertical;
+ pageIndicators.x_expand = !vertical;
+ pageIndicators.y_align = vertical ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START;
+ pageIndicators.x_align = vertical ? Clutter.ActorAlign.START : Clutter.ActorAlign.CENTER;
+
+ this._grid.layoutManager.allow_incomplete_pages = folder ? false : opt.APP_GRID_ALLOW_INCOMPLETE_PAGES;
+ const spacing = folder ? opt.APP_GRID_FOLDER_SPACING : opt.APP_GRID_SPACING;
+ this._grid.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`);
+
+ if (vertical) {
+ this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL);
+ if (!this._scrollConId) {
+ this._scrollConId = this._adjustment.connect('notify::value', adj => {
+ const value = adj.value / adj.page_size;
+ this._pageIndicators.setCurrentPosition(value);
+ });
+ }
+ pageIndicators.remove_style_class_name('page-indicators-horizontal');
+ pageIndicators.add_style_class_name('page-indicators-vertical');
+ this._prevPageIndicator.add_style_class_name('prev-page-indicator');
+ this._nextPageIndicator.add_style_class_name('next-page-indicator');
+ this._nextPageArrow.translationY = 0;
+ this._prevPageArrow.translationY = 0;
+ this._nextPageIndicator.translationX = 0;
+ this._prevPageIndicator.translationX = 0;
+ } else {
+ this._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER);
+ if (this._scrollConId) {
+ this._adjustment.disconnect(this._scrollConId);
+ this._scrollConId = 0;
+ }
+ pageIndicators.remove_style_class_name('page-indicators-vertical');
+ pageIndicators.add_style_class_name('page-indicators-horizontal');
+ this._prevPageIndicator.remove_style_class_name('prev-page-indicator');
+ this._nextPageIndicator.remove_style_class_name('next-page-indicator');
+ this._nextPageArrow.translationX = 0;
+ this._prevPageArrow.translationX = 0;
+ this._nextPageIndicator.translationY = 0;
+ this._prevPageIndicator.translationY = 0;
+ }
+
+ const scale = opt.APP_GRID_SHOW_PAGE_ARROWS ? 1 : 0;
+ this._prevPageArrow.set_scale(scale, scale);
+ this._nextPageArrow.set_scale(scale, scale);
+ },
+
+ _sortItemsByName(items) {
+ items.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ },
+
+ _updateItemPositions(icons, allowIncompletePages = false) {
+ // Avoid recursion when relocating icons
+ this._grid.layoutManager._skipRelocateSurplusItems = true;
+
+ const { itemsPerPage } = this._grid;
+
+ icons.slice().forEach((icon, index) => {
+ const [currentPage, currentPosition] = this._grid.layoutManager.getItemPosition(icon);
+
+ let page, position;
+ if (allowIncompletePages) {
+ [page, position] = this._getItemPosition(icon);
+ } else {
+ page = Math.floor(index / itemsPerPage);
+ position = index % itemsPerPage;
+ }
+
+ if (currentPage !== page || currentPosition !== position) {
+ this._moveItem(icon, page, position);
+ }
+ });
+
+ this._grid.layoutManager._skipRelocateSurplusItems = false;
+ // Disable animating the icons to their new positions
+ // since it can cause glitches when the app grid search mode is active
+ // and many icons are repositioning at once
+ this._grid.layoutManager._shouldEaseItems = false;
+ },
+
+ // Adds sorting options
+ _redisplay() {
+ // different options for main app grid and app folders
+ const thisIsFolder = this instanceof AppDisplay.FolderView;
+ const thisIsAppDisplay = !thisIsFolder;
+
+ // When an app was dragged from a folder and dropped to the main grid
+ // folders (if exist) need to be redisplayed even if we temporary block it for the appDisplay
+ this._folderIcons?.forEach(icon => {
+ icon.view._redisplay();
+ });
+
+ // Avoid unwanted updates
+ if (thisIsAppDisplay && !this._readyToRedisplay)
+ return;
+
+ const oldApps = this._orderedItems.slice();
+ const oldAppIds = oldApps.map(icon => icon.id);
+
+ const newApps = this._loadApps();
+ const newAppIds = newApps.map(icon => icon.id);
+
+ const addedApps = newApps.filter(icon => !oldAppIds.includes(icon.id));
+ const removedApps = oldApps.filter(icon => !newAppIds.includes(icon.id));
+
+ // Don't update folder without dialog if its content didn't change
+ if (!addedApps.length && !removedApps.length && thisIsFolder && !this.get_parent())
+ return;
+
+ // Remove old app icons
+ removedApps.forEach(icon => {
+ this._removeItem(icon);
+ icon.destroy();
+ });
+
+ // For the main app grid only
+ let allowIncompletePages = thisIsAppDisplay && opt.APP_GRID_ALLOW_INCOMPLETE_PAGES;
+
+ const customOrder = !((opt.APP_GRID_ORDER && thisIsAppDisplay) || (opt.APP_FOLDER_ORDER && thisIsFolder));
+ if (!customOrder) {
+ allowIncompletePages = false;
+
+ // Sort by name
+ this._sortItemsByName(newApps);
+
+ // Sort by usage
+ if ((opt.APP_GRID_USAGE && thisIsAppDisplay) ||
+ (opt.APP_FOLDER_USAGE && thisIsFolder)) {
+ newApps.sort((a, b) => Shell.AppUsage.get_default().compare(a.app?.id, b.app?.id));
+ }
+
+ // Sort favorites first
+ if (!opt.APP_GRID_EXCLUDE_FAVORITES && opt.APP_GRID_DASH_FIRST) {
+ const fav = Object.keys(this._appFavorites._favorites);
+ newApps.sort((a, b) => {
+ let aFav = fav.indexOf(a.id);
+ if (aFav < 0)
+ aFav = 999;
+ let bFav = fav.indexOf(b.id);
+ if (bFav < 0)
+ bFav = 999;
+ return bFav < aFav;
+ });
+ }
+
+ // Sort running first
+ if (!opt.APP_GRID_EXCLUDE_RUNNING && opt.APP_GRID_DASH_FIRST) {
+ newApps.sort((a, b) => a.app?.get_state() !== Shell.AppState.RUNNING && b.app?.get_state() === Shell.AppState.RUNNING);
+ }
+
+ // Sort folders first
+ if (thisIsAppDisplay && opt.APP_GRID_FOLDERS_FIRST)
+ newApps.sort((a, b) => b._folder && !a._folder);
+
+ // Sort folders last
+ else if (thisIsAppDisplay && opt.APP_GRID_FOLDERS_LAST)
+ newApps.sort((a, b) => a._folder && !b._folder);
+ } else {
+ // Sort items according to the custom order stored in pageManager
+ newApps.sort(this._compareItems.bind(this));
+ }
+
+ // Add new app icons to the grid
+ newApps.forEach(icon => {
+ const [page, position] = this._grid.getItemPosition(icon);
+ if (page === -1 && position === -1)
+ this._addItem(icon, -1, -1);
+ });
+ // When a placeholder icon was added to the custom sorted grid during DND from a folder
+ // update its initial position on the page
+ if (customOrder)
+ newApps.sort(this._compareItems.bind(this));
+
+ this._orderedItems = newApps;
+
+ // Update icon positions if needed
+ this._updateItemPositions(this._orderedItems, allowIncompletePages);
+
+ // Relocate items with invalid positions
+ if (thisIsAppDisplay) {
+ const nPages = this._grid.layoutManager.nPages;
+ for (let pageIndex = 0; pageIndex < nPages; pageIndex++)
+ this._grid.layoutManager._relocateSurplusItems(pageIndex);
+ }
+
+ this.emit('view-loaded');
+ },
+
+ _canAccept(source) {
+ return source instanceof AppDisplay.AppViewItem;
+ },
+
+ // this method is replacing BaseAppVew.acceptDrop which can't be overridden directly
+ _acceptDropCommon(source) {
+ const dropTarget = this._dropTarget;
+ delete this._dropTarget;
+ if (!this._canAccept(source))
+ return false;
+
+ if (dropTarget === this._prevPageIndicator ||
+ dropTarget === this._nextPageIndicator) {
+ let increment;
+ increment = dropTarget === this._prevPageIndicator ? -1 : 1;
+ const { currentPage, nPages } = this._grid;
+ const page = Math.min(currentPage + increment, nPages);
+ const position = page < nPages ? -1 : 0;
+
+ this._moveItem(source, page, position);
+ this.goToPage(page);
+ } else if (this._delayedMoveData) {
+ // Dropped before the icon was moved
+ const { page, position } = this._delayedMoveData;
+ try {
+ this._moveItem(source, page, position);
+ } catch (e) {
+ console.warn(`Warning:${e}`);
+ }
+ this._removeDelayedMove();
+ }
+
+ return true;
+ },
+
+ // support active preview icons
+ _onDragMotion(dragEvent) {
+ if (!(dragEvent.source instanceof AppDisplay.AppViewItem))
+ return DND.DragMotionResult.CONTINUE;
+
+ if (dragEvent.source._sourceItem)
+ dragEvent.source = dragEvent.source._sourceItem;
+
+ const appIcon = dragEvent.source;
+
+ if (appIcon instanceof AppDisplay.AppViewItem) {
+ if (!this._dragMaybeSwitchPageImmediately(dragEvent)) {
+ // Two ways of switching pages during DND:
+ // 1) When "bumping" the cursor against the monitor edge, we switch
+ // page immediately.
+ // 2) When hovering over the next-page indicator for a certain time,
+ // we also switch page.
+
+ const { targetActor } = dragEvent;
+
+ if (targetActor === this._prevPageIndicator ||
+ targetActor === this._nextPageIndicator)
+ this._maybeSetupDragPageSwitchInitialTimeout(dragEvent);
+ else
+ this._resetDragPageSwitch();
+ }
+ }
+
+ const thisIsFolder = this instanceof AppDisplay.FolderView;
+ const thisIsAppDisplay = !thisIsFolder;
+
+ // Prevent reorganizing the main app grid icons when an app folder is open and when sorting is not custom
+ // For some reason in V-Shell the drag motion events propagate from folder to main grid, which is not a problem in default code - so test the open dialog
+ if (!this._currentDialog && (!opt.APP_GRID_ORDER && thisIsAppDisplay) || (!opt.APP_FOLDER_ORDER && thisIsFolder))
+ this._maybeMoveItem(dragEvent);
+
+ return DND.DragMotionResult.CONTINUE;
+ },
+};
+
+const BaseAppViewGridLayoutHorizontal = {
+ _getIndicatorsWidth(box) {
+ const [width, height] = box.get_size();
+ const arrows = [
+ this._nextPageArrow,
+ this._previousPageArrow,
+ ];
+
+ let minArrowsWidth;
+
+ minArrowsWidth = arrows.reduce(
+ (previousWidth, accessory) => {
+ const [min] = accessory.get_preferred_width(height);
+ return Math.max(previousWidth, min);
+ }, 0);
+
+ minArrowsWidth = opt.APP_GRID_SHOW_PAGE_ARROWS ? minArrowsWidth : 0;
+
+ const indicatorWidth = !this._grid._isFolder
+ ? minArrowsWidth + ((width - minArrowsWidth) * (1 - opt.APP_GRID_PAGE_WIDTH_SCALE)) / 2
+ : minArrowsWidth + 6;
+
+ return Math.round(indicatorWidth);
+ },
+
+ vfunc_allocate(container, box) {
+ const ltr = container.get_text_direction() !== Clutter.TextDirection.RTL;
+ const indicatorsWidth = this._getIndicatorsWidth(box);
+
+ const pageIndicatorsHeight = 20; // _appDisplay._pageIndicators.height is unstable, 20 is determined by the style
+ const availHeight = box.get_height() - pageIndicatorsHeight;
+ const vPadding = Math.round((availHeight - availHeight * opt.APP_GRID_PAGE_HEIGHT_SCALE) / 2);
+ this._grid.indicatorsPadding = new Clutter.Margin({
+ left: indicatorsWidth,
+ right: indicatorsWidth,
+ top: vPadding + pageIndicatorsHeight,
+ bottom: vPadding,
+ });
+
+ this._scrollView.allocate(box);
+
+ const leftBox = box.copy();
+ leftBox.x2 = leftBox.x1 + indicatorsWidth;
+
+ const rightBox = box.copy();
+ rightBox.x1 = rightBox.x2 - indicatorsWidth;
+
+ this._previousPageIndicator.allocate(ltr ? leftBox : rightBox);
+ this._previousPageArrow.allocate_align_fill(ltr ? leftBox : rightBox,
+ 0.5, 0.5, false, false);
+ this._nextPageIndicator.allocate(ltr ? rightBox : leftBox);
+ this._nextPageArrow.allocate_align_fill(ltr ? rightBox : leftBox,
+ 0.5, 0.5, false, false);
+
+ this._pageWidth = box.get_width();
+
+ // Center page arrow buttons
+ this._previousPageArrow.translationY = pageIndicatorsHeight / 2;
+ this._nextPageArrow.translationY = pageIndicatorsHeight / 2;
+ // Reset page indicators vertical position
+ this._nextPageIndicator.translationY = 0;
+ this._previousPageIndicator.translationY = 0;
+ },
+};
+
+const BaseAppViewGridLayoutVertical = {
+ _getIndicatorsHeight(box) {
+ const [width, height] = box.get_size();
+ const arrows = [
+ this._nextPageArrow,
+ this._previousPageArrow,
+ ];
+
+ let minArrowsHeight;
+
+ minArrowsHeight = arrows.reduce(
+ (previousHeight, accessory) => {
+ const [min] = accessory.get_preferred_height(width);
+ return Math.max(previousHeight, min);
+ }, 0);
+
+ minArrowsHeight = opt.APP_GRID_SHOW_PAGE_ARROWS ? minArrowsHeight : 0;
+
+ const indicatorHeight = !this._grid._isFolder
+ ? minArrowsHeight + ((height - minArrowsHeight) * (1 - opt.APP_GRID_PAGE_HEIGHT_SCALE)) / 2
+ : minArrowsHeight + 6;
+
+ return Math.round(indicatorHeight);
+ },
+
+ _syncPageIndicators() {
+ if (!this._container)
+ return;
+
+ const { value } = this._pageIndicatorsAdjustment;
+
+ const { top, bottom } = this._grid.indicatorsPadding;
+ const topIndicatorOffset = -top * (1 - value);
+ const bottomIndicatorOffset = bottom * (1 - value);
+
+ this._previousPageIndicator.translationY =
+ topIndicatorOffset;
+ this._nextPageIndicator.translationY =
+ bottomIndicatorOffset;
+
+ const leftArrowOffset = -top * value;
+ const rightArrowOffset = bottom * value;
+
+ this._previousPageArrow.translationY =
+ leftArrowOffset;
+ this._nextPageArrow.translationY =
+ rightArrowOffset;
+
+ // Page icons
+ this._translatePreviousPageIcons(value);
+ this._translateNextPageIcons(value);
+
+ if (this._grid.nPages > 0) {
+ this._grid.getItemsAtPage(this._currentPage).forEach(icon => {
+ icon.translationY = 0;
+ });
+ }
+ },
+
+ _translatePreviousPageIcons(value) {
+ if (this._currentPage === 0)
+ return;
+
+ const pageHeight = this._grid.layoutManager._pageHeight;
+ const previousPage = this._currentPage - 1;
+ const icons = this._grid.getItemsAtPage(previousPage).filter(i => i.visible);
+ if (icons.length === 0)
+ return;
+
+ const { top } = this._grid.indicatorsPadding;
+ const { rowSpacing } = this._grid.layoutManager;
+ const endIcon = icons[icons.length - 1];
+ let iconOffset;
+
+ const currentPageOffset = pageHeight * this._currentPage;
+ iconOffset = currentPageOffset - endIcon.allocation.y1 - endIcon.width + top - rowSpacing;
+
+ for (const icon of icons)
+ icon.translationY = iconOffset * value;
+ },
+
+ _translateNextPageIcons(value) {
+ if (this._currentPage >= this._grid.nPages - 1)
+ return;
+
+ const nextPage = this._currentPage + 1;
+ const icons = this._grid.getItemsAtPage(nextPage).filter(i => i.visible);
+ if (icons.length === 0)
+ return;
+
+ const { bottom } = this._grid.indicatorsPadding;
+ const { rowSpacing } = this._grid.layoutManager;
+ let iconOffset;
+
+ const pageOffset = this._pageHeight * nextPage;
+ iconOffset = pageOffset - icons[0].allocation.y1 - bottom + rowSpacing;
+
+ for (const icon of icons)
+ icon.translationY = iconOffset * value;
+ },
+
+ vfunc_allocate(container, box) {
+ const indicatorsHeight = this._getIndicatorsHeight(box);
+
+ const pageIndicatorsWidth = 20; // _appDisplay._pageIndicators.width is not stable, 20 is determined by the style
+ const availWidth = box.get_width() - pageIndicatorsWidth;
+ const hPadding = Math.round((availWidth - availWidth * opt.APP_GRID_PAGE_WIDTH_SCALE) / 2);
+
+ this._grid.indicatorsPadding = new Clutter.Margin({
+ top: indicatorsHeight,
+ bottom: indicatorsHeight,
+ left: hPadding + pageIndicatorsWidth,
+ right: hPadding,
+ });
+
+ this._scrollView.allocate(box);
+
+ const topBox = box.copy();
+ topBox.y2 = topBox.y1 + indicatorsHeight;
+
+ const bottomBox = box.copy();
+ bottomBox.y1 = bottomBox.y2 - indicatorsHeight;
+
+ this._previousPageIndicator.allocate(topBox);
+ this._previousPageArrow.allocate_align_fill(topBox,
+ 0.5, 0.5, false, false);
+ this._nextPageIndicator.allocate(bottomBox);
+ this._nextPageArrow.allocate_align_fill(bottomBox,
+ 0.5, 0.5, false, false);
+
+ this._pageHeight = box.get_height();
+
+ // Center page arrow buttons
+ this._previousPageArrow.translationX = pageIndicatorsWidth / 2;
+ this._nextPageArrow.translationX = pageIndicatorsWidth / 2;
+ // Reset page indicators vertical position
+ this._nextPageIndicator.translationX = 0;
+ this._previousPageIndicator.translationX = 0;
+ },
+};
+
+const AppGridCommon = {
+ _updatePadding() {
+ const { rowSpacing, columnSpacing } = this.layoutManager;
+
+ const padding = this._indicatorsPadding.copy();
+
+ padding.left += rowSpacing;
+ padding.right += rowSpacing;
+ padding.top += columnSpacing;
+ padding.bottom += columnSpacing;
+
+ this.layoutManager.pagePadding = padding;
+ },
+};
+
+const FolderIcon = {
+ after__init() {
+ this.button_mask = St.ButtonMask.ONE | St.ButtonMask.TWO;
+ if (shellVersion46)
+ this.add_style_class_name('app-folder-46');
+ else
+ this.add_style_class_name('app-folder-45');
+ },
+
+ open() {
+ // Prevent switching page when an item on another page is selected
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ // Select folder icon to prevent switching page to the one with currently selected icon
+ this._parentView._selectAppInternal(this._id);
+ // Remove key focus from the selected icon to prevent switching page after dropping the removed folder icon on another page of the main grid
+ this._parentView.grab_key_focus();
+ this._ensureFolderDialog();
+ this._dialog.popup();
+ });
+ },
+
+ vfunc_clicked() {
+ this.open();
+ },
+
+ _canAccept(source) {
+ if (!(source instanceof AppDisplay.AppIcon))
+ return false;
+
+ const view = _getViewFromIcon(source);
+ if (!view /* || !(view instanceof AppDisplay.AppDisplay)*/)
+ return false;
+
+ // Disable this test to allow the user to cancel the current DND by dropping the icon on its original source
+ /* if (this._folder.get_strv('apps').includes(source.id))
+ return false;*/
+
+ return true;
+ },
+
+ acceptDrop(source) {
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+ const accepted = AppViewItemCommon.acceptDrop.bind(this)(source);
+
+ if (!accepted)
+ return false;
+
+ // If the icon is already in the folder (user dropped it back on the same folder), skip re-adding it
+ if (this._folder.get_strv('apps').includes(source.id))
+ return true;
+
+ this._onDragEnd();
+
+ this.view.addApp(source.app);
+
+ return true;
+ },
+};
+
+const FolderView = {
+ _createGrid() {
+ let grid = new FolderGrid();
+ grid._view = this;
+ return grid;
+ },
+
+ createFolderIcon(size) {
+ const layout = new Clutter.GridLayout({
+ row_homogeneous: true,
+ column_homogeneous: true,
+ });
+
+ let icon = new St.Widget({
+ layout_manager: layout,
+ x_align: Clutter.ActorAlign.CENTER,
+ style: `width: ${size}px; height: ${size}px;`,
+ });
+
+ const numItems = this._orderedItems.length;
+ // decide what number of icons switch to 3x3 grid
+ // APP_GRID_FOLDER_ICON_GRID: 3 -> more than 4
+ // : 4 -> more than 8
+ const threshold = opt.APP_GRID_FOLDER_ICON_GRID % 3 ? 8 : 4;
+ const gridSize = opt.APP_GRID_FOLDER_ICON_GRID > 2 && numItems > threshold ? 3 : 2;
+ const FOLDER_SUBICON_FRACTION = gridSize === 2 ? 0.4 : 0.27;
+
+ let subSize = Math.floor(FOLDER_SUBICON_FRACTION * size);
+ let rtl = icon.get_text_direction() === Clutter.TextDirection.RTL;
+ for (let i = 0; i < gridSize * gridSize; i++) {
+ const style = `width: ${subSize}px; height: ${subSize}px;`;
+ let bin = new St.Bin({ style, reactive: true });
+ bin.pivot_point = new Graphene.Point({ x: 0.5, y: 0.5 });
+ if (i < numItems) {
+ if (!opt.APP_GRID_ACTIVE_PREVIEW) {
+ bin.child = this._orderedItems[i].app.create_icon_texture(subSize);
+ } else {
+ const app = this._orderedItems[i].app;
+ const child = new AppDisplay.AppIcon(app, {
+ setSizeManually: true,
+ showLabel: false,
+ });
+
+ child._sourceItem = this._orderedItems[i];
+ child._sourceFolder = this;
+ child.icon.style_class = '';
+ child.set_style_class_name('');
+ child.icon.set_style('margin: 0; padding: 0;');
+ child._dot.set_style('margin-bottom: 1px;');
+ child.icon.setIconSize(subSize);
+ child._canAccept = () => false;
+
+ bin.child = child;
+
+ bin.connect('enter-event', () => {
+ bin.ease({
+ duration: 100,
+ translation_y: -3,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ });
+ bin.connect('leave-event', () => {
+ bin.ease({
+ duration: 100,
+ translation_y: 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ });
+ }
+ }
+
+ layout.attach(bin, rtl ? (i + 1) % gridSize : i % gridSize, Math.floor(i / gridSize), 1, 1);
+ }
+
+ return icon;
+ },
+
+ _loadApps() {
+ this._apps = [];
+ const excludedApps = this._folder.get_strv('excluded-apps');
+ const appSys = Shell.AppSystem.get_default();
+ const addAppId = appId => {
+ if (excludedApps.includes(appId))
+ return;
+
+ if (opt.APP_GRID_EXCLUDE_FAVORITES && this._appFavorites.isFavorite(appId))
+ return;
+
+ const app = appSys.lookup_app(appId);
+ if (!app)
+ return;
+
+ if (opt.APP_GRID_EXCLUDE_RUNNING) {
+ const runningApps = Shell.AppSystem.get_default().get_running().map(a => a.id);
+ if (runningApps.includes(appId))
+ return;
+ }
+
+ if (!this._parentalControlsManager.shouldShowApp(app.get_app_info()))
+ return;
+
+ if (this._apps.indexOf(app) !== -1)
+ return;
+
+ this._apps.push(app);
+ };
+
+ const folderApps = this._folder.get_strv('apps');
+ folderApps.forEach(addAppId);
+
+ const folderCategories = this._folder.get_strv('categories');
+ const appInfos = this._parentView.getAppInfos();
+ appInfos.forEach(appInfo => {
+ let appCategories = /* AppDisplay.*/_getCategories(appInfo);
+ if (!_listsIntersect(folderCategories, appCategories))
+ return;
+
+ addAppId(appInfo.get_id());
+ });
+
+ let items = [];
+ this._apps.forEach(app => {
+ let icon = this._items.get(app.get_id());
+ if (!icon)
+ icon = new AppDisplay.AppIcon(app);
+
+ items.push(icon);
+ });
+
+ return items;
+ },
+
+ acceptDrop(source) {
+ /* if (!BaseAppViewCommon.acceptDrop.bind(this)(source))
+ return false;*/
+ if (opt.APP_FOLDER_ORDER)
+ return false;
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+ if (!this._acceptDropCommon(source))
+ return false;
+
+ const folderApps = this._orderedItems.map(item => item.id);
+ this._folder.set_strv('apps', folderApps);
+
+ return true;
+ },
+};
+
+const FolderGrid = GObject.registerClass({
+ // Registered name should be unique
+ GTypeName: `FolderGrid${Math.floor(Math.random() * 1000)}`,
+}, class FolderGrid extends AppDisplay.AppGrid {
+ _init() {
+ super._init({
+ allow_incomplete_pages: false,
+ // For adaptive size (0), set the numbers high enough to fit all the icons
+ // to avoid splitting the icons to pages upon creating the grid
+ columns_per_page: 20,
+ rows_per_page: 20,
+ page_halign: Clutter.ActorAlign.CENTER,
+ page_valign: Clutter.ActorAlign.CENTER,
+ });
+ this.layoutManager._isFolder = true;
+ this._isFolder = true;
+ const spacing = opt.APP_GRID_FOLDER_SPACING;
+ this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`);
+ this.layoutManager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE;
+
+ this.setGridModes([
+ {
+ columns: 20,
+ rows: 20,
+ },
+ ]);
+ }
+
+ _updatePadding() {
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const padding = this._indicatorsPadding.copy();
+ const pageIndicatorSize = opt.ORIENTATION
+ ? this._view._pageIndicators.get_preferred_width(1000)[1] / scaleFactor
+ : this._view._pageIndicators.get_preferred_height(1000)[1] / scaleFactor;
+ Math.round(Math.min(...this._view._pageIndicators.get_size()));// / scaleFactor);// ~28;
+ padding.left = opt.ORIENTATION ? pageIndicatorSize : 0;
+ padding.right = 0;
+ padding.top = opt.ORIENTATION ? 0 : pageIndicatorSize;
+ padding.bottom = 0;
+ this.layoutManager.pagePadding = padding;
+ }
+});
+
+
+const FOLDER_DIALOG_ANIMATION_TIME = 200; // AppDisplay.FOLDER_DIALOG_ANIMATION_TIME
+const AppFolderDialog = {
+ // injection to _init()
+ after__init() {
+ // GS 46 changed the aligning to CENTER which restricts max folder dialog size
+ this._viewBox.set({
+ x_align: Clutter.ActorAlign.FILL,
+ y_align: Clutter.ActorAlign.FILL,
+ });
+
+ // delegate this dialog to the FolderIcon._view
+ // so its _createFolderIcon function can update the dialog if folder content changed
+ this._view._dialog = this;
+
+ // right click into the folder popup should close it
+ this.child.reactive = true;
+ const clickAction = new Clutter.ClickAction();
+ clickAction.connect('clicked', act => {
+ if (act.get_button() === Clutter.BUTTON_PRIMARY)
+ return Clutter.EVENT_STOP;
+ const [x, y] = clickAction.get_coords();
+ const actor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
+ // if it's not entry for editing folder title
+ if (actor !== this._entry)
+ this.popdown();
+ return Clutter.EVENT_STOP;
+ });
+
+ this.child.add_action(clickAction);
+ },
+
+ after__addFolderNameEntry() {
+ // edit-folder-button class has been replaced with icon-button class which is not transparent in 46
+ this._editButton.add_style_class_name('edit-folder-button');
+
+ // Edit button
+ this._removeButton = new St.Button({
+ style_class: 'icon-button edit-folder-button',
+ button_mask: St.ButtonMask.ONE,
+ toggle_mode: false,
+ reactive: true,
+ can_focus: true,
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.CENTER,
+ child: new St.Icon({
+ icon_name: 'user-trash-symbolic',
+ icon_size: 16,
+ }),
+ });
+
+ this._removeButton.connect('clicked', () => {
+ if (Date.now() - this._removeButton._lastClick < Clutter.Settings.get_default().double_click_time) {
+ // Close dialog to avoid crashes
+ this._isOpen = false;
+ this._grabHelper.ungrab({ actor: this });
+ this.emit('open-state-changed', false);
+ this.hide();
+ this._popdownCallbacks.forEach(func => func());
+ this._popdownCallbacks = [];
+ _appDisplay.ease({
+ opacity: 255,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ // Reset all keys to delete the relocatable schema
+ this._view._deletingFolder = true; // Upstream property
+ let keys = this._folder.settings_schema.list_keys();
+ for (const key of keys)
+ this._folder.reset(key);
+
+ let settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' });
+ let folders = settings.get_strv('folder-children');
+ folders.splice(folders.indexOf(this._view._id), 1);
+
+ // remove all abandoned folders (usually my own garbage and unwanted default folders...)
+ /* const appFolders = _appDisplay._folderIcons.map(icon => icon._id);
+ folders.forEach(folder => {
+ if (!appFolders.includes(folder)) {
+ folders.splice(folders.indexOf(folder._id), 1);
+ }
+ });*/
+ settings.set_strv('folder-children', folders);
+
+ this._view._deletingFolder = false;
+ return;
+ }
+ this._removeButton._lastClick = Date.now();
+ });
+
+ this._entryBox.add_child(this._removeButton);
+ this._entryBox.set_child_at_index(this._removeButton, 0);
+
+ this._closeButton = new St.Button({
+ style_class: 'icon-button edit-folder-button',
+ button_mask: St.ButtonMask.ONE,
+ toggle_mode: false,
+ reactive: true,
+ can_focus: true,
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.CENTER,
+ child: new St.Icon({
+ icon_name: 'window-close-symbolic',
+ icon_size: 16,
+ }),
+ });
+
+ this._closeButton.connect('clicked', () => {
+ this.popdown();
+ });
+
+ this._entryBox.add_child(this._closeButton);
+ },
+
+ popup() {
+ if (this._isOpen)
+ return;
+
+ this._isOpen = this._grabHelper.grab({
+ actor: this,
+ focus: this._editButton,
+ onUngrab: () => this.popdown(),
+ });
+
+ if (!this._isOpen)
+ return;
+
+ this.get_parent().set_child_above_sibling(this, null);
+
+ // _zoomAndFadeIn() is called from the dialog's allocate()
+ this._needsZoomAndFade = true;
+
+ this.show();
+ // force update folder size
+ this._folderAreaBox = null;
+ this._updateFolderSize();
+
+ this.emit('open-state-changed', true);
+ },
+
+ _setupPopdownTimeout() {
+ if (this._popdownTimeoutId > 0)
+ return;
+
+ // This timeout is handled in the original code and removed in _onDestroy()
+ // All dialogs are destroyed on extension disable()
+ this._popdownTimeoutId =
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
+ this._popdownTimeoutId = 0;
+ // Following line fixes upstream bug
+ // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6164
+ this._view._onDragEnd();
+ this.popdown();
+ return GLib.SOURCE_REMOVE;
+ });
+ },
+
+ vfunc_allocate(box) {
+ this._updateFolderSize();
+
+ // super.allocate(box)
+ St.Bin.prototype.vfunc_allocate.bind(this)(box);
+
+ // Override any attempt to resize the folder dialog, that happens when some child gets wild
+ // Re-allocate the child only if necessary, because it terminates grid animations
+ if (this._width && this._height && (this._width !== this.child.width || this._height !== this.child.height))
+ this._allocateChild();
+
+ // We can only start zooming after receiving an allocation
+ if (this._needsZoomAndFade)
+ this._zoomAndFadeIn();
+ },
+
+ _allocateChild() {
+ const childBox = new Clutter.ActorBox();
+ childBox.set_size(this._width, this._height);
+ this.child.allocate(childBox);
+ },
+
+ // Note that the appDisplay may be off-screen so its coordinates may be shifted
+ // However, for _updateFolderSize() it doesn't matter
+ // and when _zoomAndFadeIn() is called, appDisplay is on the right place
+ _getFolderAreaBox() {
+ const appDisplay = this._source._parentView;
+ const folderAreaBox = appDisplay.get_allocation_box().copy();
+ const searchEntryHeight = opt.SHOW_SEARCH_ENTRY ? Main.overview._overview.controls._searchEntryBin.height : 0;
+ folderAreaBox.y1 -= searchEntryHeight;
+
+ // _zoomAndFadeIn() needs an absolute position within a multi-monitor workspace
+ const monitorGeometry = global.display.get_monitor_geometry(global.display.get_primary_monitor());
+ folderAreaBox.x1 += monitorGeometry.x;
+ folderAreaBox.x2 += monitorGeometry.x;
+ folderAreaBox.y1 += monitorGeometry.y;
+ folderAreaBox.y2 += monitorGeometry.y;
+
+ return folderAreaBox;
+ },
+
+ _updateFolderSize() {
+ const view = this._view;
+ const nItems = view._orderedItems.length;
+ const [firstItem] = view._grid.layoutManager._container;
+ if (!firstItem)
+ return;
+
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const margin = 18; // see stylesheet .app-folder-dialog-container;
+
+ const folderAreaBox = this._getFolderAreaBox();
+
+ const maxDialogWidth = folderAreaBox.get_width() / scaleFactor;
+ const maxDialogHeight = folderAreaBox.get_height() / scaleFactor;
+
+ // We can't build folder if the available space is not available
+ if (!isFinite(maxDialogWidth) || !isFinite(maxDialogHeight) || !maxDialogWidth || !maxDialogHeight)
+ return;
+
+ // We don't need to recalculate grid if nothing changed
+ if (
+ this._folderAreaBox?.get_width() === folderAreaBox.get_width() &&
+ this._folderAreaBox?.get_height() === folderAreaBox.get_height() &&
+ nItems === this._nItems
+ )
+ return;
+
+ const layoutManager = view._grid.layoutManager;
+ const spacing = opt.APP_GRID_FOLDER_SPACING;
+ const padding = 40;
+
+ const titleBoxHeight =
+ Math.round(this._entryBox.get_preferred_height(-1)[1] / scaleFactor); // ~75
+ const minDialogWidth = Math.max(640,
+ Math.round(this._entryBox.get_preferred_width(-1)[1] / scaleFactor + 2 * margin));
+ const navigationArrowsSize = // padding + one arrow width is sufficient for both arrows
+ Math.round(view._nextPageArrow.get_preferred_width(-1)[1] / scaleFactor);
+ const pageIndicatorSize =
+ Math.round(Math.min(...view._pageIndicators.get_size()) / scaleFactor);// ~28;
+ const horizontalNavigation = opt.ORIENTATION ? pageIndicatorSize : navigationArrowsSize; // either add padding or arrows
+ const verticalNavigation = opt.ORIENTATION ? navigationArrowsSize : pageIndicatorSize;
+
+ // Horizontal size
+ const baseWidth = horizontalNavigation + 3 * padding + 2 * margin;
+ const maxGridPageWidth = maxDialogWidth - baseWidth;
+ // Vertical size
+ const baseHeight = titleBoxHeight + verticalNavigation + 2 * padding + 2 * margin;
+ const maxGridPageHeight = maxDialogHeight - baseHeight;
+
+ // Will be updated to the actual value later
+ let itemPadding = 55;
+ const minItemSize = 48 + itemPadding;
+
+ let columns = opt.APP_GRID_FOLDER_COLUMNS;
+ let rows = opt.APP_GRID_FOLDER_ROWS;
+ const maxColumns = columns ? columns : 100;
+ const maxRows = rows ? rows : 100;
+
+ // Find best icon size
+ let iconSize = opt.APP_GRID_FOLDER_ICON_SIZE < 0 ? opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT : opt.APP_GRID_FOLDER_ICON_SIZE;
+ if (opt.APP_GRID_FOLDER_ICON_SIZE === -1) {
+ let maxIconSize;
+ if (columns) {
+ const maxItemWidth = (maxGridPageWidth - (columns - 1) * opt.APP_GRID_FOLDER_SPACING) / columns;
+ maxIconSize = maxItemWidth - itemPadding;
+ }
+ if (rows) {
+ const maxItemHeight = (maxGridPageHeight - (rows - 1) * spacing) / rows;
+ maxIconSize = Math.min(maxItemHeight - itemPadding, maxIconSize);
+ }
+
+ if (maxIconSize) {
+ // We only need sizes from the default to the smallest
+ let iconSizes = Object.values(IconSize).sort((a, b) => b - a);
+ iconSizes = iconSizes.slice(iconSizes.indexOf(iconSize));
+ for (const size of iconSizes) {
+ iconSize = size;
+ if (iconSize <= maxIconSize)
+ break;
+ }
+ }
+ }
+
+ if ((!columns && !rows) || opt.APP_GRID_FOLDER_ICON_SIZE !== -1) {
+ columns = Math.ceil(Math.sqrt(nItems));
+ rows = columns;
+ if (columns * (columns - 1) >= nItems) {
+ rows = columns - 1;
+ } else if ((columns + 1) * (columns - 1) >= nItems) {
+ rows = columns - 1;
+ columns += 1;
+ }
+ } else if (columns && !rows) {
+ rows = Math.ceil(nItems / columns);
+ } else if (rows && !columns) {
+ columns = Math.ceil(nItems / rows);
+ }
+
+ columns = Math.clamp(columns, 1, maxColumns);
+ columns = Math.min(nItems, columns);
+ rows = Math.clamp(rows, 1, maxRows);
+
+ let itemSize = iconSize + itemPadding;
+ // First run sets the grid before we can read the real icon size
+ // so we estimate the size from default properties
+ // and correct it in the second run
+ if (this.realized) {
+ firstItem.icon.setIconSize(iconSize);
+ // Item height is inconsistent because it depends on its label height
+ const [, firstItemWidth] = firstItem.get_preferred_width(-1);
+ const realSize = firstItemWidth / scaleFactor;
+ itemSize = realSize;
+ itemPadding = realSize - iconSize;
+ }
+
+ const gridWidth = columns * (itemSize + spacing);
+ let width = gridWidth + baseWidth;
+ const gridHeight = rows * (itemSize + spacing);
+ let height = gridHeight + baseHeight;
+
+ // Folder must fit the appDisplay area plus searchEntryBin if visible
+ // reduce columns/rows if needed
+ while (height > maxDialogHeight && rows > 1) {
+ height -= itemSize + spacing;
+ rows -= 1;
+ }
+
+ while (width > maxDialogWidth && columns > 1) {
+ width -= itemSize + spacing;
+ columns -= 1;
+ }
+
+ // Try to compensate for the previous reduction if there is a space
+ while ((nItems > columns * rows) && ((width + (itemSize + spacing)) <= maxDialogWidth) && (columns < maxColumns)) {
+ width += itemSize + spacing;
+ columns += 1;
+ }
+
+ // remove columns that cannot be displayed
+ if (((columns * minItemSize + (columns - 1) * spacing)) > maxDialogWidth)
+ columns = Math.floor(maxDialogWidth / (minItemSize + spacing));
+
+ while ((nItems > columns * rows) && ((height + (itemSize + spacing)) <= maxDialogHeight) && (rows < maxRows)) {
+ height += itemSize + spacing;
+ rows += 1;
+ }
+ // remove rows that cannot be displayed
+ if ((((rows * minItemSize + (rows - 1) * spacing))) > maxDialogHeight)
+ rows = Math.floor(maxDialogWidth / (minItemSize + spacing));
+
+ // remove size for rows that are empty
+ const rowsNeeded = Math.ceil(nItems / columns);
+ if (rows > rowsNeeded) {
+ height -= (rows - rowsNeeded) * (itemSize + spacing);
+ rows -= rows - rowsNeeded;
+ }
+
+ // Remove space reserved for page controls and indicator if not used
+ if (rows * columns >= nItems) {
+ width -= horizontalNavigation;
+ height -= verticalNavigation;
+ }
+
+ width = Math.clamp(width, minDialogWidth, maxDialogWidth);
+ height = Math.min(height, maxDialogHeight);
+
+ layoutManager.columns_per_page = columns;
+ layoutManager.rows_per_page = rows;
+
+ layoutManager.fixedIconSize = iconSize;
+
+
+ // Store data for further use
+ this._width = width * scaleFactor;
+ this._height = height * scaleFactor;
+ this._folderAreaBox = folderAreaBox;
+ this._nItems = nItems;
+
+ // Set fixed dialog size to prevent size instability
+ this.child.set_size(this._width, this._height);
+ this._viewBox.set_style(`width: ${this._width - 2 * margin}px; height: ${this._height - 2 * margin}px;`);
+ this._viewBox.set_size(this._width - 2 * margin, this._height - 2 * margin);
+
+ view._redisplay();
+ },
+
+ _zoomAndFadeIn() {
+ let [sourceX, sourceY] =
+ this._source.get_transformed_position();
+ let [dialogX, dialogY] =
+ this.child.get_transformed_position();
+
+ const sourceCenterX = sourceX + this._source.width / 2;
+ const sourceCenterY = sourceY + this._source.height / 2;
+
+ // this. covers the whole screen
+ let dialogTargetX = dialogX;
+ let dialogTargetY = dialogY;
+
+ const appDisplay = this._source._parentView;
+
+ const folderAreaBox = this._getFolderAreaBox();
+
+ let folderAreaX = folderAreaBox.x1;
+ let folderAreaY = folderAreaBox.y1;
+ const folderAreaWidth = folderAreaBox.get_width();
+ const folderAreaHeight = folderAreaBox.get_height();
+ const folder = this.child;
+
+ if (opt.APP_GRID_FOLDER_CENTER) {
+ dialogTargetX = folderAreaX + folderAreaWidth / 2 - folder.width / 2;
+ dialogTargetY = folderAreaY + (folderAreaHeight / 2 - folder.height / 2) / 2;
+ } else {
+ const { pagePadding } = appDisplay._grid.layoutManager;
+ const hPadding = (pagePadding.left + pagePadding.right) / 2;
+ const vPadding = (pagePadding.top + pagePadding.bottom) / 2;
+ const minX = Math.min(folderAreaX + hPadding, folderAreaX + (folderAreaWidth - folder.width) / 2);
+ const maxX = Math.max(folderAreaX + folderAreaWidth - hPadding - folder.width, folderAreaX + folderAreaWidth / 2 - folder.width / 2);
+ const minY = Math.min(folderAreaY + vPadding, folderAreaY + (folderAreaHeight - folder.height) / 2);
+ const maxY = Math.max(folderAreaY + folderAreaHeight - vPadding - folder.height, folderAreaY + folderAreaHeight / 2 - folder.height / 2);
+
+ dialogTargetX = sourceCenterX - folder.width / 2;
+ dialogTargetX = Math.clamp(dialogTargetX, minX, maxX);
+ dialogTargetY = sourceCenterY - folder.height / 2;
+ dialogTargetY = Math.clamp(dialogTargetY, minY, maxY);
+
+ // keep the dialog in the appDisplay area
+ dialogTargetX = Math.clamp(
+ dialogTargetX,
+ folderAreaX,
+ folderAreaX + folderAreaWidth - folder.width
+ );
+
+ dialogTargetY = Math.clamp(
+ dialogTargetY,
+ folderAreaY,
+ folderAreaY + folderAreaHeight - folder.height
+ );
+ }
+
+ const dialogOffsetX = Math.round(dialogTargetX - dialogX);
+ const dialogOffsetY = Math.round(dialogTargetY - dialogY);
+
+ this.child.set({
+ translation_x: sourceX - dialogX,
+ translation_y: sourceY - dialogY,
+ scale_x: this._source.width / this.child.width,
+ scale_y: this._source.height / this.child.height,
+ opacity: 0,
+ });
+
+ this.child.ease({
+ translation_x: dialogOffsetX,
+ translation_y: dialogOffsetY,
+ scale_x: 1,
+ scale_y: 1,
+ opacity: 255,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ appDisplay.ease({
+ opacity: 0,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ if (opt.SHOW_SEARCH_ENTRY) {
+ Main.overview.searchEntry.ease({
+ opacity: 0,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+
+ this._needsZoomAndFade = false;
+
+ if (this._sourceMappedId === 0) {
+ this._sourceMappedId = this._source.connect(
+ 'notify::mapped', this._zoomAndFadeOut.bind(this));
+ }
+ },
+
+ _zoomAndFadeOut() {
+ if (!this._isOpen)
+ return;
+
+ if (!this._source.mapped) {
+ this.hide();
+ return;
+ }
+
+ // if the dialog was shown silently, skip animation
+ if (this.scale_y < 1) {
+ this._needsZoomAndFade = false;
+ this.hide();
+ this._popdownCallbacks.forEach(func => func());
+ this._popdownCallbacks = [];
+ return;
+ }
+
+ let [sourceX, sourceY] =
+ this._source.get_transformed_position();
+ let [dialogX, dialogY] =
+ this.child.get_transformed_position();
+
+ this.child.ease({
+ translation_x: sourceX - dialogX + this.child.translation_x,
+ translation_y: sourceY - dialogY + this.child.translation_y,
+ scale_x: this._source.width / this.child.width,
+ scale_y: this._source.height / this.child.height,
+ opacity: 0,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_IN_QUAD,
+ onComplete: () => {
+ this.child.set({
+ translation_x: 0,
+ translation_y: 0,
+ scale_x: 1,
+ scale_y: 1,
+ opacity: 255,
+ });
+ this.hide();
+
+ this._popdownCallbacks.forEach(func => func());
+ this._popdownCallbacks = [];
+ },
+ });
+
+ const appDisplay = this._source._parentView;
+ appDisplay.ease({
+ opacity: 255,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_IN_QUAD,
+ });
+
+ if (opt.SHOW_SEARCH_ENTRY) {
+ Main.overview.searchEntry.ease({
+ opacity: 255,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_IN_QUAD,
+ });
+ }
+
+ this._needsZoomAndFade = false;
+ },
+
+ _setLighterBackground(lighter) {
+ let opacity = 255;
+ if (this._isOpen)
+ opacity = lighter ? 20 : 0;
+
+ _appDisplay.ease({
+ opacity,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ },
+
+ vfunc_key_press_event(event) {
+ if (global.focus_manager.navigate_from_event(event))
+ return Clutter.EVENT_STOP;
+ return Clutter.EVENT_PROPAGATE;
+ },
+
+ _showFolderLabel() {
+ if (this._editButton.checked)
+ this._editButton.checked = false;
+
+ this._maybeUpdateFolderName();
+ this._switchActor(this._entry, this._folderNameLabel);
+ // This line has been added in 47 to fix focus after editing the folder name
+ this.navigate_focus(this, St.DirectionType.TAB_FORWARD, false);
+ },
+};
+
+const AppIcon = {
+ after__init() {
+ // update the app label behavior
+ this._updateMultiline();
+ },
+
+ // avoid accepting by placeholder when dragging active preview
+ // and also by icon if usage sorting is used
+ _canAccept(source) {
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+ // Folders in folder are not supported
+ if (!(_getViewFromIcon(this) instanceof AppDisplay.AppDisplay) || !this.opacity)
+ return false;
+
+ const view = /* AppDisplay.*/_getViewFromIcon(source);
+ return source !== this &&
+ (source instanceof this.constructor) &&
+ // Include drops from folders
+ // (view instanceof AppDisplay.AppDisplay &&
+ (view &&
+ !opt.APP_GRID_USAGE);
+ },
+};
+
+const AppViewItemCommon = {
+ _updateMultiline() {
+ const { label } = this.icon;
+ if (label)
+ label.opacity = 255;
+ if (!this._expandTitleOnHover || !this.icon.label)
+ return;
+
+ const { clutterText } = label;
+
+ const isHighlighted = this.has_key_focus() || this.hover || this._forcedHighlight;
+
+ if (opt.APP_GRID_NAMES_MODE === 2 && this._expandTitleOnHover) { // !_expandTitleOnHover indicates search result icon
+ label.opacity = isHighlighted || !this.app ? 255 : 0;
+ }
+ if (isHighlighted)
+ this.get_parent()?.set_child_above_sibling(this, null);
+
+ if (!opt.APP_GRID_NAMES_MODE) {
+ const layout = clutterText.get_layout();
+ if (!layout.is_wrapped() && !layout.is_ellipsized())
+ return;
+ }
+
+ label.remove_transition('allocation');
+
+ const id = label.connect('notify::allocation', () => {
+ label.restore_easing_state();
+ label.disconnect(id);
+ });
+
+ const expand = opt.APP_GRID_NAMES_MODE === 1 || this._forcedHighlight || this.hover || this.has_key_focus();
+
+ label.save_easing_state();
+ label.set_easing_duration(expand
+ ? APP_ICON_TITLE_EXPAND_TIME
+ : APP_ICON_TITLE_COLLAPSE_TIME);
+ clutterText.set({
+ line_wrap: expand,
+ line_wrap_mode: expand ? Pango.WrapMode.WORD_CHAR : Pango.WrapMode.NONE,
+ ellipsize: expand ? Pango.EllipsizeMode.NONE : Pango.EllipsizeMode.END,
+ });
+ },
+
+ // support active preview icons
+ acceptDrop(source, _actor, x) {
+ if (opt.APP_GRID_USAGE)
+ return DND.DragMotionResult.NO_DROP;
+
+ this._setHoveringByDnd(false);
+
+ if (!this._canAccept(source))
+ return false;
+
+ if (this._withinLeeways(x))
+ return false;
+
+ // added - remove app from the source folder after dnd to other folder
+ let view = /* AppDisplay.*/_getViewFromIcon(source);
+ if (view instanceof AppDisplay.FolderView)
+ view.removeApp(source.app);
+
+ return true;
+ },
+
+};
+
+const PageIndicatorsCommon = {
+ after_setNPages() {
+ this.visible = true;
+ this.opacity = this._nPages > 1 ? 255 : 0;
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/appFavorites.js b/extensions/47/vertical-workspaces/lib/appFavorites.js
new file mode 100644
index 0000000..977e65a
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/appFavorites.js
@@ -0,0 +1,79 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * appFavorites.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js';
+
+let Me;
+let opt;
+
+export const AppFavoritesModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('appFavoritesModule');
+
+ // if notifications are enabled no override is needed
+ reset = reset || !this.moduleEnabled || opt.SHOW_FAV_NOTIFICATION;
+
+ // don't touch original code if module disabled
+ if (reset && !this._firstActivation) {
+ this._disableModule();
+ } else if (!reset) {
+ this._firstActivation = false;
+ this._activateModule();
+ }
+ if (reset && this._firstActivation) {
+ this.moduleEnabled = false;
+ console.debug(' AppFavoritesModule - Keeping untouched');
+ }
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ // use actual instance instead of prototype
+ this._overrides.addOverride('AppFavorites', AppFavorites.getAppFavorites(), AppFavoritesCommon);
+
+ console.debug(' AppFavoritesModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ console.debug(' AppFavoritesModule - Deactivated');
+ }
+};
+
+const AppFavoritesCommon = {
+ addFavoriteAtPos(appId, pos) {
+ this._addFavorite(appId, pos);
+ },
+
+ removeFavorite(appId) {
+ this._removeFavorite(appId);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/dash.js b/extensions/47/vertical-workspaces/lib/dash.js
new file mode 100644
index 0000000..c7ebbff
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/dash.js
@@ -0,0 +1,1307 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * dash.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022-2024
+ * @license GPL-3.0
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Dash from 'resource:///org/gnome/shell/ui/dash.js';
+import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
+import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js';
+import * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js';
+import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js';
+import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+
+let Me;
+let opt;
+// gettext
+let _;
+
+let _moduleEnabled;
+let _timeouts;
+
+// added values to achieve a better ability to scale down according to available space
+export const BaseIconSizes = [16, 24, 32, 40, 44, 48, 56, 64, 72, 80, 96, 112, 128];
+
+const DASH_ITEM_LABEL_SHOW_TIME = 150;
+
+const shellVersion46 = !Clutter.Container; // Container has been removed in 46
+
+export const DashModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+ _ = Me.gettext;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ this._horizontalWorkId = null;
+ this._verticalWorkId = null;
+ this._showAppsIconBtnPressId = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ _ = null;
+ }
+
+ update(reset) {
+ this._removeTimeouts();
+
+ this.moduleEnabled = opt.get('dashModule');
+ const conflict = !!(Me.Util.getEnabledExtensions('dash-to-dock').length ||
+ Me.Util.getEnabledExtensions('dash2dock').length ||
+ Me.Util.getEnabledExtensions('ubuntu-dock').length ||
+ Me.Util.getEnabledExtensions('dash-to-panel').length);
+
+ if (conflict && !reset)
+ console.warn(`[${Me.metadata.name}] Warning: "Dash" module disabled due to potential conflict with another extension`);
+
+ reset = reset || !this.moduleEnabled || conflict;
+ this._conflict = 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(' DashModule - Keeping untouched');
+ }
+
+ updateStyle(dash) {
+ if (opt.DASH_BG_LIGHT)
+ dash._background.add_style_class_name('dash-background-light');
+ else
+ dash._background.remove_style_class_name('dash-background-light');
+
+ dash._background.opacity = opt.DASH_BG_OPACITY;
+ let radius = opt.DASH_BG_RADIUS;
+ if (radius) {
+ let style;
+ switch (opt.DASH_POSITION) {
+ case 1:
+ style = opt.DASH_BG_GS3_STYLE ? `border-radius: ${radius}px 0 0 ${radius}px;` : `border-radius: ${radius}px;`;
+ break;
+ case 3:
+ style = opt.DASH_BG_GS3_STYLE ? `border-radius: 0 ${radius}px ${radius}px 0;` : `border-radius: ${radius}px;`;
+ break;
+ default:
+ style = `border-radius: ${radius}px;`;
+ }
+ dash._background.set_style(style);
+ } else {
+ dash._background.set_style('');
+ }
+ }
+
+ _activateModule() {
+ _moduleEnabled = true;
+ _timeouts = {};
+ const dash = Main.overview._overview._controls.layoutManager._dash;
+
+ if (!this._originalWorkId)
+ this._originalWorkId = dash._workId;
+
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._resetStyle(dash);
+ this.updateStyle(dash);
+
+ this._overrides.addOverride('DashItemContainer', Dash.DashItemContainer.prototype, DashItemContainerCommon);
+ this._overrides.addOverride('DashCommon', Dash.Dash.prototype, DashCommon);
+ this._overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIconCommon);
+ this._overrides.addOverride('DashIcon', Dash.DashIcon.prototype, DashIconCommon);
+ this._overrides.addOverride('AppMenu', AppMenu.AppMenu.prototype, AppMenuCommon);
+
+ if (shellVersion46)
+ dash.add_style_class_name('dash-46');
+
+ if (opt.DASH_VERTICAL) {
+ // this._overrides.addOverride('Dash', Dash.Dash.prototype, DashVerticalOverride);
+ dash.add_style_class_name(shellVersion46
+ ? 'vertical-46'
+ : 'vertical'
+ );
+
+ this._setOrientation(Clutter.Orientation.VERTICAL);
+ } else {
+ this._setOrientation(Clutter.Orientation.HORIZONTAL);
+ }
+
+ if (opt.DASH_VERTICAL && opt.DASH_BG_GS3_STYLE) {
+ if (opt.DASH_LEFT) {
+ dash.add_style_class_name(shellVersion46
+ ? 'vertical-46-gs3-left'
+ : 'vertical-gs3-left');
+ } else if (opt.DASH_RIGHT) {
+ dash.add_style_class_name(shellVersion46
+ ? 'vertical-46-gs3-right'
+ : 'vertical-gs3-right');
+ }
+ } else {
+ dash.remove_style_class_name('vertical-gs3-left');
+ dash.remove_style_class_name('vertical-gs3-right');
+ dash.remove_style_class_name('vertical-46-gs3-left');
+ dash.remove_style_class_name('vertical-46-gs3-right');
+ }
+
+ if (!this._customWorkId)
+ this._customWorkId = Main.initializeDeferredWork(dash._box, dash._redisplay.bind(dash));
+ dash._workId = this._customWorkId;
+
+ this._moveDashAppGridIcon();
+ this._connectShowAppsIcon();
+
+ dash.visible = opt.DASH_VISIBLE;
+ // dash._background.add_style_class_name('dash-background-reduced');
+ dash._queueRedisplay();
+
+ if (opt.DASH_ISOLATE_WS && !this._wmSwitchWsConId) {
+ this._wmSwitchWsConId = global.windowManager.connect('switch-workspace', () => dash._queueRedisplay());
+ this._newWindowConId = global.display.connect_after('window-created', () => dash._queueRedisplay());
+ }
+
+ console.debug(' DashModule - Activated');
+ }
+
+ _disableModule() {
+ const dash = Main.overview._overview._controls.layoutManager._dash;
+
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ dash._workId = this._originalWorkId;
+
+ if (this._wmSwitchWsConId) {
+ global.windowManager.disconnect(this._wmSwitchWsConId);
+ this._wmSwitchWsConId = 0;
+ }
+ if (this._newWindowConId) {
+ global.windowManager.disconnect(this._newWindowConId);
+ this._newWindowConId = 0;
+ }
+
+ const reset = true;
+ this._setOrientation(Clutter.Orientation.HORIZONTAL);
+ this._moveDashAppGridIcon(reset);
+ this._connectShowAppsIcon(reset);
+
+ this._resetStyle(dash);
+ dash.visible = !this._conflict;
+ dash._background.opacity = 255;
+
+ _moduleEnabled = false;
+ console.debug(' DashModule - Disabled');
+ }
+
+ _resetStyle(dash) {
+ dash.remove_style_class_name('dash-46');
+ dash.remove_style_class_name('vertical');
+ dash.remove_style_class_name('vertical-46');
+ dash.remove_style_class_name('vertical-gs3-left');
+ dash.remove_style_class_name('vertical-gs3-right');
+ dash.remove_style_class_name('vertical-46-gs3-left');
+ dash.remove_style_class_name('vertical-46-gs3-right');
+ dash.remove_style_class_name('vertical-left');
+ dash.remove_style_class_name('vertical-right');
+ dash._background.remove_style_class_name('dash-background-light');
+ dash._background.remove_style_class_name('dash-background-reduced');
+ dash._background.set_style('');
+ }
+
+ _removeTimeouts() {
+ if (_timeouts) {
+ Object.values(_timeouts).forEach(t => {
+ if (t)
+ GLib.source_remove(t);
+ });
+ _timeouts = null;
+ }
+ }
+
+ _setOrientation(orientation, dash) {
+ dash = dash ?? Main.overview._overview._controls.layoutManager._dash;
+
+ dash._box.layout_manager.orientation = orientation;
+ dash._dashContainer.layout_manager.orientation = orientation;
+ dash._dashContainer.y_expand = !orientation;
+ dash._dashContainer.x_expand = !!orientation;
+ dash.x_align = orientation ? Clutter.ActorAlign.START : Clutter.ActorAlign.CENTER;
+ dash.y_align = orientation ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.FILL;
+
+ let sizerBox = dash._background.get_children()[0];
+ sizerBox.clear_constraints();
+ sizerBox.add_constraint(new Clutter.BindConstraint({
+ source: dash._showAppsIcon.icon,
+ coordinate: orientation ? Clutter.BindCoordinate.WIDTH : Clutter.BindCoordinate.HEIGHT,
+ }));
+ sizerBox.add_constraint(new Clutter.BindConstraint({
+ source: dash._dashContainer,
+ coordinate: orientation ? Clutter.BindCoordinate.HEIGHT : Clutter.BindCoordinate.WIDTH,
+ }));
+ dash._box.remove_all_children();
+ dash._separator = null;
+ dash._queueRedisplay();
+ dash._adjustIconSize();
+ }
+
+ _moveDashAppGridIcon(reset = false) {
+ // move dash app grid icon to the front
+ const dash = Main.overview._overview._controls.layoutManager._dash;
+
+ const appIconPosition = opt.get('showAppsIconPosition');
+ dash._showAppsIcon.remove_style_class_name('show-apps-icon-vertical-hide');
+ dash._showAppsIcon.remove_style_class_name('show-apps-icon-horizontal-hide');
+ dash._showAppsIcon.opacity = 255;
+ if (!reset && appIconPosition === 0) // 0 - start
+ dash._dashContainer.set_child_at_index(dash._showAppsIcon, 0);
+ if (reset || appIconPosition === 1) { // 1 - end
+ const index = dash._dashContainer.get_children().length - 1;
+ dash._dashContainer.set_child_at_index(dash._showAppsIcon, index);
+ }
+ if (!reset && appIconPosition === 2) { // 2 - hide
+ const style = opt.DASH_VERTICAL ? 'show-apps-icon-vertical-hide' : 'show-apps-icon-horizontal-hide';
+ dash._showAppsIcon.add_style_class_name(style);
+ // for some reason even if the icon height in vertical mode should be set to 0 by the style, it stays visible in full size returning height 1px
+ dash._showAppsIcon.opacity = 0;
+ }
+ }
+
+ _connectShowAppsIcon(reset = false, dash) {
+ dash = dash ?? Main.overview._overview._controls.layoutManager._dash;
+ if (!reset) {
+ if (this._showAppsIconBtnPressId || Me.Util.dashIsDashToDock()) {
+ // button is already connected || dash is Dash to Dock
+ return;
+ }
+ dash._showAppsIcon.reactive = true;
+ this._showAppsIconBtnPressId = dash._showAppsIcon.connect('button-press-event', (actor, event) => {
+ const button = event.get_button();
+ if (button === Clutter.BUTTON_MIDDLE)
+ Me.Util.openPreferences();
+ else if (button === Clutter.BUTTON_SECONDARY)
+ Me.Util.activateSearchProvider(Me.WSP_PREFIX);
+ else
+ return Clutter.EVENT_PROPAGATE;
+ return Clutter.EVENT_STOP;
+ });
+ } else if (this._showAppsIconBtnPressId) {
+ dash._showAppsIcon.disconnect(this._showAppsIconBtnPressId);
+ this._showAppsIconBtnPressId = 0;
+ dash._showAppsIcon.reactive = false;
+ }
+ }
+};
+
+function getAppFromSource(source) {
+ if (source instanceof AppDisplay.AppIcon)
+ return source.app;
+ else
+ return null;
+}
+
+const DashItemContainerCommon = {
+ // move labels according dash position
+ showLabel() {
+ if (!this._labelText)
+ return;
+
+ const windows = this.child.app?.get_windows();
+ const recentWindowTitle = windows && windows.length ? windows[0].get_title() : '';
+ const windowCount = this.child.app?.get_windows().length;
+ let labelSuffix = '';
+ if (windowCount > 1)
+ labelSuffix = ` (${windowCount})`;
+ if (recentWindowTitle && recentWindowTitle !== this._labelText)
+ labelSuffix += `\n ${recentWindowTitle}`;
+
+ this.label.set_text(this._labelText + labelSuffix);
+
+ this.label.opacity = 0;
+ this.label.show();
+
+ let [stageX, stageY] = this.get_transformed_position();
+
+ const itemWidth = this.allocation.get_width();
+ const itemHeight = this.allocation.get_height();
+
+ const labelWidth = this.label.get_width();
+ const labelHeight = this.label.get_height();
+ let xOffset = Math.floor((itemWidth - labelWidth) / 2);
+ let x = Math.clamp(stageX + xOffset, 0, global.stage.width - labelWidth);
+ const primaryMonitor = global.display.get_monitor_geometry(global.display.get_primary_monitor());
+ x = Math.clamp(x, primaryMonitor.x, primaryMonitor.x + primaryMonitor.width - labelWidth);
+
+ let node = this.label.get_theme_node();
+ let y;
+
+ if (opt.DASH_TOP) {
+ const yOffset = itemHeight + (shellVersion46 ? 0 : -3);
+ y = stageY + yOffset;
+ } else if (opt.DASH_BOTTOM) {
+ const yOffset = node.get_length('-y-offset');
+ y = stageY - this.label.height - yOffset;
+ } else if (opt.DASH_RIGHT) {
+ const yOffset = Math.floor((itemHeight - labelHeight) / 2);
+ xOffset = shellVersion46 ? 8 : 4;
+
+ x = stageX - xOffset - this.label.width;
+ y = Math.clamp(stageY + yOffset, 0, global.stage.height - labelHeight);
+ } else if (opt.DASH_LEFT) {
+ const yOffset = Math.floor((itemHeight - labelHeight) / 2);
+ xOffset = shellVersion46 ? 8 : 4;
+
+ x = stageX + this.width + xOffset;
+ y = Math.clamp(stageY + yOffset, 0, global.stage.height - labelHeight);
+ }
+
+ this.label.set_position(x, y);
+ this.label.ease({
+ opacity: 255,
+ duration: DASH_ITEM_LABEL_SHOW_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ this.label.set_position(x, y);
+ this.label.ease({
+ opacity: 255,
+ duration: DASH_ITEM_LABEL_SHOW_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ },
+};
+
+const DashCommon = {
+ _redisplay() {
+ // After disabling V-Shell queueRedisplay() may call this function
+ // In that case redirect the call to the current _redisplay()
+ if (!_moduleEnabled) {
+ this._redisplay();
+ return;
+ }
+
+ let favorites = AppFavorites.getAppFavorites().getFavoriteMap();
+
+ let running = this._appSystem.get_running();
+
+ if (opt.DASH_ISOLATE_WS) {
+ const currentWs = global.workspace_manager.get_active_workspace();
+ running = running.filter(app => {
+ return app.get_windows().filter(w => w.get_workspace() === currentWs).length;
+ });
+ this._box.get_children().forEach(a => a.child?._updateRunningStyle());
+ }
+
+ let children = this._box.get_children().filter(actor => {
+ return actor.child &&
+ actor.child._delegate &&
+ actor.child._delegate.app;
+ });
+ // Apps currently in the dash
+ let oldApps = children.map(actor => actor.child._delegate.app);
+ // Apps supposed to be in the dash
+ let newApps = [];
+
+ for (let id in favorites)
+ newApps.push(favorites[id]);
+
+ for (let i = 0; i < running.length; i++) {
+ let app = running[i];
+ if (app.get_id() in favorites)
+ continue;
+ newApps.push(app);
+ }
+
+ // Figure out the actual changes to the list of items; we iterate
+ // over both the list of items currently in the dash and the list
+ // of items expected there, and collect additions and removals.
+ // Moves are both an addition and a removal, where the order of
+ // the operations depends on whether we encounter the position
+ // where the item has been added first or the one from where it
+ // was removed.
+ // There is an assumption that only one item is moved at a given
+ // time; when moving several items at once, everything will still
+ // end up at the right position, but there might be additional
+ // additions/removals (e.g. it might remove all the launchers
+ // and add them back in the new order even if a smaller set of
+ // additions and removals is possible).
+ // If above assumptions turns out to be a problem, we might need
+ // to use a more sophisticated algorithm, e.g. Longest Common
+ // Subsequence as used by diff.
+ let addedItems = [];
+ let removedActors = [];
+
+ let newIndex = 0;
+ let oldIndex = 0;
+ while (newIndex < newApps.length || oldIndex < oldApps.length) {
+ let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null;
+ let newApp = newApps.length > newIndex ? newApps[newIndex] : null;
+
+ // No change at oldIndex/newIndex
+ if (oldApp === newApp) {
+ oldIndex++;
+ newIndex++;
+ continue;
+ }
+
+ // App removed at oldIndex
+ if (oldApp && !newApps.includes(oldApp)) {
+ removedActors.push(children[oldIndex]);
+ oldIndex++;
+ continue;
+ }
+
+ // App added at newIndex
+ if (newApp && !oldApps.includes(newApp)) {
+ addedItems.push({
+ app: newApp,
+ item: this._createAppItem(newApp),
+ pos: newIndex,
+ });
+ newIndex++;
+ continue;
+ }
+
+ // App moved
+ let nextApp = newApps.length > newIndex + 1
+ ? newApps[newIndex + 1] : null;
+ let insertHere = nextApp && nextApp === oldApp;
+ let alreadyRemoved = removedActors.reduce((result, actor) => {
+ let removedApp = actor.child._delegate.app;
+ return result || removedApp === newApp;
+ }, false);
+
+ if (insertHere || alreadyRemoved) {
+ let newItem = this._createAppItem(newApp);
+ addedItems.push({
+ app: newApp,
+ item: newItem,
+ pos: newIndex + removedActors.length,
+ });
+ newIndex++;
+ } else {
+ removedActors.push(children[oldIndex]);
+ oldIndex++;
+ }
+ }
+
+ for (let i = 0; i < addedItems.length; i++) {
+ this._box.insert_child_at_index(
+ addedItems[i].item,
+ addedItems[i].pos);
+ }
+
+ for (let i = 0; i < removedActors.length; i++) {
+ let item = removedActors[i];
+
+ // Don't animate item removal when the overview is transitioning
+ // or hidden
+ if (Main.overview.visible && !Main.overview.animationInProgress)
+ item.animateOutAndDestroy();
+ else
+ item.destroy();
+ }
+
+ this._adjustIconSize();
+
+ // Skip animations on first run when adding the initial set
+ // of items, to avoid all items zooming in at once
+
+ let animate = this._shownInitially && Main.overview.visible &&
+ !Main.overview.animationInProgress;
+
+ if (!this._shownInitially)
+ this._shownInitially = true;
+
+ for (let i = 0; i < addedItems.length; i++)
+ addedItems[i].item.show(animate);
+
+ // Update separator
+ const nFavorites = Object.keys(favorites).length;
+ const nIcons = children.length + addedItems.length - removedActors.length;
+ if (nFavorites > 0 && nFavorites < nIcons) {
+ // destroy the horizontal separator if it exists.
+ // this is incredibly janky, but I can't think of a better way atm.
+ if (this._separator && this._separator.height !== 1) {
+ this._separator.destroy();
+ this._separator = null;
+ }
+
+ if (!this._separator) {
+ this._separator = new St.Widget({
+ style_class: 'dash-separator',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ width: opt.DASH_VERTICAL ? this.iconSize : 1,
+ height: opt.DASH_VERTICAL ? 1 : this.iconSize,
+ });
+ this._box.add_child(this._separator);
+ }
+
+ // FIXME: separator placement is broken (also in original dash)
+ let pos = nFavorites + this._animatingPlaceholdersCount;
+ if (this._dragPlaceholder)
+ pos++;
+ this._box.set_child_at_index(this._separator, pos);
+ } else if (this._separator) {
+ this._separator.destroy();
+ this._separator = null;
+ }
+ // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744
+ // Without it, StBoxLayout may use a stale size cache
+ this._box.queue_relayout();
+ },
+
+ _createAppItem(app) {
+ let appIcon = new Dash.DashIcon(app);
+
+ let indicator = appIcon._dot;
+ if (opt.DASH_VERTICAL) {
+ indicator.x_align = opt.DASH_LEFT ? Clutter.ActorAlign.START : Clutter.ActorAlign.END;
+ indicator.y_align = Clutter.ActorAlign.CENTER;
+ } else {
+ indicator.x_align = Clutter.ActorAlign.CENTER;
+ indicator.y_align = Clutter.ActorAlign.END;
+ }
+
+ appIcon.connect('menu-state-changed',
+ (o, opened) => {
+ this._itemMenuStateChanged(item, opened);
+ });
+
+ let item = new Dash.DashItemContainer();
+ item.setChild(appIcon);
+
+ // Override default AppIcon label_actor, now the
+ // accessible_name is set at DashItemContainer.setLabelText
+ appIcon.label_actor = null;
+ item.setLabelText(app.get_name());
+
+ appIcon.icon.setIconSize(this.iconSize);
+ this._hookUpLabel(item, appIcon);
+
+ return item;
+ },
+
+ // use custom BaseIconSizes and add support for custom icons
+ _adjustIconSize() {
+ // if a user launches multiple apps at once, this function may be called again before the previous call has finished
+ // as a result, new icons will not reach their full size, or will be missing, if adding a new icon and changing the dash size due to lack of space at the same time
+ if (this._adjustingInProgress)
+ return;
+
+ // For the icon size, we only consider children which are "proper"
+ // icons (i.e. ignoring drag placeholders) and which are not
+ // animating out (which means they will be destroyed at the end of
+ // the animation)
+ let iconChildren = this._box.get_children().filter(actor => {
+ return actor.child &&
+ actor.child._delegate &&
+ actor.child._delegate.icon &&
+ !actor.animatingOut;
+ });
+
+ // add new custom icons to the list
+ if (this._showAppsIcon.visible)
+ iconChildren.push(this._showAppsIcon);
+
+
+ // showWindowsIcon and extensionsIcon can be provided by the WSP and ESP extensions
+ if (this._showWindowsIcon)
+ iconChildren.push(this._showWindowsIcon);
+
+ if (this._extensionsIcon)
+ iconChildren.push(this._extensionsIcon);
+
+
+ if (!iconChildren.length)
+ return;
+
+ if (this._maxWidth === -1 || this._maxHeight === -1)
+ return;
+
+ const dashHorizontal = !opt.DASH_VERTICAL;
+
+ const themeNode = this.get_theme_node();
+ const maxAllocation = new Clutter.ActorBox({
+ x1: 0,
+ y1: 0,
+ x2: dashHorizontal ? this._maxWidth : 42, // not whatever
+ y2: dashHorizontal ? 42 : this._maxHeight,
+ });
+
+ let maxContent = themeNode.get_content_box(maxAllocation);
+
+ let spacing = themeNode.get_length('spacing');
+
+ let firstButton = iconChildren[0].child;
+ let firstIcon = firstButton._delegate.icon;
+
+ if (!firstIcon.icon)
+ return;
+
+ // Enforce valid spacings during the size request
+ firstIcon.icon.ensure_style();
+ const [, , iconWidth, iconHeight] = firstIcon.icon.get_preferred_size();
+ const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size();
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+
+ let maxIconSize = opt.MAX_ICON_SIZE;
+ if (!maxIconSize) {
+ maxIconSize = Me.Util.monitorHasLowResolution()
+ ? 48
+ : 64;
+ }
+
+ let availWidth, availHeight;
+ if (dashHorizontal) {
+ availWidth = maxContent.x2 - maxContent.x1;
+ // Subtract icon padding and box spacing from the available width
+ availWidth -= iconChildren.length * (buttonWidth - iconWidth) +
+ (iconChildren.length - 1) * spacing +
+ 2 * this._background.get_theme_node().get_horizontal_padding();
+
+ availHeight = this._maxHeight;
+ availHeight -= this.margin_top + this.margin_bottom;
+ availHeight -= this._background.get_theme_node().get_vertical_padding();
+ availHeight -= themeNode.get_vertical_padding();
+ availHeight -= buttonHeight - iconHeight;
+
+ maxIconSize = Math.min(availWidth / iconChildren.length, availHeight, maxIconSize * scaleFactor);
+ } else {
+ availWidth = this._maxWidth;
+ availWidth -= this._background.get_theme_node().get_horizontal_padding();
+ availWidth -= themeNode.get_horizontal_padding();
+ availWidth -= buttonWidth - iconWidth;
+
+ availHeight = maxContent.y2 - maxContent.y1;
+ availHeight -= iconChildren.length * (buttonHeight - iconHeight) +
+ (iconChildren.length - 1) * spacing +
+ 2 * this._background.get_theme_node().get_vertical_padding();
+
+ maxIconSize = Math.min(availWidth, availHeight / iconChildren.length, maxIconSize * scaleFactor);
+ }
+
+ let iconSizes = BaseIconSizes.map(s => s * scaleFactor);
+
+ let newIconSize = BaseIconSizes[0];
+ for (let i = 0; i < iconSizes.length; i++) {
+ if (iconSizes[i] <= maxIconSize)
+ newIconSize = BaseIconSizes[i];
+ }
+
+ if (newIconSize === this.iconSize)
+ return;
+
+ // set the in-progress state here after all the possible cancels
+ this._adjustingInProgress = true;
+
+ let oldIconSize = this.iconSize;
+ this.iconSize = newIconSize;
+ this.emit('icon-size-changed');
+
+ let scale = oldIconSize / newIconSize;
+ for (let i = 0; i < iconChildren.length; i++) {
+ let icon = iconChildren[i].child._delegate.icon;
+
+ // Set the new size immediately, to keep the icons' sizes
+ // in sync with this.iconSize
+ icon.setIconSize(this.iconSize);
+
+ // Don't animate the icon size change when the overview
+ // is transitioning, not visible or when initially filling
+ // the dash
+ if (!Main.overview.visible || Main.overview.animationInProgress ||
+ !this._shownInitially)
+ continue;
+
+ let [targetWidth, targetHeight] = icon.icon.get_size();
+
+ // Scale the icon's texture to the previous size and
+ // tween to the new size
+ icon.icon.set_size(icon.icon.width * scale,
+ icon.icon.height * scale);
+
+ icon.icon.ease({
+ width: targetWidth,
+ height: targetHeight,
+ duration: Dash.DASH_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+
+ if (this._separator) {
+ this._separator.ease({
+ width: dashHorizontal ? 1 : this.iconSize,
+ height: dashHorizontal ? this.iconSize : 1,
+ duration: Dash.DASH_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+
+ this._adjustingInProgress = false;
+ },
+
+ handleDragOver(source, actor, x, y, _time) {
+ let app = getAppFromSource(source);
+
+ // Don't allow favoriting of transient apps
+ if (app === null || app.is_window_backed())
+ return DND.DragMotionResult.NO_DROP;
+ if (!global.settings.is_writable('favorite-apps'))
+ return DND.DragMotionResult.NO_DROP;
+ let favorites = AppFavorites.getAppFavorites().getFavorites();
+ let numFavorites = favorites.length;
+
+ let favPos = favorites.indexOf(app);
+
+ let children = this._box.get_children();
+ let numChildren = children.length;
+ let boxSize = opt.DASH_VERTICAL ? this._box.height : this._box.width;
+
+ // Keep the placeholder out of the index calculation; assuming that
+ // the remove target has the same size as "normal" items, we don't
+ // need to do the same adjustment there.
+ if (this._dragPlaceholder) {
+ boxSize -= opt.DASH_VERTICAL ? this._dragPlaceholder.height : this._dragPlaceholder.width;
+ numChildren--;
+ }
+
+ // Same with the separator
+ if (this._separator) {
+ boxSize -= opt.DASH_VERTICAL ? this._separator.height : this._separator.width;
+ numChildren--;
+ }
+
+ let pos;
+ if (this._emptyDropTarget)
+ pos = 0; // always insert at the start when dash is empty
+ else if (this.text_direction === Clutter.TextDirection.RTL)
+ pos = numChildren - Math.floor((opt.DASH_VERTICAL ? y : x) * numChildren / boxSize);
+ else
+ pos = Math.floor((opt.DASH_VERTICAL ? y : x) * numChildren / boxSize);
+
+ // Put the placeholder after the last favorite if we are not
+ // in the favorites zone
+ if (pos > numFavorites)
+ pos = numFavorites;
+
+ if (pos !== this._dragPlaceholderPos && this._animatingPlaceholdersCount === 0) {
+ this._dragPlaceholderPos = pos;
+
+ // Don't allow positioning before or after self
+ if (favPos !== -1 && (pos === favPos || pos === favPos + 1)) {
+ this._clearDragPlaceholder();
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ // If the placeholder already exists, we just move
+ // it, but if we are adding it, expand its size in
+ // an animation
+ let fadeIn;
+ if (this._dragPlaceholder) {
+ this._dragPlaceholder.destroy();
+ fadeIn = false;
+ } else {
+ fadeIn = true;
+ }
+
+ // this._dragPlaceholder = new Dash.DragPlaceholderItem(); // not exported in 45
+ this._dragPlaceholder = new Dash.DashItemContainer();
+ this._dragPlaceholder.setChild(new St.Bin({ style_class: 'placeholder' }));
+ this._dragPlaceholder.child.set_width(this.iconSize / (opt.DASH_VERTICAL ? 2 : 1));
+ this._dragPlaceholder.child.set_height(this.iconSize / (opt.DASH_VERTICAL ? 1 : 2));
+ this._box.insert_child_at_index(
+ this._dragPlaceholder,
+ this._dragPlaceholderPos);
+ this._dragPlaceholder.show(fadeIn);
+ }
+
+ if (!this._dragPlaceholder)
+ return DND.DragMotionResult.NO_DROP;
+
+ let srcIsFavorite = favPos !== -1;
+
+ if (srcIsFavorite)
+ return DND.DragMotionResult.MOVE_DROP;
+
+ return DND.DragMotionResult.COPY_DROP;
+ },
+};
+
+const AppIconCommon = {
+ after__init() {
+ if (this._updateRunningDotStyle)
+ this._updateRunningDotStyle();
+ },
+
+ _updateRunningDotStyle() {
+ if (opt.RUNNING_DOT_STYLE)
+ this._dot.add_style_class_name('app-grid-running-dot-custom');
+ else
+ this._dot.remove_style_class_name('app-grid-running-dot-custom');
+ },
+
+ activate(button) {
+ const event = Clutter.get_current_event();
+ const state = event ? event.get_state() : 0;
+ const isMiddleButton = button && button === Clutter.BUTTON_MIDDLE;
+ const isCtrlPressed = Me.Util.isCtrlPressed(state);
+ const isShiftPressed = Me.Util.isShiftPressed(state);
+
+ const currentWS = global.workspace_manager.get_active_workspace();
+ const appRecentWorkspace = this._getAppRecentWorkspace(this.app);
+ // this feature shouldn't affect search results, dash icons don't have labels, so we use them as a condition
+ const showWidowsBeforeActivation = opt.DASH_CLICK_ACTION === 1 && !this.icon.label;
+
+ let targetWindowOnCurrentWs = false;
+ if (opt.DASH_FOLLOW_RECENT_WIN) {
+ targetWindowOnCurrentWs = appRecentWorkspace === currentWS;
+ } else {
+ this.app.get_windows().forEach(
+ w => {
+ targetWindowOnCurrentWs = targetWindowOnCurrentWs || (w.get_workspace() === currentWS);
+ }
+ );
+ }
+
+ const openNewWindow = this.app.can_open_new_window() &&
+ this.app.state === Shell.AppState.RUNNING &&
+ (((isCtrlPressed || isMiddleButton) && !opt.DASH_CLICK_OPEN_NEW_WIN) ||
+ (opt.DASH_CLICK_OPEN_NEW_WIN && !this._selectedMetaWin && !isMiddleButton) ||
+ ((opt.DASH_CLICK_PREFER_WORKSPACE || opt.DASH_ISOLATE_WS) && !targetWindowOnCurrentWs));
+
+ if ((this.app.state === Shell.AppState.STOPPED || openNewWindow) && !isShiftPressed)
+ this.animateLaunch();
+
+ if (openNewWindow) {
+ this.app.open_new_window(-1);
+ // if DASH_CLICK_ACTION == "SHOW_WINS_BEFORE", the app has more than one window and has no window on the current workspace,
+ // don't activate the app immediately, only move the overview to the workspace with the app's recent window
+ } else if (showWidowsBeforeActivation && !isShiftPressed && this.app.get_n_windows() > 1 && !targetWindowOnCurrentWs/* && !(opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE)*/) {
+
+ Main.wm.actionMoveWorkspace(appRecentWorkspace);
+ Main.overview.dash.showAppsButton.checked = false;
+ return;
+ } else if (this._selectedMetaWin) {
+ this._selectedMetaWin.activate(global.get_current_time());
+ } else if (showWidowsBeforeActivation && opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && !isShiftPressed && this.app.get_n_windows() > 1) {
+ // expose windows
+ Main.overview._overview._controls._thumbnailsBox._activateThumbnailAtPoint(0, 0, global.get_current_time(), true);
+ return;
+ } else if (((opt.DASH_SHIFT_CLICK_MV && isShiftPressed) || ((opt.DASH_CLICK_PREFER_WORKSPACE || opt.DASH_ISOLATE_WS) && !openNewWindow)) && this.app.get_windows().length) {
+ this._moveAppToCurrentWorkspace();
+ if (opt.DASH_ISOLATE_WS) {
+ this.app.activate();
+ // hide the overview after the window is re-created
+ GLib.idle_add(GLib.PRIORITY_LOW, () => Main.overview.hide());
+ }
+ return;
+ } else if (isShiftPressed) {
+ return;
+ } else {
+ this.app.activate();
+ }
+
+ Main.overview.hide();
+ },
+
+ _moveAppToCurrentWorkspace() {
+ this.app.get_windows().forEach(w => w.change_workspace(global.workspace_manager.get_active_workspace()));
+ },
+
+ popupMenu(side = St.Side.LEFT) {
+ this.setForcedHighlight(true);
+ this._removeMenuTimeout();
+ this.fake_release();
+
+ if (!this._getWindowsOnCurrentWs) {
+ this._getWindowsOnCurrentWs = function () {
+ const winList = [];
+ this.app.get_windows().forEach(w => {
+ if (w.get_workspace() === global.workspace_manager.get_active_workspace())
+ winList.push(w);
+ });
+ return winList;
+ };
+
+ this._windowsOnOtherWs = function () {
+ return (this.app.get_windows().length - this._getWindowsOnCurrentWs().length) > 0;
+ };
+ }
+
+ if (!this._menu) {
+ this._menu = new AppMenu.AppMenu(this, side, {
+ favoritesSection: true,
+ showSingleWindows: true,
+ });
+
+ this._menu.setApp(this.app);
+ this._openSigId = this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
+ if (!isPoppedUp)
+ this._onMenuPoppedDown();
+ });
+ // Main.overview.connectObject('hiding',
+ this._hidingSigId = Main.overview.connect('hiding',
+ () => this._menu.close(), this);
+
+ Main.uiGroup.add_child(this._menu.actor);
+ this._menuManager.addMenu(this._menu);
+ }
+
+ // once the menu is created, it stays unchanged and we need to modify our items based on current situation
+ if (this._addedMenuItems && this._addedMenuItems.length)
+ this._addedMenuItems.forEach(i => i.destroy());
+
+
+ const popupItems = [];
+
+ const separator = new PopupMenu.PopupSeparatorMenuItem();
+ this._menu.addMenuItem(separator);
+
+ if (this.app.get_n_windows()) {
+ // if (/* opt.APP_MENU_FORCE_QUIT*/true) {}
+ popupItems.push([_('Force Quit'), () => {
+ this.app.get_windows()[0].kill();
+ }]);
+
+ // if (opt.APP_MENU_CLOSE_WS) {}
+ const nWin = this._getWindowsOnCurrentWs().length;
+ if (nWin) {
+ popupItems.push([_(`Close ${nWin} Windows on Current Workspace`), () => {
+ const windows = this._getWindowsOnCurrentWs();
+ let time = global.get_current_time();
+ for (let win of windows) {
+ // increase time by 1 ms for each window to avoid errors from GS
+ win.delete(time++);
+ }
+ }]);
+ }
+
+ popupItems.push([_('Move App to Current Workspace ( Shift + Click )'), this._moveAppToCurrentWorkspace]);
+ // WTMB (Windows Thumbnails) extension required
+ if (global.windowThumbnails) {
+ popupItems.push([_('Create Window Thumbnail/PiP'), () => {
+ global.windowThumbnails?.createThumbnail(this.app.get_windows()[0]);
+ }]);
+ }
+ }
+
+ this._addedMenuItems = [];
+ this._addedMenuItems.push(separator);
+ popupItems.forEach(i => {
+ let item = new PopupMenu.PopupMenuItem(i[0]);
+ this._menu.addMenuItem(item);
+ item.connect('activate', i[1].bind(this));
+ if (i[1] === this._moveAppToCurrentWorkspace && !this._windowsOnOtherWs())
+ item.setSensitive(false);
+ this._addedMenuItems.push(item);
+ });
+
+ this.emit('menu-state-changed', true);
+
+ this._menu.open(BoxPointer.PopupAnimation.FULL);
+ this._menuManager.ignoreRelease();
+ this.emit('sync-tooltip');
+
+ return false;
+ },
+
+ _getWindowApp(metaWin) {
+ const tracker = Shell.WindowTracker.get_default();
+ return tracker.get_window_app(metaWin);
+ },
+
+ _getAppLastUsedWindow(app) {
+ let recentWin;
+ global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => {
+ const winApp = this._getWindowApp(metaWin);
+ if (!recentWin && winApp === app)
+ recentWin = metaWin;
+ });
+ return recentWin;
+ },
+
+ _getAppRecentWorkspace(app) {
+ const recentWin = this._getAppLastUsedWindow(app);
+ if (recentWin)
+ return recentWin.get_workspace();
+
+ return null;
+ },
+};
+
+const DashIconCommon = {
+ after__init() {
+ if (opt.DASH_ICON_SCROLL && !Me.Util.dashNotDefault()) {
+ this._scrollConId = this.connect('scroll-event', DashExtensions.onScrollEvent.bind(this));
+ this._leaveConId = this.connect('leave-event', DashExtensions.onLeaveEvent.bind(this));
+ }
+ },
+
+ popupMenu() {
+ const side = opt.DASH_VERTICAL ? St.Side.LEFT : St.Side.BOTTOM;
+ AppIconCommon.popupMenu.bind(this)(side);
+ },
+
+ _updateRunningDotStyle() {
+ if (opt.RUNNING_DOT_STYLE)
+ this._dot.add_style_class_name('app-grid-running-dot-custom');
+ else
+ this._dot.remove_style_class_name('app-grid-running-dot-custom');
+
+ this._dot.translation_x = 0;
+ // _updateDotStyle() has been added in GS 46.2 to apply translation_y value from the CSS on style change
+ if (shellVersion46 && !this._updateDotStyle && !opt.DASH_VERTICAL)
+ this._dot.translation_y = 8;
+
+ // GS 46.0 (Ubuntu) only
+ if (opt.DASH_VERTICAL)
+ this._dot.translationY = 0;
+ },
+
+ _updateRunningStyle() {
+ const currentWs = global.workspace_manager.get_active_workspace();
+ const show = opt.DASH_ISOLATE_WS
+ ? this.app.get_windows().filter(w => w.get_workspace() === currentWs).length
+ : this.app.state !== Shell.AppState.STOPPED;
+
+ if (show)
+ this._dot.show();
+ else
+ this._dot.hide();
+ },
+};
+
+const DashExtensions = {
+ onScrollEvent(source, event) {
+ if ((this.app && !opt.DASH_ICON_SCROLL) || (this._isSearchWindowsIcon && !opt.SEARCH_WINDOWS_ICON_SCROLL)) {
+ if (this._scrollConId) {
+ this.disconnect(this._scrollConId);
+ this._scrollConId = 0;
+ }
+ if (this._leaveConId) {
+ this.disconnect(this._leaveConId);
+ this._leaveConId = 0;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ if (Main.overview._overview.controls._stateAdjustment.value > 1)
+ return Clutter.EVENT_PROPAGATE;
+
+ let direction = Me.Util.getScrollDirection(event);
+ if (direction === Clutter.ScrollDirection.UP)
+ direction = 1;
+ else if (direction === Clutter.ScrollDirection.DOWN)
+ direction = -1;
+ else
+ return Clutter.EVENT_STOP;
+
+ // avoid uncontrollable switching if smooth scroll wheel or trackpad is used
+ if (this._lastScroll && Date.now() - this._lastScroll < 160)
+ return Clutter.EVENT_STOP;
+
+ this._lastScroll = Date.now();
+
+ DashExtensions.switchWindow.bind(this)(direction);
+ return Clutter.EVENT_STOP;
+ },
+
+ onLeaveEvent() {
+ if (!this._selectedMetaWin || this.has_pointer || this.toggleButton?.has_pointer)
+ return;
+
+ this._selectedPreview._activateSelected = false;
+ this._selectedMetaWin = null;
+ this._scrolledWindows = null;
+ DashExtensions.showWindowPreview.bind(this)(null);
+ },
+
+
+ switchWindow(direction) {
+ if (!this._scrolledWindows) {
+ this._initialSelection = true;
+ // source is app icon
+ if (this.app) {
+ this._scrolledWindows = this.app.get_windows();
+ if (opt.DASH_ISOLATE_WS) {
+ const currentWs = global.workspaceManager.get_active_workspace();
+ this._scrolledWindows = this._scrolledWindows.filter(w => w.get_workspace() === currentWs);
+ }
+
+ const wsList = [];
+ this._scrolledWindows.forEach(w => {
+ const ws = w.get_workspace();
+ if (!wsList.includes(ws))
+ wsList.push(ws);
+ });
+
+ // sort windows by workspaces in MRU order
+ this._scrolledWindows.sort((a, b) => wsList.indexOf(a.get_workspace()) > wsList.indexOf(b.get_workspace()));
+ // source is Search Windows icon
+ } else if (this._isSearchWindowsIcon) {
+ if (opt.SEARCH_WINDOWS_ICON_SCROLL === 1) // all windows
+ this._scrolledWindows = Me.Util.getWindows(null);
+ else
+ this._scrolledWindows = Me.Util.getWindows(global.workspace_manager.get_active_workspace());
+ }
+ }
+
+ let windows = this._scrolledWindows;
+
+ if (!windows.length)
+ return;
+
+ // if window selection is in the process, the previewed window must be the current one
+ let currentWin = this._selectedMetaWin ? this._selectedMetaWin : windows[0];
+
+ const currentIdx = windows.indexOf(currentWin);
+ let targetIdx = currentIdx;
+ // const focusWindow = Me.Util.getWindows(null)[0]; // incompatible 45
+ const focusWindow = Me.Util.getWindows(null)[0];
+ const appFocused = this._scrolledWindows[0] === focusWindow && this._scrolledWindows[0].get_workspace() === global.workspace_manager.get_active_workspace();
+ // only if the app has focus, immediately switch to the previous window
+ // otherwise just set the current window above others
+ if (!this._initialSelection || appFocused)
+ targetIdx += direction;
+ else
+ this._initialSelection = false;
+
+ if (targetIdx > windows.length - 1)
+ targetIdx = 0;
+ else if (targetIdx < 0)
+ targetIdx = windows.length - 1;
+
+ const metaWin = windows[targetIdx];
+ DashExtensions.showWindowPreview.bind(this)(metaWin);
+ this._selectedMetaWin = metaWin;
+ },
+
+ showWindowPreview(metaWin) {
+ const views = Main.overview._overview.controls._workspacesDisplay._workspacesViews;
+ const viewsIter = [views[0]];
+ // secondary monitors use different structure
+ views.forEach(v => {
+ if (v._workspacesView)
+ viewsIter.push(v._workspacesView);
+ });
+
+ viewsIter.forEach(view => {
+ // if workspaces are on primary monitor only
+ if (!view || !view._workspaces)
+ return;
+
+ view._workspaces.forEach(ws => {
+ ws._windows.forEach(windowPreview => {
+ // metaWin === null resets opacity
+ let opacity = metaWin ? 50 : 255;
+ windowPreview._activateSelected = false;
+
+ // minimized windows are invisible if windows are not exposed (WORKSPACE_MODE === 0)
+ if (!windowPreview.opacity)
+ windowPreview.opacity = 255;
+
+ // app windows set to lower opacity, so they can be recognized
+ if (this._scrolledWindows && this._scrolledWindows.includes(windowPreview.metaWindow)) {
+ if (opt.DASH_ICON_SCROLL === 2)
+ opacity = 254;
+ }
+ if (windowPreview.metaWindow === metaWin) {
+ if (metaWin && metaWin.get_workspace() !== global.workspace_manager.get_active_workspace()) {
+ Main.wm.actionMoveWorkspace(metaWin.get_workspace());
+ if (_timeouts.wsSwitcherAnimation)
+ GLib.source_remove(_timeouts.wsSwitcherAnimation);
+ // setting window preview above siblings before workspace switcher animation has no effect
+ // we need to set the window above after the ws preview become visible on the screen
+ // the default switcher animation time is 250, 200 ms delay should be enough
+ _timeouts.wsSwitcherAnimation = GLib.timeout_add(0, 200 * St.Settings.get().slow_down_factor, () => {
+ windowPreview.get_parent().set_child_above_sibling(windowPreview, null);
+ _timeouts.wsSwitcherAnimation = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ } else {
+ windowPreview.get_parent().set_child_above_sibling(windowPreview, null);
+ }
+
+ opacity = 255;
+ this._selectedPreview = windowPreview;
+ windowPreview._activateSelected = true;
+ }
+
+ // if windows are exposed, highlight selected using opacity
+ if ((opt.OVERVIEW_MODE && opt.WORKSPACE_MODE) || !opt.OVERVIEW_MODE) {
+ if (metaWin && opacity === 255)
+ windowPreview.showOverlay(true);
+ else
+ windowPreview.hideOverlay(true);
+ windowPreview.ease({
+ duration: 200,
+ opacity,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+ });
+ });
+ });
+ },
+};
+
+const AppMenuCommon = {
+ _updateWindowsSection() {
+ if (global.compositor) {
+ if (this._updateWindowsLaterId) {
+ const laters = global.compositor.get_laters();
+ laters.remove(this._updateWindowsLaterId);
+ }
+ } else if (this._updateWindowsLaterId) {
+ Meta.later_remove(this._updateWindowsLaterId);
+ }
+
+ this._updateWindowsLaterId = 0;
+
+ this._windowSection.removeAll();
+ this._openWindowsHeader.hide();
+
+ if (!this._app)
+ return;
+
+ const minWindows = this._showSingleWindows ? 1 : 2;
+ const currentWs = global.workspaceManager.get_active_workspace();
+ const isolateWs = opt.DASH_ISOLATE_WS && !Main.overview.dash.showAppsButton.checked;
+ const windows = this._app.get_windows().filter(w => !w.skip_taskbar && (isolateWs ? w.get_workspace() === currentWs : true));
+ if (windows.length < minWindows)
+ return;
+
+ this._openWindowsHeader.show();
+
+ windows.forEach(window => {
+ const title = window.title || this._app.get_name();
+ const item = this._windowSection.addAction(title, event => {
+ Main.activateWindow(window, event.get_time());
+ });
+ window.connectObject('notify::title', () => {
+ item.label.text = window.title || this._app.get_name();
+ }, item);
+ });
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/iconGrid.js b/extensions/47/vertical-workspaces/lib/iconGrid.js
new file mode 100644
index 0000000..f0c6b18
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/iconGrid.js
@@ -0,0 +1,429 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * iconGrid.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import St from 'gi://St';
+import GLib from 'gi://GLib';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as IconGrid from 'resource:///org/gnome/shell/ui/iconGrid.js';
+
+let Me;
+let opt;
+
+// added sizes for better scaling
+export const IconSize = {
+ LARGEST: 256,
+ 224: 224,
+ 208: 208,
+ 192: 192,
+ 176: 176,
+ 160: 160,
+ 144: 144,
+ 128: 128,
+ 112: 112,
+ LARGE: 96,
+ 80: 80,
+ 64: 64,
+ TINY: 48,
+};
+
+export const IconGridModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('appDisplayModule');
+ // if notifications are enabled no override is needed
+ reset = reset || !this.moduleEnabled;
+
+ // don't touch original code if module disabled
+ if (reset && !this._firstActivation) {
+ this._disableModule();
+ } else if (!reset) {
+ this._firstActivation = false;
+ this._activateModule();
+ }
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('IconGrid', IconGrid.IconGrid.prototype, IconGridCommon);
+ this._overrides.addOverride('IconGridLayout', IconGrid.IconGridLayout.prototype, IconGridLayoutCommon);
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+ }
+};
+
+const IconGridCommon = {
+ getItemsAtPage(page) {
+ if (page < 0 || page >= this.nPages)
+ return [];
+ // throw new Error(`Page ${page} does not exist at IconGrid`);
+
+ const layoutManager = this.layout_manager;
+ return layoutManager.getItemsAtPage(page);
+ },
+
+ _shouldUpdateGrid(width, height) {
+ if (this.layoutManager._isFolder)
+ return false;
+ else if (this._currentMode === -1)
+ return true;
+
+ // Update if page size changed
+ // Page dimensions may change within a small range
+ const range = 5;
+ return (Math.abs(width - (this._gridForWidth ?? 0)) > range) ||
+ (Math.abs(height - (this._gridForHeight ?? 0)) > range);
+ },
+
+ _findBestModeForSize(width, height) {
+ // this function is for main grid only, folder grid calculation is in appDisplay.AppFolderDialog class
+ if (!this._shouldUpdateGrid(width, height))
+ return;
+
+ this._gridForWidth = width;
+ this._gridForHeight = height;
+
+ this._updateDefaultIconSize();
+ const { pagePadding } = this.layout_manager;
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const itemPadding = 55;
+
+ // pagePadding is already affected by the scaleFactor
+ width -= pagePadding.left + pagePadding.right;
+ height -= pagePadding.top + pagePadding.bottom;
+
+ // Sync with _findBestIconSize()
+ this.layoutManager._gridSizeChanged = true;
+ this.layoutManager._gridWidth = width;
+ this.layoutManager._gridHeight = height;
+
+ // All widgets are affected by the scaleFactor so we need to apply it also on the page size
+ width /= scaleFactor;
+ height /= scaleFactor;
+
+ const spacing = opt.APP_GRID_SPACING;
+ const iconSize = opt.APP_GRID_ICON_SIZE > 0 ? opt.APP_GRID_ICON_SIZE : opt.APP_GRID_ICON_SIZE_DEFAULT;
+ const itemSize = iconSize + itemPadding;
+ let columns = opt.APP_GRID_COLUMNS;
+ let rows = opt.APP_GRID_ROWS;
+ // 0 means adaptive size
+ let unusedSpaceH = -1;
+ if (!columns) {
+ // calculate #columns + 1 without spacing
+ columns = Math.floor(width / itemSize) + 1;
+ // check if columns with spacing fits the available width
+ // and reduce the number until it fits
+ while (unusedSpaceH < 0) {
+ columns -= 1;
+ unusedSpaceH = width - columns * itemSize - (columns - 1) * spacing;
+ }
+ }
+ let unusedSpaceV = -1;
+ if (!rows) {
+ rows = Math.floor(height / itemSize) + 1;
+ while (unusedSpaceV < 0) {
+ rows -= 1;
+ unusedSpaceV = height - rows * itemSize - ((rows - 1) * spacing);
+ }
+ }
+
+ this._gridModes = [{ columns, rows }];
+ this._currentMode = -1;
+ this._setGridMode(0);
+ this.layoutManager.updateIconSize();
+ // Call _redisplay() from timeout to avoid allocation errors
+ GLib.idle_add(GLib.PRIORITY_LOW, () =>
+ Main.overview._overview.controls.appDisplay._redisplay()
+ );
+ },
+
+ _updateDefaultIconSize() {
+ // Reduce default icon size for low resolution screens and high screen scales
+ if (Me.Util.monitorHasLowResolution()) {
+ opt.APP_GRID_ICON_SIZE_DEFAULT = opt.APP_GRID_ACTIVE_PREVIEW && !opt.APP_GRID_USAGE ? 128 : 64;
+ opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT = 64;
+ } else {
+ opt.APP_GRID_ICON_SIZE_DEFAULT = opt.APP_GRID_ACTIVE_PREVIEW && !opt.APP_GRID_USAGE ? 192 : 96;
+ }
+ },
+
+ // Workaround for the upstream bug
+ // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5753
+ // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5240
+ // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6892
+ // The appGridLayout._currentPage is not updated when the page is changed in the grid
+ // For example, when user navigates app icons using a keyboard
+ // Related issues open on GNOME's gitlab:
+ after_goToPage() {
+ if (this._delegate._appGridLayout._currentPage !== this._currentPage)
+ this._delegate._appGridLayout.goToPage(this._currentPage);
+ },
+
+ // Workaround for the upstream bug
+ // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/7700
+ // Return INVALID target if x or y is out of the grid view to prevent pages[page] undefined error (horizontal orientation only)
+ getDropTarget(x, y) {
+ if (x < 0 || y < 0)
+ return [0, 0, 0]; // [0, 0, DragLocation.INVALID]
+ const layoutManager = this.layout_manager;
+ return layoutManager.getDropTarget(x, y, this._currentPage);
+ },
+};
+
+const IconGridLayoutCommon = {
+ _findBestIconSize() {
+ if (this.fixedIconSize !== -1)
+ return this.fixedIconSize;
+
+ if (!this._isFolder && !this._gridSizeChanged)
+ return this._iconSize;
+ this._gridSizeChanged = false;
+
+
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const nColumns = this.columnsPerPage;
+ const nRows = this.rowsPerPage;
+
+ // If grid is not defined, return default icon size
+ if (nColumns < 1 && nRows < 1) {
+ return this._isFolder
+ ? opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT
+ : opt.APP_GRID_ICON_SIZE_DEFAULT;
+ }
+
+ const spacing = this._isFolder
+ ? opt.APP_GRID_FOLDER_SPACING
+ : opt.APP_GRID_SPACING;
+
+ const columnSpacingPerPage = spacing * (nColumns - 1);
+ const rowSpacingPerPage = spacing * (nRows - 1);
+ const itemPadding = 55;
+
+ const width = (this._gridWidth ? this._gridWidth : this._pageWidth) / scaleFactor;
+ let height = (this._gridHeight ? this._gridHeight : this._pageHeight) / scaleFactor;
+
+ if (!width || !height)
+ return opt.APP_GRID_ICON_SIZE_DEFAULT;
+
+ const [firstItem] = this._container;
+
+ let iconSizes = Object.values(IconSize).sort((a, b) => b - a);
+ // Limit max icon size for folders and fully adaptive folder grids, the whole range is for the main grid with active folders
+ if (this._isFolder && opt.APP_GRID_FOLDER_ICON_SIZE < 0)
+ iconSizes = iconSizes.slice(iconSizes.indexOf(opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT), -1);
+ else if (this._isFolder)
+ iconSizes = iconSizes.slice(iconSizes.indexOf(IconSize.LARGE), -1);
+ else if (opt.APP_GRID_ICON_SIZE < 0)
+ iconSizes = iconSizes.slice(iconSizes.indexOf(opt.APP_GRID_ICON_SIZE_DEFAULT), -1);
+
+ let sizeInvalid = false;
+ for (const size of iconSizes) {
+ let usedWidth, usedHeight;
+
+ if (firstItem) {
+ firstItem.icon.setIconSize(size);
+ const [firstItemWidth] = firstItem.get_preferred_size();
+
+ const itemSize = firstItemWidth / scaleFactor;
+ if (itemSize < size)
+ sizeInvalid = true;
+
+ usedWidth = itemSize * nColumns;
+ usedHeight = itemSize * nRows;
+ }
+
+ if (!firstItem || sizeInvalid) {
+ usedWidth = (size + itemPadding) * nColumns;
+ usedHeight = (size + itemPadding) * nRows;
+ }
+ const emptyHSpace =
+ width - usedWidth - columnSpacingPerPage;
+ const emptyVSpace =
+ height - usedHeight - rowSpacingPerPage;
+
+ if (emptyHSpace >= 0 && emptyVSpace >= 0)
+ return size;
+ }
+
+ return IconSize.TINY;
+ },
+
+ removeItem(item) {
+ if (!this._items.has(item)) {
+ console.error(`iconGrid: Item ${item} is not part of the IconGridLayout`);
+ return;
+ // throw new Error(`Item ${item} is not part of the IconGridLayout`);
+ }
+
+ if (!this._container)
+ return;
+
+ this._shouldEaseItems = true;
+
+ this._container.remove_child(item);
+ this._removeItemData(item);
+ },
+
+ addItem(item, page = -1, index = -1) {
+ if (this._items.has(item)) {
+ console.error(`iconGrid: Item ${item} already added to IconGridLayout`);
+ return;
+ // throw new Error(`Item ${item} already added to IconGridLayout`);
+ }
+
+ if (page > this._pages.length) {
+ console.error(`iconGrid: Cannot add ${item} to page ${page}`);
+ page = -1;
+ index = -1;
+ // throw new Error(`Cannot add ${item} to page ${page}`);
+ }
+
+ if (!this._container)
+ return;
+
+ if (page !== -1 && index === -1)
+ page = this._findBestPageToAppend(page);
+
+ this._shouldEaseItems = true;
+
+ if (!this._container.get_children().includes(item))
+ this._container.add_child(item);
+ this._addItemToPage(item, page, index);
+ },
+
+ moveItem(item, newPage, newPosition) {
+ if (!this._items.has(item)) {
+ console.error(`iconGrid: Item ${item} is not part of the IconGridLayout`);
+ return;
+ // throw new Error(`Item ${item} is not part of the IconGridLayout`);
+ }
+
+ this._shouldEaseItems = true;
+
+ this._removeItemData(item);
+
+ if (newPage !== -1 && newPosition === -1)
+ newPage = this._findBestPageToAppend(newPage);
+ this._addItemToPage(item, newPage, newPosition);
+ },
+
+ _addItemToPage(item, pageIndex, index) {
+ // Ensure we have at least one page
+ if (this._pages.length === 0)
+ this._appendPage();
+
+ // Append a new page if necessary
+ if (pageIndex === this._pages.length)
+ this._appendPage();
+
+ if (pageIndex >= this._pages.length) {
+ pageIndex = -1;
+ index = -1;
+ }
+
+ if (pageIndex === -1)
+ pageIndex = this._pages.length - 1;
+
+ if (index === -1)
+ index = this._pages[pageIndex].children.length;
+
+ this._items.set(item, {
+ actor: item,
+ pageIndex,
+ destroyId: item.connect('destroy', () => this._removeItemData(item)),
+ visibleId: item.connect('notify::visible', () => {
+ const itemData = this._items.get(item);
+
+ this._updateVisibleChildrenForPage(itemData.pageIndex);
+
+ if (item.visible)
+ this._relocateSurplusItems(itemData.pageIndex);
+ else if (!this.allowIncompletePages)
+ this._fillItemVacancies(itemData.pageIndex);
+ }),
+ queueRelayoutId: item.connect('queue-relayout', () => {
+ this._childrenMaxSize = -1;
+ }),
+ });
+
+ item.icon.setIconSize(this._iconSize);
+ this._pages[pageIndex].children.splice(index, 0, item);
+ this._updateVisibleChildrenForPage(pageIndex);
+ this._relocateSurplusItems(pageIndex);
+ },
+
+ _relocateSurplusItems(pageIndex) {
+ // Avoid recursion during relocations in _redisplay()
+ if (this._skipRelocateSurplusItems)
+ return;
+
+ const visiblePageItems = this._pages[pageIndex].visibleChildren;
+ const itemsPerPage = this.columnsPerPage * this.rowsPerPage;
+
+ // No overflow
+ if (visiblePageItems.length <= itemsPerPage)
+ return;
+
+ const nExtraItems = visiblePageItems.length - itemsPerPage;
+ for (let i = 0; i < nExtraItems; i++) {
+ const overflowIndex = visiblePageItems.length - i - 1;
+ const overflowItem = visiblePageItems[overflowIndex];
+
+ this._removeItemData(overflowItem);
+ this._addItemToPage(overflowItem, pageIndex + 1, 0);
+ }
+ },
+
+ _findBestPageToAppend(startPage) {
+ const itemsPerPage = this.columnsPerPage * this.rowsPerPage;
+
+ for (let i = startPage; i < this._pages.length; i++) {
+ const visibleItems = this._pages[i].visibleChildren;
+
+ if (visibleItems.length < itemsPerPage)
+ return i;
+ }
+
+ return this._pages.length;
+ },
+
+ updateIconSize() {
+ const iconSize = this._findBestIconSize();
+ if (this._iconSize !== iconSize) {
+ this._iconSize = iconSize;
+
+ for (const child of this._container)
+ child.icon.setIconSize(iconSize);
+
+ this.notify('icon-size');
+ }
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/layout.js b/extensions/47/vertical-workspaces/lib/layout.js
new file mode 100644
index 0000000..807f9e0
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/layout.js
@@ -0,0 +1,473 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * layout.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Gio from 'gi://Gio';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Layout from 'resource:///org/gnome/shell/ui/layout.js';
+
+let Me;
+let opt;
+let _timeouts;
+
+export const LayoutModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+ _timeouts = {};
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ this._originalUpdateHotCorners = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this._removeTimeouts();
+
+ this.moduleEnabled = opt.get('layoutModule');
+ const conflict = Me.Util.getEnabledExtensions('custom-hot-corners').length ||
+ Me.Util.getEnabledExtensions('dash-to-panel').length;
+
+ if (conflict && !reset)
+ console.warn(`[${Me.metadata.name}] Warning: "Layout" module disabled due to potential conflict with another extension`);
+
+ 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(' LayoutModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ _timeouts = {};
+
+ this._overrides.addOverride('LayoutManager', Main.layoutManager, LayoutManagerCommon);
+ this._overrides.addOverride('HotCorner', Layout.HotCorner.prototype, HotCornerCommon);
+
+ Main.layoutManager._updatePanelBarrier();
+ Main.layoutManager._updateHotCorners();
+
+ if (!this._hotCornersEnabledConId) {
+ this._interfaceSettings = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.interface',
+ });
+ this._hotCornersEnabledConId = this._interfaceSettings.connect('changed::enable-hot-corners',
+ () => Main.layoutManager._updateHotCorners());
+ }
+
+ console.debug(' LayoutModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ Main.layoutManager._updateHotCorners();
+
+ if (this._hotCornersEnabledConId) {
+ this._interfaceSettings.disconnect(this._hotCornersEnabledConId);
+ this._hotCornersEnabledConId = 0;
+ this._interfaceSettings = null;
+ }
+
+ console.debug(' LayoutModule - Disabled');
+ }
+
+ _removeTimeouts() {
+ if (_timeouts) {
+ Object.values(_timeouts).forEach(t => {
+ if (t)
+ GLib.source_remove(t);
+ });
+ _timeouts = null;
+ }
+ }
+};
+
+const LayoutManagerCommon = {
+ _updatePanelBarrier() {
+ if (this._rightPanelBarrier) {
+ this._rightPanelBarrier.destroy();
+ this._rightPanelBarrier = null;
+ }
+
+ if (this._leftPanelBarrier) {
+ this._leftPanelBarrier.destroy();
+ this._leftPanelBarrier = null;
+ }
+
+ if (!this.primaryMonitor || !opt || Me.Util.getEnabledExtensions('hidetopbar'))
+ return;
+
+ if (this.panelBox.height) {
+ const backend = !!Meta.Barrier.prototype.backend;
+ let params = {};
+ if (backend)
+ params['backend'] = global.backend;
+ else
+ params['display'] = global.display;
+
+ let primary = this.primaryMonitor;
+ if ([0, 1, 3].includes(opt.HOT_CORNER_POSITION)) {
+ params = Object.assign({}, params, {
+ x1: primary.x + primary.width, y1: this.panelBox.allocation.y1,
+ x2: primary.x + primary.width, y2: this.panelBox.allocation.y2,
+ directions: Meta.BarrierDirection.NEGATIVE_X,
+ });
+ this._rightPanelBarrier = new Meta.Barrier(params);
+ }
+
+ if ([2, 4].includes(opt.HOT_CORNER_POSITION)) {
+ params = Object.assign({}, params, {
+ x1: primary.x, y1: this.panelBox.allocation.y1,
+ x2: primary.x, y2: this.panelBox.allocation.y2,
+ directions: Meta.BarrierDirection.POSITIVE_X,
+ });
+ this._leftPanelBarrier = new Meta.Barrier(params);
+ }
+ }
+ },
+
+ _updateHotCorners() {
+ // avoid errors if called from foreign override
+ if (!opt)
+ return;
+
+ // destroy old hot corners
+ this.hotCorners.forEach(corner => corner?.destroy());
+ this.hotCorners = [];
+
+ if (!this._interfaceSettings.get_boolean('enable-hot-corners')) {
+ this.emit('hot-corners-changed');
+ return;
+ }
+
+ let size = this.panelBox.height ? this.panelBox.height : 27;
+
+ // position 0 - default, 1-TL, 2-TR, 3-BL, 4-BR
+ const position = opt.HOT_CORNER_POSITION;
+
+ // build new hot corners
+ for (let i = 0; i < this.monitors.length; i++) {
+ let monitor = this.monitors[i];
+ let cornerX, cornerY;
+
+ if (position === 0) {
+ cornerX = this._rtl ? monitor.x + monitor.width : monitor.x;
+ cornerY = monitor.y;
+ } else if (position === 1) {
+ cornerX = monitor.x;
+ cornerY = monitor.y;
+ } else if (position === 2) {
+ cornerX = monitor.x + monitor.width;
+ cornerY = monitor.y;
+ } else if (position === 3) {
+ cornerX = monitor.x;
+ cornerY = monitor.y + monitor.height;
+ } else {
+ cornerX = monitor.x + monitor.width;
+ cornerY = monitor.y + monitor.height;
+ }
+
+ let haveCorner = true;
+
+ if (i !== this.primaryIndex) {
+ // Check if we have a top left (right for RTL) corner.
+ // I.e. if there is no monitor directly above or to the left(right)
+ let besideX = this._rtl ? monitor.x + 1 : cornerX - 1;
+ let besideY = cornerY;
+ let aboveX = cornerX;
+ let aboveY = cornerY - 1;
+
+ for (let j = 0; j < this.monitors.length; j++) {
+ if (i === j)
+ continue;
+ let otherMonitor = this.monitors[j];
+ if (besideX >= otherMonitor.x &&
+ besideX < otherMonitor.x + otherMonitor.width &&
+ besideY >= otherMonitor.y &&
+ besideY < otherMonitor.y + otherMonitor.height) {
+ haveCorner = false;
+ break;
+ }
+ if (aboveX >= otherMonitor.x &&
+ aboveX < otherMonitor.x + otherMonitor.width &&
+ aboveY >= otherMonitor.y &&
+ aboveY < otherMonitor.y + otherMonitor.height) {
+ haveCorner = false;
+ break;
+ }
+ }
+ }
+
+ if (haveCorner) {
+ let corner = new Layout.HotCorner(this, monitor, cornerX, cornerY);
+ corner.setBarrierSize(size, false);
+ this.hotCorners.push(corner);
+ } else {
+ this.hotCorners.push(null);
+ }
+ }
+
+ this.emit('hot-corners-changed');
+ },
+};
+
+const HotCornerCommon = {
+ after__init() {
+ let angle = 0;
+ switch (opt.HOT_CORNER_POSITION) {
+ case 2:
+ angle = 90;
+ break;
+ case 3:
+ angle = 270;
+ break;
+ case 4:
+ angle = 180;
+ break;
+ }
+
+ this._ripples._ripple1.rotation_angle_z = angle;
+ this._ripples._ripple2.rotation_angle_z = angle;
+ this._ripples._ripple3.rotation_angle_z = angle;
+ },
+
+ setBarrierSize(size, notMyCall = true) {
+ // ignore calls from the original _updateHotCorners() callback to avoid building barriers outside screen
+ if (notMyCall && size > 0)
+ return;
+
+ if (this._verticalBarrier) {
+ this._pressureBarrier.removeBarrier(this._verticalBarrier);
+ this._verticalBarrier.destroy();
+ this._verticalBarrier = null;
+ }
+
+ if (this._horizontalBarrier) {
+ this._pressureBarrier.removeBarrier(this._horizontalBarrier);
+ this._horizontalBarrier.destroy();
+ this._horizontalBarrier = null;
+ }
+
+ if (size > 0) {
+ const primaryMonitor = global.display.get_primary_monitor();
+ const monitor = this._monitor;
+ const extendV = opt && opt.HOT_CORNER_ACTION && opt.HOT_CORNER_EDGE && opt.DASH_VERTICAL && monitor.index === primaryMonitor;
+ const extendH = opt && opt.HOT_CORNER_ACTION && opt.HOT_CORNER_EDGE && !opt.DASH_VERTICAL && monitor.index === primaryMonitor;
+
+ const backend = !!Meta.Barrier.prototype.backend;
+ let params = {};
+ if (backend)
+ params['backend'] = global.backend;
+ else
+ params['display'] = global.display;
+
+ if (opt.HOT_CORNER_POSITION <= 1) {
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x,
+ y1: this._y, y2: this._y + (extendV ? monitor.height : size),
+ directions: Meta.BarrierDirection.POSITIVE_X,
+ });
+ this._verticalBarrier = new Meta.Barrier(params);
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x + (extendH ? monitor.width : size),
+ y1: this._y, y2: this._y,
+ directions: Meta.BarrierDirection.POSITIVE_Y,
+ });
+ this._horizontalBarrier = new Meta.Barrier(params);
+ } else if (opt.HOT_CORNER_POSITION === 2) {
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x,
+ y1: this._y, y2: this._y + (extendV ? monitor.height : size),
+ directions: Meta.BarrierDirection.NEGATIVE_X,
+ });
+ this._verticalBarrier = new Meta.Barrier(params);
+ params = Object.assign({}, params, {
+ x1: this._x - size, x2: this._x,
+ y1: this._y, y2: this._y,
+ directions: Meta.BarrierDirection.POSITIVE_Y,
+ });
+ this._horizontalBarrier = new Meta.Barrier(params);
+ } else if (opt.HOT_CORNER_POSITION === 3) {
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x,
+ y1: this._y, y2: this._y - size,
+ directions: Meta.BarrierDirection.POSITIVE_X,
+ });
+ this._verticalBarrier = new Meta.Barrier(params);
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x + (extendH ? monitor.width : size),
+ y1: this._y, y2: this._y,
+ directions: Meta.BarrierDirection.NEGATIVE_Y,
+ });
+ this._horizontalBarrier = new Meta.Barrier(params);
+ } else if (opt.HOT_CORNER_POSITION === 4) {
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x,
+ y1: this._y, y2: this._y - size,
+ directions: Meta.BarrierDirection.NEGATIVE_X,
+ });
+ this._verticalBarrier = new Meta.Barrier(params);
+ params = Object.assign({}, params, {
+ x1: this._x, x2: this._x - size,
+ y1: this._y, y2: this._y,
+ directions: Meta.BarrierDirection.NEGATIVE_Y,
+ });
+ this._horizontalBarrier = new Meta.Barrier(params);
+ }
+
+ this._pressureBarrier.addBarrier(this._verticalBarrier);
+ this._pressureBarrier.addBarrier(this._horizontalBarrier);
+ }
+ },
+
+ _toggleOverview() {
+ if (!opt.HOT_CORNER_ACTION || (!opt.HOT_CORNER_FULLSCREEN && this._monitor.inFullscreen && !Main.overview.visible))
+ return;
+
+ if (Main.overview.shouldToggleByCornerOrButton()) {
+ if (Main.overview._shown) {
+ this._toggleWindowPicker(true);
+ } else if ((opt.HOT_CORNER_ACTION === 2 && !Me.Util.isCtrlPressed()) || ([3, 4, 5, 6].includes(opt.HOT_CORNER_ACTION) && Me.Util.isCtrlPressed())) {
+ // Default overview
+ opt.OVERVIEW_MODE = 0;
+ opt.OVERVIEW_MODE2 = false;
+ opt.WORKSPACE_MODE = 1;
+ this._toggleWindowPicker(true, true);
+ } else if (opt.HOT_CORNER_ACTION === 1) {
+ Main.overview.resetOverviewMode();
+ this._toggleWindowPicker(true, true);
+ } else if ((opt.HOT_CORNER_ACTION === 3 && !Me.Util.isCtrlPressed()) || (opt.HOT_CORNER_ACTION === 2 && Me.Util.isCtrlPressed()) || (opt.HOT_CORNER_ACTION === 6 && Me.Util.isCtrlPressed())) {
+ // Applications
+ this._toggleApplications(true);
+ } else if (opt.HOT_CORNER_ACTION === 4 && !Me.Util.isCtrlPressed()) {
+ // Overview - static ws preview
+ opt.OVERVIEW_MODE = 1;
+ opt.OVERVIEW_MODE2 = false;
+ opt.WORKSPACE_MODE = 0;
+ this._toggleWindowPicker(true, true);
+ } else if (opt.HOT_CORNER_ACTION === 5 && !Me.Util.isCtrlPressed()) {
+ // Overview - static ws
+ opt.OVERVIEW_MODE = 2;
+ opt.OVERVIEW_MODE2 = true;
+ opt.WORKSPACE_MODE = 0;
+ this._toggleWindowPicker(true, true);
+ } else if (opt.HOT_CORNER_ACTION === 6 && !Me.Util.isCtrlPressed()) {
+ // Window search provider
+ opt.OVERVIEW_MODE = 2;
+ opt.OVERVIEW_MODE2 = true;
+ opt.WORKSPACE_MODE = 0;
+ this._toggleWindowSearchProvider();
+ }
+ if (opt.HOT_CORNER_RIPPLES && Main.overview.animationInProgress)
+ this._ripples.playAnimation(this._x, this._y);
+ }
+ },
+
+ _toggleWindowPicker(leaveOverview = false, customOverviewMode = false) {
+ if (Main.overview._shown && (leaveOverview || !Main.overview.dash.showAppsButton.checked)) {
+ Main.overview.hide();
+ } else if (Main.overview.dash.showAppsButton.checked) {
+ Main.overview.dash.showAppsButton.checked = false;
+ } else {
+ const focusWindow = global.display.get_focus_window();
+ // at least GS 42 is unable to show overview in X11 session if VirtualBox Machine window grabbed keyboard
+ if (!Meta.is_wayland_compositor() && focusWindow && focusWindow.wm_class.includes('VirtualBox Machine')) {
+ // following should help when windowed VBox Machine has focus.
+ global.stage.set_key_focus(Main.panel);
+ // key focus doesn't take the effect immediately, we must wait for it
+ // still looking for better solution!
+ _timeouts.releaseKeyboardTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ // delay cannot be too short
+ 200,
+ () => {
+ Main.overview.show(1, customOverviewMode);
+
+ _timeouts.releaseKeyboardTimeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+ } else {
+ Main.overview.show(1, customOverviewMode);
+ }
+ }
+ },
+
+ _toggleApplications(leaveOverview = false) {
+ if ((leaveOverview && Main.overview._shown) || Main.overview.dash.showAppsButton.checked) {
+ Main.overview.hide();
+ } else {
+ const focusWindow = global.display.get_focus_window();
+ // at least GS 42 is unable to show overview in X11 session if VirtualBox Machine window grabbed keyboard
+ if (!Meta.is_wayland_compositor() && focusWindow && focusWindow.wm_class.includes('VirtualBox Machine')) {
+ // following should help when windowed VBox Machine has focus.
+ global.stage.set_key_focus(Main.panel);
+ // key focus doesn't take the effect immediately, we must wait for it
+ // still looking for better solution!
+ _timeouts.releaseKeyboardTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ // delay cannot be too short
+ 200,
+ () => {
+ Main.overview.show(2);
+
+ _timeouts.releaseKeyboardTimeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+ } else if (Main.overview._shown) {
+ Main.overview.dash.showAppsButton.checked = true;
+ } else {
+ Main.overview.show(2); // 2 for App Grid
+ }
+ }
+ },
+
+ _toggleWindowSearchProvider() {
+ if (!Main.overview.searchController._searchActive) {
+ opt.OVERVIEW_MODE = 2;
+ opt.OVERVIEW_MODE2 = true;
+ opt.WORKSPACE_MODE = 0;
+ this._toggleWindowPicker(false, true);
+ const prefix = Me.WSP_PREFIX;
+ const position = prefix.length;
+ const searchEntry = Main.overview.searchEntry;
+ searchEntry.set_text(prefix);
+ // searchEntry.grab_key_focus();
+ searchEntry.get_first_child().set_cursor_position(position);
+ searchEntry.get_first_child().set_selection(position, position);
+ } else {
+ // Main.overview.searchEntry.text = '';
+ Main.overview.hide();
+ }
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/messageTray.js b/extensions/47/vertical-workspaces/lib/messageTray.js
new file mode 100644
index 0000000..28d6b1d
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/messageTray.js
@@ -0,0 +1,91 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * messageTray.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+let Me;
+let opt;
+
+export const MessageTrayModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('messageTrayModule');
+ 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(' MessageTrayModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ this._setNotificationPosition(opt.NOTIFICATION_POSITION);
+
+ console.debug(' MessageTrayModule - Activated');
+ }
+
+ _disableModule() {
+ this._setNotificationPosition(1);
+
+ console.debug(' MessageTrayModule - Disabled');
+ }
+
+ _setNotificationPosition(position) {
+ switch (position) {
+ case 0:
+ Main.messageTray._bannerBin.x_align = Clutter.ActorAlign.START;
+ Main.messageTray._bannerBin.y_align = Clutter.ActorAlign.START;
+ break;
+ case 1:
+ Main.messageTray._bannerBin.x_align = Clutter.ActorAlign.CENTER;
+ Main.messageTray._bannerBin.y_align = Clutter.ActorAlign.START;
+ break;
+ case 2:
+ Main.messageTray._bannerBin.x_align = Clutter.ActorAlign.END;
+ Main.messageTray._bannerBin.y_align = Clutter.ActorAlign.START;
+ break;
+ case 3:
+ Main.messageTray._bannerBin.x_align = Clutter.ActorAlign.START;
+ Main.messageTray._bannerBin.y_align = Clutter.ActorAlign.END;
+ break;
+ case 4:
+ Main.messageTray._bannerBin.x_align = Clutter.ActorAlign.CENTER;
+ Main.messageTray._bannerBin.y_align = Clutter.ActorAlign.END;
+ break;
+ case 5:
+ Main.messageTray._bannerBin.x_align = Clutter.ActorAlign.END;
+ Main.messageTray._bannerBin.y_align = Clutter.ActorAlign.END;
+ break;
+ }
+ }
+};
diff --git a/extensions/47/vertical-workspaces/lib/optionsFactory.js b/extensions/47/vertical-workspaces/lib/optionsFactory.js
new file mode 100644
index 0000000..7284085
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/optionsFactory.js
@@ -0,0 +1,496 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * optionsFactory.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ */
+
+'use strict';
+
+import Adw from 'gi://Adw';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+
+let Me;
+
+// gettext
+let _;
+
+export function init(me) {
+ Me = me;
+ _ = Me.gettext;
+}
+
+export const ItemFactory = class ItemFactory {
+ constructor() {
+ this._settings = Me.Opt._gsettings;
+ }
+
+ getRowWidget(text, caption, widget, variable, options = [], dependsOn) {
+ let item = [];
+ let label;
+ if (widget) {
+ label = new Gtk.Box({
+ orientation: Gtk.Orientation.VERTICAL,
+ spacing: 4,
+ halign: Gtk.Align.START,
+ valign: Gtk.Align.CENTER,
+ });
+ const option = new Gtk.Label({
+ halign: Gtk.Align.START,
+ });
+ option.set_text(text);
+ label.append(option);
+
+ if (caption) {
+ const captionLabel = new Gtk.Label({
+ halign: Gtk.Align.START,
+ wrap: true,
+ /* width_chars: 80, */
+ xalign: 0,
+ });
+ const context = captionLabel.get_style_context();
+ context.add_class('dim-label');
+ context.add_class('caption');
+ captionLabel.set_text(caption);
+ label.append(captionLabel);
+ }
+ label._title = text;
+ } else {
+ label = text;
+ }
+ item.push(label);
+ item.push(widget);
+
+ let key;
+
+ if (variable && Me.Opt.options[variable]) {
+ const opt = Me.Opt.options[variable];
+ key = opt[1];
+ }
+
+ if (widget) {
+ if (widget._isSwitch)
+ this._connectSwitch(widget, key, variable);
+ else if (widget._isSpinButton || widget._isScale)
+ this._connectSpinButton(widget, key, variable);
+ else if (widget._isComboBox)
+ this._connectComboBox(widget, key, variable, options);
+ else if (widget._isDropDown)
+ this._connectDropDown(widget, key, variable, options);
+
+ if (dependsOn) {
+ const dKey = Me.Opt.options[dependsOn][1];
+ this._settings.bind(dKey, widget, 'sensitive', Gio.SettingsBindFlags.GET);
+ }
+ }
+
+ return item;
+ }
+
+ _connectSwitch(widget, key /* , variable */) {
+ this._settings.bind(key, widget, 'active', Gio.SettingsBindFlags.DEFAULT);
+ }
+
+ _connectSpinButton(widget, key /* , variable */) {
+ this._settings.bind(key, widget.adjustment, 'value', Gio.SettingsBindFlags.DEFAULT);
+ }
+
+ _connectComboBox(widget, key, variable, options) {
+ let model = widget.get_model();
+ widget._comboMap = {};
+ const currentValue = Me.Opt.get(variable);
+ for (const [label, value] of options) {
+ let iter;
+ model.set(iter = model.append(), [0, 1], [label, value]);
+ if (value === currentValue)
+ widget.set_active_iter(iter);
+
+ widget._comboMap[value] = iter;
+ }
+ Me.Opt.connect(`changed::${key}`, () => {
+ widget.set_active_iter(widget._comboMap[Me.Opt.get(variable, true)]);
+ });
+ widget.connect('changed', () => {
+ const [success, iter] = widget.get_active_iter();
+
+ if (!success)
+ return;
+
+ Me.Opt.set(variable, model.get_value(iter, 1));
+ });
+ }
+
+ _connectDropDown(widget, key, variable, options) {
+ const model = widget.get_model();
+ const currentValue = Me.Opt.get(variable);
+ for (let i = 0; i < options.length; i++) {
+ const text = options[i][0];
+ const id = options[i][1];
+ model.append(new DropDownItem({ text, id }));
+ if (id === currentValue)
+ widget.set_selected(i);
+ }
+
+ const factory = new Gtk.SignalListItemFactory();
+ factory.connect('setup', (fact, listItem) => {
+ const label = new Gtk.Label({ xalign: 0 });
+ listItem.set_child(label);
+ });
+ factory.connect('bind', (fact, listItem) => {
+ const label = listItem.get_child();
+ const item = listItem.get_item();
+ label.set_text(item.text);
+ });
+
+ widget.connect('notify::selected-item', dropDown => {
+ const item = dropDown.get_selected_item();
+ Me.Opt.set(variable, item.id);
+ });
+
+ Me.Opt.connect(`changed::${key}`, () => {
+ const newId = Me.Opt.get(variable, true);
+ for (let i = 0; i < options.length; i++) {
+ const id = options[i][1];
+ if (id === newId)
+ widget.set_selected(i);
+ }
+ });
+
+ widget.set_factory(factory);
+ }
+
+ newSwitch() {
+ let sw = new Gtk.Switch({
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ });
+ sw._isSwitch = true;
+ return sw;
+ }
+
+ newSpinButton(adjustment) {
+ let spinButton = new Gtk.SpinButton({
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ vexpand: false,
+ xalign: 0.5,
+ });
+ spinButton.set_adjustment(adjustment);
+ spinButton._isSpinButton = true;
+ return spinButton;
+ }
+
+ newComboBox() {
+ const model = new Gtk.ListStore();
+ model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_INT]);
+ const comboBox = new Gtk.ComboBox({
+ model,
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ });
+ const renderer = new Gtk.CellRendererText();
+ comboBox.pack_start(renderer, true);
+ comboBox.add_attribute(renderer, 'text', 0);
+ comboBox._isComboBox = true;
+ return comboBox;
+ }
+
+ newDropDown() {
+ const dropDown = new Gtk.DropDown({
+ model: new Gio.ListStore({
+ item_type: DropDownItem,
+ }),
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ });
+ dropDown._isDropDown = true;
+ return dropDown;
+ }
+
+ newScale(adjustment) {
+ const scale = new Gtk.Scale({
+ orientation: Gtk.Orientation.HORIZONTAL,
+ draw_value: true,
+ has_origin: false,
+ value_pos: Gtk.PositionType.LEFT,
+ digits: 0,
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ vexpand: false,
+ });
+ scale.set_size_request(300, -1);
+ scale.set_adjustment(adjustment);
+ scale._isScale = true;
+ return scale;
+ }
+
+ newLabel(text = '') {
+ const label = new Gtk.Label({
+ label: text,
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ });
+ label._activatable = false;
+ return label;
+ }
+
+ newLinkButton(uri) {
+ const linkBtn = new Gtk.LinkButton({
+ uri,
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ icon_name: 'emblem-symbolic-link',
+ });
+ return linkBtn;
+ }
+
+ newButton() {
+ const btn = new Gtk.Button({
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ });
+
+ btn._activatable = true;
+ return btn;
+ }
+
+ newPresetButton(opt, profileIndex) {
+ const load = opt.loadProfile.bind(opt);
+ const save = opt.storeProfile.bind(opt);
+ const reset = opt.resetProfile.bind(opt);
+
+ const box = new Gtk.Box({
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ spacing: 8,
+ });
+ box.is_profile_box = true;
+
+ const entry = new Gtk.Entry({
+ width_chars: 40,
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ xalign: 0,
+ });
+ entry.set_text(opt.get(`profileName${profileIndex}`));
+ entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'edit-clear-symbolic');
+ entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, true);
+
+ const resetProfile = this.newButton();
+ resetProfile.set({
+ tooltip_text: _('Reset profile to defaults'),
+ icon_name: 'document-revert-symbolic',
+ hexpand: false,
+ css_classes: ['destructive-action'],
+ });
+
+ function setName() {
+ const ProfileNames = [
+ _('GNOME 3 Layout (Vertical WS)'),
+ _('GNOME 4x Layout, Bottom Hot Edge (Horizontal WS)'),
+ _('Top Left Hot Corner Centric (Vertical WS)'),
+ _('Dock-Like Overview, Bottom Hot Edge (Horizontal WS)'),
+ ];
+
+ let name = opt.get(`profileName${profileIndex}`, true);
+ if (!name)
+ name = ProfileNames[profileIndex - 1];
+ entry.set_text(name);
+ }
+
+ setName();
+
+ entry.connect('icon-press', e => e.set_text(''));
+ entry.connect('changed', e => opt.set(`profileName${profileIndex}`, e.get_text()));
+
+ resetProfile.connect('clicked', () => {
+ reset(profileIndex);
+ setName();
+ });
+ resetProfile._activatable = false;
+
+ const loadProfile = this.newButton();
+ loadProfile.set({
+ tooltip_text: _('Load profile'),
+ icon_name: 'view-refresh-symbolic',
+ hexpand: false,
+ });
+ loadProfile.connect('clicked', () => load(profileIndex));
+ loadProfile._activatable = false;
+
+ const saveProfile = this.newButton();
+ saveProfile.set({
+ tooltip_text: _('Save current settings into this profile'),
+ icon_name: 'document-save-symbolic',
+ hexpand: false,
+ });
+ saveProfile.connect('clicked', () => save(profileIndex));
+ saveProfile._activatable = false;
+
+ box.append(resetProfile);
+ box.append(entry);
+ box.append(saveProfile);
+ box.append(loadProfile);
+ return box;
+ }
+
+ newResetButton(callback) {
+ const btn = this.newButton();
+ btn.set({
+ css_classes: ['destructive-action'],
+ icon_name: 'edit-delete-symbolic',
+ });
+
+ btn.connect('clicked', callback);
+ btn._activatable = false;
+ return btn;
+ }
+
+ newOptionsResetButton() {
+ const btn = new Gtk.Button({
+ halign: Gtk.Align.END,
+ valign: Gtk.Align.CENTER,
+ hexpand: true,
+ css_classes: ['destructive-action'],
+ icon_name: 'document-revert-symbolic',
+ });
+
+ btn.connect('clicked', () => {
+ const settings = this._settings;
+ settings.list_keys().forEach(
+ key => settings.reset(key)
+ );
+ });
+ btn._activatable = false;
+ return btn;
+ }
+};
+
+export const AdwPrefs = class {
+ constructor(gOptions) {
+ Me.Opt = gOptions;
+ }
+
+ getFilledWindow(window, pages) {
+ for (let page of pages) {
+ const title = page.title;
+ const iconName = page.iconName;
+ const optionList = page.optionList;
+
+ window.add(
+ this._getAdwPage(optionList, {
+ title,
+ icon_name: iconName,
+ })
+ );
+ }
+
+ window.set_search_enabled(true);
+
+ return window;
+ }
+
+ _getAdwPage(optionList, pageProperties = {}) {
+ // pageProperties.width_request = 740;
+ const page = new Adw.PreferencesPage(pageProperties);
+ let group;
+ for (let item of optionList) {
+ // label can be plain text for Section Title
+ // or GtkBox for Option
+ const option = item[0];
+ const widget = item[1];
+ if (!widget) {
+ if (group)
+ page.add(group);
+
+ group = new Adw.PreferencesGroup({
+ title: option,
+ hexpand: true,
+ width_request: 700,
+ });
+ continue;
+ }
+
+ const row = new Adw.ActionRow({
+ title: option._title,
+ });
+
+ const grid = new Gtk.Grid({
+ column_homogeneous: false,
+ column_spacing: 20,
+ margin_start: 8,
+ margin_end: 8,
+ margin_top: 8,
+ margin_bottom: 8,
+ hexpand: true,
+ });
+ /* for (let i of item) {
+ box.append(i);*/
+ grid.attach(option, 0, 0, 1, 1);
+ if (widget)
+ grid.attach(widget, 1, 0, 1, 1);
+
+ row.set_child(grid);
+ if (widget._activatable === false)
+ row.activatable = false;
+ else
+ row.activatable_widget = widget;
+
+ group.add(row);
+ }
+ page.add(group);
+ return page;
+ }
+};
+
+const DropDownItem = GObject.registerClass({
+ // Registered name should be unique
+ GTypeName: `DropDownItem${Math.floor(Math.random() * 1000)}`,
+ Properties: {
+ 'text': GObject.ParamSpec.string(
+ 'text',
+ 'Text',
+ 'DropDown item text',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ 'id': GObject.ParamSpec.int(
+ 'id',
+ 'Id',
+ 'Item id stored in settings',
+ GObject.ParamFlags.READWRITE,
+ // min, max, default
+ -2147483648, 2147483647, 0
+ ),
+ },
+}, class DropDownItem extends GObject.Object {
+ get text() {
+ return this._text;
+ }
+
+ set text(text) {
+ this._text = text;
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ set id(id) {
+ this._id = id;
+ }
+});
diff --git a/extensions/47/vertical-workspaces/lib/osdWindow.js b/extensions/47/vertical-workspaces/lib/osdWindow.js
new file mode 100644
index 0000000..a06a331
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/osdWindow.js
@@ -0,0 +1,118 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * osdWindow.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as OsdWindow from 'resource:///org/gnome/shell/ui/osdWindow.js';
+
+let Me;
+let opt;
+
+let OsdPositions;
+
+export const OsdWindowModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+
+ OsdPositions = {
+ 1: {
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.START,
+ },
+ 2: {
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.START,
+ },
+ 3: {
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.START,
+ },
+ 4: {
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ },
+ 5: {
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.END,
+ },
+ 6: {
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.END,
+ },
+ 7: {
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.END,
+ },
+ };
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ OsdPositions = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('osdWindowModule');
+ 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(' OsdWindowModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('osdWindow', OsdWindow.OsdWindow.prototype, OsdWindowCommon);
+ console.debug(' OsdWindowModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+ this._updateExistingOsdWindows(6);
+
+ console.debug(' WorkspaceSwitcherPopupModule - Disabled');
+ }
+
+ _updateExistingOsdWindows(position) {
+ position = position ? position : opt.OSD_POSITION;
+ Main.osdWindowManager._osdWindows.forEach(osd => {
+ osd.set(OsdPositions[position]);
+ });
+ }
+};
+
+const OsdWindowCommon = {
+ after_show() {
+ if (!opt.OSD_POSITION)
+ this.opacity = 0;
+ this.set(OsdPositions[opt.OSD_POSITION]);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/overlayKey.js b/extensions/47/vertical-workspaces/lib/overlayKey.js
new file mode 100644
index 0000000..5ffd973
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/overlayKey.js
@@ -0,0 +1,170 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * overlayKey.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+import St from 'gi://St';
+import Meta from 'gi://Meta';
+import GObject from 'gi://GObject';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Overview from 'resource:///org/gnome/shell/ui/overview.js';
+
+let Me;
+let opt;
+
+export const OverlayKeyModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._originalOverlayKeyHandlerId = 0;
+ this._overlayKeyHandlerId = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('overlayKeyModule');
+ const conflict = false;
+ // Avoid modifying the overlay key if its configuration is consistent with the GNOME default
+ const defaultConfig = opt.OVERVIEW_MODE === 0 && opt.OVERLAY_KEY_PRIMARY === 2 && opt.OVERLAY_KEY_SECONDARY === 1;
+
+ reset = reset || !this.moduleEnabled || conflict || defaultConfig;
+
+ if (reset && !this._firstActivation) {
+ this._disableModule();
+ } else if (!reset) {
+ this._firstActivation = false;
+ this._activateModule();
+ }
+ if (reset && this._firstActivation)
+ console.debug(' OverlayKeyModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._originalOverlayKeyHandlerId) {
+ this._originalOverlayKeyHandlerId = GObject.signal_handler_find(global.display, { signalId: 'overlay-key' });
+ if (this._originalOverlayKeyHandlerId !== null) {
+ global.display.block_signal_handler(this._originalOverlayKeyHandlerId);
+ this._connectOverlayKey();
+ }
+ }
+ console.debug(' OverlayKeyModule - Activated');
+ }
+
+ _disableModule() {
+ this._restoreOverlayKeyHandler();
+
+ console.debug(' OverlayKeyModule - Disabled');
+ }
+
+ _restoreOverlayKeyHandler() {
+ // Disconnect modified overlay key handler
+ if (this._overlayKeyHandlerId) {
+ global.display.disconnect(this._overlayKeyHandlerId);
+ this._overlayKeyHandlerId = 0;
+ }
+
+ // Unblock original overlay key handler
+ if (this._originalOverlayKeyHandlerId) {
+ global.display.unblock_signal_handler(this._originalOverlayKeyHandlerId);
+ this._originalOverlayKeyHandlerId = 0;
+ }
+ }
+
+ _connectOverlayKey() {
+ if (this._overlayKeyHandlerId)
+ return;
+
+ this._overlayKeyHandlerId = global.display.connect('overlay-key', this._onOverlayKeyPressed.bind(Main.overview._overview.controls));
+ }
+
+ _onOverlayKeyPressed() {
+ if (this._a11ySettings.get_boolean('stickykeys-enable'))
+ return;
+
+ const { initialState, finalState, transitioning } =
+ this._stateAdjustment.getStateTransitionParams();
+
+ const time = GLib.get_monotonic_time() / 1000;
+ const timeDiff = time - this._lastOverlayKeyTime;
+ this._lastOverlayKeyTime = time;
+
+ const shouldShift = St.Settings.get().enable_animations
+ ? transitioning && finalState > initialState
+ : Main.overview.visible && timeDiff < Overview.ANIMATION_TIME;
+
+ const mode = opt.OVERLAY_KEY_SECONDARY;
+ if (shouldShift) {
+ Me.Util.activateSearchProvider('');
+ if (mode === 1) {
+ this._shiftState(Meta.MotionDirection.UP);
+ } else if (mode === 2) {
+ Me.Util.activateSearchProvider(Me.WSP_PREFIX);
+ } else if (mode === 3) {
+ // Changing the overview mode automatically changes the overview transition
+ opt.OVERVIEW_MODE = 0;
+ opt.OVERVIEW_MODE2 = false;
+ opt.WORKSPACE_MODE = 1;
+ }
+ } else {
+ if (Main.overview._shown) {
+ Main.overview.hide();
+ return;
+ }
+ switch (opt.OVERLAY_KEY_PRIMARY) {
+ case 0: // Disabled
+ return;
+ case 1: // Follow global overview mode
+ Main.overview.resetOverviewMode();
+ break;
+ case 2: // Default overview
+ opt.OVERVIEW_MODE = 0;
+ opt.OVERVIEW_MODE2 = false;
+ opt.WORKSPACE_MODE = 1;
+ break;
+ case 3: // App grid
+ if (Main.overview._shown)
+ Main.overview.hide();
+ else
+ Main.overview.show(2);
+ return;
+ case 4: // Static WS preview
+ opt.OVERVIEW_MODE = 1;
+ opt.OVERVIEW_MODE2 = false;
+ if (!Main.overview._shown)
+ opt.WORKSPACE_MODE = 0;
+ break;
+ case 5: // Static WS
+ opt.OVERVIEW_MODE = 2;
+ opt.OVERVIEW_MODE2 = true;
+ opt.WORKSPACE_MODE = 0;
+ break;
+ case 6: // Window Search
+ opt.OVERVIEW_MODE = 2;
+ opt.OVERVIEW_MODE2 = true;
+ if (!Main.overview._shown)
+ opt.WORKSPACE_MODE = 0;
+ break;
+ }
+ const customOverviewMode = !Main.overview._shown;
+ Main.overview.toggle(customOverviewMode);
+ if (opt.OVERLAY_KEY_PRIMARY === 6)
+ Me.Util.activateSearchProvider(Me.WSP_PREFIX);
+ }
+ }
+};
diff --git a/extensions/47/vertical-workspaces/lib/overview.js b/extensions/47/vertical-workspaces/lib/overview.js
new file mode 100644
index 0000000..30cc5db
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/overview.js
@@ -0,0 +1,162 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * overview.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+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';
+
+let Me;
+let opt;
+
+export const OverviewModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ 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(' OverviewModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('Overview', Overview.Overview.prototype, OverviewCommon);
+ console.debug(' OverviewModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ console.debug(' OverviewModule - Disabled');
+ }
+};
+
+const OverviewCommon = {
+ show(state = OverviewControls.ControlsState.WINDOW_PICKER, customOverviewMode) {
+ if (!customOverviewMode)
+ this.resetOverviewMode();
+
+ if (state === OverviewControls.ControlsState.HIDDEN)
+ throw new Error('Invalid state, use hide() to hide');
+
+ if (this.isDummy)
+ return;
+ if (this._shown)
+ return;
+ this._shown = true;
+
+ if (!this._syncGrab())
+ return;
+
+ Main.layoutManager.showOverview();
+ this._animateVisible(state);
+ },
+
+ toggle(customOverviewMode) {
+ if (this.isDummy)
+ return;
+
+ if (this._visible)
+ this.hide();
+ else
+ this.show(OverviewControls.ControlsState.WINDOW_PICKER, customOverviewMode);
+ },
+
+ resetOverviewMode() {
+ // reset Overview Mode do default
+ opt.OVERVIEW_MODE = opt.get('overviewMode');
+ opt.OVERVIEW_MODE2 = opt.OVERVIEW_MODE === 2;
+ opt.WORKSPACE_MODE = opt.OVERVIEW_MODE > 0 ? 0 : 1;
+ },
+
+ _showDone() {
+ this._animationInProgress = false;
+ this._coverPane.hide();
+
+ if (this._shownState !== 'SHOWN')
+ this._changeShownState('SHOWN');
+
+ // Handle any calls to hide* while we were showing
+ if (!this._shown)
+ this._animateNotVisible();
+
+ // if user activates overview during startup animation, transition needs to be shifted to the state 2 here
+ const controls = this._overview._controls;
+ if (controls._searchController._searchActive && controls._stateAdjustment.value === 1) {
+ if (opt.SEARCH_VIEW_ANIMATION)
+ controls._onSearchChanged();
+ else if (!opt.OVERVIEW_MODE2)
+ controls._stateAdjustment.value = 2;
+ }
+
+ this._syncGrab();
+ },
+
+ // Workaround - should probably be fixed elsewhere in the upstream code
+ // If a new window is opened from the overview
+ // and is realized before the overview animation is complete,
+ // the new window will not get focus
+ after__hideDone() {
+ if (!opt.FIX_NEW_WINDOW_FOCUS)
+ return;
+
+ const workspace = global.workspace_manager.get_active_workspace();
+ const recentDesktopWin = global.display.get_tab_list(1, workspace)[0];
+ let recentNormalWin = null;
+ const tabList = global.display.get_tab_list(0, workspace);
+
+ for (let i = 0; i < tabList.length; i++) {
+ if (tabList[i].minimized === false) {
+ recentNormalWin = tabList[i];
+ break;
+ }
+ }
+
+ let recentWin = recentNormalWin;
+ if (recentNormalWin && recentDesktopWin) {
+ recentWin = recentNormalWin.get_user_time() > recentDesktopWin.get_user_time()
+ ? recentNormalWin
+ : recentDesktopWin;
+ }
+
+ const focusedWin = global.display.focus_window;
+
+ if (recentWin && focusedWin !== recentWin)
+ recentWin.activate(global.get_current_time());
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/overviewControls.js b/extensions/47/vertical-workspaces/lib/overviewControls.js
new file mode 100644
index 0000000..c5a74f1
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/overviewControls.js
@@ -0,0 +1,1633 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * overviewControls.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @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;
+ }
+}
diff --git a/extensions/47/vertical-workspaces/lib/panel.js b/extensions/47/vertical-workspaces/lib/panel.js
new file mode 100644
index 0000000..ba6d01a
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/panel.js
@@ -0,0 +1,253 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * panel.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Overview from 'resource:///org/gnome/shell/ui/overview.js';
+
+let Me;
+let opt;
+
+const ANIMATION_TIME = Overview.ANIMATION_TIME;
+
+export const PanelModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+
+ this._showingOverviewConId = 0;
+ this._hidingOverviewConId = 0;
+ this._styleChangedConId = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('panelModule');
+ const conflict = Me.Util.getEnabledExtensions('dash-to-panel').length ||
+ Me.Util.getEnabledExtensions('hidetopbar').length;
+
+ if (conflict && !reset)
+ console.warn(`[${Me.metadata.name}] Warning: "Panel" module disabled due to potential conflict with another extension`);
+
+ reset = reset || !this.moduleEnabled || conflict;
+
+ this.moduleEnabled = !reset;
+
+ // don't touch 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(' PanelModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ const panelBox = Main.layoutManager.panelBox;
+
+ this._setPanelPosition();
+ this._updateStyleChangedConnection();
+
+ if (!opt.PANEL_MODE) {
+ this._updateOverviewConnection(true);
+ this._reparentPanel(false);
+ panelBox.translation_y = 0;
+ Main.panel.opacity = 255;
+ this._setPanelStructs(true);
+ } else if (opt.PANEL_OVERVIEW_ONLY) {
+ if (opt.SHOW_WS_PREVIEW_BG) {
+ this._reparentPanel(true);
+ if (opt.OVERVIEW_MODE2) {
+ // in OM2 if the panel has been moved to the overviewGroup move panel above all
+ Main.layoutManager.overviewGroup.set_child_above_sibling(panelBox, null);
+ this._updateOverviewConnection();
+ } else {
+ this._updateOverviewConnection(true);
+ }
+ this._showPanel(true);
+ } else {
+ // if ws preview bg is disabled, panel can stay in uiGroup
+ this._reparentPanel(false);
+ this._showPanel(false);
+ this._updateOverviewConnection();
+ }
+ // _connectPanel();
+ } else if (opt.PANEL_DISABLED) {
+ this._updateOverviewConnection(true);
+ this._reparentPanel(false);
+ this._showPanel(false);
+ // _connectPanel();
+ }
+ this._setPanelStructs(!opt.PANEL_MODE);
+ Main.layoutManager._updateHotCorners();
+
+ this._overrides.addOverride('ActivitiesButton', Main.panel.statusArea.activities, ActivitiesButton);
+
+ console.debug(' PanelModule - Activated');
+ }
+
+ _disableModule() {
+ const reset = true;
+ this._setPanelPosition(reset);
+ this._updateOverviewConnection(reset);
+ this._reparentPanel(false);
+
+ this._updateStyleChangedConnection(reset);
+
+ const panelBox = Main.layoutManager.panelBox;
+ panelBox.translation_y = 0;
+ Main.panel.opacity = 255;
+ this._setPanelStructs(true);
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ console.debug(' PanelModule - Disabled');
+ }
+
+ _setPanelPosition(reset = false) {
+ const geometry = global.display.get_monitor_geometry(global.display.get_primary_monitor());
+ const panelBox = Main.layoutManager.panelBox;
+ const panelHeight = Main.panel.height; // panelBox height can be 0 after shell start
+
+ if (opt.PANEL_POSITION_TOP || reset)
+ panelBox.set_position(geometry.x, geometry.y);
+ else
+ panelBox.set_position(geometry.x, geometry.y + geometry.height - panelHeight);
+ }
+
+ _updateStyleChangedConnection(reset = false) {
+ if (reset) {
+ if (this._styleChangedConId) {
+ Main.panel.disconnect(this._styleChangedConId);
+ this._styleChangedConId = 0;
+ }
+ } else if (!this._styleChangedConId) {
+ this._styleChangedConId = Main.panel.connect('style-changed', () => {
+ if (opt.PANEL_OVERVIEW_ONLY && !opt.OVERVIEW_MODE2)
+ Main.panel.add_style_pseudo_class('overview');
+ else if (opt.OVERVIEW_MODE2)
+ Main.panel.remove_style_pseudo_class('overview');
+ });
+ }
+ }
+
+ _updateOverviewConnection(reset = false) {
+ if (reset) {
+ if (this._hidingOverviewConId) {
+ Main.overview.disconnect(this._hidingOverviewConId);
+ this._hidingOverviewConId = 0;
+ }
+ if (this._showingOverviewConId) {
+ Main.overview.disconnect(this._showingOverviewConId);
+ this._showingOverviewConId = 0;
+ }
+ } else {
+ if (!this._hidingOverviewConId) {
+ this._hidingOverviewConId = Main.overview.connect('hiding', () => {
+ if (!opt.SHOW_WS_PREVIEW_BG || opt.OVERVIEW_MODE2)
+ this._showPanel(false);
+ });
+ }
+ if (!this._showingOverviewConId) {
+ this._showingOverviewConId = Main.overview.connect('showing', () => {
+ if (Main.layoutManager._startingUp)
+ return;
+ if (!opt.SHOW_WS_PREVIEW_BG || opt.OVERVIEW_MODE2 || Main.layoutManager.panelBox.translation_y)
+ this._showPanel(true);
+ });
+ }
+ }
+ }
+
+ _reparentPanel(reparent = false) {
+ const panel = Main.layoutManager.panelBox;
+ if (reparent && panel.get_parent() === Main.layoutManager.uiGroup && !Main.sessionMode.isLocked) {
+ Main.layoutManager.uiGroup.remove_child(panel);
+ Main.layoutManager.overviewGroup.add_child(panel);
+ } else if ((!reparent || Main.sessionMode.isLocked) && panel.get_parent() === Main.layoutManager.overviewGroup) {
+ Main.layoutManager.overviewGroup.remove_child(panel);
+ // return the panel at default position, panel shouldn't cover objects that should be above
+ Main.layoutManager.uiGroup.insert_child_at_index(panel, 4);
+ }
+ }
+
+ _setPanelStructs(state) {
+ Main.layoutManager._trackedActors.forEach(a => {
+ if (a.actor === Main.layoutManager.panelBox)
+ a.affectsStruts = state;
+ });
+
+ // workaround to force maximized windows to resize after removing affectsStruts
+ // simulation of minimal swipe gesture to the opposite direction
+ // todo - needs better solution!!!!!!!!!!!
+ // const direction = _getAppGridAnimationDirection() === 2 ? 1 : -1;
+ // Main.overview._swipeTracker._beginTouchSwipe(null, global.get_current_time(), 1, 1);
+ // Main.overview._swipeTracker._updateGesture(null, global.get_current_time(), direction, 1);
+ // GLib.timeout_add(0, 50, () => Main.overview._swipeTracker._endGesture(global.get_current_time(), 1, true));*/
+ }
+
+ _showPanel(show = true) {
+ if (show) {
+ Main.panel.opacity = 255;
+ Main.layoutManager.panelBox.ease({
+ duration: ANIMATION_TIME,
+ translation_y: 0,
+ onComplete: () => {
+ this._setPanelStructs(!opt.PANEL_MODE);
+ },
+ });
+ } else if (!Main.layoutManager._startingUp) {
+ const panelHeight = Main.panel.height;
+ Main.layoutManager.panelBox.ease({
+ duration: ANIMATION_TIME,
+ translation_y: opt.PANEL_POSITION_TOP ? -panelHeight + 1 : panelHeight - 1,
+ onComplete: () => {
+ Main.panel.opacity = 0;
+ this._setPanelStructs(!opt.PANEL_MODE);
+ },
+ });
+ }
+ }
+};
+
+const ActivitiesButton = {
+ vfunc_event(event) {
+ if (event.type() === Clutter.EventType.TOUCH_END ||
+ event.type() === Clutter.EventType.BUTTON_RELEASE) {
+ if (Main.overview.shouldToggleByCornerOrButton()) {
+ if (event.get_button() === Clutter.BUTTON_SECONDARY && !Main.overview.dash.showAppsButton.checked) {
+ Main.overview.show(2);
+ Main.overview.dash.showAppsButton.checked = true;
+ } else {
+ Main.overview.toggle();
+ }
+ }
+ }
+
+ return Main.wm.handleWorkspaceScroll(event);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/recentFilesSearchProvider.js b/extensions/47/vertical-workspaces/lib/recentFilesSearchProvider.js
new file mode 100644
index 0000000..f050cf9
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/recentFilesSearchProvider.js
@@ -0,0 +1,316 @@
+/**
+ * Vertical Workspaces
+ * recentFilesSearchProvider.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+import St from 'gi://St';
+import Gio from 'gi://Gio';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+let Me;
+let opt;
+// gettext
+let _;
+
+// prefix helps to eliminate results from other search providers
+// so it needs to be something less common
+// needs to be accessible from vw module
+export const PREFIX = 'fq//';
+const ID = 'recent-files';
+
+export const RecentFilesSearchProviderModule = class {
+ // export for other modules
+ static _PREFIX = PREFIX;
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+ _ = Me.gettext;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._recentFilesSearchProvider = null;
+ this._enableTimeoutId = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ _ = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('recentFilesSearchProviderModule');
+
+ reset = reset || !this.moduleEnabled;
+
+ if (reset && !this._firstActivation) {
+ this._disableModule();
+ } else if (!reset) {
+ this._firstActivation = false;
+ this._activateModule();
+ }
+ if (reset && this._firstActivation)
+ console.debug(' RecentFilesSearchProviderModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ // delay because Fedora had problem to register a new provider soon after Shell restarts
+ this._enableTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ 2000,
+ () => {
+ if (!this._recentFilesSearchProvider) {
+ this._recentFilesSearchProvider = new RecentFilesSearchProvider();
+ this._registerProvider(this._recentFilesSearchProvider);
+ }
+ this._enableTimeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+
+ console.debug(' RecentFilesSearchProviderModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._recentFilesSearchProvider) {
+ this._unregisterProvider(this._recentFilesSearchProvider);
+ this._recentFilesSearchProvider = null;
+ }
+ if (this._enableTimeoutId) {
+ GLib.source_remove(this._enableTimeoutId);
+ this._enableTimeoutId = 0;
+ }
+
+ console.debug(' RecentFilesSearchProviderModule - Disabled');
+ }
+
+ _registerProvider(provider) {
+ const searchResults = Main.overview.searchController._searchResults;
+ provider.searchInProgress = false;
+
+ searchResults._providers.push(provider);
+
+ // create results display and add it to the _content
+ searchResults._ensureProviderDisplay.bind(searchResults)(provider);
+ }
+
+ _unregisterProvider(provider) {
+ const searchResults = Main.overview.searchController._searchResults;
+ searchResults._unregisterProvider(provider);
+ }
+};
+
+class RecentFilesSearchProvider {
+ constructor() {
+ this.id = ID;
+ const appId = 'org.gnome.Nautilus.desktop';
+
+ // A real appInfo created from a commandline has often issues with overriding get_id() method, so we use dict instead
+ this.appInfo = {
+ get_id: () => appId,
+ get_name: () => _('Recent Files'),
+ get_icon: () => Gio.icon_new_for_string('focus-windows-symbolic'),
+ should_show: () => true,
+ get_commandline: () => '/usr/bin/nautilus -w recent:///',
+ launch: () => {},
+ };
+
+ this.canLaunchSearch = true;
+ this.isRemoteProvider = false;
+
+ this._recentFilesManager = new RecentFilesManager();
+ }
+
+ getInitialResultSet(terms/* , cancellable*/) {
+ const rfm = this._recentFilesManager;
+ rfm.loadFromFile();
+
+ const uris = rfm.getUris();
+ const dict = {};
+ for (let uri of uris) {
+ dict[uri] = {};
+ dict[uri]['uri'] = uri;
+ dict[uri]['path'] = rfm.getPath(uri);
+ dict[uri]['filename'] = rfm.getDisplayName(uri);
+ dict[uri]['dir'] = rfm.getDirPath(uri);
+ dict[uri]['age'] = rfm.getAge(uri);
+ dict[uri]['appInfo'] = rfm.getDefaultAppAppInfo(uri);
+ }
+ this.files = dict;
+
+ return new Promise(resolve => resolve(this._getResultSet(terms)));
+ }
+
+ _getResultSet(terms) {
+ if (!terms[0].startsWith(PREFIX))
+ return [];
+ // do not modify original terms
+ let termsCopy = [...terms];
+ // search for terms without prefix
+ termsCopy[0] = termsCopy[0].replace(PREFIX, '');
+
+ const candidates = Object.values(this.files);
+ const _terms = [].concat(termsCopy);
+
+ const term = _terms.join(' ');
+
+ const results = [];
+ let m;
+ for (let file of candidates) {
+ if (opt.SEARCH_FUZZY)
+ m = Me.Util.fuzzyMatch(term, file.filename);
+ else
+ m = Me.Util.strictMatch(term, file.filename);
+
+ if (m !== -1)
+ results.push(file);
+ }
+
+ results.sort((a, b) => a.age > b.age);
+
+ const resultIds = results.map(item => item.uri);
+ return resultIds;
+ }
+
+ getResultMetas(resultIds/* , callback = null*/) {
+ const metas = resultIds.map(id => this.getResultMeta(id));
+ return new Promise(resolve => resolve(metas));
+ }
+
+ getResultMeta(resultId) {
+ const result = this.files[resultId];
+ return {
+ 'id': resultId,
+ 'name': `${Math.floor(result.age)}: ${result.filename}`,
+ 'description': `${result.dir}`,
+ 'createIcon': size =>
+ this._recentFilesManager.getDefaultAppIcon(resultId, size),
+ };
+ }
+
+ launchSearch(terms, timeStamp) {
+ const appInfo = Gio.AppInfo.create_from_commandline('/usr/bin/nautilus -w recent:///', 'Nautilus', null);
+ appInfo.launch([], global.create_app_launch_context(timeStamp, -1));
+
+ // unlike on 42, on 44 if a window with the same uri is already open it will not get focus/activation
+ // Gio.app_info_launch_default_for_uri('recent:///', global.create_app_launch_context(timeStamp, -1));
+
+ // following solution for some reason ignores the recent:/// uri
+ // this.appInfo.launch_uris(['recent:///'], global.create_app_launch_context(timeStamp, -1));
+ }
+
+ activateResult(resultId, terms, timeStamp) {
+ const uri = resultId;
+ const context = global.create_app_launch_context(timeStamp, -1);
+ if (Me.Util.isShiftPressed()) {
+ Main.overview.toggle();
+ this.appInfo.launch_uris([uri], context);
+ } else if (Gio.app_info_launch_default_for_uri(uri, context)) {
+ // update recent list after successful activation
+ this._recentFilesManager.updateAdded(resultId);
+ this._recentFilesManager.saveToFile();
+ } else {
+ this.appInfo.launch_uris([uri], context);
+ }
+ }
+
+ filterResults(results /* , maxResults*/) {
+ // return results.slice(0, maxResults);
+ return results.slice(0, 20);
+ }
+
+ getSubsearchResultSet(previousResults, terms/* , cancellable*/) {
+ return this.getInitialResultSet(terms);
+ }
+}
+
+class RecentFilesManager {
+ constructor(path) {
+ path = path ?? GLib.build_filenamev([GLib.get_user_data_dir(), 'recently-used.xbel']);
+ this._recentlyUsedPath = path;
+ this._bookmarks = new GLib.BookmarkFile();
+ }
+
+ loadFromFile() {
+ try {
+ this._bookmarks.load_from_file(this._recentlyUsedPath);
+ } catch (e) {
+ if (!e.matches(GLib.BookmarkFileError, GLib.BookmarkFileError.FILE_NOT_FOUND))
+ console.error(`Could not open recent files: ${e.message}`);
+ }
+ }
+
+ saveToFile() {
+ try {
+ this._bookmarks.to_file(this._recentlyUsedPath);
+ } catch (e) {
+ if (!e.matches(GLib.BookmarkFileError, GLib.BookmarkFileError.FILE_NOT_FOUND))
+ console.error(`Could not open recent files to save data: ${e.message}`);
+ }
+ }
+
+ getUris() {
+ return this._bookmarks.get_uris();
+ }
+
+ getPath(uri) {
+ // GLib.filename_from_uri() removes uri schema and converts string to utf-8
+ return GLib.filename_from_uri(uri)[0]; // result is array
+ }
+
+ getDisplayName(uri) {
+ const path = this.getPath(uri);
+ return GLib.filename_display_basename(path);
+ }
+
+ getDirPath(uri) {
+ const path = this.getPath(uri);
+ const filename = this.getDisplayName(uri);
+ return path.replace(`${filename}`, '');
+ }
+
+ getMimeType(uri) {
+ return this._bookmarks.get_mime_type(uri);
+ }
+
+ getAdded(uri) {
+ return this._bookmarks.get_added(uri);
+ }
+
+ updateAdded(uri) {
+ this._bookmarks.set_added_date_time(uri, GLib.DateTime.new_now_local());
+ }
+
+ // age in days (float)
+ getAge(uri) {
+ return (Date.now() / 1000 - this._bookmarks.get_added(uri)) / 60 / 60 / 24;
+ }
+
+ getDefaultAppAppInfo(uri) {
+ const mimeType = this.getMimeType(uri);
+ return Gio.AppInfo.get_default_for_type(mimeType, false);
+ }
+
+ getDefaultAppIcon(uri, size) {
+ let icon, gicon;
+
+ const appInfo = this.getDefaultAppAppInfo(uri);
+ if (appInfo)
+ gicon = appInfo.get_icon();
+
+ if (gicon)
+ icon = new St.Icon({ gicon, icon_size: size });
+ else
+ icon = new St.Icon({ icon_name: 'icon-missing', icon_size: size });
+
+ return icon;
+ }
+}
diff --git a/extensions/47/vertical-workspaces/lib/search.js b/extensions/47/vertical-workspaces/lib/search.js
new file mode 100644
index 0000000..47198a7
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/search.js
@@ -0,0 +1,474 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * search.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+import Shell from 'gi://Shell';
+import GObject from 'gi://GObject';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Search from 'resource:///org/gnome/shell/ui/search.js';
+import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
+
+import * as SystemActions from 'resource:///org/gnome/shell/misc/systemActions.js';
+import { Highlighter } from 'resource:///org/gnome/shell/misc/util.js';
+
+let Me;
+// gettext
+let _;
+let opt;
+
+const SEARCH_MAX_WIDTH = 1092;
+
+export const SearchModule = 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.moduleEnabled = opt.get('searchModule');
+ 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(' SearchModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ this._updateSearchViewWidth();
+
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('AppSearchProvider', AppDisplay.AppSearchProvider.prototype, AppSearchProvider);
+ this._overrides.addOverride('SearchResult', Search.SearchResult.prototype, SearchResult);
+ this._overrides.addOverride('SearchResultsView', Search.SearchResultsView.prototype, SearchResultsView);
+ this._overrides.addOverride('ListSearchResults', Search.ListSearchResults.prototype, ListSearchResults);
+ this._overrides.addOverride('ListSearchResult', Search.ListSearchResult.prototype, ListSearchResultOverride);
+ this._overrides.addOverride('Highlighter', Highlighter.prototype, HighlighterOverride);
+
+ // Don't expand the search view vertically and align it to the top
+ // this is important in the static workspace mode when the search view bg is not transparent
+ // also the "Searching..." and "No Results" notifications will be closer to the search entry, with the distance given by margin-top in the stylesheet
+ Main.overview.searchController.y_align = Clutter.ActorAlign.START;
+ // Increase the maxResults for app search so that it can show more results in case the user decreases the size of the result icon
+ const appSearchDisplay = Main.overview.searchController._searchResults._providers.filter(p => p.id === 'applications')[0]?.display;
+ if (appSearchDisplay)
+ appSearchDisplay._maxResults = 12;
+ console.debug(' SearchModule - Activated');
+ }
+
+ _disableModule() {
+ const reset = true;
+
+ const searchResults = Main.overview.searchController._searchResults;
+ if (searchResults?._searchTimeoutId) {
+ GLib.source_remove(searchResults._searchTimeoutId);
+ searchResults._searchTimeoutId = 0;
+ }
+
+ this._updateSearchViewWidth(reset);
+
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ Main.overview.searchController.y_align = Clutter.ActorAlign.FILL;
+
+ console.debug(' WorkspaceSwitcherPopupModule - Disabled');
+ }
+
+ _updateSearchViewWidth(reset = false) {
+ const searchContent = Main.overview.searchController._searchResults._content;
+
+ if (reset) {
+ searchContent.set_style('');
+ } else {
+ let width = SEARCH_MAX_WIDTH;
+ if (Me.Util.monitorHasLowResolution())
+ width = Math.round(width * 0.8);
+ width = Math.round(width * opt.SEARCH_VIEW_SCALE);
+ searchContent.set_style(`max-width: ${width}px;`);
+ }
+ }
+};
+
+const ListSearchResults = {
+ _getMaxDisplayedResults() {
+ return opt.SEARCH_MAX_ROWS;
+ },
+};
+
+// AppDisplay.AppSearchProvider
+const AppSearchProvider = {
+ getInitialResultSet(terms, cancellable) {
+ // Defer until the parental controls manager is initialized, so the
+ // results can be filtered correctly.
+ if (!this._parentalControlsManager.initialized) {
+ return new Promise(resolve => {
+ let initializedId = this._parentalControlsManager.connect('app-filter-changed', async () => {
+ if (this._parentalControlsManager.initialized) {
+ this._parentalControlsManager.disconnect(initializedId);
+ resolve(await this.getInitialResultSet(terms, cancellable));
+ }
+ });
+ });
+ }
+
+ const pattern = terms.join(' ');
+
+ let appInfoList = Shell.AppSystem.get_default().get_installed();
+
+ let weightList = {};
+ appInfoList = appInfoList.filter(appInfo => {
+ try {
+ appInfo.get_id(); // catch invalid file encodings
+ } catch (e) {
+ return false;
+ }
+
+ let string = '';
+ let name;
+ let shouldShow = false;
+ if (appInfo.get_display_name) {
+ // show only launchers that should be visible in this DE
+ shouldShow = appInfo.should_show() && this._parentalControlsManager.shouldShowApp(appInfo);
+
+ if (shouldShow) {
+ let id = appInfo.get_id().split('.');
+ id = id[id.length - 2] || '';
+ let baseName = appInfo.get_string('Name') || '';
+ let dispName = appInfo.get_display_name() || '';
+ let gName = appInfo.get_generic_name() || '';
+ let description = appInfo.get_description() || '';
+ let categories = appInfo.get_string('Categories')?.replace(/;/g, ' ') || '';
+ let keywords = appInfo.get_string('Keywords')?.replace(/;/g, ' ') || '';
+ name = `${dispName} ${id}`;
+ string = `${dispName} ${gName} ${baseName} ${description} ${categories} ${keywords} ${id}`;
+ }
+ }
+
+ let m = -1;
+ if (shouldShow && opt.SEARCH_FUZZY) {
+ m = Me.Util.fuzzyMatch(pattern, name);
+ m = (m + Me.Util.strictMatch(pattern, string)) / 2;
+ } else if (shouldShow) {
+ m = Me.Util.strictMatch(pattern, string);
+ }
+
+ if (m !== -1)
+ weightList[appInfo.get_id()] = m;
+
+ return shouldShow && (m !== -1);
+ });
+
+ appInfoList.sort((a, b) => weightList[a.get_id()] > weightList[b.get_id()]);
+
+ const usage = Shell.AppUsage.get_default();
+ // sort apps by usage list
+ appInfoList.sort((a, b) => usage.compare(a.get_id(), b.get_id()));
+ // prefer apps where any word in their name starts with the pattern
+ appInfoList.sort((a, b) => Me.Util.isMoreRelevant(a.get_display_name(), b.get_display_name(), pattern));
+
+ let results = appInfoList.map(app => app.get_id());
+
+ if (opt.SEARCH_APP_GRID_MODE && Main.overview.dash.showAppsButton.checked)
+ this._filterAppGrid(results);
+
+ results = results.concat(this._systemActions.getMatchingActions(terms));
+
+ return new Promise(resolve => resolve(results));
+ },
+
+ _filterAppGrid(results) {
+ const icons = Main.overview._overview.controls._appDisplay._orderedItems;
+ icons.forEach(icon => {
+ icon.visible = results.includes(icon.id);
+ });
+ },
+
+ // App search result size
+ createResultObject(resultMeta) {
+ let iconSize = opt.SEARCH_ICON_SIZE;
+ if (!iconSize) {
+ iconSize = Me.Util.monitorHasLowResolution()
+ ? 64
+ : 96;
+ }
+
+ if (resultMeta.id.endsWith('.desktop')) {
+ const icon = new AppDisplay.AppIcon(this._appSys.lookup_app(resultMeta['id']), {
+ expandTitleOnHover: false,
+ });
+ icon.icon.setIconSize(iconSize);
+ return icon;
+ } else {
+ this._iconSize = iconSize;
+ return new SystemActionIcon(this, resultMeta);
+ }
+ },
+};
+
+const SystemActionIcon = GObject.registerClass({
+ // Registered name should be unique
+ GTypeName: `SystemAction${Math.floor(Math.random() * 1000)}`,
+}, class SystemActionIcon extends Search.GridSearchResult {
+ _init(provider, metaInfo, resultsView) {
+ super._init(provider, metaInfo, resultsView);
+ if (!Clutter.Container)
+ this.add_style_class_name('grid-search-result-46');
+ this.icon._setSizeManually = true;
+ this.icon.setIconSize(provider._iconSize);
+ }
+
+ activate() {
+ SystemActions.getDefault().activateAction(this.metaInfo['id']);
+ Main.overview.hide();
+ }
+});
+
+const SearchResult = {
+ activate() {
+ this.provider.activateResult(this.metaInfo.id, this._resultsView.terms);
+
+ if (this.metaInfo.clipboardText) {
+ St.Clipboard.get_default().set_text(
+ St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText);
+ }
+ // don't close overview if Shift key is pressed - Shift moves windows to the workspace
+ if (!Me.Util.isShiftPressed())
+ Main.overview.toggle();
+ },
+};
+
+const SearchResultsView = {
+ setTerms(terms) {
+ // Check for the case of making a duplicate previous search before
+ // setting state of the current search or cancelling the search.
+ // This will prevent incorrect state being as a result of a duplicate
+ // search while the previous search is still active.
+ let searchString = terms.join(' ');
+ let previousSearchString = this._terms.join(' ');
+ if (searchString === previousSearchString)
+ return;
+
+ this._startingSearch = true;
+
+ this._cancellable.cancel();
+ this._cancellable.reset();
+
+ if (terms.length === 0) {
+ this._reset();
+ return;
+ }
+
+ let isSubSearch = false;
+ if (this._terms.length > 0)
+ isSubSearch = searchString.indexOf(previousSearchString) === 0;
+
+ this._terms = terms;
+ this._isSubSearch = isSubSearch;
+ this._updateSearchProgress();
+
+ if (!this._searchTimeoutId)
+ this._searchTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, opt.SEARCH_DELAY, this._onSearchTimeout.bind(this));
+
+ this._highlighter = new Highlighter(this._terms);
+
+ this.emit('terms-changed');
+ },
+
+ _doSearch() {
+ this._startingSearch = false;
+
+ let previousResults = this._results;
+ this._results = {};
+
+ const term0 = this._terms[0];
+ const onlySupportedProviders = term0.startsWith(Me.WSP_PREFIX) || term0.startsWith(Me.ESP_PREFIX) || term0.startsWith(Me.RFSP_PREFIX);
+
+ this._providers.forEach(provider => {
+ const supportedProvider = ['open-windows', 'extensions', 'recent-files'].includes(provider.id);
+ if (!onlySupportedProviders || (onlySupportedProviders && supportedProvider)) {
+ let previousProviderResults = previousResults[provider.id];
+ this._doProviderSearch(provider, previousProviderResults);
+ } else {
+ // hide unwanted providers, they will show() automatically when needed
+ provider.display.visible = false;
+ }
+ });
+
+ this._updateSearchProgress();
+ this._clearSearchTimeout();
+ },
+
+ _updateSearchProgress() {
+ let haveResults = this._providers.some(provider => {
+ let display = provider.display;
+ return display.getFirstResult() !== null;
+ });
+
+ this._scrollView.visible = haveResults;
+ this._statusBin.visible = !haveResults;
+
+ if (!haveResults) {
+ if (this.searchInProgress)
+ this._statusText.set_text(_('Searching…'));
+ else
+ this._statusText.set_text(_('No results.'));
+ }
+ },
+
+ _highlightFirstVisibleAppGridIcon() {
+ const appDisplay = Main.overview._overview.controls._appDisplay;
+ // appDisplay.grab_key_focus();
+ for (const icon of appDisplay._orderedItems) {
+ if (icon.visible) {
+ appDisplay.selectApp(icon.id);
+ break;
+ }
+ }
+ },
+
+ _maybeSetInitialSelection() {
+ if (opt.SEARCH_APP_GRID_MODE && Main.overview.dash.showAppsButton.checked) {
+ this._highlightFirstVisibleAppGridIcon();
+ return;
+ }
+
+ let newDefaultResult = null;
+
+ let providers = this._providers;
+ for (let i = 0; i < providers.length; i++) {
+ let provider = providers[i];
+ let display = provider.display;
+
+ if (!display.visible)
+ continue;
+
+ let firstResult = display.getFirstResult();
+ if (firstResult) {
+ newDefaultResult = firstResult;
+ break; // select this one!
+ }
+ }
+
+ if (newDefaultResult !== this._defaultResult) {
+ this._setSelected(this._defaultResult, false);
+ this._setSelected(newDefaultResult, this._highlightDefault);
+
+ this._defaultResult = newDefaultResult;
+ }
+ },
+
+ highlightDefault(highlight) {
+ if (opt.SEARCH_APP_GRID_MODE && Main.overview.dash.showAppsButton.checked) {
+ if (highlight)
+ this._highlightFirstVisibleAppGridIcon();
+ } else {
+ this._highlightDefault = highlight;
+ this._setSelected(this._defaultResult, highlight);
+ }
+ },
+};
+
+// Add highlighting of the "name" part of the result for all providers
+const ListSearchResultOverride = {
+ _highlightTerms() {
+ let markup = this._resultsView.highlightTerms(this.metaInfo['name']);
+ this.label_actor.clutter_text.set_markup(markup);
+ markup = this._resultsView.highlightTerms(this.metaInfo['description'].split('\n')[0]);
+ this._descriptionLabel.clutter_text.set_markup(markup);
+ },
+};
+
+const HighlighterOverride = {
+ /**
+ * @param {?string[]} terms - list of terms to highlight
+ */
+ /* constructor(terms) {
+ if (!terms)
+ return;
+
+ const escapedTerms = terms
+ .map(term => Shell.util_regex_escape(term))
+ .filter(term => term.length > 0);
+
+ if (escapedTerms.length === 0)
+ return;
+
+ this._highlightRegex = new RegExp(
+ `(${escapedTerms.join('|')})`, 'gi');
+ },*/
+
+ /**
+ * Highlight all occurences of the terms defined for this
+ * highlighter in the provided text using markup.
+ *
+ * @param {string} text - text to highlight the defined terms in
+ * @returns {string}
+ */
+ highlight(text, options) {
+ if (!this._highlightRegex)
+ return GLib.markup_escape_text(text, -1);
+
+ // force use local settings if the class is overridden by another extension (WSP, ESP)
+ const o = options || opt;
+ let escaped = [];
+ let lastMatchEnd = 0;
+ let match;
+ let style = ['', ''];
+ if (o.HIGHLIGHT_DEFAULT)
+ style = ['<b>', '</b>'];
+ // The default highlighting by the bold style causes text to be "randomly" ellipsized in cases where it's not necessary
+ // and also blurry
+ // Underscore doesn't affect label size and all looks better
+ else if (o.HIGHLIGHT_UNDERLINE)
+ style = ['<u>', '</u>'];
+
+ while ((match = this._highlightRegex.exec(text))) {
+ if (match.index > lastMatchEnd) {
+ let unmatched = GLib.markup_escape_text(
+ text.slice(lastMatchEnd, match.index), -1);
+ escaped.push(unmatched);
+ }
+ let matched = GLib.markup_escape_text(match[0], -1);
+ escaped.push(`${style[0]}${matched}${style[1]}`);
+ lastMatchEnd = match.index + match[0].length;
+ }
+ let unmatched = GLib.markup_escape_text(
+ text.slice(lastMatchEnd), -1);
+ escaped.push(unmatched);
+ return escaped.join('');
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/searchController.js b/extensions/47/vertical-workspaces/lib/searchController.js
new file mode 100644
index 0000000..1722f15
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/searchController.js
@@ -0,0 +1,94 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * searchController.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+let Me;
+let opt;
+
+export const SearchControllerModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._originalOnStageKeyPress = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('searchControllerModule');
+ 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(' SearchControllerModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._originalOnStageKeyPress)
+ this._originalOnStageKeyPress = Main.overview.searchController._onStageKeyPress;
+
+ Main.overview.searchController._onStageKeyPress = SearchControllerCommon._onStageKeyPress;
+ console.debug(' SearchControllerModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._originalOnStageKeyPress)
+ Main.overview.searchController._onStageKeyPress = this._originalOnStageKeyPress;
+ this._originalOnStageKeyPress = null;
+
+ console.debug(' SearchControlerModule - Disabled');
+ }
+};
+
+// if opt.ESC_BEHAVIOR > 0 force close the overview
+const SearchControllerCommon = {
+ _onStageKeyPress(actor, event) {
+ // Ignore events while anything but the overview has
+ // pushed a modal (system modals, looking glass, ...)
+ if (Main.modalCount > 1)
+ return Clutter.EVENT_PROPAGATE;
+
+ let symbol = event.get_key_symbol();
+ if (symbol === Clutter.KEY_Escape) {
+ if (this._searchActive && !opt.ESC_BEHAVIOR) {
+ this.reset();
+ } else if (this._showAppsButton.checked && !opt.ESC_BEHAVIOR) {
+ this._showAppsButton.checked = false;
+ } else {
+ this.reset();
+ Main.overview.hide();
+ }
+
+ return Clutter.EVENT_STOP;
+ } else if (this._shouldTriggerSearch(symbol)) {
+ this.startSearch(event);
+ }
+ return Clutter.EVENT_PROPAGATE;
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/settings.js b/extensions/47/vertical-workspaces/lib/settings.js
new file mode 100644
index 0000000..563063c
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/settings.js
@@ -0,0 +1,539 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * settings.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+
+let Me;
+
+export const Options = class Options {
+ constructor(me) {
+ Me = me;
+
+ this._gsettings = Me.gSettings;
+ this._connectionIds = [];
+ this._writeTimeoutId = 0;
+ this._gsettings.delay();
+ this.connect('changed', () => {
+ if (this._writeTimeoutId)
+ GLib.Source.remove(this._writeTimeoutId);
+
+ this._writeTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ 400,
+ () => {
+ this._gsettings.apply();
+ this._updateSettings();
+ this._writeTimeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+ });
+ this.options = {
+ workspaceThumbnailsPosition: ['int', 'ws-thumbnails-position'],
+ wsMaxSpacing: ['int', 'ws-max-spacing'],
+ wsPreviewScale: ['int', 'ws-preview-scale'],
+ secWsPreviewScale: ['int', 'secondary-ws-preview-scale'],
+ secWsPreviewShift: ['boolean', 'secondary-ws-preview-shift'],
+ wsThumbnailsFull: ['boolean', 'ws-thumbnails-full'],
+ secWsThumbnailsPosition: ['int', 'secondary-ws-thumbnails-position'],
+ dashPosition: ['int', 'dash-position'],
+ dashPositionAdjust: ['int', 'dash-position-adjust'],
+ wsTmbPositionAdjust: ['int', 'wst-position-adjust'],
+ showWsTmbLabels: ['int', 'show-wst-labels'],
+ showWsTmbLabelsOnHover: ['boolean', 'show-wst-labels-on-hover'],
+ closeWsButtonMode: ['int', 'close-ws-button-mode'],
+ secWsTmbPositionAdjust: ['int', 'sec-wst-position-adjust'],
+ dashMaxIconSize: ['int', 'dash-max-icon-size'],
+ centerDashToWs: ['boolean', 'center-dash-to-ws'],
+ showAppsIconPosition: ['int', 'show-app-icon-position'],
+ wsThumbnailScale: ['int', 'ws-thumbnail-scale'],
+ wsThumbnailScaleAppGrid: ['int', 'ws-thumbnail-scale-appgrid'],
+ secWsThumbnailScale: ['int', 'secondary-ws-thumbnail-scale'],
+ showSearchEntry: ['boolean', 'show-search-entry'],
+ centerSearch: ['boolean', 'center-search'],
+ centerAppGrid: ['boolean', 'center-app-grid'],
+ dashBgOpacity: ['int', 'dash-bg-opacity'],
+ dashBgColor: ['int', 'dash-bg-color'],
+ dashBgRadius: ['int', 'dash-bg-radius'],
+ dashBgGS3Style: ['boolean', 'dash-bg-gs3-style'],
+ runningDotStyle: ['int', 'running-dot-style'],
+ enablePageShortcuts: ['boolean', 'enable-page-shortcuts'],
+ showWsSwitcherBg: ['boolean', 'show-ws-switcher-bg'],
+ showWsPreviewBg: ['boolean', 'show-ws-preview-bg'],
+ wsPreviewBgRadius: ['int', 'ws-preview-bg-radius'],
+ showBgInOverview: ['boolean', 'show-bg-in-overview'],
+ overviewBgBrightness: ['int', 'overview-bg-brightness'],
+ searchBgBrightness: ['int', 'search-bg-brightness'],
+ overviewBgBlurSigma: ['int', 'overview-bg-blur-sigma'],
+ appGridBgBlurSigma: ['int', 'app-grid-bg-blur-sigma'],
+ smoothBlurTransitions: ['boolean', 'smooth-blur-transitions'],
+ appGridAnimation: ['int', 'app-grid-animation'],
+ searchViewAnimation: ['int', 'search-view-animation'],
+ workspaceAnimation: ['int', 'workspace-animation'],
+ animationSpeedFactor: ['int', 'animation-speed-factor'],
+ winPreviewIconSize: ['int', 'win-preview-icon-size'],
+ winTitlePosition: ['int', 'win-title-position'],
+ startupState: ['int', 'startup-state'],
+ overviewMode: ['int', 'overview-mode'],
+ workspaceSwitcherAnimation: ['int', 'workspace-switcher-animation'],
+ wsSwitcherMode: ['int', 'ws-switcher-mode'],
+ searchIconSize: ['int', 'search-icon-size'],
+ searchViewScale: ['int', 'search-width-scale'],
+ appGridIconSize: ['int', 'app-grid-icon-size'],
+ appGridColumns: ['int', 'app-grid-columns'],
+ appGridRows: ['int', 'app-grid-rows'],
+ appGridFolderIconSize: ['int', 'app-grid-folder-icon-size'],
+ appGridFolderColumns: ['int', 'app-grid-folder-columns'],
+ appGridFolderRows: ['int', 'app-grid-folder-rows'],
+ appGridFolderIconGrid: ['int', 'app-grid-folder-icon-grid'],
+ appGridContent: ['int', 'app-grid-content'],
+ appGridIncompletePages: ['boolean', 'app-grid-incomplete-pages'],
+ appGridOrder: ['int', 'app-grid-order'],
+ appFolderOrder: ['int', 'app-folder-order'],
+ appGridNamesMode: ['int', 'app-grid-names'],
+ appGridActivePreview: ['boolean', 'app-grid-active-preview'],
+ appGridFolderCenter: ['boolean', 'app-grid-folder-center'],
+ appGridPageWidthScale: ['int', 'app-grid-page-width-scale'],
+ appGridPageHeightScale: ['int', 'app-grid-page-height-scale'],
+ appGridSpacing: ['int', 'app-grid-spacing'],
+ appGridFolderSpacing: ['int', 'app-grid-folder-spacing'],
+ appGridShowPageArrows: ['boolean', 'app-grid-show-page-arrows'],
+ searchWindowsOrder: ['int', 'search-windows-order'],
+ searchFuzzy: ['boolean', 'search-fuzzy'],
+ searchMaxResultsRows: ['int', 'search-max-results-rows'],
+ searchAppGridMode: ['int', 'search-app-grid-mode'],
+ dashShowWindowsBeforeActivation: ['int', 'dash-show-windows-before-activation'],
+ dashIconScroll: ['int', 'dash-icon-scroll'],
+ dashIsolateWorkspaces: ['boolean', 'dash-isolate-workspaces'],
+ searchWindowsIconScroll: ['int', 'search-windows-icon-scroll'],
+ panelVisibility: ['int', 'panel-visibility'],
+ panelPosition: ['int', 'panel-position'],
+ windowAttentionMode: ['int', 'window-attention-mode'],
+ wsSwPopupHPosition: ['int', 'ws-sw-popup-h-position'],
+ wsSwPopupVPosition: ['int', 'ws-sw-popup-v-position'],
+ wsSwPopupMode: ['int', 'ws-sw-popup-mode'],
+ wsSwitcherWraparound: ['boolean', 'ws-switcher-wraparound'],
+ wsSwitcherIgnoreLast: ['boolean', 'ws-switcher-ignore-last'],
+ favoritesNotify: ['int', 'favorites-notify'],
+ notificationPosition: ['int', 'notification-position'],
+ osdPosition: ['int', 'osd-position'],
+ hotCornerAction: ['int', 'hot-corner-action'],
+ hotCornerPosition: ['int', 'hot-corner-position'],
+ hotCornerFullscreen: ['boolean', 'hot-corner-fullscreen'],
+ hotCornerRipples: ['boolean', 'hot-corner-ripples'],
+ alwaysActivateSelectedWindow: ['boolean', 'always-activate-selected-window'],
+ winPreviewSecBtnAction: ['int', 'win-preview-sec-mouse-btn-action'],
+ winPreviewMidBtnAction: ['int', 'win-preview-mid-mouse-btn-action'],
+ winPreviewShowCloseButton: ['boolean', 'win-preview-show-close-button'],
+ windowIconClickAction: ['int', 'window-icon-click-action'],
+ overlayKeyPrimary: ['int', 'overlay-key-primary'],
+ overlayKeySecondary: ['int', 'overlay-key-secondary'],
+ overviewEscBehavior: ['int', 'overview-esc-behavior'],
+ clickEmptyClose: ['boolean', 'click-empty-close'],
+ newWindowFocusFix: ['boolean', 'new-window-focus-fix'],
+ newWindowMonitorFix: ['boolean', 'new-window-monitor-fix'],
+ appGridPerformance: ['boolean', 'app-grid-performance'],
+ highlightingStyle: ['int', 'highlighting-style'],
+ delayStartup: ['boolean', 'delay-startup'],
+
+ workspaceSwitcherPopupModule: ['boolean', 'workspace-switcher-popup-module'],
+ workspaceAnimationModule: ['boolean', 'workspace-animation-module'],
+ workspaceModule: ['boolean', 'workspace-module'],
+ windowManagerModule: ['boolean', 'window-manager-module'],
+ windowPreviewModule: ['boolean', 'window-preview-module'],
+ windowAttentionHandlerModule: ['boolean', 'win-attention-handler-module'],
+ swipeTrackerModule: ['boolean', 'swipe-tracker-module'],
+ searchControllerModule: ['boolean', 'search-controller-module'],
+ searchModule: ['boolean', 'search-module'],
+ panelModule: ['boolean', 'panel-module'],
+ overlayKeyModule: ['boolean', 'overlay-key-module'],
+ osdWindowModule: ['boolean', 'osd-window-module'],
+ messageTrayModule: ['boolean', 'message-tray-module'],
+ layoutModule: ['boolean', 'layout-module'],
+ dashModule: ['boolean', 'dash-module'],
+ appFavoritesModule: ['boolean', 'app-favorites-module'],
+ appDisplayModule: ['boolean', 'app-display-module'],
+
+ profileName1: ['string', 'profile-name-1'],
+ profileName2: ['string', 'profile-name-2'],
+ profileName3: ['string', 'profile-name-3'],
+ profileName4: ['string', 'profile-name-4'],
+ };
+ this.cachedOptions = {};
+ this._updateSettings();
+ }
+
+ connect(name, callback) {
+ const id = this._gsettings.connect(name, callback);
+ this._connectionIds.push(id);
+ return id;
+ }
+
+ destroy() {
+ this._connectionIds.forEach(id => this._gsettings.disconnect(id));
+ if (this._writeTimeoutId) {
+ GLib.source_remove(this._writeTimeoutId);
+ this._writeTimeoutId = 0;
+ }
+
+ Me = null;
+ }
+
+ _updateCachedSettings() {
+ Object.keys(this.options).forEach(v => this.get(v, true));
+ }
+
+ get(option, updateCache = false) {
+ if (!this.options[option]) {
+ console.error(`[${Me.metadata.name}] Error: Option ${option} is undefined.`);
+ return null;
+ }
+
+ if (updateCache || this.cachedOptions[option] === undefined) {
+ const [, key, settings] = this.options[option];
+ let gSettings;
+ if (settings !== undefined)
+ gSettings = settings();
+ else
+ gSettings = this._gsettings;
+
+ this.cachedOptions[option] = gSettings.get_value(key).deep_unpack();
+ }
+
+ return this.cachedOptions[option];
+ }
+
+ set(option, value) {
+ const [format, key, settings] = this.options[option];
+
+ let gSettings = this._gsettings;
+
+ if (settings !== undefined)
+ gSettings = settings();
+
+
+ switch (format) {
+ case 'boolean':
+ gSettings.set_boolean(key, value);
+ break;
+ case 'int':
+ gSettings.set_int(key, value);
+ break;
+ case 'string':
+ gSettings.set_string(key, value);
+ break;
+ case 'strv':
+ gSettings.set_strv(key, value);
+ break;
+ }
+ }
+
+ getDefault(option) {
+ const [, key, settings] = this.options[option];
+
+ let gSettings = this._gsettings;
+
+ if (settings !== undefined)
+ gSettings = settings();
+
+
+ return gSettings.get_default_value(key).deep_unpack();
+ }
+
+ storeProfile(index) {
+ const profile = {};
+ Object.keys(this.options).forEach(v => {
+ if (!v.startsWith('profileName'))
+ profile[v] = this.get(v).toString();
+ });
+
+ this._gsettings.set_value(`profile-data-${index}`, new GLib.Variant('a{ss}', profile));
+ }
+
+ loadProfile(index) {
+ const options = this._gsettings.get_value(`profile-data-${index}`).deep_unpack();
+ // set the aaa-loading-data so extension.js doesn't reset V-Shell after each profile item
+ // delayed gsettings writes are processed alphabetically, so this key will be processed first
+ this._gsettings.set_boolean('aaa-loading-profile', !this._gsettings.get_boolean('aaa-loading-profile'));
+ for (let o of Object.keys(options)) {
+ if (!this.options[o]) {
+ console.error(`[${Me.metadata.name}] Error: "${o}" is not a valid profile key -> Update your profile`);
+ continue;
+ }
+ const [type] = this.options[o];
+ let value = options[o];
+ switch (type) {
+ case 'string':
+ break;
+ case 'boolean':
+ value = value === 'true';
+ break;
+ case 'int':
+ value = parseInt(value);
+ break;
+ }
+
+ this.set(o, value);
+ }
+ }
+
+ resetProfile(index) {
+ this._gsettings.reset(`profile-data-${index}`);
+ this._gsettings.reset(`profile-name-${index}`);
+ }
+
+ _updateSettings() {
+ this._updateCachedSettings();
+
+ // Basic spacing of the overview elements
+ this.SPACING = 12;
+
+ this.DASH_BG_ALPHA = this.get('dashBgOpacity') / 100;
+ this.DASH_BG_OPACITY = this.get('dashBgOpacity') * 2.5;
+ this.DASH_BG_COLOR = this.get('dashBgColor');
+ this.DASH_BG_RADIUS = this.get('dashBgRadius');
+ this.DASH_BG_LIGHT = this.DASH_BG_COLOR === 1;
+ this.DASH_BG_GS3_STYLE = this.get('dashBgGS3Style');
+ this.DASH_POSITION = this.get('dashModule') ? this.get('dashPosition') : 2;
+ this.DASH_TOP = this.DASH_POSITION === 0;
+ this.DASH_RIGHT = this.DASH_POSITION === 1;
+ this.DASH_BOTTOM = this.DASH_POSITION === 2;
+ this.DASH_LEFT = this.DASH_POSITION === 3;
+ this.DASH_VERTICAL = this.DASH_LEFT || this.DASH_RIGHT;
+ this.DASH_VISIBLE = this.DASH_POSITION !== 4; // 4 - disable
+ this.DASH_FOLLOW_RECENT_WIN = false;
+
+ this.DASH_ISOLATE_WS = this.get('dashIsolateWorkspaces');
+
+ this.DASH_CLICK_ACTION = this.get('dashShowWindowsBeforeActivation');
+ this.DASH_CLICK_SWITCH_BEFORE_ACTIVATION = this.DASH_CLICK_ACTION === 1;
+ this.DASH_CLICK_OPEN_NEW_WIN = this.DASH_CLICK_ACTION === 2;
+ this.DASH_CLICK_PREFER_WORKSPACE = this.DASH_CLICK_ACTION === 3;
+
+ this.DASH_ICON_SCROLL = this.get('dashIconScroll');
+ this.DASH_SHIFT_CLICK_MV = true;
+
+ this.RUNNING_DOT_STYLE = this.get('runningDotStyle');
+
+ this.SEARCH_WINDOWS_ICON_SCROLL = this.get('searchWindowsIconScroll');
+
+ this.DASH_POSITION_ADJUSTMENT = this.get('dashPositionAdjust');
+ this.DASH_POSITION_ADJUSTMENT = this.DASH_POSITION_ADJUSTMENT * -1 / 100; // range 1 to -1
+ this.CENTER_DASH_WS = this.get('centerDashToWs');
+
+ this.MAX_ICON_SIZE = this.get('dashMaxIconSize');
+
+ this.WS_TMB_POSITION = this.get('workspaceThumbnailsPosition');
+ this.ORIENTATION = this.WS_TMB_POSITION > 4 ? 0 : 1;
+ this.WORKSPACE_MAX_SPACING = this.get('wsMaxSpacing');
+ this.WS_MAX_SPACING_OFF_SCREEN = 350;
+ this.FORCE_SINGLE_WS_TRANSITION = false;
+ // ORIENTATION || DASH_LEFT || DASH_RIGHT ? 350 : 80;
+ this.SHOW_WS_TMB = ![4, 9].includes(this.WS_TMB_POSITION); // 4, 9 - disable
+ this.WS_TMB_FULL = this.get('wsThumbnailsFull');
+ // translate ws tmb position to 0 top, 1 right, 2 bottom, 3 left
+ // 0L 1R, 2LF, 3RF, 4DV, 5T, 6B, 7TF, 8BF, 9DH
+ this.WS_TMB_POSITION = [3, 1, 3, 1, 4, 0, 2, 0, 2, 8][this.WS_TMB_POSITION];
+ this.WS_TMB_TOP = this.WS_TMB_POSITION === 0;
+ this.WS_TMB_RIGHT = this.WS_TMB_POSITION === 1;
+ this.WS_TMB_BOTTOM = this.WS_TMB_POSITION === 2;
+ this.WS_TMB_LEFT = this.WS_TMB_POSITION === 3;
+ this.WS_TMB_POSITION_ADJUSTMENT = this.get('wsTmbPositionAdjust') * -1 / 100; // range 1 to -1
+ this.SEC_WS_TMB_POSITION = this.get('secWsThumbnailsPosition');
+ this.SHOW_SEC_WS_TMB = this.SEC_WS_TMB_POSITION !== 3 && this.SHOW_WS_TMB;
+ this.SEC_WS_TMB_TOP = (this.SEC_WS_TMB_POSITION === 0 && !this.ORIENTATION) || (this.SEC_WS_TMB_POSITION === 2 && this.WS_TMB_TOP);
+ this.SEC_WS_TMB_RIGHT = (this.SEC_WS_TMB_POSITION === 1 && this.ORIENTATION) || (this.SEC_WS_TMB_POSITION === 2 && this.WS_TMB_RIGHT);
+ this.SEC_WS_TMB_BOTTOM = (this.SEC_WS_TMB_POSITION === 1 && !this.ORIENTATION) || (this.SEC_WS_TMB_POSITION === 2 && this.WS_TMB_BOTTOM);
+ this.SEC_WS_TMB_LEFT = (this.SEC_WS_TMB_POSITION === 0 && this.ORIENTATION) || (this.SEC_WS_TMB_POSITION === 2 && this.WS_TMB_LEFT);
+
+ this.SEC_WS_TMB_POSITION_ADJUSTMENT = this.get('secWsTmbPositionAdjust') * -1 / 100; // range 1 to -1
+ this.SEC_WS_PREVIEW_SHIFT = this.get('secWsPreviewShift');
+ this.SHOW_WST_LABELS = this.get('showWsTmbLabels');
+ this.SHOW_WST_LABELS_ON_HOVER = this.get('showWsTmbLabelsOnHover');
+ this.CLOSE_WS_BUTTON_MODE = this.get('closeWsButtonMode');
+
+ this.MAX_THUMBNAIL_SCALE = this.get('wsThumbnailScale') / 100 + 0.01;
+ this.MAX_THUMBNAIL_SCALE_APPGRID = this.get('wsThumbnailScaleAppGrid') / 100 + 0.01;
+ this.SHOW_WS_TMB_APPGRID = true;
+ this.MAX_THUMBNAIL_SCALE_STABLE = this.MAX_THUMBNAIL_SCALE === this.MAX_THUMBNAIL_SCALE_APPGRID;
+ this.SEC_MAX_THUMBNAIL_SCALE = this.get('secWsThumbnailScale') / 100 + 0.01;
+
+ this.WS_PREVIEW_SCALE = this.get('wsPreviewScale') / 100;
+ this.SEC_WS_PREVIEW_SCALE = this.get('secWsPreviewScale') / 100;
+ // calculate number of possibly visible neighbor previews according to ws scale
+ this.NUMBER_OF_VISIBLE_NEIGHBORS = Math.round(2 + (1 - this.WS_PREVIEW_SCALE));
+
+ this.SHOW_WS_TMB_BG = this.get('showWsSwitcherBg') && this.SHOW_WS_TMB;
+ this.WS_PREVIEW_BG_RADIUS = this.get('wsPreviewBgRadius');
+ this.SHOW_WS_PREVIEW_BG = this.get('showWsPreviewBg');
+
+ this.CENTER_APP_GRID = this.get('centerAppGrid');
+
+ this.SHOW_SEARCH_ENTRY = this.get('showSearchEntry');
+ this.CENTER_SEARCH_VIEW = this.get('centerSearch');
+ this.APP_GRID_ANIMATION = this.get('appGridAnimation');
+ if (this.APP_GRID_ANIMATION === 4)
+ this.APP_GRID_ANIMATION = this._getAnimationDirection();
+
+ this.SEARCH_VIEW_ANIMATION = this.get('searchViewAnimation');
+ if (this.SEARCH_VIEW_ANIMATION === 4)
+ this.SEARCH_VIEW_ANIMATION = 3;
+
+ this.WIN_PREVIEW_ICON_SIZE = [64, 48, 32, 22, 8][this.get('winPreviewIconSize')];
+ this.WIN_TITLES_POSITION = this.get('winTitlePosition');
+ this.ALWAYS_SHOW_WIN_TITLES = this.WIN_TITLES_POSITION === 1;
+
+ this.STARTUP_STATE = this.get('startupState');
+ this.SHOW_BG_IN_OVERVIEW = this.get('showBgInOverview');
+ this.OVERVIEW_BG_BRIGHTNESS = this.get('overviewBgBrightness') / 100;
+ this.SEARCH_BG_BRIGHTNESS = this.get('searchBgBrightness') / 100;
+ this.OVERVIEW_BG_BLUR_SIGMA = this.get('overviewBgBlurSigma');
+ this.APP_GRID_BG_BLUR_SIGMA = this.get('appGridBgBlurSigma');
+ this.SMOOTH_BLUR_TRANSITIONS = this.get('smoothBlurTransitions');
+
+ this.OVERVIEW_MODE = this.get('overviewMode');
+ this.OVERVIEW_MODE2 = this.OVERVIEW_MODE === 2;
+ this.WORKSPACE_MODE = this.OVERVIEW_MODE ? 0 : 1;
+
+ this.STATIC_WS_SWITCHER_BG = this.get('workspaceSwitcherAnimation');
+
+ this.ANIMATION_TIME_FACTOR = this.get('animationSpeedFactor') / 100;
+
+ this.SEARCH_ICON_SIZE = this.get('searchIconSize');
+ this.SEARCH_VIEW_SCALE = this.get('searchViewScale') / 100;
+ this.SEARCH_MAX_ROWS = this.get('searchMaxResultsRows');
+ this.SEARCH_FUZZY = this.get('searchFuzzy');
+ this.SEARCH_DELAY = 0;
+ this.SEARCH_APP_GRID_MODE = this.get('searchAppGridMode');
+
+ this.APP_GRID_ALLOW_INCOMPLETE_PAGES = this.get('appGridIncompletePages');
+ this.APP_GRID_ICON_SIZE = this.get('appGridIconSize');
+ this.APP_GRID_COLUMNS = this.get('appGridColumns');
+ this.APP_GRID_ROWS = this.get('appGridRows');
+ this.APP_GRID_ADAPTIVE = !this.APP_GRID_COLUMNS && !this.APP_GRID_ROWS;
+
+ this.APP_GRID_ORDER = this.get('appGridOrder');
+ this.APP_GRID_ALPHABET = [1, 2, 4].includes(this.APP_GRID_ORDER);
+ this.APP_GRID_FOLDERS_FIRST = this.APP_GRID_ORDER === 1;
+ this.APP_GRID_FOLDERS_LAST = this.APP_GRID_ORDER === 2;
+ this.APP_GRID_USAGE = this.APP_GRID_ORDER === 3;
+
+ this.APP_FOLDER_ORDER = this.get('appFolderOrder');
+ this.APP_FOLDER_ALPHABET = this.APP_FOLDER_ORDER === 1;
+ this.APP_FOLDER_USAGE = this.APP_FOLDER_ORDER === 2;
+
+ this.APP_GRID_INCLUDE_DASH = this.get('appGridContent');
+ /* APP_GRID_INCLUDE_DASH
+ 0 - Include All
+ 1 - Include All - Favorites and Runnings First
+ 2 - Exclude Favorites (Default)
+ 3 - Exclude Running
+ 4 - Exclude Favorites and Running
+ */
+ this.APP_GRID_EXCLUDE_FAVORITES = this.APP_GRID_INCLUDE_DASH === 2 || this.APP_GRID_INCLUDE_DASH === 4;
+ this.APP_GRID_EXCLUDE_RUNNING = this.APP_GRID_INCLUDE_DASH === 3 || this.APP_GRID_INCLUDE_DASH === 4;
+ this.APP_GRID_DASH_FIRST = this.APP_GRID_INCLUDE_DASH === 1;
+
+ this.APP_GRID_NAMES_MODE = this.get('appGridNamesMode');
+
+ this.APP_GRID_FOLDER_ICON_SIZE = this.get('appGridFolderIconSize');
+ this.APP_GRID_FOLDER_ICON_GRID = this.get('appGridFolderIconGrid');
+ this.APP_GRID_FOLDER_COLUMNS = this.get('appGridFolderColumns');
+ this.APP_GRID_FOLDER_ROWS = this.get('appGridFolderRows');
+ this.APP_GRID_SPACING = this.get('appGridSpacing');
+ this.APP_GRID_FOLDER_SPACING = this.get('appGridFolderSpacing');
+ this.APP_GRID_FOLDER_DEFAULT = this.APP_GRID_FOLDER_ROWS === 3 && this.APP_GRID_FOLDER_COLUMNS === 3;
+ this.APP_GRID_FOLDER_ADAPTIVE = !this.APP_GRID_FOLDER_COLUMNS && !this.APP_GRID_FOLDER_ROWS;
+ this.APP_GRID_ACTIVE_PREVIEW = this.get('appGridActivePreview');
+ this.APP_GRID_FOLDER_CENTER = this.get('appGridFolderCenter');
+ this.APP_GRID_PAGE_WIDTH_SCALE = this.get('appGridPageWidthScale') / 100;
+ this.APP_GRID_PAGE_HEIGHT_SCALE = this.get('appGridPageHeightScale') / 100;
+ this.APP_GRID_SHOW_PAGE_ARROWS = this.get('appGridShowPageArrows');
+
+ // Default icon sizes updates in the IconGrid._findBestModeForSize()
+ this.APP_GRID_ICON_SIZE_DEFAULT = this.APP_GRID_ACTIVE_PREVIEW && !this.APP_GRID_USAGE ? 192 : 96;
+ this.APP_GRID_FOLDER_ICON_SIZE_DEFAULT = 96;
+
+ this.APP_GRID_PERFORMANCE = this.get('appGridPerformance');
+
+ this.PANEL_POSITION_TOP = this.get('panelPosition') === 0;
+ this.PANEL_POSITION_BOTTOM = this.get('panelPosition') === 1;
+ this.PANEL_MODE = this.get('panelVisibility');
+ this.PANEL_DISABLED = this.PANEL_MODE === 2;
+ this.PANEL_OVERVIEW_ONLY = this.PANEL_MODE === 1;
+
+ this.WINDOW_ATTENTION_MODE = this.get('windowAttentionMode');
+ this.WINDOW_ATTENTION_DISABLE_NOTIFICATIONS = this.WINDOW_ATTENTION_MODE === 1;
+ this.WINDOW_ATTENTION_FOCUS_IMMEDIATELY = this.WINDOW_ATTENTION_MODE === 2;
+
+ this.WS_SW_POPUP_H_POSITION = this.get('wsSwPopupHPosition') / 100;
+ this.WS_SW_POPUP_V_POSITION = this.get('wsSwPopupVPosition') / 100;
+ this.WS_SW_POPUP_MODE = this.get('wsSwPopupMode');
+
+ this.WS_ANIMATION = this.get('workspaceAnimation');
+ this.WS_WRAPAROUND = this.get('wsSwitcherWraparound');
+ this.WS_IGNORE_LAST = this.get('wsSwitcherIgnoreLast');
+ this.WS_SWITCHER_CURRENT_MONITOR = this.get('wsSwitcherMode') === 1;
+
+ this.SHOW_FAV_NOTIFICATION = this.get('favoritesNotify');
+ this.NOTIFICATION_POSITION = this.get('notificationPosition');
+
+ this.OSD_POSITION = this.get('osdPosition');
+
+ this.HOT_CORNER_ACTION = this.get('hotCornerAction');
+ this.HOT_CORNER_POSITION = this.get('hotCornerPosition');
+ if (this.HOT_CORNER_POSITION === 6 && this.DASH_VISIBLE)
+ this.HOT_CORNER_EDGE = true;
+ else
+ this.HOT_CORNER_EDGE = false;
+ if ([5, 6].includes(this.HOT_CORNER_POSITION)) {
+ if (this.DASH_TOP || this.DASH_LEFT)
+ this.HOT_CORNER_POSITION = 1;
+ else if (this.DASH_RIGHT)
+ this.HOT_CORNER_POSITION = 2;
+ else if (this.DASH_BOTTOM)
+ this.HOT_CORNER_POSITION = 3;
+ else
+ this.HOT_CORNER_POSITION = 0;
+ }
+ this.HOT_CORNER_FULLSCREEN = this.get('hotCornerFullscreen');
+ this.HOT_CORNER_RIPPLES = this.get('hotCornerRipples');
+
+ this.ALWAYS_ACTIVATE_SELECTED_WINDOW = this.get('alwaysActivateSelectedWindow');
+ this.WIN_PREVIEW_SEC_BTN_ACTION = this.get('winPreviewSecBtnAction');
+ this.WIN_PREVIEW_MID_BTN_ACTION = this.get('winPreviewMidBtnAction');
+ this.SHOW_CLOSE_BUTTON = this.get('winPreviewShowCloseButton');
+ this.WINDOW_ICON_CLICK_ACTION = this.get('windowIconClickAction');
+
+ this.OVERLAY_KEY_PRIMARY = this.get('overlayKeyPrimary');
+ this.OVERLAY_KEY_SECONDARY = this.get('overlayKeySecondary');
+
+ this.ESC_BEHAVIOR = this.get('overviewEscBehavior');
+ this.CLICK_EMPTY_CLOSE = this.get('clickEmptyClose');
+
+ this.FIX_NEW_WINDOW_FOCUS = this.get('newWindowFocusFix');
+ this.FIX_NEW_WINDOW_MONITOR = this.get('newWindowMonitorFix');
+
+ this.HIGHLIGHTING_STYLE = this.get('highlightingStyle');
+ this.HIGHLIGHT_DEFAULT = this.HIGHLIGHTING_STYLE === 0;
+ this.HIGHLIGHT_UNDERLINE = this.HIGHLIGHTING_STYLE === 1;
+ this.HIGHLIGHT_NONE = this.HIGHLIGHTING_STYLE === 2;
+
+ this.DELAY_STARTUP = this.get('delayStartup');
+ }
+
+ _getAnimationDirection() {
+ if (this.ORIENTATION)
+ return this.WS_TMB_LEFT || !this.SHOW_WS_TMB ? 1 : 2; // 1 right, 2 left
+ else
+ return this.WS_TMB_TOP || !this.SHOW_WS_TMB ? 3 : 5; // 3 bottom, 5 top
+ }
+};
diff --git a/extensions/47/vertical-workspaces/lib/swipeTracker.js b/extensions/47/vertical-workspaces/lib/swipeTracker.js
new file mode 100644
index 0000000..560b296
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/swipeTracker.js
@@ -0,0 +1,116 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * swipeTracker.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as SwipeTracker from 'resource:///org/gnome/shell/ui/swipeTracker.js';
+
+let Me;
+let opt;
+
+export const SwipeTrackerModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('swipeTrackerModule');
+ 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(' SwipeTrackerModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (opt.ORIENTATION) { // 1-VERTICAL, 0-HORIZONTAL
+ this._setVertical();
+ } else {
+ this._setHorizontal();
+ }
+ console.debug(' SwipeTrackerModule - Activated');
+ }
+
+ _disableModule() {
+ this._setHorizontal();
+
+ console.debug(' SwipeTrackerModule - Disabled');
+ }
+
+ _setVertical() {
+ // reverse swipe gestures for enter/leave overview and ws switching
+ Main.overview._swipeTracker.orientation = Clutter.Orientation.HORIZONTAL;
+ Main.wm._workspaceAnimation._swipeTracker.orientation = Clutter.Orientation.VERTICAL;
+ // overview's updateGesture() function should reflect ws tmb position to match appGrid/ws animation direction
+ // function in connection cannot be overridden in prototype of its class because connected is actually another copy of the original function
+ if (!this._originalGestureUpdateId) {
+ this._originalGestureUpdateId = GObject.signal_handler_find(Main.overview._swipeTracker._touchpadGesture, { signalId: 'update' });
+ Main.overview._swipeTracker._touchpadGesture.block_signal_handler(this._originalGestureUpdateId);
+ Main.overview._swipeTracker._updateGesture = SwipeTrackerVertical._updateGesture;
+ this._vwGestureUpdateId = Main.overview._swipeTracker._touchpadGesture.connect('update', SwipeTrackerVertical._updateGesture.bind(Main.overview._swipeTracker));
+ }
+ }
+
+ _setHorizontal() {
+ // original swipeTrackers' orientation and updateGesture function
+ Main.overview._swipeTracker.orientation = Clutter.Orientation.VERTICAL;
+ Main.wm._workspaceAnimation._swipeTracker.orientation = Clutter.Orientation.HORIZONTAL;
+ Main.overview._swipeTracker._updateGesture = SwipeTracker.SwipeTracker.prototype._updateGesture;
+ if (this._vwGestureUpdateId) {
+ Main.overview._swipeTracker._touchpadGesture.disconnect(this._vwGestureUpdateId);
+ this._vwGestureUpdateId = 0;
+ }
+ if (this._originalGestureUpdateId) {
+ Main.overview._swipeTracker._touchpadGesture.unblock_signal_handler(this._originalGestureUpdateId);
+ this._originalGestureUpdateId = 0;
+ }
+ }
+};
+
+const SwipeTrackerVertical = {
+ _updateGesture(gesture, time, delta, distance) {
+ if (this._state !== 1) // State.SCROLLING)
+ return;
+
+ if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) {
+ this._interrupt();
+ return;
+ }
+
+ if (opt.WS_TMB_RIGHT)
+ delta = -delta;
+ this._progress += delta / distance;
+ this._history.append(time, delta);
+
+ this._progress = Math.clamp(this._progress, ...this._getBounds(this._initialProgress));
+ this.emit('update', this._progress);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/util.js b/extensions/47/vertical-workspaces/lib/util.js
new file mode 100644
index 0000000..0da67ce
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/util.js
@@ -0,0 +1,445 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * util.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js';
+import { InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
+
+let Me;
+let _;
+let _installedExtensions;
+
+export function init(me) {
+ Me = me;
+ _ = Me.gettext;
+}
+
+export function cleanGlobals() {
+ Me = null;
+ _ = null;
+ _installedExtensions = null;
+}
+
+export class Overrides extends InjectionManager {
+ constructor() {
+ super();
+ this._overrides = {};
+ }
+
+ addOverride(name, prototype, overrideList) {
+ const backup = this.overrideProto(prototype, overrideList, name);
+ // don't update originals when override's just refreshing, keep initial content
+ let originals = this._overrides[name]?.originals;
+ if (!originals)
+ originals = backup;
+ this._overrides[name] = {
+ originals,
+ prototype,
+ };
+ }
+
+ removeOverride(name) {
+ const override = this._overrides[name];
+ if (!override)
+ return false;
+
+ this.overrideProto(override.prototype, override.originals, name);
+ delete this._overrides[name];
+ return true;
+ }
+
+ removeAll() {
+ for (let name in this._overrides) {
+ this.removeOverride(name);
+ delete this._overrides[name];
+ }
+ }
+
+ overrideProto(proto, overrides, name) {
+ const backup = {};
+ const originals = this._overrides[name]?.originals;
+ for (let symbol in overrides) {
+ if (symbol.startsWith('after_')) {
+ const actualSymbol = symbol.slice('after_'.length);
+ let fn;
+ if (originals && originals[actualSymbol])
+ fn = originals[actualSymbol];
+ else
+ fn = proto[actualSymbol];
+ const afterFn = overrides[symbol];
+ proto[actualSymbol] = function (...args) {
+ args = Array.prototype.slice.call(args);
+ const res = fn.apply(this, args);
+ afterFn.apply(this, args);
+ return res;
+ };
+ backup[actualSymbol] = fn;
+ } else if (overrides[symbol] !== null) {
+ backup[symbol] = proto[symbol];
+ this._installMethod(proto, symbol, overrides[symbol]);
+ }
+ }
+ return backup;
+ }
+}
+
+export function openPreferences(metadata) {
+ if (!metadata)
+ metadata = Me.metadata;
+ const windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null);
+ let tracker = Shell.WindowTracker.get_default();
+ let metaWin, isMe = null;
+
+ for (let win of windows) {
+ const app = tracker.get_window_app(win);
+ if (win.get_title()?.includes(metadata.name) && app.get_name() === 'Extensions') {
+ // this is our existing window
+ metaWin = win;
+ isMe = true;
+ break;
+ } else if (win.wm_class?.includes('org.gnome.Shell.Extensions')) {
+ // this is prefs window of another extension
+ metaWin = win;
+ isMe = false;
+ }
+ }
+
+ if (metaWin && !isMe) {
+ // other prefs window blocks opening another prefs window, so close it
+ metaWin.delete(global.get_current_time());
+ } else if (metaWin && isMe) {
+ // if prefs window already exist, move it to the current WS and activate it
+ metaWin.change_workspace(global.workspace_manager.get_active_workspace());
+ metaWin.activate(global.get_current_time());
+ }
+
+ if (!metaWin || (metaWin && !isMe)) {
+ // delay to avoid errors if previous prefs window has been colsed
+ GLib.idle_add(GLib.PRIORITY_LOW, () => {
+ try {
+ Main.extensionManager.openExtensionPrefs(metadata.uuid, '', {});
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ }
+}
+
+export function activateSearchProvider(prefix = '') {
+ const searchEntry = Main.overview.searchEntry;
+ if (!searchEntry.get_text() || !searchEntry.get_text().startsWith(prefix)) {
+ prefix = `${prefix} `;
+ const position = prefix.length;
+ searchEntry.set_text(prefix);
+ searchEntry.get_first_child().set_cursor_position(position);
+ searchEntry.get_first_child().set_selection(position, position);
+ } else {
+ searchEntry.set_text('');
+ }
+}
+
+export function dashNotDefault() {
+ return Main.overview.dash !== Main.overview._overview._controls.layoutManager._dash;
+}
+
+export function dashIsDashToDock() {
+ return Main.overview.dash._isHorizontal !== undefined;
+}
+
+// Reorder Workspaces - callback for Dash and workspacesDisplay
+export function reorderWorkspace(direction = 0) {
+ let activeWs = global.workspace_manager.get_active_workspace();
+ let activeWsIdx = activeWs.index();
+ let targetIdx = activeWsIdx + direction;
+ if (targetIdx > -1 && targetIdx < global.workspace_manager.get_n_workspaces())
+ global.workspace_manager.reorder_workspace(activeWs, targetIdx);
+}
+
+export function activateKeyboardForWorkspaceView() {
+ Main.ctrlAltTabManager._items.forEach(i => {
+ if (i.sortGroup === 1 && i.name === 'Windows')
+ Main.ctrlAltTabManager.focusGroup(i);
+ });
+}
+
+export function exposeWindows(adjustment, activateKeyboard) {
+ // expose windows for static overview modes
+ if (!adjustment.value && !Main.overview._animationInProgress) {
+ if (adjustment.value === 0) {
+ adjustment.value = 0;
+ adjustment.ease(1, {
+ duration: 200,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ if (activateKeyboard) {
+ Main.ctrlAltTabManager._items.forEach(i => {
+ if (i.sortGroup === 1 && i.name === 'Windows')
+ Main.ctrlAltTabManager.focusGroup(i);
+ });
+ }
+ },
+ });
+ }
+ }
+}
+
+export function isShiftPressed(state = null) {
+ if (state === null)
+ [,, state] = global.get_pointer();
+ return (state & Clutter.ModifierType.SHIFT_MASK) !== 0;
+}
+
+export function isCtrlPressed(state = null) {
+ if (state === null)
+ [,, state] = global.get_pointer();
+ return (state & Clutter.ModifierType.CONTROL_MASK) !== 0;
+}
+
+export function isAltPressed(state = null) {
+ if (state === null)
+ [,, state] = global.get_pointer();
+ return (state & Clutter.ModifierType.MOD1_MASK) !== 0;
+}
+
+export function fuzzyMatch(term, text) {
+ let pos = -1;
+ const matches = [];
+ // convert all accented chars to their basic form and to lower case
+ const _text = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+ const _term = term.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+
+ // if term matches the substring exactly, gains the highest weight
+ if (_text.includes(_term))
+ return 0;
+
+ for (let i = 0; i < _term.length; i++) {
+ let c = _term[i];
+ let p;
+ if (pos > 0)
+ p = _term[i - 1];
+ while (true) {
+ pos += 1;
+ if (pos >= _text.length)
+ return -1;
+
+ if (_text[pos] === c) {
+ matches.push(pos);
+ break;
+ } else if (_text[pos] === p) {
+ matches.pop();
+ matches.push(pos);
+ }
+ }
+ }
+
+ // add all position to get a weight of the result
+ // results closer to the beginning of the text and term characters closer to each other will gain more weight.
+ return matches.reduce((r, p) => r + p) - matches.length * matches[0] + matches[0];
+}
+
+export function strictMatch(term, text) {
+ // remove diacritics and accents from letters
+ let s = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+ let p = term.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+ let ps = p.split(/ +/);
+
+ // allows to use multiple exact patterns separated by a space in arbitrary order
+ for (let w of ps) { // escape regex control chars
+ if (!s.match(w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
+ return -1;
+ }
+ return 0;
+}
+
+export function isMoreRelevant(stringA, stringB, pattern) {
+ let regex = /[^a-zA-Z\d]/;
+ let strSplitA = stringA.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().split(regex);
+ let strSplitB = stringB.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().split(regex);
+ let aAny = false;
+ strSplitA.forEach(w => {
+ aAny = aAny || w.startsWith(pattern);
+ });
+ let bAny = false;
+ strSplitB.forEach(w => {
+ bAny = bAny || w.startsWith(pattern);
+ });
+
+ // if both strings contain a word that starts with the pattern
+ // prefer the one whose first word starts with the pattern
+ if (aAny && bAny)
+ return !strSplitA[0].startsWith(pattern) && strSplitB[0].startsWith(pattern);
+ else
+ return !aAny && bAny;
+}
+
+export function getEnabledExtensions(pattern = '') {
+ let result = [];
+ // extensionManager is unreliable at startup because it is uncertain whether all extensions have been loaded
+ // also gsettings key can contain already removed extensions (user deleted them without disabling them first)
+ // therefore we have to check what's really installed in the filesystem
+ if (!_installedExtensions) {
+ const extensionFiles = [...collectFromDatadirs('extensions', true)];
+ _installedExtensions = extensionFiles.map(({ info }) => {
+ let fileType = info.get_file_type();
+ if (fileType !== Gio.FileType.DIRECTORY)
+ return null;
+ const uuid = info.get_name();
+ return uuid;
+ });
+ }
+ // _enabledExtensions contains content of the enabled-extensions key from gsettings, not actual state
+ const enabled = Main.extensionManager._enabledExtensions;
+ result = _installedExtensions.filter(ext => enabled.includes(ext));
+ // _extensions contains already loaded extensions, so we can try to filter out broken or incompatible extensions
+ const active = Main.extensionManager._extensions;
+ result = result.filter(ext => {
+ const extension = active.get(ext);
+ if (extension)
+ return ![3, 4].includes(extension.state); // 3 - ERROR, 4 - OUT_OF_TIME (not supported by shell-version in metadata)
+ // extension can be enabled but not yet loaded, we just cannot see its state at this moment, so let it pass as enabled
+ return true;
+ });
+ // return only extensions matching the search pattern
+ return result.filter(uuid => uuid !== null && uuid.includes(pattern));
+}
+
+function* collectFromDatadirs(subdir, includeUserDir) {
+ let dataDirs = GLib.get_system_data_dirs();
+ if (includeUserDir)
+ dataDirs.unshift(GLib.get_user_data_dir());
+
+ for (let i = 0; i < dataDirs.length; i++) {
+ let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', subdir]);
+ let dir = Gio.File.new_for_path(path);
+
+ let fileEnum;
+ try {
+ fileEnum = dir.enumerate_children('standard::name,standard::type',
+ Gio.FileQueryInfoFlags.NONE, null);
+ } catch (e) {
+ fileEnum = null;
+ }
+ if (fileEnum !== null) {
+ let info;
+ while ((info = fileEnum.next_file(null)))
+ yield { dir: fileEnum.get_child(info), info };
+ }
+ }
+}
+
+export function getScrollDirection(event) {
+ // scroll wheel provides two types of direction information:
+ // 1. Clutter.ScrollDirection.DOWN / Clutter.ScrollDirection.UP
+ // 2. Clutter.ScrollDirection.SMOOTH + event.get_scroll_delta()
+ // first SMOOTH event returns 0 delta,
+ // so we need to always read event.direction
+ // since mouse without smooth scrolling provides exactly one SMOOTH event on one wheel rotation click
+ // on the other hand, under X11, one wheel rotation click sometimes doesn't send direction event, only several SMOOTH events
+ // so we also need to convert the delta to direction
+ let direction = event.get_scroll_direction();
+
+ if (direction !== Clutter.ScrollDirection.SMOOTH)
+ return direction;
+
+ let [, delta] = event.get_scroll_delta();
+
+ if (!delta)
+ return null;
+
+ direction = delta > 0 ? Clutter.ScrollDirection.DOWN : Clutter.ScrollDirection.UP;
+
+ return direction;
+}
+
+export function getWindows(workspace) {
+ // We ignore skip-taskbar windows in switchers, but if they are attached
+ // to their parent, their position in the MRU list may be more appropriate
+ // than the parent; so start with the complete list ...
+ let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace);
+ // ... map windows to their parent where appropriate ...
+ return windows.map(w => {
+ return w.is_attached_dialog() ? w.get_transient_for() : w;
+ // ... and filter out skip-taskbar windows and duplicates
+ }).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) === i);
+}
+
+export function monitorHasLowResolution(monitorIndex, resolutionLimit) {
+ resolutionLimit = resolutionLimit ?? 1200000;
+ monitorIndex = monitorIndex ?? global.display.get_primary_monitor();
+ const monitorGeometry = global.display.get_monitor_geometry(monitorIndex);
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const monitorResolution = monitorGeometry.width * monitorGeometry.height;
+ return (monitorResolution / scaleFactor) < resolutionLimit;
+}
+
+// /////////////////////////////////////////////////////////////////////////////////////////////
+// Status dialog that appears during updating V-Shell configuration and blocks inputs
+
+export const RestartMessage = GObject.registerClass({
+ // Registered name should be unique
+ GTypeName: `RestartMessage${Math.floor(Math.random() * 1000)}`,
+}, class RestartMessage extends ModalDialog.ModalDialog {
+ _init() {
+ super._init({
+ shellReactive: false,
+ styleClass: 'restart-message headline update-message',
+ shouldFadeIn: false,
+ destroyOnClose: false,
+ });
+
+ const label = new St.Label({
+ text: _('Updating V-Shell'),
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ this.contentLayout.add_child(label);
+ this.buttonLayout.hide();
+ this.connect('destroy', () => this.removeMessage());
+ }
+
+ showMessage(timeout = 500) {
+ if (this._timeoutId || Me._resetInProgress || Main.layoutManager._startingUp)
+ return;
+ this._removeTimeout();
+ this.open();
+ this._timeoutId = GLib.timeout_add(
+ GLib.PRIORITY_LOW,
+ timeout,
+ () => {
+ this._timeoutId = 0;
+ this.removeMessage();
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+ }
+
+ _removeTimeout() {
+ if (this._timeoutId) {
+ GLib.source_remove(this._timeoutId);
+ this._timeoutId = 0;
+ }
+ }
+
+ removeMessage() {
+ this._removeTimeout();
+ this.close();
+ }
+});
diff --git a/extensions/47/vertical-workspaces/lib/windowAttentionHandler.js b/extensions/47/vertical-workspaces/lib/windowAttentionHandler.js
new file mode 100644
index 0000000..ae115ed
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/windowAttentionHandler.js
@@ -0,0 +1,185 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * windowAttentionHandler.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
+
+const shellVersion46 = !Clutter.Container;
+
+let Me;
+let opt;
+
+export const WindowAttentionHandlerModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('windowAttentionHandlerModule');
+ 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(' WindowAttentionHandlerModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ this._updateConnections();
+ console.debug(' WindowAttentionHandlerModule - Activated');
+ }
+
+ _disableModule() {
+ const reset = true;
+ this._updateConnections(reset);
+
+ console.debug(' WindowAttentionHandlerModule - Disabled');
+ }
+
+ _updateConnections(reset) {
+ global.display.disconnectObject(Main.windowAttentionHandler);
+
+ const handlerFnc = reset
+ ? Main.windowAttentionHandler._onWindowDemandsAttention
+ : WindowAttentionHandlerCommon._onWindowDemandsAttention;
+
+ global.display.connectObject(
+ 'window-demands-attention', handlerFnc.bind(Main.windowAttentionHandler),
+ 'window-marked-urgent', handlerFnc.bind(Main.windowAttentionHandler),
+ Main.windowAttentionHandler);
+ }
+};
+
+const WindowAttentionHandlerCommon = {
+ _onWindowDemandsAttention(display, window) {
+ // Deny attention notifications if the App Grid is open, to avoid notification spree when opening a folder
+ if (Main.overview._shown && Main.overview.dash.showAppsButton.checked) {
+ return;
+ } else if (opt.WINDOW_ATTENTION_FOCUS_IMMEDIATELY) {
+ if (!Main.overview._shown)
+ Main.activateWindow(window);
+ return;
+ }
+
+ const app = this._tracker.get_window_app(window);
+ let args;
+ if (shellVersion46)
+ args = { title: app.get_name() };
+ else
+ args = app.get_name();
+
+ const source = new MessageTray.Source(args);
+ new Me.Util.Overrides().addOverride('MessageSource', source, WindowAttentionSourceCommon);
+ source._init(app, window);
+ Main.messageTray.add(source);
+
+ let [title, body] = this._getTitleAndBanner(app, window);
+ args = shellVersion46
+ ? [{ source, title, body, forFeedback: true }]
+ : [source, title, body];
+
+ const notification = new MessageTray.Notification(...args);
+ if (!shellVersion46)
+ notification.setForFeedback(true);
+
+ notification.connect('activated', () => {
+ source.open();
+ });
+
+ if (shellVersion46) {
+ notification.acknowledged = opt.WINDOW_ATTENTION_DISABLE_NOTIFICATIONS;
+ source.addNotification(notification);
+ if (opt.WINDOW_ATTENTION_DISABLE_NOTIFICATIONS) {
+ // just push the notification to the message tray without showing notification
+ notification.acknowledged = true;
+ Main.messageTray._notificationQueue.push(notification);
+ Main.panel.statusArea.dateMenu._indicator.show();
+ }
+ window.connectObject('notify::title', () => {
+ [title, body] = this._getTitleAndBanner(app, window);
+ notification.set({ title, body });
+ }, source);
+ } else {
+ if (opt.WINDOW_ATTENTION_DISABLE_NOTIFICATIONS)
+ // just push the notification to the message tray without showing notification
+ source.pushNotification(notification);
+ else
+ source.showNotification(notification);
+
+ window.connectObject('notify::title', () => {
+ [title, body] = this._getTitleAndBanner(app, window);
+ notification.update(title, body);
+ }, source);
+ }
+ },
+};
+
+const WindowAttentionSourceCommon = {
+ _init(app, window) {
+ this._window = window;
+ this._app = app;
+
+ this._window.connectObject(
+ 'notify::demands-attention', this._sync.bind(this),
+ 'notify::urgent', this._sync.bind(this),
+ 'focus', () => this.destroy(),
+ 'unmanaged', () => this.destroy(), this);
+ },
+
+ _sync() {
+ if (this._window.demands_attention || this._window.urgent)
+ return;
+ this.destroy();
+ },
+
+ _createPolicy() {
+ if (this._app && this._app.get_app_info()) {
+ let id = this._app.get_id().replace(/\.desktop$/, '');
+ return new MessageTray.NotificationApplicationPolicy(id);
+ } else {
+ return new MessageTray.NotificationGenericPolicy();
+ }
+ },
+
+ createIcon(size) {
+ return this._app.create_icon_texture(size);
+ },
+
+ destroy(params) {
+ this._window.disconnectObject(this);
+
+ MessageTray.Source.prototype.destroy.bind(this)(params);
+ },
+
+ open() {
+ Main.activateWindow(this._window);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/windowManager.js b/extensions/47/vertical-workspaces/lib/windowManager.js
new file mode 100644
index 0000000..a6f9b09
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/windowManager.js
@@ -0,0 +1,380 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * windowManager.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import Meta from 'gi://Meta';
+import GObject from 'gi://GObject';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as WindowManager from 'resource:///org/gnome/shell/ui/windowManager.js';
+import * as WorkspaceAnimation from 'resource:///org/gnome/shell/ui/workspaceAnimation.js';
+
+const MINIMIZE_WINDOW_ANIMATION_TIME = 400; // windowManager.MINIMIZE_WINDOW_ANIMATION_TIME
+const MINIMIZE_WINDOW_ANIMATION_MODE = Clutter.AnimationMode.EASE_OUT_EXPO; // WindowManager.MINIMIZE_WINDOW_ANIMATION_MODE
+
+let Me;
+let opt;
+
+export const WindowManagerModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+
+ this._originalMinimizeSigId = 0;
+ this._minimizeSigId = 0;
+ this._originalUnminimizeSigId = 0;
+ this._unminimizeSigId = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('windowManagerModule');
+ const conflict = false;
+
+ reset = reset || !this.moduleEnabled || conflict;
+
+ // don't even 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(' WindowManagerModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('WindowManager', WindowManager.WindowManager.prototype, WindowManagerCommon);
+ if (opt.WS_SWITCHER_CURRENT_MONITOR)
+ this._overrides.addOverride('WorkspaceAnimationController', WorkspaceAnimation.WorkspaceAnimationController.prototype, WorkspaceAnimationController);
+
+ if (!this._minimizeSigId) {
+ this._originalMinimizeSigId = GObject.signal_handler_find(Main.wm._shellwm, { signalId: 'minimize' });
+ if (this._originalMinimizeSigId) {
+ Main.wm._shellwm.block_signal_handler(this._originalMinimizeSigId);
+ this._minimizeSigId = Main.wm._shellwm.connect('minimize', WindowManagerCommon._minimizeWindow.bind(Main.wm));
+ }
+
+ this._originalUnminimizeSigId = GObject.signal_handler_find(Main.wm._shellwm, { signalId: 'unminimize' });
+ if (this._originalUnminimizeSigId) {
+ Main.wm._shellwm.block_signal_handler(this._originalUnminimizeSigId);
+ this._unminimizeSigId = Main.wm._shellwm.connect('unminimize', WindowManagerCommon._unminimizeWindow.bind(Main.wm));
+ }
+ }
+ console.debug(' WindowManagerModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ if (this._minimizeSigId) {
+ Main.wm._shellwm.disconnect(this._minimizeSigId);
+ this._minimizeSigId = 0;
+ }
+ if (this._originalMinimizeSigId) {
+ Main.wm._shellwm.unblock_signal_handler(this._originalMinimizeSigId);
+ this._originalMinimizeSigId = 0;
+ }
+
+ if (this._unminimizeSigId) {
+ Main.wm._shellwm.disconnect(this._unminimizeSigId);
+ this._unminimizeSigId = 0;
+ }
+ if (this._originalUnminimizeSigId) {
+ Main.wm._shellwm.unblock_signal_handler(this._originalUnminimizeSigId);
+ this._originalUnminimizeSigId = 0;
+ }
+
+ console.debug(' WindowManagerModule - Disabled');
+ }
+};
+
+const WindowManagerCommon = {
+ actionMoveWorkspace(workspace) {
+ if (!Main.sessionMode.hasWorkspaces)
+ return;
+
+ if (opt.WS_SWITCHER_CURRENT_MONITOR)
+ this._switchWorkspaceCurrentMonitor(workspace);
+ else if (!workspace.active)
+ workspace.activate(global.get_current_time());
+ },
+
+ actionMoveWindow(window, workspace) {
+ if (!Main.sessionMode.hasWorkspaces)
+ return;
+
+ if (!workspace.active) {
+ // This won't have any effect for "always sticky" windows
+ // (like desktop windows or docks)
+
+ this._workspaceAnimation.movingWindow = window;
+ window.change_workspace(workspace);
+
+ global.display.clear_mouse_mode();
+
+ if (opt.SWITCH_ONLY_CURRENT_MONITOR_WS) {
+ this._switchWorkspaceCurrentMonitor(workspace, window.get_monitor());
+ window.activate(global.get_current_time());
+ } else {
+ workspace.activate_with_focus(window, global.get_current_time());
+ }
+ }
+ },
+
+ _switchWorkspaceCurrentMonitor(workspace, monitor) {
+ // const focusedWindow = global.display.get_focus_window();
+ // const currentMonitor = focusedWindow ? focusedWindow.get_monitor() : global.display.get_current_monitor();
+ // using focused window to determine the current monitor can lead to inconsistent behavior and switching monitors between switches
+ // depending on which window takes focus on each workspace
+ // mouse pointer is more stable and predictable source
+ const currentMonitor = monitor ? monitor : global.display.get_current_monitor();
+ const primaryMonitor = currentMonitor === Main.layoutManager.primaryIndex;
+ const nMonitors = Main.layoutManager.monitors.length;
+ const lastIndexCorrection = Meta.prefs_get_dynamic_workspaces() ? 2 : 1;
+ const lastIndex = global.workspaceManager.get_n_workspaces() - lastIndexCorrection;
+ const targetWsIndex = workspace.index();
+ const activeWs = global.workspaceManager.get_active_workspace();
+ const activeWsIndex = activeWs.index();
+ const diff = activeWsIndex - targetWsIndex;
+
+ let direction = diff > 0 ? Meta.MotionDirection.UP : Meta.MotionDirection.DOWN;
+ if (diff === 0) {
+ // no actual ws to switch, but secondary monitors are always in wraparound mode so we need to get direction
+ direction = activeWsIndex >= lastIndex ? Meta.MotionDirection.DOWN : Meta.MotionDirection.UP;
+ }
+ if (Math.abs(diff) > 1) {
+ // workspace is probably in wraparound mode and just wrapped so so we need to translate direction
+ direction = diff > 0 ? Meta.MotionDirection.DOWN : Meta.MotionDirection.UP;
+ }
+
+ if (!primaryMonitor) {
+ this._rotateWorkspaces(direction, currentMonitor);
+ return;
+ }
+
+ // avoid ws rotations if the last empty dynamic workspace is involved, but allow to rotate from the last to the first, if wraparound is enabled
+ if (workspace !== activeWs && !((targetWsIndex > lastIndex && direction === Meta.MotionDirection.DOWN) || (activeWsIndex > lastIndex && targetWsIndex >= lastIndex))) {
+ for (let i = 0; i < nMonitors; i++) {
+ if (i !== currentMonitor) {
+ const oppositeDirection = direction === Meta.MotionDirection.UP ? Meta.MotionDirection.DOWN : Meta.MotionDirection.UP;
+ this._rotateWorkspaces(oppositeDirection, i);
+ }
+ }
+ }
+ workspace.activate(global.get_current_time());
+ },
+
+ _rotateWorkspaces(direction = 0, monitorIndex = -1, step = 1) {
+ step = direction === Meta.MotionDirection.UP ? Number(step) : -step;
+ const monitor = monitorIndex > -1 ? monitorIndex : global.display.get_current_monitor();
+ // don't move windows to the last empty workspace if dynamic workspaces are enabled
+ const lastIndexCorrection = Meta.prefs_get_dynamic_workspaces() ? 2 : 1;
+ const lastIndex = global.workspaceManager.get_n_workspaces() - lastIndexCorrection;
+ let windows = Me.Util.getWindows(null);
+ for (let win of windows.reverse()) {
+ // avoid moving modal windows as they move with their parents (and vice versa) immediately, before we move the parent window.
+ if (win.get_monitor() === monitor && !win.is_always_on_all_workspaces() && !win.is_attached_dialog() && !win.get_transient_for()) {
+ let wWs = win.get_workspace().index();
+ wWs += step;
+ if (wWs < 0)
+ wWs = lastIndex;
+ if (wWs > lastIndex)
+ wWs = 0;
+ const ws = global.workspaceManager.get_workspace_by_index(wWs);
+ win.change_workspace(ws);
+ }
+ }
+ },
+
+ // fix for mainstream bug - fullscreen windows should minimize using opacity transition
+ // but its being applied directly on window actor and that doesn't work
+ // anyway, animation is better, even if the Activities button is not visible...
+ // and also add support for bottom position of the panel
+ _minimizeWindow(shellwm, actor) {
+ const types = [
+ Meta.WindowType.NORMAL,
+ Meta.WindowType.MODAL_DIALOG,
+ Meta.WindowType.DIALOG,
+ ];
+ if (!this._shouldAnimateActor(actor, types)) {
+ shellwm.completed_minimize(actor);
+ return;
+ }
+
+ actor.set_scale(1.0, 1.0);
+
+ this._minimizing.add(actor);
+
+ /* if (actor.meta_window.is_monitor_sized()) {
+ actor.get_first_child().ease({
+ opacity: 0,
+ duration: MINIMIZE_WINDOW_ANIMATION_TIME,
+ mode: MINIMIZE_WINDOW_ANIMATION_MODE,
+ onStopped: () => this._minimizeWindowDone(shellwm, actor),
+ });
+ } else { */
+ let xDest, yDest, xScale, yScale;
+ let [success, geom] = actor.meta_window.get_icon_geometry();
+ if (success) {
+ xDest = geom.x;
+ yDest = geom.y;
+ xScale = geom.width / actor.width;
+ yScale = geom.height / actor.height;
+ } else {
+ let monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()];
+ if (!monitor) {
+ this._minimizeWindowDone();
+ return;
+ }
+ xDest = monitor.x;
+ yDest = opt.PANEL_POSITION_TOP ? monitor.y : monitor.y + monitor.height;
+ if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
+ xDest += monitor.width;
+ xScale = 0;
+ yScale = 0;
+ }
+
+ actor.ease({
+ scale_x: xScale,
+ scale_y: yScale,
+ x: xDest,
+ y: yDest,
+ duration: MINIMIZE_WINDOW_ANIMATION_TIME,
+ mode: MINIMIZE_WINDOW_ANIMATION_MODE,
+ onStopped: () => this._minimizeWindowDone(shellwm, actor),
+ });
+ // }
+ },
+
+ _minimizeWindowDone(shellwm, actor) {
+ if (this._minimizing.delete(actor)) {
+ actor.remove_all_transitions();
+ actor.set_scale(1.0, 1.0);
+ actor.get_first_child().set_opacity(255);
+ actor.set_pivot_point(0, 0);
+
+ shellwm.completed_minimize(actor);
+ }
+ },
+
+ _unminimizeWindow(shellwm, actor) {
+ const types = [
+ Meta.WindowType.NORMAL,
+ Meta.WindowType.MODAL_DIALOG,
+ Meta.WindowType.DIALOG,
+ ];
+ if (!this._shouldAnimateActor(actor, types)) {
+ shellwm.completed_unminimize(actor);
+ return;
+ }
+
+ this._unminimizing.add(actor);
+
+ /* if (false/* actor.meta_window.is_monitor_sized()) {
+ actor.opacity = 0;
+ actor.set_scale(1.0, 1.0);
+ actor.ease({
+ opacity: 255,
+ duration: MINIMIZE_WINDOW_ANIMATION_TIME,
+ mode: MINIMIZE_WINDOW_ANIMATION_MODE,
+ onStopped: () => this._unminimizeWindowDone(shellwm, actor),
+ });
+ } else { */
+ let [success, geom] = actor.meta_window.get_icon_geometry();
+ if (success) {
+ actor.set_position(geom.x, geom.y);
+ actor.set_scale(geom.width / actor.width,
+ geom.height / actor.height);
+ } else {
+ let monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()];
+ if (!monitor) {
+ actor.show();
+ this._unminimizeWindowDone();
+ return;
+ }
+ actor.set_position(monitor.x, opt.PANEL_POSITION_TOP ? monitor.y : monitor.y + monitor.height);
+ if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
+ actor.x += monitor.width;
+ actor.set_scale(0, 0);
+ }
+
+ let rect = actor.meta_window.get_buffer_rect();
+ let [xDest, yDest] = [rect.x, rect.y];
+
+ actor.show();
+ actor.ease({
+ scale_x: 1,
+ scale_y: 1,
+ x: xDest,
+ y: yDest,
+ duration: MINIMIZE_WINDOW_ANIMATION_TIME,
+ mode: MINIMIZE_WINDOW_ANIMATION_MODE,
+ onStopped: () => this._unminimizeWindowDone(shellwm, actor),
+ });
+ // }
+ },
+};
+
+const WorkspaceAnimationController = {
+ _prepareWorkspaceSwitch(workspaceIndices) {
+ if (this._switchData)
+ return;
+
+ const workspaceManager = global.workspace_manager;
+ const nWorkspaces = workspaceManager.get_n_workspaces();
+
+ const switchData = {};
+
+ this._switchData = switchData;
+ switchData.monitors = [];
+
+ switchData.gestureActivated = false;
+ switchData.inProgress = false;
+
+ if (!workspaceIndices)
+ workspaceIndices = [...Array(nWorkspaces).keys()];
+
+ let monitors = opt.WS_SWITCHER_CURRENT_MONITOR
+ ? [Main.layoutManager.currentMonitor] : Main.layoutManager.monitors;
+ monitors = Meta.prefs_get_workspaces_only_on_primary()
+ ? [Main.layoutManager.primaryMonitor] : monitors;
+
+ for (const monitor of monitors) {
+ if (Meta.prefs_get_workspaces_only_on_primary() &&
+ monitor.index !== Main.layoutManager.primaryIndex)
+ continue;
+
+ const group = new WorkspaceAnimation.MonitorGroup(monitor, workspaceIndices, this.movingWindow);
+
+ Main.uiGroup.insert_child_above(group, global.window_group);
+
+ switchData.monitors.push(group);
+ }
+
+ Meta.disable_unredirect_for_display(global.display);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/windowPreview.js b/extensions/47/vertical-workspaces/lib/windowPreview.js
new file mode 100644
index 0000000..a529dc1
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/windowPreview.js
@@ -0,0 +1,629 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * windowPreview.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import Pango from 'gi://Pango';
+import Graphene from 'gi://Graphene';
+import Atk from 'gi://Atk';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
+import * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js';
+import * as WindowPreview from 'resource:///org/gnome/shell/ui/windowPreview.js';
+
+let Me;
+let opt;
+
+const WINDOW_SCALE_TIME = 200;
+const WINDOW_ACTIVE_SIZE_INC = 5;
+const WINDOW_OVERLAY_FADE_TIME = 200;
+const WINDOW_DND_SIZE = 256;
+const DRAGGING_WINDOW_OPACITY = 100;
+const ICON_OVERLAP = 0.7;
+const ICON_TITLE_SPACING = 6;
+
+const ControlsState = OverviewControls.ControlsState;
+
+export const WindowPreviewModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('windowPreviewModule');
+ 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(' WindowPreviewModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('WindowPreview', WindowPreview.WindowPreview.prototype, WindowPreviewCommon);
+ // A shorter timeout allows user to quickly cancel the selection by leaving the preview with the mouse pointer
+ // if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW)
+ // WindowPreview.WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 150; // incompatible
+ console.debug(' WindowPreviewModule - Activated');
+ }
+
+ _disableModule() {
+ // If WindowPreview._init was injected by another extension (like Burn My Windows)
+ // which enables/disables before V-Shell
+ // don't restore the original if it's not injected,
+ // because it would restore injected _init and recursion would freeze GS when extensions are enabled again.
+ // This can happen when all extension re-enabled, not only when screen is locked/unlocked
+ // If _init doesn't include "fn.apply(this, args)" when reset === true, some extension already restored the original
+ const skipReset = WindowPreview.WindowPreview.prototype._init.toString().includes('fn.apply(this, args)');
+ if (this._overrides && skipReset) {
+ // skip restoring original _init()
+ this._overrides['_init'] = null;
+ }
+
+ if (this._overrides)
+ this._overrides.removeAll();
+
+ this._overrides = null;
+
+ console.debug(' WindowPreviewModule - Disabled');
+ }
+};
+
+const WindowPreviewCommon = {
+ _init(metaWindow, workspace, overviewAdjustment) {
+ this.metaWindow = metaWindow;
+ this.metaWindow._delegate = this;
+ this._windowActor = metaWindow.get_compositor_private();
+ this._workspace = workspace;
+ this._overviewAdjustment = overviewAdjustment;
+
+ const ICON_SIZE = opt.WIN_PREVIEW_ICON_SIZE;
+
+ Shell.WindowPreview.prototype._init.bind(this)({
+ reactive: true,
+ can_focus: true,
+ accessible_role: Atk.Role.PUSH_BUTTON,
+ offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
+ });
+
+ const windowContainer = new Clutter.Actor({
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ });
+ this.window_container = windowContainer;
+
+ windowContainer.connect('notify::scale-x',
+ () => this._adjustOverlayOffsets());
+ // gjs currently can't handle setting an actors layout manager during
+ // the initialization of the actor if that layout manager keeps track
+ // of its container, so set the layout manager after creating the
+ // container
+ windowContainer.layout_manager = new Shell.WindowPreviewLayout();
+ this.add_child(windowContainer);
+
+ this._addWindow(metaWindow);
+
+ this._delegate = this;
+
+ this._stackAbove = null;
+
+ this._cachedBoundingBox = {
+ x: windowContainer.layout_manager.bounding_box.x1,
+ y: windowContainer.layout_manager.bounding_box.y1,
+ width: windowContainer.layout_manager.bounding_box.get_width(),
+ height: windowContainer.layout_manager.bounding_box.get_height(),
+ };
+
+ windowContainer.layout_manager.connect(
+ 'notify::bounding-box', layout => {
+ this._cachedBoundingBox = {
+ x: layout.bounding_box.x1,
+ y: layout.bounding_box.y1,
+ width: layout.bounding_box.get_width(),
+ height: layout.bounding_box.get_height(),
+ };
+
+ // A bounding box of 0x0 means all windows were removed
+ if (layout.bounding_box.get_area() > 0)
+ this.emit('size-changed');
+ });
+
+ this._windowActor.connectObject('destroy', () => this.destroy(), this);
+
+ this._updateAttachedDialogs();
+
+ let clickAction = new Clutter.ClickAction();
+ clickAction.connect('clicked', act => {
+ const button = act.get_button();
+ if (button === Clutter.BUTTON_SECONDARY) {
+ if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 1) {
+ this._closeWinAction();
+ return Clutter.EVENT_STOP;
+ } else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 2) {
+ this._searchAppWindowsAction();
+ return Clutter.EVENT_STOP;
+ } else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 3 && global.windowThumbnails) {
+ this._removeLaters();
+ global.windowThumbnails?.createThumbnail(metaWindow);
+ return Clutter.EVENT_STOP;
+ }
+ } else if (button === Clutter.BUTTON_MIDDLE) {
+ if (opt.WIN_PREVIEW_MID_BTN_ACTION === 1) {
+ this._closeWinAction();
+ return Clutter.EVENT_STOP;
+ } else if (opt.WIN_PREVIEW_MID_BTN_ACTION === 2) {
+ this._searchAppWindowsAction();
+ return Clutter.EVENT_STOP;
+ } else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 3 && global.windowThumbnails) {
+ this._removeLaters();
+ global.windowThumbnails?.createThumbnail(metaWindow);
+ return Clutter.EVENT_STOP;
+ }
+ }
+ return this._activate();
+ });
+
+
+ if (this._onLongPress) {
+ clickAction.connect('long-press', this._onLongPress.bind(this));
+ } else {
+ clickAction.connect('long-press', (action, actor, state) => {
+ if (state === Clutter.LongPressState.ACTIVATE)
+ this.showOverlay(true);
+ return true;
+ });
+ }
+
+ this.connect('destroy', this._onDestroy.bind(this));
+
+ this._draggable = DND.makeDraggable(this, {
+ restoreOnSuccess: true,
+ manualMode: !!this._onLongPress,
+ dragActorMaxSize: WINDOW_DND_SIZE,
+ dragActorOpacity: DRAGGING_WINDOW_OPACITY,
+ });
+
+ // _draggable.addClickAction is new in GS45
+ if (this._draggable.addClickAction)
+ this._draggable.addClickAction(clickAction);
+ else
+ this.add_action(clickAction);
+
+ 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;
+
+ this._selected = false;
+ this._overlayEnabled = true;
+ this._overlayShown = false;
+ this._closeRequested = false;
+ this._idleHideOverlayId = 0;
+
+ const tracker = Shell.WindowTracker.get_default();
+ const app = tracker.get_window_app(this.metaWindow);
+ this._icon = app.create_icon_texture(ICON_SIZE);
+ this._icon.add_style_class_name('icon-dropshadow');
+ this._icon.set({
+ reactive: true,
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ });
+ this._icon.add_constraint(new Clutter.BindConstraint({
+ source: windowContainer,
+ coordinate: Clutter.BindCoordinate.POSITION,
+ }));
+ this._icon.add_constraint(new Clutter.AlignConstraint({
+ source: windowContainer,
+ align_axis: Clutter.AlignAxis.X_AXIS,
+ factor: 0.5,
+ }));
+ this._icon.add_constraint(new Clutter.AlignConstraint({
+ source: windowContainer,
+ align_axis: Clutter.AlignAxis.Y_AXIS,
+ pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }),
+ factor: 1,
+ }));
+
+ if (opt.WINDOW_ICON_CLICK_ACTION) {
+ const iconClickAction = new Clutter.ClickAction();
+ iconClickAction.connect('clicked', act => {
+ if (act.get_button() === Clutter.BUTTON_PRIMARY) {
+ if (opt.WINDOW_ICON_CLICK_ACTION === 1) {
+ this._searchAppWindowsAction();
+ return Clutter.EVENT_STOP;
+ } else if (opt.WINDOW_ICON_CLICK_ACTION === 2 && global.windowThumbnails) {
+ this._removeLaters();
+ global.windowThumbnails?.createThumbnail(metaWindow);
+ return Clutter.EVENT_STOP;
+ }
+ } /* else if (act.get_button() === Clutter.BUTTON_SECONDARY) {
+ return Clutter.EVENT_STOP;
+ }*/
+ return Clutter.EVENT_PROPAGATE;
+ });
+ this._icon.add_action(iconClickAction);
+ }
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ this._title = new St.Label({
+ visible: false,
+ style_class: 'window-caption',
+ text: this._getCaption(),
+ reactive: true,
+ });
+ this._title.clutter_text.single_line_mode = true;
+ this._title.add_constraint(new Clutter.BindConstraint({
+ source: windowContainer,
+ coordinate: Clutter.BindCoordinate.X,
+ }));
+
+ let offset;
+ if (opt.WIN_TITLES_POSITION < 2) {
+ // we cannot get proper title height before it gets to the stage, so 35 is estimated height + spacing
+ offset = -scaleFactor * (ICON_SIZE * ICON_OVERLAP + 35);
+ } else {
+ offset = scaleFactor * (ICON_SIZE * (1 - ICON_OVERLAP) + 4);
+ }
+ this._title.add_constraint(new Clutter.BindConstraint({
+ source: windowContainer,
+ coordinate: Clutter.BindCoordinate.Y,
+ offset,
+ }));
+ this._title.add_constraint(new Clutter.AlignConstraint({
+ source: windowContainer,
+ align_axis: Clutter.AlignAxis.X_AXIS,
+ factor: 0.5,
+ }));
+ this._title.add_constraint(new Clutter.AlignConstraint({
+ source: windowContainer,
+ align_axis: Clutter.AlignAxis.Y_AXIS,
+ pivot_point: new Graphene.Point({ x: -1, y: 0 }),
+ factor: 1,
+ }));
+ this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ this.label_actor = this._title;
+ this.metaWindow.connectObject(
+ 'notify::title', () => (this._title.text = this._getCaption()),
+ this);
+
+ const layout = Meta.prefs_get_button_layout();
+ this._closeButtonSide =
+ layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
+ ? St.Side.LEFT : St.Side.RIGHT;
+ this._closeButton = new St.Button({
+ visible: false,
+ style_class: 'window-close',
+ icon_name: 'preview-close-symbolic',
+ });
+ this._closeButton.add_constraint(new Clutter.BindConstraint({
+ source: windowContainer,
+ coordinate: Clutter.BindCoordinate.POSITION,
+ }));
+ this._closeButton.add_constraint(new Clutter.AlignConstraint({
+ source: windowContainer,
+ align_axis: Clutter.AlignAxis.X_AXIS,
+ pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
+ factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
+ }));
+ this._closeButton.add_constraint(new Clutter.AlignConstraint({
+ source: windowContainer,
+ align_axis: Clutter.AlignAxis.Y_AXIS,
+ pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
+ factor: 0,
+ }));
+ this._closeButton.connect('clicked', () => this._deleteAll());
+
+ this.add_child(this._title);
+ this.add_child(this._icon);
+ this.add_child(this._closeButton);
+
+ this._overviewAdjustment.connectObject(
+ 'notify::value', () => this._updateIconScale(), this);
+ this._updateIconScale();
+
+ this.connect('notify::realized', () => {
+ if (!this.realized)
+ return;
+
+ this._title.ensure_style();
+ this._icon.ensure_style();
+ });
+
+ if (ICON_SIZE < 22) {
+ // disable app icon
+ this._icon.hide();
+ } else {
+ this._updateIconScale();
+ }
+
+
+
+ // if window is created while the overview is shown, icon and title should be visible immediately
+ if (Main.overview._overview._controls._stateAdjustment.value < 1) {
+ this._icon.scale_x = 0;
+ this._icon.scale_y = 0;
+ this._title.opacity = 0;
+ }
+
+ if (opt.ALWAYS_SHOW_WIN_TITLES)
+ this._title.show();
+
+ if (opt.OVERVIEW_MODE === 1) {
+ // spread windows on hover
+ this._wsStateConId = this.connect('enter-event', () => {
+ // don't spread windows if user don't use pointer device at this moment
+ if (global.get_pointer()[0] === opt.showingPointerX || Main.overview._overview._controls._stateAdjustment.value < 1)
+ return;
+
+ opt.WORKSPACE_MODE = 1;
+ const view = this._workspace.get_parent();
+ view.exposeWindows(this._workspace.metaWorkspace.index());
+ this.disconnect(this._wsStateConId);
+ });
+ }
+
+ if (opt.OVERVIEW_MODE) {
+ // show window icon and title on ws windows spread
+ this._stateAdjustmentSigId = this._workspace.stateAdjustment.connect('notify::value', this._updateIconScale.bind(this));
+ }
+
+ const metaWin = this.metaWindow;
+ if (opt.DASH_ISOLATE_WS && !metaWin._wsChangedConId) {
+ metaWin._wsChangedConId = metaWin.connect('workspace-changed',
+ () => Main.overview.dash._queueRedisplay());
+ } else if (!opt.DASH_ISOLATE_WS && metaWin._wsChangedConId) {
+ metaWin.disconnect(metaWin._wsChangedConId);
+ }
+ },
+
+ _closeWinAction() {
+ this.hide();
+ this._deleteAll();
+ },
+
+ _removeLaters() {
+ if (this._longPressLater) {
+ const laters = global.compositor.get_laters();
+ laters.remove(this._longPressLater);
+ delete this._longPressLater;
+ }
+ },
+
+ _searchAppWindowsAction() {
+ // this action cancels long-press event and the 'long-press-cancel' event is used by the Shell to actually initiate DnD
+ // so the dnd initiation needs to be removed
+ this._removeLaters();
+ const tracker = Shell.WindowTracker.get_default();
+ const appName = tracker.get_window_app(this.metaWindow).get_name();
+ Me.Util.activateSearchProvider(`${Me.WSP_PREFIX} ${appName}`);
+ },
+
+ _updateIconScale() {
+ let { currentState, initialState, finalState } =
+ this._overviewAdjustment.getStateTransitionParams();
+
+ // Current state - 0 - HIDDEN, 1 - WINDOW_PICKER, 2 - APP_GRID
+ const primaryMonitor = this.metaWindow.get_monitor() === global.display.get_primary_monitor();
+
+ const visible =
+ (initialState > ControlsState.HIDDEN || finalState > ControlsState.HIDDEN) &&
+ !(finalState === ControlsState.APP_GRID && opt.WS_ANIMATION && primaryMonitor);
+
+ let scale = 0;
+ if (visible)
+ scale = currentState >= 1 ? 1 : currentState % 1;
+
+ if (!primaryMonitor && opt.WORKSPACE_MODE &&
+ ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
+ (initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER))
+ )
+ scale = 1;
+ else if (!primaryMonitor && opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE)
+ scale = 0;
+ /* } else if (primaryMonitor && ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
+ initialState === ControlsState.APP_GRID && finalState === ControlsState.HIDDEN)) {*/
+ else if (primaryMonitor && currentState > ControlsState.WINDOW_PICKER)
+ scale = 0;
+
+ // in static workspace mode show icon and title on windows expose
+ if (opt.OVERVIEW_MODE) {
+ if (currentState === 1)
+ scale = this._workspace._background._stateAdjustment.value;
+ else if ((finalState === 1 && !opt.WORKSPACE_MODE) || (finalState === 0 && !opt.WORKSPACE_MODE))
+ return;
+ }
+
+ if (!opt.WS_ANIMATION && (Main.overview.searchController.searchActive ||
+ ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
+ (initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER)))
+ )
+ return;
+
+ // if titles are in 'always show' mode, we need to add transition between visible/invisible state
+ // but the transition is quite expensive,
+ // showing the titles at the end of the transition is good enough and workspace preview transition is much smoother
+ if (scale === 1) {
+ this._icon.set({
+ scale_x: 1,
+ scale_y: 1,
+ });
+ this._title.ease({
+ duration: 100,
+ opacity: 255,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ } else {
+ this._title.opacity = 0;
+ this._icon.set({
+ scale_x: scale,
+ scale_y: scale,
+ });
+ }
+ },
+
+ showOverlay(animate) {
+ if (!this._overlayEnabled)
+ return;
+
+ if (this._overlayShown)
+ return;
+
+ this._overlayShown = true;
+ if (opt.WIN_TITLES_POSITION === 2)
+ this._restack();
+
+ // If we're supposed to animate and an animation in our direction
+ // is already happening, let that one continue
+ const ongoingTransition = this._title.get_transition('opacity');
+ if (animate &&
+ ongoingTransition &&
+ ongoingTransition.get_interval().peek_final_value() === 255)
+ return;
+
+ const toShow = this._windowCanClose() && opt.SHOW_CLOSE_BUTTON
+ ? [this._closeButton]
+ : [];
+
+ if (!opt.ALWAYS_SHOW_WIN_TITLES)
+ toShow.push(this._title);
+
+
+ toShow.forEach(a => {
+ a.opacity = 0;
+ a.show();
+ a.ease({
+ opacity: 255,
+ duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ });
+
+ const [width, height] = this.window_container.get_size();
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * 2 * scaleFactor;
+ const origSize = Math.max(width, height);
+ const scale = (origSize + activeExtraSize) / origSize;
+
+ this.window_container.ease({
+ scale_x: scale,
+ scale_y: scale,
+ duration: animate ? WINDOW_SCALE_TIME : 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ this.emit('show-chrome');
+ },
+
+ hideOverlay(animate) {
+ if (!this._overlayShown)
+ return;
+ this._overlayShown = false;
+
+ if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW && Main.overview._overview.controls._stateAdjustment.value < 1)
+ this._activateSelected = true;
+
+
+ if (opt.WIN_TITLES_POSITION === 2)
+ this._restack();
+
+ // If we're supposed to animate and an animation in our direction
+ // is already happening, let that one continue
+ const ongoingTransition = this._title.get_transition('opacity');
+ if (animate &&
+ ongoingTransition &&
+ ongoingTransition.get_interval().peek_final_value() === 0)
+ return;
+
+ const toHide = [this._closeButton];
+
+ if (!opt.ALWAYS_SHOW_WIN_TITLES)
+ toHide.push(this._title);
+
+ toHide.forEach(a => {
+ a.opacity = 255;
+ a.ease({
+ opacity: 0,
+ duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => a.hide(),
+ });
+ });
+
+ if (this.window_container) {
+ this.window_container.ease({
+ scale_x: 1,
+ scale_y: 1,
+ duration: animate ? WINDOW_SCALE_TIME : 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+ },
+
+ overlapHeights() {
+ const [, titleHeight] = this._title.get_preferred_height(-1);
+
+ const topOverlap = 0;
+ const bottomOverlap = opt.WIN_TITLES_POSITION === 2 ? titleHeight + ICON_TITLE_SPACING : 0;
+
+ return [topOverlap, bottomOverlap];
+ },
+
+ _onDestroy() {
+ if (this._activateSelected)
+ this._activate();
+
+ this.metaWindow._delegate = null;
+ this._delegate = null;
+ this._destroyed = true;
+
+ if (this._longPressLater) {
+ const laters = global.compositor.get_laters();
+ laters.remove(this._longPressLater);
+ delete this._longPressLater;
+ }
+
+ if (this._idleHideOverlayId > 0) {
+ GLib.source_remove(this._idleHideOverlayId);
+ this._idleHideOverlayId = 0;
+ }
+
+ if (this.inDrag) {
+ this.emit('drag-end');
+ this.inDrag = false;
+ }
+
+ if (this._stateAdjustmentSigId)
+ this._workspace.stateAdjustment.disconnect(this._stateAdjustmentSigId);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/workspace.js b/extensions/47/vertical-workspaces/lib/workspace.js
new file mode 100644
index 0000000..9f1dbbc
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/workspace.js
@@ -0,0 +1,472 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * workspace.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import St from 'gi://St';
+// import Graphene from 'gi://Graphene';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Workspace from 'resource:///org/gnome/shell/ui/workspace.js';
+import * as Params from 'resource:///org/gnome/shell/misc/params.js';
+import * as Util from 'resource:///org/gnome/shell/misc/util.js';
+
+let Me;
+let opt;
+
+let WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95;
+
+export const WorkspaceModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('workspaceModule');
+ 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(' WorkspaceModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('WorkspaceBackground', Workspace.WorkspaceBackground.prototype, WorkspaceBackground);
+
+ // fix overlay base for Vertical Workspaces
+ this._overrides.addOverride('WorkspaceLayout', Workspace.WorkspaceLayout.prototype, WorkspaceLayout);
+ console.debug(' WorkspaceModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+ console.debug(' WorkspaceModule - Disabled');
+ }
+
+ setWindowPreviewMaxScale(scale) {
+ WINDOW_PREVIEW_MAXIMUM_SCALE = scale;
+ }
+};
+
+// workaround for upstream bug (that is not that invisible in default shell)
+// smaller window cannot be scaled below 0.95 (WINDOW_PREVIEW_MAXIMUM_SCALE)
+// when its target scale for exposed windows view (workspace state 1) is bigger than the scale needed for ws state 0.
+// in workspace state 0 where windows are not spread and window scale should follow workspace scale,
+// this window follows proper top left corner position, but doesn't scale with the workspace
+// so it looks bad and the window can exceed border of the workspace
+// extremely annoying in OVERVIEW_MODE 1 with single smaller window on the workspace, also affects appGrid transition animation
+
+// disadvantage of following workaround - the WINDOW_PREVIEW_MAXIMUM_SCALE value is common for every workspace,
+// on multi-monitor system can be visible unwanted scaling of windows on workspace in WORKSPACE_MODE 0 (windows not spread)
+// when leaving overview while any other workspace is in the WORKSPACE_MODE 1.
+const WorkspaceLayout = {
+ // injection to _init()
+ after__init() {
+ if (opt.OVERVIEW_MODE !== 1)
+ WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95;
+ if (opt.OVERVIEW_MODE === 1) {
+ this._stateAdjustment.connect('notify::value', () => {
+ // scale 0.1 for window state 0 just needs to be smaller then possible scale of any window in spread view
+ const scale = this._stateAdjustment.value ? 0.95 : 0.1;
+ if (scale !== WINDOW_PREVIEW_MAXIMUM_SCALE) {
+ WINDOW_PREVIEW_MAXIMUM_SCALE = scale;
+ // when transition to ws state 1 (WINDOW_PICKER) begins, replace the constant with the original one
+ // and force recalculation of the target layout, so the transition will be smooth
+ this._needsLayout = true;
+ }
+ });
+ }
+ },
+
+ _adjustSpacingAndPadding(rowSpacing, colSpacing, containerBox) {
+ if (this._sortedWindows.length === 0)
+ return [rowSpacing, colSpacing, containerBox];
+
+ // All of the overlays have the same chrome sizes,
+ // so just pick the first one.
+ const window = this._sortedWindows[0];
+
+ const [topOversize, bottomOversize] = window.chromeHeights();
+ const [leftOversize, rightOversize] = window.chromeWidths();
+
+ let oversize = Math.max(topOversize, bottomOversize, leftOversize, rightOversize);
+
+ if (rowSpacing !== null)
+ rowSpacing += oversize;
+ if (colSpacing !== null)
+ colSpacing += oversize;
+
+ // Chrome highlights and window titles may exceed the workspace preview area
+ // and also the screen area if there is no overview element below/above/on_the_right of the workspace
+ // The original code tests whether window titles are out of the screen and applies correction accordingly
+ // That is a problem when workspaces are vertically stacked, because this method is called even during transitions between workspaces
+ // In V-Shell, this issue can be solved by reducing the workspace preview scale in the Settings
+
+ // Original code - horizontal orientation only
+ /* if (containerBox) {
+ const monitor = Main.layoutManager.monitors[this._monitorIndex];
+
+ const bottomPoint = new Graphene.Point3D({ y: containerBox.y2 });
+ const transformedBottomPoint =
+ this._container.apply_transform_to_point(bottomPoint);
+ const bottomFreeSpace =
+ (monitor.y + monitor.height) - transformedBottomPoint.y;
+
+ const [, bottomOverlap] = window.overlapHeights();
+
+ if ((bottomOverlap + oversize) > bottomFreeSpace)
+ containerBox.y2 -= (bottomOverlap + oversize) - bottomFreeSpace;
+ }*/
+
+ // Alternative code reducing the box size unconditionally
+ /* if (containerBox) {
+ const [, bottomOverlap] = window.overlapHeights();
+
+ // Adjusting x1/x2 here is pointless,
+ // x1 only moves window previews to the right and down, x2 has no effect
+ // Prevent window previews from overlapping a workspace preview
+ oversize *= 1.5;
+ containerBox.y1 += oversize;
+ containerBox.y2 -= bottomOverlap + oversize;
+ }*/
+
+ return [rowSpacing, colSpacing, containerBox];
+ },
+
+ _createBestLayout(area) {
+ const [rowSpacing, columnSpacing] =
+ this._adjustSpacingAndPadding(this._spacing, this._spacing, null);
+
+ // We look for the largest scale that allows us to fit the
+ // largest row/tallest column on the workspace.
+ this._layoutStrategy = new UnalignedLayoutStrategy({
+ monitor: Main.layoutManager.monitors[this._monitorIndex],
+ rowSpacing,
+ columnSpacing,
+ });
+
+ let lastLayout = null;
+ let lastNumColumns = -1;
+ let lastScale = 0;
+ let lastSpace = 0;
+
+ for (let numRows = 1; ; numRows++) {
+ const numColumns = Math.ceil(this._sortedWindows.length / numRows);
+
+ // If adding a new row does not change column count just stop
+ // (for instance: 9 windows, with 3 rows -> 3 columns, 4 rows ->
+ // 3 columns as well => just use 3 rows then)
+ if (numColumns === lastNumColumns)
+ break;
+
+ const layout = this._layoutStrategy.computeLayout(this._sortedWindows, {
+ numRows,
+ });
+
+ const [scale, space] = this._layoutStrategy.computeScaleAndSpace(layout, area);
+
+ if (lastLayout && !this._isBetterScaleAndSpace(lastScale, lastSpace, scale, space))
+ break;
+
+ lastLayout = layout;
+ lastNumColumns = numColumns;
+ lastScale = scale;
+ lastSpace = space;
+ }
+
+ return lastLayout;
+ },
+};
+
+class UnalignedLayoutStrategy extends Workspace.LayoutStrategy {
+ _newRow() {
+ // Row properties:
+ //
+ // * x, y are the position of row, relative to area
+ //
+ // * width, height are the scaled versions of fullWidth, fullHeight
+ //
+ // * width also has the spacing in between windows. It's not in
+ // fullWidth, as the spacing is constant, whereas fullWidth is
+ // meant to be scaled
+ //
+ // * neither height/fullHeight have any sort of spacing or padding
+ return {
+ x: 0, y: 0,
+ width: 0, height: 0,
+ fullWidth: 0, fullHeight: 0,
+ windows: [],
+ };
+ }
+
+ // Computes and returns an individual scaling factor for @window,
+ // to be applied in addition to the overall layout scale.
+ _computeWindowScale(window) {
+ // Since we align windows next to each other, the height of the
+ // thumbnails is much more important to preserve than the width of
+ // them, so two windows with equal height, but maybe differering
+ // widths line up.
+ let ratio = window.boundingBox.height / this._monitor.height;
+
+ // The purpose of this manipulation here is to prevent windows
+ // from getting too small. For something like a calculator window,
+ // we need to bump up the size just a bit to make sure it looks
+ // good. We'll use a multiplier of 1.5 for this.
+
+ // Map from [0, 1] to [1.5, 1]
+ return Util.lerp(1.5, 1, ratio);
+ }
+
+ _computeRowSizes(layout) {
+ let { rows, scale } = layout;
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+ row.width = row.fullWidth * scale + (row.windows.length - 1) * this._columnSpacing;
+ row.height = row.fullHeight * scale;
+ }
+ }
+
+ _keepSameRow(row, window, width, idealRowWidth) {
+ if (row.fullWidth + width <= idealRowWidth)
+ return true;
+
+ let oldRatio = row.fullWidth / idealRowWidth;
+ let newRatio = (row.fullWidth + width) / idealRowWidth;
+
+ if (Math.abs(1 - newRatio) < Math.abs(1 - oldRatio))
+ return true;
+
+ return false;
+ }
+
+ _sortRow(row) {
+ // Sort windows horizontally to minimize travel distance.
+ // This affects in what order the windows end up in a row.
+ row.windows.sort((a, b) => a.windowCenter.x - b.windowCenter.x);
+ }
+
+ computeLayout(windows, layoutParams) {
+ layoutParams = Params.parse(layoutParams, {
+ numRows: 0,
+ });
+
+ if (layoutParams.numRows === 0)
+ throw new Error(`${this.constructor.name}: No numRows given in layout params`);
+
+ const numRows = layoutParams.numRows;
+
+ let rows = [];
+ let totalWidth = 0;
+ for (let i = 0; i < windows.length; i++) {
+ let window = windows[i];
+ let s = this._computeWindowScale(window);
+ totalWidth += window.boundingBox.width * s;
+ }
+
+ let idealRowWidth = totalWidth / numRows;
+
+ // Sort windows vertically to minimize travel distance.
+ // This affects what rows the windows get placed in.
+ let sortedWindows = windows.slice();
+ sortedWindows.sort((a, b) => a.windowCenter.y - b.windowCenter.y);
+
+ let windowIdx = 0;
+ for (let i = 0; i < numRows; i++) {
+ let row = this._newRow();
+ rows.push(row);
+
+ for (; windowIdx < sortedWindows.length; windowIdx++) {
+ let window = sortedWindows[windowIdx];
+ let s = this._computeWindowScale(window);
+ let width = window.boundingBox.width * s;
+ let height = window.boundingBox.height * s;
+ row.fullHeight = Math.max(row.fullHeight, height);
+
+ // either new width is < idealWidth or new width is nearer from idealWidth then oldWidth
+ if (this._keepSameRow(row, window, width, idealRowWidth) || (i === numRows - 1)) {
+ row.windows.push(window);
+ row.fullWidth += width;
+ } else {
+ break;
+ }
+ }
+ }
+
+ let gridHeight = 0;
+ let maxRow;
+ for (let i = 0; i < numRows; i++) {
+ let row = rows[i];
+ this._sortRow(row);
+
+ if (!maxRow || row.fullWidth > maxRow.fullWidth)
+ maxRow = row;
+ gridHeight += row.fullHeight;
+ }
+
+ return {
+ numRows,
+ rows,
+ maxColumns: maxRow.windows.length,
+ gridWidth: maxRow.fullWidth,
+ gridHeight,
+ };
+ }
+
+ computeScaleAndSpace(layout, area) {
+ let hspacing = (layout.maxColumns - 1) * this._columnSpacing;
+ let vspacing = (layout.numRows - 1) * this._rowSpacing;
+
+ let spacedWidth = area.width - hspacing;
+ let spacedHeight = area.height - vspacing;
+
+ let horizontalScale = spacedWidth / layout.gridWidth;
+ let verticalScale = spacedHeight / layout.gridHeight;
+
+ // Thumbnails should be less than 70% of the original size
+ let scale = Math.min(
+ horizontalScale, verticalScale, WINDOW_PREVIEW_MAXIMUM_SCALE);
+
+ let scaledLayoutWidth = layout.gridWidth * scale + hspacing;
+ let scaledLayoutHeight = layout.gridHeight * scale + vspacing;
+ let space = (scaledLayoutWidth * scaledLayoutHeight) / (area.width * area.height);
+
+ layout.scale = scale;
+
+ return [scale, space];
+ }
+
+ computeWindowSlots(layout, area) {
+ this._computeRowSizes(layout);
+
+ let { rows, scale } = layout;
+
+ let slots = [];
+
+ // Do this in three parts.
+ let heightWithoutSpacing = 0;
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+ heightWithoutSpacing += row.height;
+ }
+
+ let verticalSpacing = (rows.length - 1) * this._rowSpacing;
+ let additionalVerticalScale = Math.min(1, (area.height - verticalSpacing) / heightWithoutSpacing);
+
+ // keep track how much smaller the grid becomes due to scaling
+ // so it can be centered again
+ let compensation = 0;
+ let y = 0;
+
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+
+ // If this window layout row doesn't fit in the actual
+ // geometry, then apply an additional scale to it.
+ let horizontalSpacing = (row.windows.length - 1) * this._columnSpacing;
+ let widthWithoutSpacing = row.width - horizontalSpacing;
+ let additionalHorizontalScale = Math.min(1, (area.width - horizontalSpacing) / widthWithoutSpacing);
+
+ if (additionalHorizontalScale < additionalVerticalScale) {
+ row.additionalScale = additionalHorizontalScale;
+ // Only consider the scaling in addition to the vertical scaling for centering.
+ compensation += (additionalVerticalScale - additionalHorizontalScale) * row.height;
+ } else {
+ row.additionalScale = additionalVerticalScale;
+ // No compensation when scaling vertically since centering based on a too large
+ // height would undo what vertical scaling is trying to achieve.
+ }
+
+ row.x = area.x + (Math.max(area.width - (widthWithoutSpacing * row.additionalScale + horizontalSpacing), 0) / 2);
+ row.y = area.y + (Math.max(area.height - (heightWithoutSpacing + verticalSpacing), 0) / 2) + y;
+ y += row.height * row.additionalScale + this._rowSpacing;
+ }
+
+ compensation /= 2;
+
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ const rowY = row.y + compensation;
+ const rowHeight = row.height * row.additionalScale;
+
+ let x = row.x;
+ for (let j = 0; j < row.windows.length; j++) {
+ let window = row.windows[j];
+
+ let s = scale * this._computeWindowScale(window) * row.additionalScale;
+ let cellWidth = window.boundingBox.width * s;
+ let cellHeight = window.boundingBox.height * s;
+
+ s = Math.min(s, WINDOW_PREVIEW_MAXIMUM_SCALE);
+ let cloneWidth = window.boundingBox.width * s;
+ const cloneHeight = window.boundingBox.height * s;
+
+ let cloneX = x + (cellWidth - cloneWidth) / 2;
+ let cloneY;
+
+ // If there's only one row, align windows vertically centered inside the row
+ if (rows.length === 1)
+ cloneY = rowY + (rowHeight - cloneHeight) / 2;
+ // If there are multiple rows, align windows to the bottom edge of the row
+ else
+ cloneY = rowY + rowHeight - cellHeight;
+
+ // Align with the pixel grid to prevent blurry windows at scale = 1
+ cloneX = Math.floor(cloneX);
+ cloneY = Math.floor(cloneY);
+
+ slots.push([cloneX, cloneY, cloneWidth, cloneHeight, window]);
+ x += cellWidth + this._columnSpacing;
+ }
+ }
+ return slots;
+ }
+}
+
+const WorkspaceBackground = {
+ _updateBorderRadius(value = false) {
+ // don't round already rounded corners during exposing windows
+ if (value === false && opt.OVERVIEW_MODE === 1)
+ return;
+
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const cornerRadius = scaleFactor * opt.WS_PREVIEW_BG_RADIUS;
+
+ const backgroundContent = this._bgManager.backgroundActor.content;
+ value = value !== false
+ ? value
+ : this._stateAdjustment.value;
+
+ backgroundContent.rounded_clip_radius =
+ Util.lerp(0, cornerRadius, value);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/workspaceAnimation.js b/extensions/47/vertical-workspaces/lib/workspaceAnimation.js
new file mode 100644
index 0000000..e29e3ef
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/workspaceAnimation.js
@@ -0,0 +1,262 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * workspacesAnimation.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Layout from 'resource:///org/gnome/shell/ui/layout.js';
+import * as WorkspaceSwitcherPopup from 'resource:///org/gnome/shell/ui/workspaceSwitcherPopup.js';
+import * as WorkspaceAnimation from 'resource:///org/gnome/shell/ui/workspaceAnimation.js';
+import * as Util from 'resource:///org/gnome/shell/misc/util.js';
+
+let Me;
+let opt;
+
+export const WorkspaceAnimationModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ this._origBaseDistance = null;
+ this._wsAnimationSwipeBeginId = 0;
+ this._wsAnimationSwipeUpdateId = 0;
+ this._wsAnimationSwipeEndId = 0;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('workspaceAnimationModule');
+ const conflict = !WorkspaceAnimation.MonitorGroup;
+ if (conflict)
+ console.warn(`[${Me.metadata.name}] Warning: "WorkspaceAnimation" module disabled due to compatibility - GNOME Shell 45.1 or later is required`);
+
+ 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(' WorkspaceAnimationModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('MonitorGroup', WorkspaceAnimation.MonitorGroup.prototype, MonitorGroup);
+ this._connectWsAnimationSwipeTracker();
+
+ console.debug(' WorkspaceAnimationModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+ const reset = true;
+ this._connectWsAnimationSwipeTracker(reset);
+
+ console.debug(' WorkspaceAnimationModule - Disabled');
+ }
+
+ _connectWsAnimationSwipeTracker(reset = false) {
+ if (reset) {
+ if (this._wsAnimationSwipeBeginId) {
+ Main.wm._workspaceAnimation._swipeTracker.disconnect(this._wsAnimationSwipeBeginId);
+ this._wsAnimationSwipeBeginId = 0;
+ }
+ if (this._wsAnimationSwipeEndId) {
+ Main.wm._workspaceAnimation._swipeTracker.disconnect(this._wsAnimationSwipeEndId);
+ this._wsAnimationSwipeEndId = 0;
+ }
+ } else if (!this._wsAnimationSwipeBeginId) {
+ // display ws switcher popup when gesture begins and connect progress
+ this._wsAnimationSwipeBeginId = Main.wm._workspaceAnimation._swipeTracker.connect('begin', () => this._connectWsAnimationProgress(true));
+ // we want to be sure that popup with the final ws index show up when gesture ends
+ this._wsAnimationSwipeEndId = Main.wm._workspaceAnimation._swipeTracker.connect('end', (tracker, duration, endProgress) => this._connectWsAnimationProgress(false, endProgress));
+ }
+ }
+
+ _connectWsAnimationProgress(connect, endProgress = null) {
+ if (Main.overview.visible)
+ return;
+
+ if (connect && !this._wsAnimationSwipeUpdateId) {
+ this._wsAnimationSwipeUpdateId = Main.wm._workspaceAnimation._swipeTracker.connect('update', (tracker, progress) => this._showWsSwitcherPopup(progress));
+ } else if (!connect && this._wsAnimationSwipeUpdateId) {
+ Main.wm._workspaceAnimation._swipeTracker.disconnect(this._wsAnimationSwipeUpdateId);
+ this._wsAnimationSwipeUpdateId = 0;
+ this._showWsSwitcherPopup(Math.round(endProgress));
+ }
+ }
+
+ _showWsSwitcherPopup(progress) {
+ if (Main.overview.visible)
+ return;
+
+ const wsIndex = Math.round(progress);
+ if (Main.wm._workspaceSwitcherPopup === null) {
+ Main.wm._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup();
+ Main.wm._workspaceSwitcherPopup.connect('destroy', () => {
+ Main.wm._workspaceSwitcherPopup = null;
+ });
+ }
+
+ Main.wm._workspaceSwitcherPopup.display(wsIndex);
+ }
+};
+
+const MonitorGroup = {
+ _init(monitor, workspaceIndices, movingWindow) {
+ St.Widget.prototype._init.bind(this)({
+ clip_to_allocation: true,
+ style_class: 'workspace-animation',
+ });
+
+ this._monitor = monitor;
+
+ const constraint = new Layout.MonitorConstraint({ index: monitor.index });
+ this.add_constraint(constraint);
+
+ this._container = new Clutter.Actor();
+ this.add_child(this._container);
+
+ const stickyGroup = new WorkspaceAnimation.WorkspaceGroup(null, monitor, movingWindow);
+ stickyGroup._windowRecords.forEach(r => {
+ const metaWin = r.windowActor.metaWindow;
+ // conky is sticky but should never get above other windows during ws animation
+ // so we hide it from the overlay group, we will see the original if not covered by other windows
+ if (metaWin.wm_class === 'conky')
+ r.clone.opacity = 0;
+ });
+ this.add_child(stickyGroup);
+
+ this._workspaceGroups = [];
+
+ const workspaceManager = global.workspace_manager;
+ const vertical = workspaceManager.layout_rows === -1;
+ const activeWorkspace = workspaceManager.get_active_workspace();
+
+ let x = 0;
+ let y = 0;
+
+ for (const i of workspaceIndices) {
+ const ws = workspaceManager.get_workspace_by_index(i);
+ const fullscreen = ws.list_windows().some(w => w.get_monitor() === monitor.index && w.is_fullscreen());
+
+ if (i > 0 && vertical && !fullscreen && monitor.index === Main.layoutManager.primaryIndex) {
+ // We have to shift windows up or down by the height of the panel to prevent having a
+ // visible gap between the windows while switching workspaces. Since fullscreen windows
+ // hide the panel, they don't need to be shifted up or down.
+ y -= Main.panel.height;
+ }
+
+ const group = new WorkspaceAnimation.WorkspaceGroup(ws, monitor, movingWindow);
+
+ this._workspaceGroups.push(group);
+ this._container.add_child(group);
+ group.set_position(x, y);
+
+ if (vertical)
+ y += this.baseDistance;
+ else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
+ x -= this.baseDistance;
+ else
+ x += this.baseDistance;
+ }
+
+ this.progress = this.getWorkspaceProgress(activeWorkspace);
+
+ if (monitor.index === Main.layoutManager.primaryIndex) {
+ this._workspacesAdjustment = Main.createWorkspacesAdjustment(this);
+ this.bind_property_full('progress',
+ this._workspacesAdjustment, 'value',
+ GObject.BindingFlags.SYNC_CREATE,
+ (bind, source) => {
+ const indices = [
+ workspaceIndices[Math.floor(source)],
+ workspaceIndices[Math.ceil(source)],
+ ];
+ return [true, Util.lerp(...indices, source % 1.0)];
+ },
+ null);
+
+ this.connect('destroy', () => {
+ // for some reason _workspaceAdjustment bound to the progress property in V-Shell
+ // causes the adjustment doesn't reach a whole number
+ // when switching ws up and that breaks the showing overview animation
+ // as a workaround round workspacesDisplay._scrollAdjustment value on destroy
+ // but it should be handled elsewhere as this workaround doesn't work when this module is disabled
+ const workspacesAdj = Main.overview._overview.controls._workspacesDisplay._scrollAdjustment;
+ workspacesAdj.value = Math.round(workspacesAdj.value);
+ delete this._workspacesAdjustment;
+ });
+ }
+
+ if (!opt.STATIC_WS_SWITCHER_BG)
+ return;
+
+ // we have two options to implement static bg feature
+ // one is adding background to monitorGroup
+ // but this one has disadvantage - sticky windows will be always on top of animated windows
+ // which is bad for conky, for example, that window should be always below
+ /* this._bgManager = new Background.BackgroundManager({
+ container: this,
+ monitorIndex: this._monitor.index,
+ controlPosition: false,
+ });*/
+
+ // the second option is to make background of the monitorGroup transparent so the real desktop content will stay visible,
+ // hide windows that should be animated and keep only sticky windows
+ // we can keep certain sticky windows bellow and also extensions like DING (icons on desktop) will stay visible
+ this.set_style('background-color: transparent;');
+ // stickyGroup holds the Always on Visible Workspace windows to keep them static and above other windows during animation
+ this._hiddenWindows = [];
+ // remove (hide) background wallpaper from the animation, we will see the original one
+ this._workspaceGroups.forEach(w => {
+ w._background.opacity = 0;
+ });
+ // hide (scale to 0) all non-sticky windows, their clones will be animated
+ global.get_window_actors().forEach(actor => {
+ const metaWin = actor.metaWindow;
+ if (metaWin?.get_monitor() === this._monitor.index &&
+ !(metaWin?.wm_class === 'conky' && metaWin?.is_on_all_workspaces()) &&
+ !(metaWin?.wm_class === 'Gjs' && metaWin?.is_on_all_workspaces())) { // DING extension uses window with Gjs class
+ // hide original window. we cannot use opacity since it also affects clones.
+ // scaling them to 0 works well
+ actor.scale_x = 0;
+ this._hiddenWindows.push(actor);
+ }
+ });
+
+ // restore all hidden windows at the end of animation
+ // todo - actors removed during transition need to be removed from the list to avoid access to destroyed actor
+ this.connect('destroy', () => {
+ this._hiddenWindows.forEach(actor => {
+ actor.scale_x = 1;
+ });
+ });
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/workspaceSwitcherPopup.js b/extensions/47/vertical-workspaces/lib/workspaceSwitcherPopup.js
new file mode 100644
index 0000000..cf3d4c1
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/workspaceSwitcherPopup.js
@@ -0,0 +1,107 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * workspacesSwitcherPopup.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as WorkspaceSwitcherPopup from 'resource:///org/gnome/shell/ui/workspaceSwitcherPopup.js';
+
+let Me;
+let opt;
+
+export const WorkspaceSwitcherPopupModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ this.moduleEnabled = opt.get('workspaceSwitcherPopupModule');
+ const conflict = Me.Util.getEnabledExtensions('workspace-switcher-manager').length ||
+ Me.Util.getEnabledExtensions('WsSwitcherPopupManager').length;
+ if (conflict && !reset)
+ console.warn(`[${Me.metadata.name}] Warning: "WorkspaceSwitcherPopup" module disabled due to potential conflict with another extension`);
+
+ reset = reset || !this.moduleEnabled || conflict;
+
+ // don't touch 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(' WorkspaceSwitcherPopupModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ this._overrides.addOverride('WorkspaceSwitcherPopup', WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype, WorkspaceSwitcherPopupCommon);
+ console.debug(' WorkspaceSwitcherPopupModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ console.debug(' WorkspaceSwitcherPopupModule - Disabled');
+ }
+};
+
+const WorkspaceSwitcherPopupCommon = {
+ // injection to _init()
+ after__init() {
+ if (opt.ORIENTATION) { // 1-VERTICAL, 0-HORIZONTAL
+ this._list.vertical = true;
+ this._list.add_style_class_name('ws-switcher-vertical');
+ }
+ this._list.set_style('margin: 0;');
+ if (this.get_constraints()[0])
+ this.remove_constraint(this.get_constraints()[0]);
+ },
+
+ // injection to display()
+ after_display() {
+ if (opt.WS_SW_POPUP_MODE)
+ this._setPopupPosition();
+ else
+ this.opacity = 0;
+ },
+
+ _setPopupPosition() {
+ let workArea;
+ if (opt.WS_SW_POPUP_MODE === 1)
+ workArea = global.display.get_monitor_geometry(Main.layoutManager.primaryIndex);
+ else
+ workArea = global.display.get_monitor_geometry(global.display.get_current_monitor());
+
+
+ let [, natHeight] = this.get_preferred_height(global.screen_width);
+ let [, natWidth] = this.get_preferred_width(natHeight);
+ let h = opt.WS_SW_POPUP_H_POSITION;
+ let v = opt.WS_SW_POPUP_V_POSITION;
+ this.x = workArea.x + Math.floor((workArea.width - natWidth) * h);
+ this.y = workArea.y + Math.floor((workArea.height - natHeight) * v);
+ this.set_position(this.x, this.y);
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/workspaceThumbnail.js b/extensions/47/vertical-workspaces/lib/workspaceThumbnail.js
new file mode 100644
index 0000000..cce5046
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/workspaceThumbnail.js
@@ -0,0 +1,1261 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * workspaceThumbnail.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import GLib from 'gi://GLib';
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
+import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
+import * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js';
+import * as WorkspaceThumbnail from 'resource:///org/gnome/shell/ui/workspaceThumbnail.js';
+import * as Background from 'resource:///org/gnome/shell/ui/background.js';
+
+let Me;
+let opt;
+
+const ThumbnailState = {
+ NEW: 0,
+ EXPANDING: 1,
+ EXPANDED: 2,
+ ANIMATING_IN: 3,
+ NORMAL: 4,
+ REMOVING: 5,
+ ANIMATING_OUT: 6,
+ ANIMATED_OUT: 7,
+ COLLAPSING: 8,
+ DESTROYED: 9,
+};
+
+const ControlsState = OverviewControls.ControlsState;
+
+const WORKSPACE_CUT_SCALE = 0.15;
+const WORKSPACE_KEEP_ALIVE_TIME = 100;
+
+export const WorkspaceThumbnailModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ 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(' WorkspaceThumbnailModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ // don't limit max thumbnail scale for other clients than overview, specifically AATWS.
+ // this variable is not yet implemented in 45.beta.1
+
+ this._overrides.addOverride('WorkspaceThumbnail', WorkspaceThumbnail.WorkspaceThumbnail.prototype, WorkspaceThumbnailCommon);
+ this._overrides.addOverride('ThumbnailsBoxCommon', WorkspaceThumbnail.ThumbnailsBox.prototype, ThumbnailsBoxCommon);
+ this._overrides.addOverride('WindowClone', WorkspaceThumbnail.WindowClone.prototype, WindowClone);
+
+ // replacing opt.ORIENTATION local constant with boxOrientation internal variable allows external customers such as the AATWS extension to control the box orientation.
+ Main.overview._overview.controls._thumbnailsBox._boxOrientation = opt.ORIENTATION;
+
+ console.debug(' WorkspaceThumbnailModule - Activated');
+ }
+
+ _disableModule() {
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ console.debug(' WorkspaceThumbnailModule - Disabled');
+ }
+};
+
+const WorkspaceThumbnailCommon = {
+ // injection to _init()
+ after__init() {
+ // layout manager allows aligning widget children
+ this.layout_manager = new Clutter.BinLayout();
+ // adding layout manager to tmb widget breaks wallpaper background aligning and rounded corners
+ // unless border is removed
+ if (opt.SHOW_WS_TMB_BG)
+ this.add_style_class_name('ws-tmb-labeled');
+ else
+ this.add_style_class_name('ws-tmb-transparent');
+
+ // add workspace thumbnails labels if enabled
+ if (opt.SHOW_WST_LABELS) { // 0 - disable
+ const getLabel = function () {
+ const wsIndex = this.metaWorkspace.index();
+ let label = `${wsIndex + 1}`;
+ if (opt.SHOW_WST_LABELS === 2) { // 2 - index + workspace name
+ const settings = Me.getSettings('org.gnome.desktop.wm.preferences');
+ const wsLabels = settings.get_strv('workspace-names');
+ if (wsLabels.length > wsIndex && wsLabels[wsIndex])
+ label += `: ${wsLabels[wsIndex]}`;
+ } else if (opt.SHOW_WST_LABELS === 3) { // 3- index + app name
+ // global.display.get_tab_list offers workspace filtering using the second argument, but...
+ // ... it sometimes includes windows from other workspaces, like minimized VBox machines, after Shell restarts
+ const metaWin = global.display.get_tab_list(0, null).filter(
+ w => w.get_monitor() === this.monitorIndex && w.get_workspace().index() === wsIndex)[0];
+
+ if (metaWin) {
+ const tracker = Shell.WindowTracker.get_default();
+ const app = tracker.get_window_app(metaWin);
+ label += `: ${app ? app.get_name() : ''}`;
+ }
+ } else if (opt.SHOW_WST_LABELS === 4) {
+ const metaWin = global.display.get_tab_list(0, null).filter(
+ w => w.get_monitor() === this.monitorIndex && w.get_workspace().index() === wsIndex)[0];
+
+ if (metaWin)
+ label += `: ${metaWin.title}`;
+ }
+ return label;
+ }.bind(this);
+
+ const label = getLabel();
+
+ this._wsLabel = new St.Label({
+ text: label,
+ style_class: 'ws-tmb-label',
+ x_align: Clutter.ActorAlign.FILL,
+ y_align: Clutter.ActorAlign.END,
+ x_expand: true,
+ y_expand: true,
+ });
+
+ this._wsLabel._maxOpacity = 255;
+ this._wsLabel.opacity = this._wsLabel._maxOpacity;
+
+ this.add_child(this._wsLabel);
+ this.set_child_above_sibling(this._wsLabel, null);
+
+ this._wsIndexConId = this.metaWorkspace.connect('notify::workspace-index', () => {
+ const newLabel = getLabel();
+ this._wsLabel.text = newLabel;
+ // avoid possibility of accessing non existing ws
+ if (this._updateLabelTimeout) {
+ GLib.source_remove(this._updateLabelTimeout);
+ this._updateLabelTimeout = 0;
+ }
+ });
+ this._nWindowsConId = this.metaWorkspace.connect('notify::n-windows', () => {
+ if (this._updateLabelTimeout)
+ return;
+ // wait for new data
+ this._updateLabelTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => {
+ const newLabel = getLabel();
+ this._wsLabel.text = newLabel;
+ this._updateLabelTimeout = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ });
+ }
+
+ if (opt.CLOSE_WS_BUTTON_MODE) {
+ const closeButton = new St.Icon({
+ style_class: 'workspace-close-button',
+ icon_name: 'window-close-symbolic',
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.START,
+ x_expand: true,
+ y_expand: true,
+ reactive: true,
+ opacity: 0,
+ });
+
+ closeButton.connect('button-release-event', () => {
+ if (opt.CLOSE_WS_BUTTON_MODE) {
+ this._closeWorkspace();
+ return Clutter.EVENT_STOP;
+ } else {
+ return Clutter.EVENT_PROPAGATE;
+ }
+ });
+
+ closeButton.connect('button-press-event', () => {
+ return Clutter.EVENT_STOP;
+ });
+
+ closeButton.connect('enter-event', () => {
+ closeButton.opacity = 255;
+ if (!Meta.prefs_get_dynamic_workspaces() || (Meta.prefs_get_dynamic_workspaces() && global.workspace_manager.get_n_workspaces() - 1 !== this.metaWorkspace.index())) {
+ // color the button red if ready to react on clicks
+ if (opt.CLOSE_WS_BUTTON_MODE < 3 || (opt.CLOSE_WS_BUTTON_MODE === 3 && Me.Util.isCtrlPressed()))
+ closeButton.add_style_class_name('workspace-close-button-hover');
+ }
+ });
+
+ closeButton.connect('leave-event', () => {
+ closeButton.remove_style_class_name('workspace-close-button-hover');
+ });
+
+ this.add_child(closeButton);
+ this._closeButton = closeButton;
+
+ this.reactive = true;
+ this._lastCloseClickTime = 0;
+ }
+
+ if (opt.SHOW_WST_LABELS_ON_HOVER)
+ this._wsLabel.opacity = 0;
+
+ this.connect('enter-event', () => {
+ if (opt.CLOSE_WS_BUTTON_MODE && (!Meta.prefs_get_dynamic_workspaces() || (Meta.prefs_get_dynamic_workspaces() && global.workspace_manager.get_n_workspaces() - 1 !== this.metaWorkspace.index())))
+ this._closeButton.opacity = 200;
+ if (opt.SHOW_WST_LABELS_ON_HOVER) {
+ this._wsLabel.ease({
+ duration: 100,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ opacity: this._wsLabel._maxOpacity,
+ });
+ }
+ });
+
+ this.connect('leave-event', () => {
+ this._closeButton.opacity = 0;
+ if (opt.SHOW_WST_LABELS_ON_HOVER) {
+ this._wsLabel.ease({
+ duration: 100,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ opacity: 0,
+ });
+ }
+ });
+
+ if (opt.SHOW_WS_TMB_BG) {
+ this._bgManager = new Background.BackgroundManager({
+ monitorIndex: this.monitorIndex,
+ container: this._viewport,
+ vignette: false,
+ controlPosition: false,
+ });
+
+ this._viewport.set_child_below_sibling(this._bgManager.backgroundActor, null);
+
+ // full brightness of the thumbnail bg draws unnecessary attention
+ // there is a grey bg under the wallpaper
+ this._bgManager.backgroundActor.opacity = 220;
+ }
+
+ this.connect('destroy', () => {
+ if (this._wsIndexConId)
+ this.metaWorkspace.disconnect(this._wsIndexConId);
+
+ if (this._nWindowsConId)
+ this.metaWorkspace.disconnect(this._nWindowsConId);
+
+ if (this._updateLabelTimeout)
+ GLib.source_remove(this._updateLabelTimeout);
+
+ if (this._bgManager)
+ this._bgManager.destroy();
+ });
+ },
+
+ _closeWorkspace() {
+ // CLOSE_WS_BUTTON_MODE 1: single click, 2: double-click, 3: Ctrl
+
+ if (opt.CLOSE_WS_BUTTON_MODE === 2) {
+ const doubleClickTime = Clutter.Settings.get_default().double_click_time;
+ const clickDelay = Date.now() - this._lastCloseClickTime;
+ if (clickDelay > doubleClickTime) {
+ this._lastCloseClickTime = Date.now();
+ return;
+ }
+ } else if (opt.CLOSE_WS_BUTTON_MODE === 3 && !Me.Util.isCtrlPressed()) {
+ return;
+ }
+
+ // close windows on this monitor
+ const windows = global.display.get_tab_list(0, null).filter(
+ w => w.get_monitor() === this.monitorIndex && w.get_workspace() === this.metaWorkspace
+ );
+
+ for (let i = 0; i < windows.length; i++) {
+ if (!windows[i].is_on_all_workspaces())
+ windows[i].delete(global.get_current_time() + i);
+ }
+ },
+
+ activate(time) {
+ if (this.state > ThumbnailState.NORMAL)
+ return;
+
+ // if Static Workspace overview mode active, a click on the already active workspace should activate the window picker mode
+ const wsIndex = this.metaWorkspace.index();
+ const lastWsIndex = global.display.get_workspace_manager().get_n_workspaces() - 1;
+ const stateAdjustment = Main.overview._overview.controls._stateAdjustment;
+
+ if (stateAdjustment.value === ControlsState.APP_GRID) {
+ if (this.metaWorkspace.active) {
+ Main.overview._overview.controls._shiftState(Meta.MotionDirection.DOWN);
+ // if searchActive, hide it immediately
+ Main.overview.searchEntry.set_text('');
+ } else {
+ this.metaWorkspace.activate(time);
+ }
+ } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && wsIndex < lastWsIndex) {
+ if (stateAdjustment.value > 1)
+ stateAdjustment.value = 1;
+
+
+ // spread windows
+ // in OVERVIEW MODE 2 windows are not spread and workspace is not scaled
+ // we need to repeat transition to the overview state 1 (window picker), but with spreading windows animation
+ if (this.metaWorkspace.active) {
+ Main.overview.searchController._setSearchActive(false);
+ opt.WORKSPACE_MODE = 1;
+ // setting value to 0 would reset WORKSPACE_MODE
+ stateAdjustment.value = 0.01;
+ stateAdjustment.ease(1, {
+ duration: 200,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ } else {
+ // switch ws
+ this.metaWorkspace.activate(time);
+ }
+ // a click on the current workspace should go back to the main view
+ } else if (this.metaWorkspace.active) {
+ 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.metaWindow &&
+ !this._isMyWindow(source.metaWindow.get_compositor_private()))
+ 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;
+
+ if (source instanceof AppDisplay.FolderIcon)
+ return DND.DragMotionResult.COPY_DROP;
+
+
+ return DND.DragMotionResult.CONTINUE;
+ },
+
+ acceptDropInternal(source, actor, time) {
+ if (this.state > ThumbnailState.NORMAL)
+ return false;
+
+ if (source.metaWindow) {
+ let win = source.metaWindow.get_compositor_private();
+ if (this._isMyWindow(win))
+ return false;
+
+ let metaWindow = win.get_meta_window();
+ Main.moveWindowToMonitorAndWorkspace(metaWindow,
+ this.monitorIndex, this.metaWorkspace.index());
+ 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;
+ } else if (source instanceof AppDisplay.FolderIcon) {
+ for (let app of source.view._apps) {
+ // const app = Shell.AppSystem.get_default().lookup_app(id);
+ app.open_new_window(this.metaWorkspace.index());
+ }
+ }
+
+ return false;
+ },
+};
+
+const ThumbnailsBoxCommon = {
+ after__init(scrollAdjustment, monitorIndex, orientation = opt.ORIENTATION) {
+ this._boxOrientation = orientation;
+ },
+
+ _activateThumbnailAtPoint(stageX, stageY, time, activateCurrent = false) {
+ if (activateCurrent) {
+ const thumbnail = this._thumbnails.find(t => t.metaWorkspace.active);
+ if (thumbnail)
+ thumbnail.activate(time);
+ return;
+ }
+ const [r_, x, y] = this.transform_stage_point(stageX, stageY);
+
+ let thumbnail;
+
+ if (this._boxOrientation)
+ thumbnail = this._thumbnails.find(t => y >= t.y && y <= t.y + t.height);
+ else
+ thumbnail = this._thumbnails.find(t => x >= t.x && x <= t.x + t.width);
+
+ if (thumbnail)
+ thumbnail.activate(time);
+ },
+
+ 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.metaWindow &&
+ (!source.app || !source.app.can_open_new_window()) &&
+ (source.app || !source.shellWorkspaceLaunch) &&
+ !(source instanceof AppDisplay.FolderIcon))
+ return false;
+
+
+ let isWindow = !!source.metaWindow;
+
+ 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;
+ Main.moveWindowToMonitorAndWorkspace(source.metaWindow,
+ thumbMonitor, 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,
+ });
+ } else if (source instanceof AppDisplay.FolderIcon) {
+ for (let app of source.view._apps) {
+ // const app = Shell.AppSystem.get_default().lookup_app(id);
+ app.open_new_window(newWorkspaceIndex);
+ }
+ }
+
+ 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;
+ thumbnail.collapse_fraction = 1;
+
+ this._queueUpdateStates();
+
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ handleDragOver(source, actor, x, y, time) {
+ // switch axis for vertical orientation
+ if (this._boxOrientation)
+ x = y;
+
+ if (!source.metaWindow &&
+ (!source.app || !source.app.can_open_new_window()) &&
+ (source.app || !source.shellWorkspaceLaunch) &&
+ source !== Main.xdndHandler && !(source instanceof AppDisplay.FolderIcon))
+ return DND.DragMotionResult.CONTINUE;
+
+ const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+ let canCreateWorkspaces = Meta.prefs_get_dynamic_workspaces();
+ let spacing = this.get_theme_node().get_length('spacing');
+
+ this._dropWorkspace = -1;
+ let placeholderPos = -1;
+ let length = this._thumbnails.length;
+ for (let i = 0; i < length; i++) {
+ const index = rtl ? length - i - 1 : i;
+
+ if (canCreateWorkspaces && source !== Main.xdndHandler) {
+ const [targetStart, targetEnd] =
+ this._getPlaceholderTarget(index, spacing, rtl);
+
+ if (x > targetStart && x <= targetEnd) {
+ placeholderPos = index;
+ break;
+ }
+ }
+
+ if (this._withinWorkspace(x, index, rtl)) {
+ this._dropWorkspace = index;
+ break;
+ }
+ }
+
+ 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.metaWindow ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.COPY_DROP;
+ else
+ return DND.DragMotionResult.CONTINUE;
+ },
+
+ _updateStates() {
+ const controlsManager = Main.overview._overview.controls;
+ const { currentState } = controlsManager._stateAdjustment.getStateTransitionParams();
+ this.SLIDE_ANIMATION_TIME = 200;
+ this.RESCALE_ANIMATION_TIME = 200;
+ // remove rescale animation during this scale transition, it is redundant and delayed
+ if ((currentState < 2 && currentState > 1) || controlsManager._searchController.searchActive)
+ this.RESCALE_ANIMATION_TIME = 0;
+
+ this._updateStateId = 0;
+
+ // If we are animating the indicator, wait
+ if (this._animatingIndicator)
+ return;
+
+ // Likewise if we are in the process of hiding
+ if (!this._shouldShow && this.visible)
+ 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: this.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,
+ // collapse any removed thumbnails and expand added ones
+ this._iterateStateThumbnails(ThumbnailState.ANIMATED_OUT, thumbnail => {
+ this._setThumbnailState(thumbnail, ThumbnailState.COLLAPSING);
+ thumbnail.ease_property('collapse-fraction', 1, {
+ duration: this.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();
+ },
+ });
+ });
+
+ this._iterateStateThumbnails(ThumbnailState.NEW, thumbnail => {
+ this._setThumbnailState(thumbnail, ThumbnailState.EXPANDING);
+ thumbnail.ease_property('collapse-fraction', 0, {
+ duration: this.SLIDE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._setThumbnailState(thumbnail, ThumbnailState.EXPANDED);
+ this._queueUpdateStates();
+ },
+ });
+ });
+
+ if (this._pendingScaleUpdate) {
+ this.ease_property('scale', this._targetScale, {
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ duration: this.RESCALE_ANIMATION_TIME,
+ onComplete: () => this._queueUpdateStates(),
+ });
+ this._queueUpdateStates();
+ this._pendingScaleUpdate = false;
+ }
+
+ // Wait until that's done
+ if (this._scale !== this._targetScale ||
+ this._stateCounts[ThumbnailState.COLLAPSING] > 0 ||
+ this._stateCounts[ThumbnailState.EXPANDING] > 0)
+ return;
+
+ // And then slide in any new thumbnails
+ this._iterateStateThumbnails(ThumbnailState.EXPANDED, thumbnail => {
+ this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_IN);
+ thumbnail.ease_property('slide-position', 0, {
+ duration: this.SLIDE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._setThumbnailState(thumbnail, ThumbnailState.NORMAL);
+ },
+ });
+ });
+ },
+
+ _getPlaceholderTarget(...args) {
+ if (this._boxOrientation)
+ return ThumbnailsBoxVertical._getPlaceholderTarget.bind(this)(...args);
+ else
+ return ThumbnailsBoxHorizontal._getPlaceholderTarget.bind(this)(...args);
+ },
+
+ _withinWorkspace(...args) {
+ if (this._boxOrientation)
+ return ThumbnailsBoxVertical._withinWorkspace.bind(this)(...args);
+ else
+ return ThumbnailsBoxHorizontal._withinWorkspace.bind(this)(...args);
+ },
+
+ vfunc_get_preferred_width(...args) {
+ if (this._boxOrientation)
+ return ThumbnailsBoxVertical.vfunc_get_preferred_width.bind(this)(...args);
+ else
+ return ThumbnailsBoxHorizontal.vfunc_get_preferred_width.bind(this)(...args);
+ },
+
+ vfunc_get_preferred_height(...args) {
+ if (this._boxOrientation)
+ return ThumbnailsBoxVertical.vfunc_get_preferred_height.bind(this)(...args);
+ else
+ return ThumbnailsBoxHorizontal.vfunc_get_preferred_height.bind(this)(...args);
+ },
+
+ vfunc_allocate(...args) {
+ if (this._boxOrientation)
+ return ThumbnailsBoxVertical.vfunc_allocate.bind(this)(...args);
+ else
+ return ThumbnailsBoxHorizontal.vfunc_allocate.bind(this)(...args);
+ },
+
+ _updateShouldShow(...args) {
+ if (this._boxOrientation)
+ return ThumbnailsBoxVertical._updateShouldShow.bind(this)(...args);
+ else
+ return ThumbnailsBoxHorizontal._updateShouldShow.bind(this)(...args);
+ },
+};
+
+function _getWorkspaceCutSize(tmbSize, index) {
+ let cutSize = WORKSPACE_CUT_SCALE * tmbSize;
+ // Compensate for the missing thumbnail in front of the first one
+ if (index === 0)
+ cutSize *= 1.5;
+ return Math.floor(cutSize);
+}
+
+const ThumbnailsBoxVertical = {
+ _getPlaceholderTarget(index, spacing, rtl) {
+ this._dropPlaceholder.add_style_class_name('placeholder-vertical');
+ const workspace = this._thumbnails[index];
+ const WORKSPACE_CUT_SIZE = _getWorkspaceCutSize(workspace.height, index);
+
+ let targetY1;
+ let targetY2;
+
+ if (rtl) {
+ const baseY = workspace.y + workspace.height;
+ targetY1 = baseY - WORKSPACE_CUT_SIZE;
+ targetY2 = baseY + spacing + WORKSPACE_CUT_SIZE;
+ } else {
+ targetY1 = workspace.y - spacing - WORKSPACE_CUT_SIZE;
+ targetY2 = workspace.y + WORKSPACE_CUT_SIZE;
+ }
+
+ if (index === 0) {
+ if (rtl)
+ targetY2 -= spacing + WORKSPACE_CUT_SIZE;
+ else
+ targetY1 += spacing + WORKSPACE_CUT_SIZE;
+ }
+
+ if (index === this._dropPlaceholderPos) {
+ const placeholderHeight = this._dropPlaceholder.get_height() + spacing;
+ if (rtl)
+ targetY2 += placeholderHeight;
+ else
+ targetY1 -= placeholderHeight;
+ }
+
+ return [targetY1, targetY2];
+ },
+
+ _withinWorkspace(y, index, rtl) {
+ const length = this._thumbnails.length;
+ const workspace = this._thumbnails[index];
+ const WORKSPACE_CUT_SIZE = _getWorkspaceCutSize(workspace.height, index);
+
+ let workspaceY1 = workspace.y + WORKSPACE_CUT_SIZE;
+ let workspaceY2 = workspace.y + workspace.height - WORKSPACE_CUT_SIZE;
+
+ if (index === length - 1) {
+ if (rtl)
+ workspaceY1 -= WORKSPACE_CUT_SIZE;
+ else
+ workspaceY2 += WORKSPACE_CUT_SIZE;
+ }
+
+ return y > workspaceY1 && y <= workspaceY2;
+ },
+
+ vfunc_get_preferred_width(forHeight) {
+ if (forHeight < 10)
+ return [this._porthole.width, this._porthole.width];
+
+ let themeNode = this.get_theme_node();
+
+ forHeight = themeNode.adjust_for_width(forHeight);
+
+ let spacing = themeNode.get_length('spacing');
+ let nWorkspaces = this._thumbnails.length;
+ let totalSpacing = (nWorkspaces - 1) * spacing;
+
+ const avail = forHeight - totalSpacing;
+
+ let scale = (avail / nWorkspaces) / this._porthole.height;
+
+ const width = Math.round(this._porthole.width * scale);
+ return themeNode.adjust_preferred_height(width, width);
+ },
+
+ vfunc_get_preferred_height(forWidth) {
+ if (forWidth < 10)
+ return [0, this._porthole.height];
+ let themeNode = this.get_theme_node();
+
+ let spacing = themeNode.get_length('spacing');
+ let nWorkspaces = this._thumbnails.length;
+
+ // remove also top/bottom box padding
+ let totalSpacing = (nWorkspaces - 3) * spacing;
+
+ const ratio = this._porthole.width / this._porthole.height;
+ const tmbHeight = themeNode.adjust_for_width(forWidth) / ratio;
+
+ const naturalHeight = Math.round(
+ this._thumbnails.reduce((accumulator, thumbnail/* , index*/) => {
+ const progress = 1 - thumbnail.collapse_fraction;
+ const height = tmbHeight * progress;
+ return accumulator + height;
+ }, 0)
+ );
+ return themeNode.adjust_preferred_width(totalSpacing, naturalHeight);
+ },
+
+ // removes extra space (extraWidth in the original function), we need the box as accurate as possible
+ // for precise app grid transition animation
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ let rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+
+ if (this._thumbnails.length === 0) // not visible
+ return;
+
+ let themeNode = this.get_theme_node();
+ box = themeNode.get_content_box(box);
+
+ const portholeWidth = this._porthole.width;
+ const portholeHeight = this._porthole.height;
+ const spacing = themeNode.get_length('spacing');
+
+ /* const nWorkspaces = this._thumbnails.length;*/
+
+ // Compute the scale we'll need once everything is updated,
+ // unless we are currently transitioning
+ if (this._expandFraction === 1) {
+ // remove size "breathing" during adding/removing workspaces
+
+ /* const totalSpacing = (nWorkspaces - 1) * spacing;
+ const availableHeight = (box.get_height() - totalSpacing) / nWorkspaces; */
+
+ const hScale = box.get_width() / portholeWidth;
+ /* const vScale = availableHeight / portholeHeight;*/
+ const vScale = box.get_height() / portholeHeight;
+ const newScale = Math.min(hScale, vScale);
+
+ if (newScale !== this._targetScale) {
+ if (this._targetScale > 0) {
+ // We don't ease 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();
+ }
+ }
+
+ const ratio = portholeWidth / portholeHeight;
+ const thumbnailFullHeight = Math.round(portholeHeight * this._scale);
+ const thumbnailWidth = Math.round(thumbnailFullHeight * ratio);
+ const thumbnailHeight = thumbnailFullHeight * this._expandFraction;
+ const roundedVScale = thumbnailHeight / portholeHeight;
+
+ let indicatorValue = this._scrollAdjustment.value;
+ let indicatorUpperWs = Math.ceil(indicatorValue);
+ let indicatorLowerWs = Math.floor(indicatorValue);
+
+ let indicatorLowerY1 = 0;
+ let indicatorLowerY2 = 0;
+ let indicatorUpperY1 = 0;
+ let indicatorUpperY2 = 0;
+
+ 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);
+
+ let y = box.y1;
+
+ if (this._dropPlaceholderPos === -1) {
+ this._dropPlaceholder.allocate_preferred_size(
+ ...this._dropPlaceholder.get_position());
+
+ const laters = global.compositor.get_laters();
+ laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
+ this._dropPlaceholder.hide();
+ });
+ }
+
+ let childBox = new Clutter.ActorBox();
+
+ for (let i = 0; i < this._thumbnails.length; i++) {
+ const thumbnail = this._thumbnails[i];
+ if (i > 0)
+ y += spacing - Math.round(thumbnail.collapse_fraction * spacing);
+
+ const x1 = box.x1;
+ const x2 = x1 + thumbnailWidth;
+
+ if (i === this._dropPlaceholderPos) {
+ let [, placeholderHeight] = this._dropPlaceholder.get_preferred_width(-1);
+ childBox.x1 = x1;
+ childBox.x2 = x2;
+
+ if (rtl) {
+ childBox.y2 = box.y2 - Math.round(y);
+ childBox.y1 = box.y2 - Math.round(y + placeholderHeight);
+ } else {
+ childBox.y1 = Math.round(y);
+ childBox.y2 = Math.round(y + placeholderHeight);
+ }
+
+ this._dropPlaceholder.allocate(childBox);
+
+ const laters = global.compositor.get_laters();
+ laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
+ this._dropPlaceholder.show();
+ });
+ y += placeholderHeight + spacing;
+ }
+
+ // We might end up with thumbnailWidth being something like 99.33
+ // pixels. To make this work and not end up with a gap at the end,
+ // we need some thumbnails to be 99 pixels and some 100 pixels width;
+ // we compute an actual scale separately for each thumbnail.
+ const y1 = Math.round(y);
+ const y2 = Math.round(y + thumbnailHeight);
+ const roundedHScale = (y2 - y1) / portholeHeight;
+
+ // Allocating a scaled actor is funny - x1/y1 correspond to the origin
+ // of the actor, but x2/y2 are increased by the *unscaled* size.
+ if (rtl) {
+ childBox.y2 = box.y2 - y1;
+ childBox.y1 = box.y2 - (y1 + thumbnailHeight);
+ } else {
+ childBox.y1 = y1;
+ childBox.y2 = y1 + thumbnailHeight;
+ }
+ childBox.x1 = x1;
+ childBox.x2 = x1 + thumbnailWidth;
+
+ thumbnail.setScale(roundedHScale, roundedVScale);
+ thumbnail.allocate(childBox);
+
+ if (i === indicatorUpperWs) {
+ indicatorUpperY1 = childBox.y1;
+ indicatorUpperY2 = childBox.y2;
+ }
+ if (i === indicatorLowerWs) {
+ indicatorLowerY1 = childBox.y1;
+ indicatorLowerY2 = childBox.y2;
+ }
+
+ // 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);
+ }
+
+ childBox.x1 = box.x1;
+ childBox.x2 = box.x1 + thumbnailWidth;
+
+ const indicatorY1 = indicatorLowerY1 +
+ (indicatorUpperY1 - indicatorLowerY1) * (indicatorValue % 1);
+ const indicatorY2 = indicatorLowerY2 +
+ (indicatorUpperY2 - indicatorLowerY2) * (indicatorValue % 1);
+
+ childBox.y1 = indicatorY1 - indicatorTopFullBorder;
+ childBox.y2 = indicatorY2 + indicatorBottomFullBorder;
+ childBox.x1 -= indicatorLeftFullBorder;
+ childBox.x2 += indicatorRightFullBorder;
+ this._indicator.allocate(childBox);
+ },
+
+ _updateShouldShow() {
+ const shouldShow = opt.SHOW_WS_TMB;
+ if (this._shouldShow === shouldShow)
+ return;
+
+ this._shouldShow = shouldShow;
+ this.notify('should-show');
+ },
+};
+
+// ThumbnailsBox Horizontal
+
+const ThumbnailsBoxHorizontal = {
+ _getPlaceholderTarget(index, spacing, rtl) {
+ const workspace = this._thumbnails[index];
+ const WORKSPACE_CUT_SIZE = _getWorkspaceCutSize(workspace.width, index);
+
+ let targetX1;
+ let targetX2;
+
+ if (rtl) {
+ const baseX = workspace.x + workspace.width;
+ targetX1 = baseX - WORKSPACE_CUT_SIZE;
+ targetX2 = baseX + spacing + WORKSPACE_CUT_SIZE;
+ } else {
+ targetX1 = workspace.x - spacing - WORKSPACE_CUT_SIZE;
+ targetX2 = workspace.x + WORKSPACE_CUT_SIZE;
+ }
+
+ if (index === 0) {
+ if (rtl)
+ targetX2 -= spacing + WORKSPACE_CUT_SIZE;
+ else
+ targetX1 += spacing + WORKSPACE_CUT_SIZE;
+ }
+
+ if (index === this._dropPlaceholderPos) {
+ const placeholderWidth = this._dropPlaceholder.get_width() + spacing;
+ if (rtl)
+ targetX2 += placeholderWidth;
+ else
+ targetX1 -= placeholderWidth;
+ }
+
+ return [targetX1, targetX2];
+ },
+
+ _withinWorkspace(x, index, rtl) {
+ const length = this._thumbnails.length;
+ const workspace = this._thumbnails[index];
+ const WORKSPACE_CUT_SIZE = _getWorkspaceCutSize(workspace.width, index);
+
+ let workspaceX1 = workspace.x + WORKSPACE_CUT_SIZE;
+ let workspaceX2 = workspace.x + workspace.width - WORKSPACE_CUT_SIZE;
+
+ if (index === length - 1) {
+ if (rtl)
+ workspaceX1 -= WORKSPACE_CUT_SIZE;
+ else
+ workspaceX2 += WORKSPACE_CUT_SIZE;
+ }
+
+ return x > workspaceX1 && x <= workspaceX2;
+ },
+
+ vfunc_get_preferred_height(forWidth) {
+ if (forWidth < 10)
+ return [this._porthole.height, this._porthole.height];
+
+ let themeNode = this.get_theme_node();
+
+ forWidth = themeNode.adjust_for_width(forWidth);
+
+ let spacing = themeNode.get_length('spacing');
+ let nWorkspaces = this._thumbnails.length;
+ let totalSpacing = (nWorkspaces - 1) * spacing;
+
+ const avail = forWidth - totalSpacing;
+
+ let scale = (avail / nWorkspaces) / this._porthole.width;
+
+ const height = Math.round(this._porthole.height * scale);
+
+ return themeNode.adjust_preferred_height(height, height);
+ },
+
+ vfunc_get_preferred_width(forHeight) {
+ if (forHeight < 10)
+ return [0, this._porthole.width];
+
+ let themeNode = this.get_theme_node();
+
+ let spacing = themeNode.get_length('spacing');
+ let nWorkspaces = this._thumbnails.length;
+ // remove also left/right box padding from the total spacing
+ let totalSpacing = (nWorkspaces - 3) * spacing;
+
+ const ratio = this._porthole.height / this._porthole.width;
+
+ const tmbWidth = themeNode.adjust_for_height(forHeight) / ratio;
+
+ const naturalWidth = Math.round(
+ this._thumbnails.reduce((accumulator, thumbnail) => {
+ const progress = 1 - thumbnail.collapse_fraction;
+ const width = tmbWidth * progress;
+ return accumulator + width;
+ }, 0)
+ );
+
+ return themeNode.adjust_preferred_width(totalSpacing, naturalWidth);
+ },
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ let rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+
+ if (this._thumbnails.length === 0) // not visible
+ return;
+
+ let themeNode = this.get_theme_node();
+ box = themeNode.get_content_box(box);
+
+ const portholeWidth = this._porthole.width;
+ const portholeHeight = this._porthole.height;
+ const spacing = themeNode.get_length('spacing');
+
+ /* const nWorkspaces = this._thumbnails.length; */
+
+ // Compute the scale we'll need once everything is updated,
+ // unless we are currently transitioning
+ if (this._expandFraction === 1) {
+ // remove size "breathing" during adding/removing workspaces
+
+ /* const totalSpacing = (nWorkspaces - 1) * spacing;
+ const availableWidth = (box.get_width() - totalSpacing) / nWorkspaces;
+
+ const hScale = availableWidth / portholeWidth; */
+ const hScale = box.get_width() / portholeWidth;
+ const vScale = box.get_height() / portholeHeight;
+ const newScale = Math.min(hScale, vScale);
+
+ if (newScale !== this._targetScale) {
+ if (this._targetScale > 0) {
+ // We don't ease 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();
+ }
+ }
+
+ const ratio = portholeWidth / portholeHeight;
+ const thumbnailFullHeight = Math.round(portholeHeight * this._scale);
+ const thumbnailWidth = Math.round(thumbnailFullHeight * ratio);
+ const thumbnailHeight = thumbnailFullHeight * this._expandFraction;
+ const roundedVScale = thumbnailHeight / portholeHeight;
+
+ let indicatorValue = this._scrollAdjustment.value;
+ let indicatorUpperWs = Math.ceil(indicatorValue);
+ let indicatorLowerWs = Math.floor(indicatorValue);
+
+ let indicatorLowerX1 = 0;
+ let indicatorLowerX2 = 0;
+ let indicatorUpperX1 = 0;
+ let indicatorUpperX2 = 0;
+
+ 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);
+
+ let x = box.x1;
+
+ if (this._dropPlaceholderPos === -1) {
+ this._dropPlaceholder.allocate_preferred_size(
+ ...this._dropPlaceholder.get_position());
+
+ const laters = global.compositor.get_laters();
+ laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
+ this._dropPlaceholder.hide();
+ });
+ }
+
+ let childBox = new Clutter.ActorBox();
+
+ for (let i = 0; i < this._thumbnails.length; i++) {
+ const thumbnail = this._thumbnails[i];
+ if (i > 0)
+ x += spacing - Math.round(thumbnail.collapse_fraction * spacing);
+
+ const y1 = box.y1;
+ const y2 = y1 + thumbnailHeight;
+
+ if (i === this._dropPlaceholderPos) {
+ const [, placeholderWidth] = this._dropPlaceholder.get_preferred_width(-1);
+ childBox.y1 = y1;
+ childBox.y2 = y2;
+
+ if (rtl) {
+ childBox.x2 = box.x2 - Math.round(x);
+ childBox.x1 = box.x2 - Math.round(x + placeholderWidth);
+ } else {
+ childBox.x1 = Math.round(x);
+ childBox.x2 = Math.round(x + placeholderWidth);
+ }
+
+ this._dropPlaceholder.allocate(childBox);
+
+ const laters = global.compositor.get_laters();
+ laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
+ this._dropPlaceholder.show();
+ });
+ x += placeholderWidth + spacing;
+ }
+
+ // We might end up with thumbnailWidth being something like 99.33
+ // pixels. To make this work and not end up with a gap at the end,
+ // we need some thumbnails to be 99 pixels and some 100 pixels width;
+ // we compute an actual scale separately for each thumbnail.
+ const x1 = Math.round(x);
+ const x2 = Math.round(x + thumbnailWidth);
+ const roundedHScale = (x2 - x1) / portholeWidth;
+
+ // Allocating a scaled actor is funny - x1/y1 correspond to the origin
+ // of the actor, but x2/y2 are increased by the *unscaled* size.
+ if (rtl) {
+ childBox.x2 = box.x2 - x1;
+ childBox.x1 = box.x2 - (x1 + thumbnailWidth);
+ } else {
+ childBox.x1 = x1;
+ childBox.x2 = x1 + thumbnailWidth;
+ }
+ childBox.y1 = y1;
+ childBox.y2 = y1 + thumbnailHeight;
+
+ thumbnail.setScale(roundedHScale, roundedVScale);
+ thumbnail.allocate(childBox);
+
+ if (i === indicatorUpperWs) {
+ indicatorUpperX1 = childBox.x1;
+ indicatorUpperX2 = childBox.x2;
+ }
+ if (i === indicatorLowerWs) {
+ indicatorLowerX1 = childBox.x1;
+ indicatorLowerX2 = childBox.x2;
+ }
+
+ // 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);
+ }
+
+ childBox.y1 = box.y1;
+ childBox.y2 = box.y1 + thumbnailHeight;
+
+ const indicatorX1 = indicatorLowerX1 +
+ (indicatorUpperX1 - indicatorLowerX1) * (indicatorValue % 1);
+ const indicatorX2 = indicatorLowerX2 +
+ (indicatorUpperX2 - indicatorLowerX2) * (indicatorValue % 1);
+
+ childBox.x1 = indicatorX1 - indicatorLeftFullBorder;
+ childBox.x2 = indicatorX2 + indicatorRightFullBorder;
+ childBox.y1 -= indicatorTopFullBorder;
+ childBox.y2 += indicatorBottomFullBorder;
+ this._indicator.allocate(childBox);
+ },
+
+ _updateShouldShow: ThumbnailsBoxVertical._updateShouldShow,
+};
+
+const WindowClone = {
+ after__init() {
+ // Make it transparent and smaller than usual while dragging
+ this._draggable._dragActorOpacity = 200;
+ this._draggable._dragActorMaxSize = 150;
+ },
+};
diff --git a/extensions/47/vertical-workspaces/lib/workspacesView.js b/extensions/47/vertical-workspaces/lib/workspacesView.js
new file mode 100644
index 0000000..5c0d36b
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/workspacesView.js
@@ -0,0 +1,1013 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * workspacesView.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2024
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+import Meta from 'gi://Meta';
+import GObject from 'gi://GObject';
+
+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 Util from 'resource:///org/gnome/shell/misc/util.js';
+
+let Me;
+let opt;
+
+const ControlsState = OverviewControls.ControlsState;
+const FitMode = WorkspacesView.FitMode;
+
+export const WorkspacesViewModule = class {
+ constructor(me) {
+ Me = me;
+ opt = Me.opt;
+
+ this._firstActivation = true;
+ this.moduleEnabled = false;
+ this._overrides = null;
+ }
+
+ cleanGlobals() {
+ Me = null;
+ opt = null;
+ }
+
+ update(reset) {
+ 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(' WorkspacesViewModule - Keeping untouched');
+ }
+
+ _activateModule() {
+ if (!this._overrides)
+ this._overrides = new Me.Util.Overrides();
+
+ const desktopCubeEnabled = Me.Util.getEnabledExtensions('desktop-cube@schneegans.github.com').length;
+ const desktopCubeConflict = desktopCubeEnabled && !opt.ORIENTATION && !opt.OVERVIEW_MODE;
+
+ if (!desktopCubeConflict)
+ this._overrides.addOverride('WorkspacesView', WorkspacesView.WorkspacesView.prototype, WorkspacesViewCommon);
+ else
+ this._overrides.removeOverride('WorkspacesView');
+
+ this._overrides.addOverride('WorkspacesDisplay', WorkspacesView.WorkspacesDisplay.prototype, WorkspacesDisplayCommon);
+ this._overrides.addOverride('ExtraWorkspaceView', WorkspacesView.ExtraWorkspaceView.prototype, ExtraWorkspaceViewCommon);
+ this._overrides.addOverride('SecondaryMonitorDisplayCommon', WorkspacesView.SecondaryMonitorDisplay.prototype, SecondaryMonitorDisplayCommon);
+
+ if (opt.ORIENTATION) {
+ // switch internal workspace orientation in GS
+ global.workspace_manager.override_workspace_layout(Meta.DisplayCorner.TOPLEFT, false, -1, 1);
+ this._overrides.addOverride('SecondaryMonitorDisplay', WorkspacesView.SecondaryMonitorDisplay.prototype, SecondaryMonitorDisplayVertical);
+ } else {
+ global.workspace_manager.override_workspace_layout(Meta.DisplayCorner.TOPLEFT, false, 1, -1);
+ this._overrides.addOverride('SecondaryMonitorDisplay', WorkspacesView.SecondaryMonitorDisplay.prototype, SecondaryMonitorDisplayHorizontal);
+ }
+
+ console.debug(' WorkspacesViewModule - Activated');
+ }
+
+ _disableModule() {
+ global.workspace_manager.override_workspace_layout(Meta.DisplayCorner.TOPLEFT, false, 1, -1);
+ if (this._overrides)
+ this._overrides.removeAll();
+ this._overrides = null;
+
+ console.debug(' WorkspacesViewModule - Disabled');
+ }
+};
+
+const WorkspacesViewCommon = {
+ _getFirstFitSingleWorkspaceBox(box, spacing, vertical) {
+ let [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();
+ const [, workspaceWidth] = workspace ? workspace.get_preferred_width(Math.floor(height)) : [0, width];
+ const [, workspaceHeight] = workspace ? workspace.get_preferred_height(workspaceWidth) : [0, height];
+
+ if (vertical) {
+ x1 += (width - workspaceWidth) / 2;
+ y1 -= currentWorkspace * (workspaceHeight + spacing);
+ } else {
+ x1 += (width - workspaceWidth) / 2;
+ x1 -= currentWorkspace * (workspaceWidth + spacing);
+ }
+
+ const fitSingleBox = new Clutter.ActorBox({ x1, y1 });
+ fitSingleBox.set_size(workspaceWidth, workspaceHeight);
+
+ return fitSingleBox;
+ },
+
+ // set spacing between ws previews
+ _getSpacing(box, fitMode, vertical) {
+ const [width, height] = box.get_size();
+ const [workspace] = this._workspaces;
+
+ if (!workspace)
+ return 0;
+
+ let availableSpace;
+ let workspaceSize;
+ if (vertical) {
+ [, workspaceSize] = workspace.get_preferred_height(width);
+ availableSpace = height;
+ } else {
+ [, workspaceSize] = workspace.get_preferred_width(height);
+ availableSpace = width;
+ }
+
+ const spacing = (availableSpace - workspaceSize * 0.4) * (1 - fitMode);
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ return Math.clamp(spacing,
+ opt.WORKSPACE_MIN_SPACING * scaleFactor,
+ opt.WORKSPACE_MAX_SPACING * scaleFactor);
+ },
+
+ // this function has duplicate in OverviewControls so we use one function for both to avoid issues with syncing them
+ _getFitModeForState(state) {
+ return _getFitModeForState(state);
+ },
+
+ // normal view 0, spread windows 1
+ _getWorkspaceModeForOverviewState(state) {
+
+ switch (state) {
+ case ControlsState.HIDDEN:
+ return 0;
+ case ControlsState.WINDOW_PICKER:
+ return opt.WORKSPACE_MODE;
+ case ControlsState.APP_GRID:
+ return (this._monitorIndex !== global.display.get_primary_monitor() || !opt.WS_ANIMATION) && !opt.OVERVIEW_MODE ? 1 : 0;
+ }
+
+ return 0;
+ },
+
+ _updateVisibility() {
+ // visibility handles _updateWorkspacesState()
+ },
+
+ // disable scaling and hide inactive workspaces
+ _updateWorkspacesState() {
+ const adj = this._scrollAdjustment;
+ const fitMode = this._fitModeAdjustment.value;
+
+ let { initialState, finalState, progress, currentState } =
+ this._overviewAdjustment.getStateTransitionParams();
+
+ const workspaceMode = (1 - fitMode) * Util.lerp(
+ this._getWorkspaceModeForOverviewState(initialState),
+ this._getWorkspaceModeForOverviewState(finalState),
+ progress);
+
+ const primaryMonitor = Main.layoutManager.primaryMonitor.index;
+
+ const wsScrollProgress = adj.value % 1;
+ const secondaryMonitor = this._monitorIndex !== global.display.get_primary_monitor();
+ const blockSecondaryAppGrid = opt.OVERVIEW_MODE && currentState > 1;
+
+ // Hide inactive workspaces
+ this._workspaces.forEach((w, index) => {
+ if (!(blockSecondaryAppGrid && secondaryMonitor))
+ w.stateAdjustment.value = workspaceMode;
+
+ let distance = adj.value - index;
+ const distanceToCurrentWorkspace = Math.abs(distance);
+
+ const scaleProgress = 1 - Math.clamp(distanceToCurrentWorkspace, 0, 1);
+ // const scale = Util.lerp(0.94, 1, scaleProgress);
+ // w.set_scale(scale, scale);
+
+ // if we disable workspaces that we can't or don't need to see, transition animations will be noticeably smoother
+ // only the current ws needs to be visible during overview transition animations
+ // and only current and adjacent ws when switching ws
+ w.visible =
+ (this._animating && wsScrollProgress && distanceToCurrentWorkspace <= (opt.NUMBER_OF_VISIBLE_NEIGHBORS + 1)) ||
+ scaleProgress === 1 ||
+ (opt.WORKSPACE_MAX_SPACING >= opt.WS_MAX_SPACING_OFF_SCREEN &&
+ distanceToCurrentWorkspace <= opt.NUMBER_OF_VISIBLE_NEIGHBORS &&
+ currentState === ControlsState.WINDOW_PICKER
+ ) ||
+ (this._monitorIndex !== primaryMonitor && distanceToCurrentWorkspace <= opt.NUMBER_OF_VISIBLE_NEIGHBORS) ||
+ (!opt.WS_ANIMATION && distanceToCurrentWorkspace < opt.NUMBER_OF_VISIBLE_NEIGHBORS) ||
+ (distanceToCurrentWorkspace <= opt.NUMBER_OF_VISIBLE_NEIGHBORS &&
+ currentState <= ControlsState.WINDOW_PICKER &&
+ (initialState < ControlsState.APP_GRID && finalState < ControlsState.APP_GRID)
+ );
+
+ // after transition from APP_GRID to WINDOW_PICKER state,
+ // adjacent workspaces are hidden and we need them to show up
+ // make them visible during animation can impact smoothness of the animation
+ // so we show them after the animation finished, move them to their position from outside of the monitor
+ if (currentState === ControlsState.WINDOW_PICKER && !w.visible && distanceToCurrentWorkspace <= opt.NUMBER_OF_VISIBLE_NEIGHBORS && initialState === ControlsState.APP_GRID) {
+ w.remove_all_transitions();
+ w.visible = true;
+ const directionNext = distance > 0;
+ if (!opt.ORIENTATION) {
+ const width = w.width * 0.6 * opt.WS_PREVIEW_SCALE;
+ w.translation_x = directionNext ? -width : width;
+ }
+ if (opt.ORIENTATION) {
+ const height = w.height * 0.6 * opt.WS_PREVIEW_SCALE;
+ w.translation_y = directionNext ? -height : height;
+ }
+
+ w.opacity = 10;
+ w.get_parent().set_child_below_sibling(w, null);
+ w.ease({
+ duration: 300,
+ translation_x: 0,
+ translation_y: 0,
+ opacity: 255,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+
+ // force ws preview bg corner radiuses where GS doesn't do it
+ if (opt.SHOW_WS_PREVIEW_BG && opt.OVERVIEW_MODE === 1 && distanceToCurrentWorkspace < 2)
+ w._background._updateBorderRadius(Math.min(1, w._overviewAdjustment.value));
+
+
+ // hide workspace background
+ if (!opt.SHOW_WS_PREVIEW_BG && w._background.opacity)
+ w._background.opacity = 0;
+ });
+ },
+
+ exposeWindows(workspaceIndex = null, callback) {
+ let adjustments = [];
+ if (workspaceIndex === null) {
+ this._workspaces.forEach(ws => {
+ adjustments.push(ws._background._stateAdjustment);
+ });
+ } else {
+ adjustments.push(this._workspaces[workspaceIndex]._background._stateAdjustment);
+ }
+
+ opt.WORKSPACE_MODE = 1;
+ adjustments.forEach(adj => {
+ if (adj.value === 0) {
+ adj.value = 0;
+ adj.ease(1, {
+ duration: 200,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ if (callback)
+ callback();
+ },
+ });
+ }
+ });
+ },
+};
+
+const SecondaryMonitorDisplayCommon = {
+ exposeWindows(...args) {
+ this._workspacesView.exposeWindows(...args);
+ },
+};
+
+const SecondaryMonitorDisplayVertical = {
+ _getThumbnailParamsForState(state) {
+
+ let opacity, scale, translationX;
+ switch (state) {
+ case ControlsState.HIDDEN:
+ opacity = 255;
+ scale = 1;
+ translationX = 0;
+ if (!Main.layoutManager._startingUp && (!opt.SHOW_WS_PREVIEW_BG || opt.OVERVIEW_MODE2))
+ translationX = this._thumbnails.width * (opt.SEC_WS_TMB_LEFT ? -1 : 1);
+
+ break;
+ case ControlsState.WINDOW_PICKER:
+ case ControlsState.APP_GRID:
+ opacity = 255;
+ scale = 1;
+ translationX = 0;
+ break;
+ default:
+ opacity = 255;
+ scale = 1;
+ translationX = 0;
+ break;
+ }
+
+ return { opacity, scale, translationX };
+ },
+
+ _getWorkspacesBoxForState(state, box, workArea, wsTmbWidth, spacing) {
+ let workspaceBox = box.copy();
+
+ if (
+ (state === ControlsState.WINDOW_PICKER || state === ControlsState.APP_GRID) &&
+ !(opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE)
+ ) {
+ workspaceBox = workArea.copy();
+ const [startX, startY] = workspaceBox.get_origin();
+ let [width, height] = workspaceBox.get_size();
+
+ let wsBoxWidth = width - (wsTmbWidth ? wsTmbWidth + spacing : 0) - 2 * spacing;
+ let wsBoxHeight = height - 2 * spacing;
+
+ 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.SEC_WS_PREVIEW_SCALE);
+ wsBoxWidth = Math.round(wsBoxWidth * opt.SEC_WS_PREVIEW_SCALE);
+
+ let offset = Math.round(width - wsTmbWidth - wsBoxWidth - spacing) / 2;
+
+ const wsbX = startX + opt.SEC_WS_TMB_LEFT
+ ? wsTmbWidth + spacing + offset
+ : offset;
+
+ const wsbY = Math.round((startY + height - wsBoxHeight) / 2);
+
+ workspaceBox.set_origin(wsbX, wsbY);
+ workspaceBox.set_size(wsBoxWidth, wsBoxHeight);
+ }
+
+ return workspaceBox;
+ },
+
+ _getWorkAreaBox(box) {
+ if (!opt.SEC_WS_PREVIEW_SHIFT || !Main.panel.visible)
+ return box;
+
+ const workArea = box.copy();
+ const panelHeight = Main.panel.height;
+ workArea.y1 += opt.PANEL_POSITION_TOP ? panelHeight : 0;
+ workArea.y2 -= opt.PANEL_POSITION_BOTTOM ? panelHeight : 0;
+
+ return workArea;
+ },
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ const themeNode = this.get_theme_node();
+ const contentBox = themeNode.get_content_box(box);
+
+ const workArea = this._getWorkAreaBox(contentBox);
+
+ let [width, height] = workArea.get_size();
+ let [startX, startY] = workArea.get_origin();
+
+ const spacing = opt.SPACING;
+
+ let wsTmbWidth = 0;
+ let wsTmbHeight = 0;
+ this._thumbnails.visible = !opt.SEC_WS_TMB_HIDDEN;
+ if (this._thumbnails.visible) {
+ wsTmbWidth = Math.round(width * opt.SEC_MAX_THUMBNAIL_SCALE);
+
+ let totalTmbSpacing;
+ [totalTmbSpacing, wsTmbHeight] = this._thumbnails.get_preferred_height(wsTmbWidth);
+ wsTmbHeight += totalTmbSpacing;
+
+ const thumbnailsHeightMax = height - spacing;
+
+ if (wsTmbHeight > thumbnailsHeightMax) {
+ wsTmbHeight = thumbnailsHeightMax;
+ wsTmbWidth = Math.round(this._thumbnails.get_preferred_width(wsTmbHeight)[1]);
+ }
+
+ let wsTmbX = opt.SEC_WS_TMB_LEFT
+ ? startX + spacing
+ : startX + width - wsTmbWidth - spacing;
+
+ let offset = (height - wsTmbHeight) / 2;
+ const wsTmbY = startY + Math.round(offset - opt.SEC_WS_TMB_POSITION_ADJUSTMENT * (offset - spacing));
+
+ const childBox = new Clutter.ActorBox();
+ childBox.set_origin(wsTmbX, wsTmbY);
+ childBox.set_size(wsTmbWidth, wsTmbHeight);
+ this._thumbnails.allocate(childBox);
+ }
+
+ const {
+ currentState, initialState, finalState, transitioning, progress,
+ } = this._overviewAdjustment.getStateTransitionParams();
+
+ let workspacesBox;
+ const workspaceParams = [contentBox, workArea, wsTmbWidth, 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);
+ },
+
+ _updateThumbnailVisibility() {
+ if (opt.OVERVIEW_MODE2)
+ this.set_child_above_sibling(this._thumbnails, null);
+
+ const visible = !opt.SEC_WS_TMB_HIDDEN;
+
+ if (this._thumbnails.visible === visible)
+ return;
+
+ this._thumbnails.show();
+ this._updateThumbnailParams();
+ this._thumbnails.ease_property('expand-fraction', visible ? 1 : 0, {
+ duration: Overview.ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._thumbnails.visible = visible;
+ this._thumbnails._indicator.visible = visible;
+ },
+ });
+ },
+
+ _updateThumbnailParams() {
+ if (opt.SEC_WS_TMB_HIDDEN)
+ return;
+
+ // workaround for upstream bug - secondary thumbnails boxes don't catch 'showing' signal on the shell startup and don't populate the box with thumbnails
+ // the tmbBox contents is also destroyed when overview state adjustment gets above 1 when swiping gesture from window picker to app grid
+ if (!this._thumbnails._thumbnails.length)
+ this._thumbnails._createThumbnails();
+
+ 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);*/
+
+ // 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);
+ const translationX = !Main.layoutManager._startingUp && ((!opt.SHOW_WS_PREVIEW_BG && !opt.OVERVIEW_MODE2) || animateOverviewMode2)
+ ? Util.lerp(initialParams.translationX, finalParams.translationX, progress)
+ : 0;
+
+ this._thumbnails.set({
+ opacity: 255,
+ // scale_x: scale,
+ // scale_y: scale,
+ translation_x: translationX,
+ });
+ },
+
+ _updateWorkspacesView() {
+ if (this._workspacesView)
+ this._workspacesView.destroy();
+
+ if (this._settings.get_boolean('workspaces-only-on-primary')) {
+ opt.SEC_WS_TMB_HIDDEN = true;
+ this._workspacesView = new WorkspacesView.ExtraWorkspaceView(
+ this._monitorIndex,
+ this._overviewAdjustment);
+ } else {
+ opt.SEC_WS_TMB_HIDDEN = !opt.SHOW_SEC_WS_TMB;
+ this._workspacesView = new WorkspacesView.WorkspacesView(
+ this._monitorIndex,
+ this._controls,
+ this._scrollAdjustment,
+ // Secondary monitors don't need FitMode.ALL since there is workspace switcher always visible
+ // this._fitModeAdjustment,
+ new St.Adjustment({
+ actor: this,
+ value: 0, // FitMode.SINGLE,
+ lower: 0, // FitMode.SINGLE,
+ upper: 0, // FitMode.SINGLE,
+ }),
+ // secondaryOverviewAdjustment);
+ this._overviewAdjustment);
+ }
+ this.add_child(this._workspacesView);
+ this._thumbnails.opacity = 0;
+ },
+};
+
+const SecondaryMonitorDisplayHorizontal = {
+ _getThumbnailParamsForState(state) {
+ // const { ControlsState } = OverviewControls;
+
+ let opacity, scale, translationY;
+ switch (state) {
+ case ControlsState.HIDDEN:
+ opacity = 255;
+ scale = 1;
+ translationY = 0;
+ if (!Main.layoutManager._startingUp && (!opt.SHOW_WS_PREVIEW_BG || opt.OVERVIEW_MODE2))
+ translationY = this._thumbnails.height * (opt.SEC_WS_TMB_TOP ? -1 : 1);
+
+ break;
+ case ControlsState.WINDOW_PICKER:
+ case ControlsState.APP_GRID:
+ opacity = 255;
+ scale = 1;
+ translationY = 0;
+ break;
+ default:
+ opacity = 255;
+ scale = 1;
+ translationY = 0;
+ break;
+ }
+
+ return { opacity, scale, translationY };
+ },
+
+ _getWorkspacesBoxForState(state, box, workArea, wsTmbHeight, spacing) {
+ let workspaceBox = box.copy();
+
+ if (
+ (state === ControlsState.WINDOW_PICKER || state === ControlsState.APP_GRID) &&
+ !(opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE)
+ ) {
+ workspaceBox = workArea.copy();
+ const [startX, startY] = workspaceBox.get_origin();
+ let [width, height] = workspaceBox.get_size();
+
+ let wsBoxWidth = width - 2 * spacing;
+ let wsBoxHeight = height - (wsTmbHeight ? wsTmbHeight + spacing : 0) - 2 * spacing;
+
+ 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.SEC_WS_PREVIEW_SCALE);
+ wsBoxWidth = Math.round(wsBoxWidth * opt.SEC_WS_PREVIEW_SCALE);
+
+ let offset = Math.round(height - wsTmbHeight - wsBoxHeight - spacing) / 2;
+
+ const wsbX = Math.round((startX + width - wsBoxWidth) / 2);
+
+ const wsbY = startY + opt.SEC_WS_TMB_TOP
+ ? wsTmbHeight + spacing + offset
+ : offset;
+
+ workspaceBox.set_origin(wsbX, wsbY);
+ workspaceBox.set_size(wsBoxWidth, wsBoxHeight);
+ }
+
+ return workspaceBox;
+ },
+
+ _getWorkAreaBox: SecondaryMonitorDisplayVertical._getWorkAreaBox,
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ const themeNode = this.get_theme_node();
+ const contentBox = themeNode.get_content_box(box);
+
+ const workArea = this._getWorkAreaBox(contentBox);
+
+ let [width, height] = workArea.get_size();
+ let [startX, startY] = workArea.get_origin();
+
+ const spacing = opt.SPACING;
+
+ let wsTmbWidth = 0;
+ let wsTmbHeight = 0;
+ this._thumbnails.visible = !opt.SEC_WS_TMB_HIDDEN;
+ if (this._thumbnails.visible) {
+ wsTmbHeight = Math.round(height * opt.SEC_MAX_THUMBNAIL_SCALE);
+
+ let totalTmbSpacing;
+ [totalTmbSpacing, wsTmbWidth] = this._thumbnails.get_preferred_width(wsTmbHeight);
+ wsTmbWidth += totalTmbSpacing;
+
+ const thumbnailsWidthMax = width - 2 * spacing;
+
+ if (wsTmbWidth > thumbnailsWidthMax) {
+ wsTmbWidth = thumbnailsWidthMax;
+ wsTmbHeight = Math.round(this._thumbnails.get_preferred_height(wsTmbWidth)[1]);
+ }
+
+ let wsTmbY = opt.SEC_WS_TMB_TOP
+ ? startY + spacing
+ : startY + height - wsTmbHeight - spacing;
+
+ let offset = (width - wsTmbWidth) / 2;
+ const wsTmbX = startX + Math.round(offset - opt.SEC_WS_TMB_POSITION_ADJUSTMENT * (offset - spacing));
+
+ const childBox = new Clutter.ActorBox();
+ childBox.set_origin(wsTmbX, wsTmbY);
+ childBox.set_size(wsTmbWidth, wsTmbHeight);
+ this._thumbnails.allocate(childBox);
+ }
+
+ const {
+ currentState, initialState, finalState, transitioning, progress,
+ } = this._overviewAdjustment.getStateTransitionParams();
+
+ let workspacesBox;
+ const workspaceParams = [contentBox, workArea, wsTmbHeight, 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);
+ },
+
+ _updateThumbnailVisibility: SecondaryMonitorDisplayVertical._updateThumbnailVisibility,
+
+ _updateThumbnailParams() {
+ if (opt.SEC_WS_TMB_HIDDEN)
+ return;
+
+ // workaround for upstream bug - secondary thumbnails boxes don't catch 'showing' signal on the shell startup and don't populate the box with thumbnails
+ // the tmbBox contents is also destroyed when overview state adjustment gets above 1 when swiping gesture from window picker to app grid
+ if (!this._thumbnails._thumbnails.length)
+ this._thumbnails._createThumbnails();
+
+ 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);*/
+
+ // 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);
+ const translationY = !Main.layoutManager._startingUp && ((!opt.SHOW_WS_PREVIEW_BG && !opt.OVERVIEW_MODE2) || animateOverviewMode2)
+ ? Util.lerp(initialParams.translationY, finalParams.translationY, progress)
+ : 0;
+
+ this._thumbnails.set({
+ opacity: 255,
+ // scale_x: scale,
+ // scale_y: scale,
+ translation_y: translationY,
+ });
+ },
+
+ _updateWorkspacesView() {
+ if (this._workspacesView)
+ this._workspacesView.destroy();
+
+ if (this._settings.get_boolean('workspaces-only-on-primary')) {
+ opt.SEC_WS_TMB_HIDDEN = true;
+ this._workspacesView = new WorkspacesView.ExtraWorkspaceView(
+ this._monitorIndex,
+ this._overviewAdjustment);
+ } else {
+ opt.SEC_WS_TMB_HIDDEN = !opt.SHOW_SEC_WS_TMB;
+ this._workspacesView = new WorkspacesView.WorkspacesView(
+ this._monitorIndex,
+ this._controls,
+ this._scrollAdjustment,
+ // Secondary monitors don't need FitMode.ALL since there is workspace switcher always visible
+ // this._fitModeAdjustment,
+ new St.Adjustment({
+ actor: this,
+ value: 0, // FitMode.SINGLE,
+ lower: 0, // FitMode.SINGLE,
+ upper: 0, // FitMode.SINGLE,
+ }),
+ // secondaryOverviewAdjustment);
+ this._overviewAdjustment);
+ }
+ this.add_child(this._workspacesView);
+ this._thumbnails.opacity = 0;
+ },
+};
+
+const ExtraWorkspaceViewCommon = {
+ _updateWorkspaceMode() {
+ const overviewState = this._overviewAdjustment.value;
+
+ const progress = Math.clamp(overviewState,
+ ControlsState.HIDDEN,
+ opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE ? ControlsState.HIDDEN : ControlsState.WINDOW_PICKER);
+
+ this._workspace.stateAdjustment.value = progress;
+
+ // force ws preview bg corner radiuses where GS doesn't do it
+ if (opt.SHOW_WS_PREVIEW_BG && opt.OVERVIEW_MODE === 1)
+ this._workspace._background._updateBorderRadius(Math.min(1, this._workspace._overviewAdjustment.value));
+
+
+ // hide workspace background
+ if (!opt.SHOW_WS_PREVIEW_BG && this._workspace._background.opacity)
+ this._workspace._background.opacity = 0;
+ },
+
+ exposeWindows() {
+ const adjustment = this._workspace._background._stateAdjustment;
+ opt.WORKSPACE_MODE = 1;
+ if (adjustment.value === 0) {
+ adjustment.value = 0;
+ adjustment.ease(1, {
+ duration: 200,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+ },
+};
+
+const WorkspacesDisplayCommon = {
+ _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.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 WorkspacesView.SecondaryMonitorDisplay(i,
+ this._controls,
+ this._scrollAdjustment,
+ // Secondary monitors don't need FitMode.ALL since there is workspace switcher always visible
+ // this._fitModeAdjustment,
+ new St.Adjustment({
+ actor: this,
+ value: 0, // FitMode.SINGLE,
+ lower: 0, // FitMode.SINGLE,
+ upper: 0, // FitMode.SINGLE,
+ }),
+ this._overviewAdjustment);
+ Main.layoutManager.overviewGroup.add_child(view);
+
+ if (opt.CLICK_EMPTY_CLOSE) {
+ // Allow users to close the overview by clicking on an empty space on the secondary monitor
+ // The primary monitor overview is handled in the overviewControls
+ const clickAction = new Clutter.ClickAction();
+ clickAction.connect('clicked', () => {
+ Main.overview.hide();
+ });
+ view.reactive = true;
+ view.add_action(clickAction);
+ }
+ }
+
+ this._workspacesViews.push(view);
+ }
+ },
+
+ _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;
+
+ if (opt.PANEL_MODE === 1) {
+ const panelBox = Main.layoutManager.panelBox;
+ const [, y] = global.get_pointer();
+ if (y > panelBox.allocation.y1 && y < panelBox.allocation.y2)
+ return Clutter.EVENT_STOP;
+ }
+
+ if (Me.Util.isShiftPressed()) {
+ let direction = Me.Util.getScrollDirection(event);
+ if (direction === null || (Date.now() - this._lastScrollTime) < 150)
+ return Clutter.EVENT_STOP;
+ this._lastScrollTime = Date.now();
+
+ if (direction === Clutter.ScrollDirection.UP)
+ direction = -1;
+
+ else if (direction === Clutter.ScrollDirection.DOWN)
+ direction = 1;
+ else
+ direction = 0;
+
+ if (direction) {
+ Me.Util.reorderWorkspace(direction);
+ // make all workspaces on primary monitor visible for case the new position is hidden
+ const primaryMonitorIndex = global.display.get_primary_monitor();
+ Main.overview._overview._controls._workspacesDisplay._workspacesViews[primaryMonitorIndex]._workspaces.forEach(w => {
+ w.visible = true;
+ });
+ return Clutter.EVENT_STOP;
+ }
+ }
+
+ return Main.wm.handleWorkspaceScroll(event);
+ },
+
+ _onKeyPressEvent(actor, event) {
+ const symbol = event.get_key_symbol();
+ /* const { ControlsState } = OverviewControls;
+ if (this._overviewAdjustment.value !== ControlsState.WINDOW_PICKER && symbol !== Clutter.KEY_space)
+ 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;
+ const state = this._overviewAdjustment.value;
+
+ let which;
+ switch (symbol) {
+ case Clutter.KEY_Return:
+ case Clutter.KEY_KP_Enter:
+ if (Me.Util.isCtrlPressed()) {
+ Main.ctrlAltTabManager._items.forEach(i => {
+ if (i.sortGroup === 1 && i.name === 'Dash')
+ Main.ctrlAltTabManager.focusGroup(i);
+ });
+ }
+ return Clutter.EVENT_STOP;
+ 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;
+ case Clutter.KEY_space:
+ if (Me.Util.isCtrlPressed() && Me.Util.isShiftPressed()) {
+ Me.Util.openPreferences();
+ } else if (Me.Util.isAltPressed()) {
+ Main.ctrlAltTabManager._items.forEach(i => {
+ if (i.sortGroup === 1 && i.name === 'Dash')
+ Main.ctrlAltTabManager.focusGroup(i);
+ });
+ } else if (Me.Util.getEnabledExtensions('extensions-search-provider').length && Me.Util.isCtrlPressed()) {
+ Me.Util.activateSearchProvider(Me.ESP_PREFIX);
+ } else if (Me.Util.getEnabledExtensions('windows-search-provider').length) {
+ Me.Util.activateSearchProvider(Me.WSP_PREFIX);
+ }
+
+ return Clutter.EVENT_STOP;
+ case Clutter.KEY_Down:
+ case Clutter.KEY_Left:
+ case Clutter.KEY_Right:
+ case Clutter.KEY_Up:
+ case Clutter.KEY_Tab:
+ if (Main.overview.searchController.searchActive) {
+ Main.overview.searchEntry.grab_key_focus();
+ } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && state === 1) {
+ // expose windows by "clicking" on ws thumbnail
+ // in this case overview stateAdjustment will be used for transition
+ Main.overview._overview.controls._thumbnailsBox._activateThumbnailAtPoint(0, 0, global.get_current_time(), true);
+ Main.ctrlAltTabManager._items.forEach(i => {
+ if (i.sortGroup === 1 && i.name === 'Windows')
+ Main.ctrlAltTabManager.focusGroup(i);
+ });
+ } else if (opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE && state === 1) {
+ // expose windows for OVERVIEW_MODE 1
+ const wsIndex = global.workspace_manager.get_active_workspace().index();
+ // after expose animation activate keyboard for window selection
+ const callback = Me.Util.activateKeyboardForWorkspaceView;
+ this._workspacesViews.forEach(
+ view => {
+ view.exposeWindows(wsIndex, callback);
+ }
+ );
+ } else {
+ if (state === 2)
+ return Clutter.EVENT_PROPAGATE;
+ Me.Util.activateKeyboardForWorkspaceView();
+ }
+
+ return Clutter.EVENT_STOP;
+ default:
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ if (state === 2)
+ return Clutter.EVENT_PROPAGATE;
+
+ let ws;
+ if (which < 0)
+ // Negative workspace numbers are directions
+ ws = workspaceManager.get_active_workspace().get_neighbor(which);
+ else
+ // Otherwise it is a workspace index
+ ws = workspaceManager.get_workspace_by_index(which);
+
+ if (Me.Util.isShiftPressed()) {
+ let direction;
+ if (which === Meta.MotionDirection.UP || which === Meta.MotionDirection.LEFT)
+ direction = -1;
+ else if (which === Meta.MotionDirection.DOWN || which === Meta.MotionDirection.RIGHT)
+ direction = 1;
+ if (direction)
+ Me.Util.reorderWorkspace(direction);
+ // make all workspaces on primary monitor visible for case the new position is hidden
+ Main.overview._overview._controls._workspacesDisplay._workspacesViews[0]._workspaces.forEach(w => {
+ w.visible = true;
+ });
+ return Clutter.EVENT_STOP;
+ }
+
+ if (ws)
+ Main.wm.actionMoveWorkspace(ws);
+
+ return Clutter.EVENT_STOP;
+ },
+};
+
+// 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;
+ }
+}