diff options
Diffstat (limited to 'extensions/vertical-workspaces/lib')
26 files changed, 10933 insertions, 0 deletions
diff --git a/extensions/vertical-workspaces/lib/appDisplay.js b/extensions/vertical-workspaces/lib/appDisplay.js new file mode 100644 index 0000000..2ac70b1 --- /dev/null +++ b/extensions/vertical-workspaces/lib/appDisplay.js @@ -0,0 +1,1474 @@ +/** + * V-Shell (Vertical Workspaces) + * appDisplay.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Clutter, GLib, GObject, Meta, Shell, St, Graphene, Pango } = imports.gi; + +const DND = imports.ui.dnd; +const Main = imports.ui.main; +const AppDisplay = imports.ui.appDisplay; +const IconGrid = imports.ui.iconGrid; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const IconGridOverride = Me.imports.lib.iconGrid; +const _Util = Me.imports.lib.util; + +const DIALOG_SHADE_NORMAL = Clutter.Color.from_pixel(0x00000022); +const DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000000); + +// gettext +const _ = Me.imports.lib.settings._; + +let _overrides; + +let _appGridLayoutSettings; +let _appDisplayScrollConId; +let _appSystemStateConId; +let _appGridLayoutConId; +let _origAppViewItemAcceptDrop; +let _updateFolderIcons; + +let opt; +let shellVersion = _Util.shellVersion; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('appDisplayModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + if (reset) { + _setAppDisplayOrientation(false); + _updateAppGridProperties(reset); + _updateAppGridDND(reset); + _restoreOverviewGroup(); + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + + if (opt.ORIENTATION === Clutter.Orientation.VERTICAL) { + _overrides.addOverride('AppDisplayVertical', AppDisplay.AppDisplay.prototype, AppDisplayVertical); + _overrides.addOverride('BaseAppViewVertical', AppDisplay.BaseAppView.prototype, BaseAppViewVertical); + } + + // Custom App Grid + _overrides.addOverride('AppFolderDialog', AppDisplay.AppFolderDialog.prototype, AppFolderDialog); + if (shellVersion >= 43) { + // const defined class needs to be touched before real access + AppDisplay.BaseAppViewGridLayout; + _overrides.addOverride('BaseAppViewGridLayout', AppDisplay.BaseAppViewGridLayout.prototype, BaseAppViewGridLayout); + } + _overrides.addOverride('FolderView', AppDisplay.FolderView.prototype, FolderView); + _overrides.addOverride('FolderIcon', AppDisplay.FolderIcon.prototype, FolderIcon); + _overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIcon); + _overrides.addOverride('AppDisplay', AppDisplay.AppDisplay.prototype, AppDisplayCommon); + _overrides.addOverride('AppViewItem', AppDisplay.AppViewItem.prototype, AppViewItemCommon); + _overrides.addOverride('BaseAppViewCommon', AppDisplay.BaseAppView.prototype, BaseAppViewCommon); + + _setAppDisplayOrientation(opt.ORIENTATION === Clutter.Orientation.VERTICAL); + _updateAppGridProperties(); + _updateAppGridDND(); + opt._appGridNeedsRedisplay = true; +} + +function _setAppDisplayOrientation(vertical = false) { + const CLUTTER_ORIENTATION = vertical ? Clutter.Orientation.VERTICAL : Clutter.Orientation.HORIZONTAL; + const scroll = vertical ? 'vscroll' : 'hscroll'; + // app display to vertical has issues - page indicator not working + // global appDisplay orientation switch is not built-in + let appDisplay = Main.overview._overview._controls._appDisplay; + // following line itself only changes in which axis will operate overshoot detection which switches appDisplay pages while dragging app icon to vertical + appDisplay._orientation = CLUTTER_ORIENTATION; + appDisplay._grid.layoutManager._orientation = CLUTTER_ORIENTATION; + appDisplay._swipeTracker.orientation = CLUTTER_ORIENTATION; + appDisplay._swipeTracker._reset(); + if (vertical) { + appDisplay._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); + + // move and change orientation of page indicators + // needs corrections in appgrid page calculations, e.g. appDisplay.adaptToSize() fnc, + // which complicates use of super call inside the function + const pageIndicators = appDisplay._pageIndicators; + pageIndicators.vertical = true; + appDisplay._box.vertical = false; + pageIndicators.x_expand = false; + pageIndicators.y_align = Clutter.ActorAlign.CENTER; + pageIndicators.x_align = Clutter.ActorAlign.START; + + const scrollContainer = appDisplay._scrollView.get_parent(); + if (shellVersion < 43) { + // remove touch friendly side navigation bars / arrows + if (appDisplay._hintContainer && appDisplay._hintContainer.get_parent()) + scrollContainer.remove_child(appDisplay._hintContainer); + } else { + // moving these bars needs more patching of the appDisplay's code + // for now we just change bars style to be more like vertically oriented arrows indicating direction to prev/next page + appDisplay._nextPageIndicator.add_style_class_name('nextPageIndicator'); + appDisplay._prevPageIndicator.add_style_class_name('prevPageIndicator'); + } + + // setting their x_scale to 0 removes the arrows and avoid allocation issues compared to .hide() them + appDisplay._nextPageArrow.scale_x = 0; + appDisplay._prevPageArrow.scale_x = 0; + } else { + appDisplay._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER); + if (_appDisplayScrollConId) { + appDisplay._adjustment.disconnect(_appDisplayScrollConId); + _appDisplayScrollConId = 0; + } + + // restore original page indicators + const pageIndicators = appDisplay._pageIndicators; + pageIndicators.vertical = false; + appDisplay._box.vertical = true; + pageIndicators.x_expand = true; + pageIndicators.y_align = Clutter.ActorAlign.END; + pageIndicators.x_align = Clutter.ActorAlign.CENTER; + + // put back touch friendly navigation bars/buttons + const scrollContainer = appDisplay._scrollView.get_parent(); + if (appDisplay._hintContainer && !appDisplay._hintContainer.get_parent()) { + scrollContainer.add_child(appDisplay._hintContainer); + // the hit container covers the entire app grid and added at the top of the stack blocks DND drops + // so it needs to be pushed below + scrollContainer.set_child_below_sibling(appDisplay._hintContainer, null); + } + + appDisplay._nextPageArrow.scale_x = 1; + appDisplay._prevPageArrow.scale_x = 1; + + appDisplay._nextPageIndicator.remove_style_class_name('nextPageIndicator'); + appDisplay._prevPageIndicator.remove_style_class_name('prevPageIndicator'); + } + + // value for page indicator is calculated from scroll adjustment, horizontal needs to be replaced by vertical + appDisplay._adjustment = appDisplay._scrollView[scroll].adjustment; + + // no need to connect already connected signal (wasn't removed the original one before) + if (!vertical) { + // reset used appDisplay properties + Main.overview._overview._controls._appDisplay.scale_y = 1; + Main.overview._overview._controls._appDisplay.scale_x = 1; + Main.overview._overview._controls._appDisplay.opacity = 255; + return; + } + + // update appGrid dot pages indicators + _appDisplayScrollConId = appDisplay._adjustment.connect('notify::value', adj => { + const value = adj.value / adj.page_size; + appDisplay._pageIndicators.setCurrentPosition(value); + }); +} + +// Set App Grid columns, rows, icon size, incomplete pages +function _updateAppGridProperties(reset = false) { + opt._appGridNeedsRedisplay = false; + // columns, rows, icon size + const appDisplay = Main.overview._overview._controls._appDisplay; + appDisplay.visible = true; + + if (reset) { + appDisplay._grid.layoutManager.fixedIconSize = -1; + appDisplay._grid.layoutManager.allow_incomplete_pages = true; + appDisplay._grid.setGridModes(); + if (_appGridLayoutSettings) { + _appGridLayoutSettings.disconnect(_appGridLayoutConId); + _appGridLayoutConId = 0; + _appGridLayoutSettings = null; + } + appDisplay._redisplay(); + + appDisplay._grid.set_style(''); + _resetAppGrid(); + } else { + // update grid on layout reset + if (!_appGridLayoutSettings) { + _appGridLayoutSettings = ExtensionUtils.getSettings('org.gnome.shell'); + _appGridLayoutConId = _appGridLayoutSettings.connect('changed::app-picker-layout', _resetAppGrid); + } + + appDisplay._grid.layoutManager.allow_incomplete_pages = opt.APP_GRID_ALLOW_INCOMPLETE_PAGES; + appDisplay._grid.set_style(`column-spacing: ${opt.APP_GRID_SPACING}px; row-spacing: ${opt.APP_GRID_SPACING}px;`); + + // force redisplay + appDisplay._grid._currentMode = -1; + appDisplay._grid.setGridModes(); + appDisplay._grid.layoutManager.fixedIconSize = opt.APP_GRID_ICON_SIZE; + // appDisplay._folderIcons.forEach(folder => folder._dialog?._updateFolderSize()); + _resetAppGrid(); + } +} + +function _updateAppGridDND(reset) { + if (!reset) { + if (!_appSystemStateConId && opt.APP_GRID_INCLUDE_DASH >= 3) { + _appSystemStateConId = Shell.AppSystem.get_default().connect( + 'app-state-changed', + () => { + _updateFolderIcons = true; + Main.overview._overview._controls._appDisplay._redisplay(); + } + ); + } + } else if (_appSystemStateConId) { + Shell.AppSystem.get_default().disconnect(_appSystemStateConId); + _appSystemStateConId = 0; + } + if (opt.APP_GRID_ORDER && !reset) { + if (!_origAppViewItemAcceptDrop) + _origAppViewItemAcceptDrop = AppDisplay.AppViewItem.prototype.acceptDrop; + AppDisplay.AppViewItem.prototype.acceptDrop = () => false; + } else if (_origAppViewItemAcceptDrop) { + AppDisplay.AppViewItem.prototype.acceptDrop = _origAppViewItemAcceptDrop; + } +} + +function _restoreOverviewGroup() { + Main.overview.dash.showAppsButton.checked = false; + Main.layoutManager.overviewGroup.opacity = 255; + Main.layoutManager.overviewGroup.scale_x = 1; + Main.layoutManager.overviewGroup.hide(); +} + +const AppDisplayVertical = { + // correction of the appGrid size when page indicators were moved from the bottom to the right + adaptToSize(width, height) { + const [, indicatorWidth] = this._pageIndicators.get_preferred_width(-1); + width -= indicatorWidth; + + this._grid.findBestModeForSize(width, height); + + const adaptToSize = AppDisplay.BaseAppView.prototype.adaptToSize.bind(this); + adaptToSize(width, height); + }, +}; + +const AppDisplayCommon = { + _ensureDefaultFolders() { + // disable creation of default folders if user deleted them + }, + + _redisplay() { + this._folderIcons.forEach(icon => { + icon.view._redisplay(); + }); + + BaseAppViewCommon._redisplay.bind(this)(); + }, + + // 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_ORDER) { + 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 (_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 + _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_ORDER && 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; + }, + + // support active preview icons + _onDragBegin(overview, source) { + if (source._sourceItem) + source = source._sourceItem; + + this._dragMonitor = { + dragMotion: this._onDragMotion.bind(this), + }; + DND.addDragMonitor(this._dragMonitor); + if (shellVersion < 43) + this._slideSidePages(AppDisplay.SidePages.PREVIOUS | AppDisplay.SidePages.NEXT | AppDisplay.SidePages.DND); + else + 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 preview + acceptDrop(source) { + if (opt.APP_GRID_ORDER) + return false; + if (source._sourceItem) + source = source._sourceItem; + + let dropTarget = null; + if (shellVersion >= 43) { + dropTarget = this._dropTarget; + delete this._dropTarget; + } + + if (!this._canAccept(source)) + return false; + + if ((shellVersion < 43 && this._dropPage) || + (shellVersion >= 43 && (dropTarget === this._prevPageIndicator || + dropTarget === this._nextPageIndicator))) { + let increment; + + if (shellVersion < 43) + increment = this._dropPage === AppDisplay.SidePages.NEXT ? 1 : -1; + else + 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) { + log(`Warning:${e}`); + } + this._removeDelayedMove(); + } + + this._savePages(); + + let 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; + }, +}; + +const BaseAppViewVertical = { + after__init() { + this._grid.layoutManager._orientation = Clutter.Orientation.VERTICAL; + this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); + this._orientation = Clutter.Orientation.VERTICAL; + this._swipeTracker.orientation = Clutter.Orientation.VERTICAL; + this._swipeTracker._reset(); + this._pageIndicators.vertical = true; + this._box.vertical = false; + this._pageIndicators.x_expand = false; + this._pageIndicators.y_align = Clutter.ActorAlign.CENTER; + this._pageIndicators.x_align = Clutter.ActorAlign.START; + this._pageIndicators.set_style('margin-right: 10px;'); + const scrollContainer = this._scrollView.get_parent(); + if (shellVersion < 43) { + // remove touch friendly side navigation bars / arrows + if (this._hintContainer && this._hintContainer.get_parent()) + scrollContainer.remove_child(this._hintContainer); + } else { + // moving these bars needs more patching of the this's code + // for now we just change bars style to be more like vertically oriented arrows indicating direction to prev/next page + this._nextPageIndicator.add_style_class_name('nextPageIndicator'); + this._prevPageIndicator.add_style_class_name('prevPageIndicator'); + } + + // setting their x_scale to 0 removes the arrows and avoid allocation issues compared to .hide() them + this._nextPageArrow.scale_x = 0; + this._prevPageArrow.scale_x = 0; + + this._adjustment = this._scrollView.vscroll.adjustment; + + this._adjustment.connect('notify::value', adj => { + const value = adj.value / adj.page_size; + this._pageIndicators.setCurrentPosition(value); + }); + }, + // <= 42 only, this fixes dnd from appDisplay to the workspace thumbnail on the left if appDisplay is on page 1 because of appgrid left overshoot + _pageForCoords() { + return AppDisplay.SidePages.NONE; + }, +}; + +const BaseAppViewCommon = { + _sortOrderedItemsAlphabetically(icons = null) { + if (!icons) + icons = this._orderedItems; + icons.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); + }, + + _setLinearPositions(icons) { + const { itemsPerPage } = this._grid; + icons.forEach((icon, i) => { + const page = Math.floor(i / itemsPerPage); + const position = i % itemsPerPage; + try { + this._moveItem(icon, page, position); + } catch (e) { + log(`Warning:${e}`); + } + }); + }, + + // adds sorting options and option to add favorites and running apps + _redisplay() { + let oldApps = this._orderedItems.slice(); + let oldAppIds = oldApps.map(icon => icon.id); + + let newApps = this._loadApps().sort(this._compareItems.bind(this)); + let newAppIds = newApps.map(icon => icon.id); + + let addedApps = newApps.filter(icon => !oldAppIds.includes(icon.id)); + let removedApps = oldApps.filter(icon => !newAppIds.includes(icon.id)); + + // Remove old app icons + removedApps.forEach(icon => { + this._removeItem(icon); + icon.destroy(); + }); + + // Add new app icons, or move existing ones + newApps.forEach(icon => { + const [page, position] = this._getItemPosition(icon); + if (addedApps.includes(icon)) { + this._addItem(icon, page, position); + } else if (page !== -1 && position !== -1) { + this._moveItem(icon, page, position); + } else { + // App is part of a folder + } + }); + + // sort all alphabetically + if (opt.APP_GRID_ORDER > 0) { + // const { itemsPerPage } = this._grid; + let appIcons = this._orderedItems; + this._sortOrderedItemsAlphabetically(appIcons); + // appIcons.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); + // then sort used apps by usage + if (opt.APP_GRID_ORDER === 2) + appIcons.sort((a, b) => Shell.AppUsage.get_default().compare(a.app.id, b.app.id)); + + // sort favorites first + if (opt.APP_GRID_DASH_FIRST) { + const fav = Object.keys(this._appFavorites._favorites); + appIcons.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_DASH_FIRST) + appIcons.sort((a, b) => a.app.get_state() !== Shell.AppState.RUNNING && b.app.get_state() === Shell.AppState.RUNNING); + + this._setLinearPositions(appIcons); + + this._orderedItems = appIcons; + } + + this.emit('view-loaded'); + if (!opt.APP_GRID_ALLOW_INCOMPLETE_PAGES) { + for (let i = 0; i < this._grid.nPages; i++) + this._grid.layoutManager._fillItemVacancies(i); + } + }, + + _canAccept(source) { + return opt.APP_GRID_ORDER ? false : source instanceof AppDisplay.AppViewItem; + }, + + // support active preview icons + acceptDrop(source) { + if (!this._canAccept(source)) + return false; + + if (source._sourceItem) + source = source._sourceItem; + + + if (this._dropPage) { + const increment = this._dropPage === AppDisplay.SidePages.NEXT ? 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; + + this._moveItem(source, page, position); + 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 (shellVersion < 43) { + this._dropPage = this._pageForCoords(dragEvent.x, dragEvent.y); + if (this._dropPage && + this._dropPage === AppDisplay.SidePages.PREVIOUS && + this._grid.currentPage === 0) { + delete this._dropPage; + return DND.DragMotionResult.NO_DROP; + } + } + + if (appIcon instanceof AppDisplay.AppViewItem) { + if (shellVersion < 44) { + // Handle the drag overshoot. When dragging to above the + // icon grid, move to the page above; when dragging below, + // move to the page below. + this._handleDragOvershoot(dragEvent); + } else 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(); + } + } + + this._maybeMoveItem(dragEvent); + + return DND.DragMotionResult.CONTINUE; + }, + + // adjustable page width for GS <= 42 + adaptToSize(width, height, isFolder = false) { + let box = new Clutter.ActorBox({ + x2: width, + y2: height, + }); + box = this.get_theme_node().get_content_box(box); + box = this._scrollView.get_theme_node().get_content_box(box); + box = this._grid.get_theme_node().get_content_box(box); + + const availWidth = box.get_width(); + const availHeight = box.get_height(); + + let pageWidth, pageHeight; + + pageHeight = availHeight; + pageWidth = Math.ceil(availWidth * (isFolder ? 1 : opt.APP_GRID_PAGE_WIDTH_SCALE)); + // subtract space for navigation arrows in horizontal mode + pageWidth -= opt.ORIENTATION ? 0 : 128; + + this._grid.layout_manager.pagePadding.left = + Math.floor(availWidth * 0.02); + this._grid.layout_manager.pagePadding.right = + Math.ceil(availWidth * 0.02); + + this._grid.adaptToSize(pageWidth, pageHeight); + + const leftPadding = Math.floor( + (availWidth - this._grid.layout_manager.pageWidth) / 2); + const rightPadding = Math.ceil( + (availWidth - this._grid.layout_manager.pageWidth) / 2); + const topPadding = Math.floor( + (availHeight - this._grid.layout_manager.pageHeight) / 2); + const bottomPadding = Math.ceil( + (availHeight - this._grid.layout_manager.pageHeight) / 2); + + this._scrollView.content_padding = new Clutter.Margin({ + left: leftPadding, + right: rightPadding, + top: topPadding, + bottom: bottomPadding, + }); + + this._availWidth = availWidth; + this._availHeight = availHeight; + + this._pageIndicatorOffset = leftPadding; + this._pageArrowOffset = Math.max( + leftPadding - 80, 0); // 80 is AppDisplay.PAGE_PREVIEW_MAX_ARROW_OFFSET + }, +}; + +const BaseAppViewGridLayout = { + _getIndicatorsWidth(box) { + const [width, height] = box.get_size(); + const arrows = [ + this._nextPageArrow, + this._previousPageArrow, + ]; + + const minArrowsWidth = arrows.reduce( + (previousWidth, accessory) => { + const [min] = accessory.get_preferred_width(height); + return Math.max(previousWidth, min); + }, 0); + + const idealIndicatorWidth = (width * 0.1/* PAGE_PREVIEW_RATIO*/) / 2; + + return Math.max(idealIndicatorWidth, minArrowsWidth); + }, +}; + +const FolderIcon = { + after__init() { + /* // If folder preview icons are clickable, + // disable opening the folder with primary mouse button and enable the secondary one + const buttonMask = opt.APP_GRID_ACTIVE_PREVIEW + ? St.ButtonMask.TWO | St.ButtonMask.THREE + : St.ButtonMask.ONE | St.ButtonMask.TWO; + this.button_mask = buttonMask;*/ + this.button_mask = St.ButtonMask.ONE | St.ButtonMask.TWO; + + // build the folders now to avoid node errors when dragging active folder preview icons + if (this.visible && opt.APP_GRID_ACTIVE_PREVIEW) + this._ensureFolderDialog(); + }, + + open() { + this._ensureFolderDialog(); + if (this._dialog._designCapacity !== this.view._orderedItems.length) + this._dialog._updateFolderSize(); + + this.view._scrollView.vscroll.adjustment.value = 0; + this._dialog.popup(); + }, +}; + +const FolderView = { + _createGrid() { + let grid; + if (shellVersion < 43) + grid = new FolderGrid(); + else + grid = new FolderGrid43(); + + // IconGrid algorithm for adaptive icon size + // counts with different default(max) size for folders + grid.layoutManager._isFolder = true; + 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 ActiveFolderIcon(app); + child._sourceItem = this._orderedItems[i]; + child._sourceFolder = this; + child.icon.style_class = ''; + child.icon.set_style('margin: 0; padding: 0;'); + child.icon.setIconSize(subSize); + + bin.child = child; + + bin.connect('enter-event', () => { + bin.ease({ + duration: 100, + scale_x: 1.14, + scale_y: 1.14, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }); + bin.connect('leave-event', () => { + bin.ease({ + duration: 100, + scale_x: 1, + scale_y: 1, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }); + } + } + + layout.attach(bin, rtl ? (i + 1) % gridSize : i % gridSize, Math.floor(i / gridSize), 1, 1); + } + + // if folder content changed, update folder size + if (this._dialog && this._dialog._designCapacity !== this._orderedItems.length) + this._dialog._updateFolderSize(); + + return icon; + }, + + // this just overrides _redisplay() for GS < 44 + _redisplay() { + // super._redisplay(); - super doesn't work in my overrides + AppDisplay.BaseAppView.prototype._redisplay.bind(this)(); + }, + + _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 (!AppDisplay._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); + }); + this._appIds = this._apps.map(app => app.get_id()); + return items; + }, + + // 42 only - don't apply appGrid scale on folders + adaptToSize(width, height) { + if (!opt.ORIENTATION) { + const [, indicatorHeight] = this._pageIndicators.get_preferred_height(-1); + height -= indicatorHeight; + } + BaseAppViewCommon.adaptToSize.bind(this)(width, height, true); + }, +}; + +// folder columns and rows +const FolderGrid = GObject.registerClass( +class FolderGrid extends IconGrid.IconGrid { + _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 + columns_per_page: opt.APP_GRID_FOLDER_COLUMNS ? opt.APP_GRID_FOLDER_COLUMNS : 20, + rows_per_page: opt.APP_GRID_FOLDER_ROWS ? opt.APP_GRID_FOLDER_ROWS : 20, + page_halign: Clutter.ActorAlign.CENTER, + page_valign: Clutter.ActorAlign.CENTER, + }); + + // if (!opt.APP_GRID_FOLDER_DEFAULT) + const spacing = opt.APP_GRID_SPACING; + this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`); + this.layout_manager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE; + } + + adaptToSize(width, height) { + this.layout_manager.adaptToSize(width, height); + } +}); + + +let FolderGrid43; +// first reference to constant defined using const in other module returns undefined, the AppGrid const will remain empty and unused +const AppGrid = AppDisplay.AppGrid; +if (AppDisplay.AppGrid) { + FolderGrid43 = GObject.registerClass( + class FolderGrid43 extends AppDisplay.AppGrid { + _init() { + super._init({ + allow_incomplete_pages: false, + columns_per_page: opt.APP_GRID_FOLDER_COLUMNS ? opt.APP_GRID_FOLDER_COLUMNS : 20, + rows_per_page: opt.APP_GRID_FOLDER_ROWS ? opt.APP_GRID_FOLDER_ROWS : 20, + page_halign: Clutter.ActorAlign.CENTER, + page_valign: Clutter.ActorAlign.CENTER, + }); + + const spacing = opt.APP_GRID_SPACING; + this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`); + this.layout_manager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE; + + this.setGridModes([ + { + columns: opt.APP_GRID_FOLDER_COLUMNS ? opt.APP_GRID_FOLDER_COLUMNS : 3, + rows: opt.APP_GRID_FOLDER_ROWS ? opt.APP_GRID_FOLDER_ROWS : 3, + }, + ]); + } + + adaptToSize(width, height) { + this.layout_manager.adaptToSize(width, height); + } + }); +} + +const FOLDER_DIALOG_ANIMATION_TIME = 200; // AppDisplay.FOLDER_DIALOG_ANIMATION_TIME +const AppFolderDialog = { + // injection to _init() + after__init() { + // 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); + }, + + popup() { + if (this._isOpen) + return; + + /* if (!this._correctSize) { + // update folder with the precise app item size when the dialog is realized + GLib.idle_add(0, () => this._updateFolderSize(true)); + this._correctSize = true; + }*/ + + this._isOpen = this._grabHelper.grab({ + actor: this, + onUngrab: () => this.popdown(), + }); + + if (!this._isOpen) + return; + + this.get_parent().set_child_above_sibling(this, null); + + this._needsZoomAndFade = true; + this.show(); + + this.emit('open-state-changed', true); + }, + + _updateFolderSize() { + // adapt folder size according to the settings and number of icons + const view = this._view; + view._grid.layoutManager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE; + view._grid.set_style(`column-spacing: ${opt.APP_GRID_SPACING}px; row-spacing: ${opt.APP_GRID_SPACING}px;`); + + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + const dialogMargin = 30; + const nItems = view._orderedItems.length; + let columns = opt.APP_GRID_FOLDER_COLUMNS; + let rows = opt.APP_GRID_FOLDER_ROWS; + let spacing = opt.APP_GRID_SPACING; + const monitor = global.display.get_monitor_geometry(global.display.get_primary_monitor()); + + if (!columns && !rows) { + 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) { + columns = Math.ceil(nItems / rows); + } else if (columns && !rows) { + rows = Math.ceil(nItems / columns); + } + + const iconSize = opt.APP_GRID_FOLDER_ICON_SIZE < 0 ? opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT : opt.APP_GRID_FOLDER_ICON_SIZE; + let itemSize = iconSize + 53; // icon padding + // 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._notFirstRun) { + const [firstItem] = view._grid.layoutManager._container; + firstItem.icon.setIconSize(iconSize); + const [firstItemWidth] = firstItem.get_preferred_size(); + const realSize = firstItemWidth / scaleFactor; + if (realSize > iconSize) + itemSize = realSize; + } else { + this._needsUpdateSize = true; + this._notFirstRun = true; + } + + + let width = columns * (itemSize + spacing) + /* padding for nav arrows*/64; + width = Math.round(width + (opt.ORIENTATION || !opt.APP_GRID_FOLDER_COLUMNS ? 100 : 160/* space for navigation arrows*/)); + let height = rows * (itemSize + spacing) + /* header*/75 + /* padding*/100; + + // folder must fit the primary monitor + // reduce columns/rows if needed and count with the scaled values + while (width * scaleFactor > monitor.width - 2 * dialogMargin) { + width -= itemSize + spacing; + columns -= 1; + } + while (height * scaleFactor > monitor.height - 2 * dialogMargin) { + height -= itemSize + spacing; + rows -= 1; + } + width = Math.max(540, width); + + const layoutManager = view._grid.layoutManager; + layoutManager.rows_per_page = rows; + layoutManager.columns_per_page = columns; + + // this line is required by GS 43 + view._grid.setGridModes([{ columns, rows }]); + + this.child.set_style(` + width: ${width}px; + height: ${height}px; + padding: 30px; + `); + + view._redisplay(); + + // store original item count + this._designCapacity = nItems; + }, + + _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; + if (!opt.APP_GRID_FOLDER_CENTER) { + const appDisplay = this._source._parentView; + + dialogTargetX = Math.round(sourceCenterX - this.child.width / 2); + dialogTargetY = Math.round(sourceCenterY - this.child.height / 2); + + // keep the dialog in appDisplay area if possible + dialogTargetX = Math.clamp( + dialogTargetX, + this.x + appDisplay.x, + this.x + appDisplay.x + appDisplay.width - this.child.width + ); + + dialogTargetY = Math.clamp( + dialogTargetY, + this.y + appDisplay.y, + this.y + appDisplay.y + appDisplay.height - this.child.height + ); + // or at least in the monitor area + const monitor = global.display.get_monitor_geometry(global.display.get_primary_monitor()); + dialogTargetX = Math.clamp( + dialogTargetX, + this.x + monitor.x, + this.x + monitor.x + monitor.width - this.child.width + ); + + dialogTargetY = Math.clamp( + dialogTargetY, + this.y + monitor.y, + this.y + monitor.y + monitor.height - this.child.height + ); + } + const dialogOffsetX = -dialogX + dialogTargetX; + const dialogOffsetY = -dialogY + dialogTargetY; + + 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.ease({ + background_color: DIALOG_SHADE_NORMAL, + duration: FOLDER_DIALOG_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + 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, + onComplete: () => { + // if the folder grid was build with the estimated icon item size because the real size wasn't available + // rebuild it with the real size now, after the folder was realized + if (this._needsUpdateSize) { + this._updateFolderSize(); + this._view._redisplay(); + this._needsUpdateSize = false; + } + }, + }); + + 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; + } + + let [sourceX, sourceY] = + this._source.get_transformed_position(); + let [dialogX, dialogY] = + this.child.get_transformed_position(); + + this.ease({ + background_color: Clutter.Color.from_pixel(0x00000000), + duration: FOLDER_DIALOG_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + 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_OUT_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 = []; + }, + }); + + this._needsZoomAndFade = false; + }, + + _setLighterBackground(lighter) { + const backgroundColor = lighter + ? DIALOG_SHADE_HIGHLIGHT + : DIALOG_SHADE_NORMAL; + + this.ease({ + backgroundColor, + duration: FOLDER_DIALOG_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }, +}; + +// just make app grid to update all invalid positions that may be result of grid/icon size change +function _updateIconPositions() { + const appDisplay = Main.overview._overview._controls._appDisplay; + const icons = [...appDisplay._orderedItems]; + for (let i = 0; i < icons.length; i++) + appDisplay._moveItem(icons[i], -1, -1); +} + +function _removeIcons() { + const appDisplay = Main.overview._overview._controls._appDisplay; + const icons = [...appDisplay._orderedItems]; + for (let i = 0; i < icons.length; i++) { + const icon = icons[i]; + if (icon._dialog) + Main.layoutManager.overviewGroup.remove_child(icon._dialog); + appDisplay._removeItem(icon); + icon.destroy(); + } + appDisplay._folderIcons = []; +} + +function _resetAppGrid(settings) { + const appDisplay = Main.overview._overview._controls._appDisplay; + // reset the grid only if called directly without args or if all folders where removed by using reset button in Settings window + // otherwise this function is called every time a user moves icon to another position as a settings callback + if (settings) { + const currentValue = JSON.stringify(global.settings.get_value('app-picker-layout').deep_unpack()); + const emptyValue = JSON.stringify([]); + const customLayout = currentValue !== emptyValue; + // appDisplay._customLayout = customLayout; + if (customLayout) + return; + else + opt._appGridNeedsRedisplay = true; + } + + // force update icon size using adaptToSize(). the page size cannot be the same as the current one + appDisplay._grid.layoutManager._pageWidth += 1; + appDisplay._grid.layoutManager.adaptToSize(appDisplay._grid.layoutManager._pageWidth - 1, appDisplay._grid.layoutManager._pageHeight); + _removeIcons(); + appDisplay._redisplay(); + // force appDisplay to move all icons to proper positions and update all properties + GLib.idle_add(0, () => { + _updateIconPositions(); + if (appDisplay._sortOrderedItemsAlphabetically) { + appDisplay._sortOrderedItemsAlphabetically(); + appDisplay._grid.layoutManager._pageWidth += 1; + appDisplay._grid.layoutManager.adaptToSize(appDisplay._grid.layoutManager._pageWidth - 1, appDisplay._grid.layoutManager._pageHeight); + appDisplay._setLinearPositions(appDisplay._orderedItems); + } else { + appDisplay._removeItem(appDisplay._orderedItems[0]); + appDisplay._redisplay(); + } + + appDisplay._redisplay(); + }); +} + +function _getWindowApp(metaWin) { + const tracker = Shell.WindowTracker.get_default(); + return tracker.get_window_app(metaWin); +} + +function _getAppLastUsedWindow(app) { + let recentWin; + global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => { + const winApp = _getWindowApp(metaWin); + if (!recentWin && winApp === app) + recentWin = metaWin; + }); + return recentWin; +} + +function _getAppRecentWorkspace(app) { + const recentWin = _getAppLastUsedWindow(app); + if (recentWin) + return recentWin.get_workspace(); + + return null; +} + +const AppIcon = { + after__init() { + // update the app label behavior + this._updateMultiline(); + }, + + // avoid accepting by placeholder when dragging active preview + // and also by icon if alphabet or usage sorting are used + _canAccept(source) { + if (source._sourceItem) + source = source._sourceItem; + let view = AppDisplay._getViewFromIcon(source); + + return source !== this && + (source instanceof this.constructor) && + (view instanceof AppDisplay.AppDisplay && + !opt.APP_GRID_ORDER); + }, +}; + +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 + ? AppDisplay.APP_ICON_TITLE_EXPAND_TIME + : AppDisplay.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_ORDER) + 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 + if (source._sourceItem) { + const app = source._sourceItem.app; + source._sourceFolder.removeApp(app); + } + + return true; + }, + +}; + +const ActiveFolderIcon = GObject.registerClass( +class ActiveFolderIcon extends AppDisplay.AppIcon { + _init(app) { + super._init(app, { + setSizeManually: true, + showLabel: false, + }); + } + + handleDragOver() { + return DND.DragMotionResult.CONTINUE; + } + + acceptDrop() { + return false; + } + + _onDragEnd() { + this._dragging = false; + this.undoScaleAndFade(); + Main.overview.endItemDrag(this._sourceItem.icon); + } +}); diff --git a/extensions/vertical-workspaces/lib/appFavorites.js b/extensions/vertical-workspaces/lib/appFavorites.js new file mode 100644 index 0000000..50ebce9 --- /dev/null +++ b/extensions/vertical-workspaces/lib/appFavorites.js @@ -0,0 +1,61 @@ +/** + * V-Shell (Vertical Workspaces) + * appFavorites.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Shell } = imports.gi; +const AppFavorites = imports.ui.appFavorites; +const Main = imports.ui.main; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +let opt; +let _overrides; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('appFavoritesModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + + // if notifications are enabled no override is needed + if (reset || opt.SHOW_FAV_NOTIFICATION) { + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + + // AppFavorites.AppFavorites is const, first access returns undefined + const dummy = AppFavorites.AppFavorites; + _overrides.addOverride('AppFavorites', AppFavorites.AppFavorites.prototype, AppFavoritesCommon); +} + +const AppFavoritesCommon = { + addFavoriteAtPos(appId, pos) { + this._addFavorite(appId, pos); + }, + + removeFavorite(appId) { + this._removeFavorite(appId); + }, +}; diff --git a/extensions/vertical-workspaces/lib/dash.js b/extensions/vertical-workspaces/lib/dash.js new file mode 100644 index 0000000..bf832bd --- /dev/null +++ b/extensions/vertical-workspaces/lib/dash.js @@ -0,0 +1,1186 @@ +/** + * V-Shell (Vertical Workspaces) + * dash.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022-2023 + * @license GPL-3.0 + * modified dash module of https://github.com/RensAlthuis/vertical-overview extension + */ + +const { Clutter, GObject, St, Shell, Meta } = imports.gi; +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const Overview = imports.ui.overview; +const Dash = imports.ui.dash; +const PopupMenu = imports.ui.popupMenu; +const { AppMenu } = imports.ui.appMenu; +const BoxPointer = imports.ui.boxpointer; +const AltTab = imports.ui.altTab; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Util = Me.imports.lib.util; +const _ = Me.imports.lib.settings._; + +const shellVersion = Util.shellVersion; +let _origWorkId; +let _newWorkId; +let _showAppsIconBtnPressId; + +// added values to achieve a better ability to scale down according to available space +var BaseIconSizes = [16, 24, 32, 40, 44, 48, 56, 64, 72, 80, 96, 112, 128]; + +const RecentFilesSearchProviderPrefix = Me.imports.lib.recentFilesSearchProvider.prefix; +const WindowSearchProviderPrefix = Me.imports.lib.windowSearchProvider.prefix; + +let _overrides; +let opt; +let _firstRun = true; + +const DASH_ITEM_LABEL_SHOW_TIME = 150; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('dashModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + + const dash = Main.overview._overview._controls.layoutManager._dash; + + setToHorizontal(); + + dash.remove_style_class_name('vertical'); + dash.remove_style_class_name('vertical-left'); + dash.remove_style_class_name('vertical-right'); + + if (reset) { + _moveDashAppGridIcon(reset); + _connectShowAppsIcon(reset); + _updateSearchWindowsIcon(false); + _updateRecentFilesIcon(false); + dash.visible = true; + dash._background.opacity = 255; + dash._background.remove_style_class_name('v-shell-dash-background'); + _overrides = null; + opt = null; + return; + } + + _overrides = new Util.Overrides(); + + _overrides.addOverride('DashItemContainer', Dash.DashItemContainer.prototype, DashItemContainerCommon); + _overrides.addOverride('DashCommon', Dash.Dash.prototype, DashCommon); + _overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIconCommon); + _overrides.addOverride('DashIcon', Dash.DashIcon.prototype, DashIconCommon); + + if (opt.DASH_VERTICAL) { + _overrides.addOverride('Dash', Dash.Dash.prototype, DashOverride); + setToVertical(); + dash.add_style_class_name('vertical'); + + if (!_newWorkId) { + _origWorkId = dash._workId; + dash._workId = Main.initializeDeferredWork(dash._box, dash._redisplay.bind(dash)); + _newWorkId = dash._workId; + } else { + dash._workId = _newWorkId; + } + } else { + setToHorizontal(); + if (_origWorkId) + dash._workId = _origWorkId; + } + + _updateSearchWindowsIcon(); + _updateRecentFilesIcon(); + _moveDashAppGridIcon(); + _connectShowAppsIcon(); + + if (dash._showWindowsIcon && !dash._showWindowsIconClickedId) + dash._showWindowsIconClickedId = dash._showWindowsIcon.toggleButton.connect('clicked', (a, c) => c && Util.activateSearchProvider(WindowSearchProviderPrefix)); + + if (dash._recentFilesIcon && !dash._recentFilesIconClickedId) + dash._recentFilesIconClickedId = dash._recentFilesIcon.toggleButton.connect('clicked', (a, c) => c && Util.activateSearchProvider(RecentFilesSearchProviderPrefix)); + + dash.visible = opt.DASH_VISIBLE; + dash._background.add_style_class_name('v-shell-dash-background'); + dash._redisplay(); +} + +function setToVertical() { + let dash = Main.overview._overview._controls.layoutManager._dash; + + dash._box.layout_manager.orientation = Clutter.Orientation.VERTICAL; + dash._dashContainer.layout_manager.orientation = Clutter.Orientation.VERTICAL; + dash._dashContainer.y_expand = false; + dash._dashContainer.x_expand = true; + dash.x_align = Clutter.ActorAlign.START; + dash.y_align = Clutter.ActorAlign.CENTER; + + let sizerBox = dash._background.get_children()[0]; + sizerBox.clear_constraints(); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: dash._showAppsIcon.icon, + coordinate: Clutter.BindCoordinate.WIDTH, + })); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: dash._dashContainer, + coordinate: Clutter.BindCoordinate.HEIGHT, + })); + dash._box.remove_all_children(); + dash._separator = null; + dash._queueRedisplay(); + dash._adjustIconSize(); + + dash.add_style_class_name(opt.DASH_LEFT ? 'vertical-left' : 'vertical-right'); +} + +function setToHorizontal() { + let dash = Main.overview._overview._controls.layoutManager._dash; + if (_origWorkId) + dash._workId = _origWorkId; // pretty sure this is a leak, but there no provided way to disconnect these... + dash._box.layout_manager.orientation = Clutter.Orientation.HORIZONTAL; + dash._dashContainer.layout_manager.orientation = Clutter.Orientation.HORIZONTAL; + dash._dashContainer.y_expand = true; + dash._dashContainer.x_expand = false; + dash.x_align = Clutter.ActorAlign.CENTER; + dash.y_align = 0; + + let sizerBox = dash._background.get_children()[0]; + sizerBox.clear_constraints(); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: dash._showAppsIcon.icon, + coordinate: Clutter.BindCoordinate.HEIGHT, + })); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: dash._dashContainer, + coordinate: Clutter.BindCoordinate.WIDTH, + })); + + dash._box.remove_all_children(); + dash._separator = null; + dash._queueRedisplay(); + dash._adjustIconSize(); +} + +function _moveDashAppGridIcon(reset = false) { + // move dash app grid icon to the front + const dash = Main.overview._overview._controls.layoutManager._dash; + + const appIconPosition = opt.get('showAppsIconPosition', true); + 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; + } +} + +function _connectShowAppsIcon(reset = false) { + if (!reset) { + if (_showAppsIconBtnPressId || Util.dashIsDashToDock()) { + // button is already connected || dash is Dash to Dock + return; + } + + Main.overview.dash._showAppsIcon.reactive = true; + _showAppsIconBtnPressId = Main.overview.dash._showAppsIcon.connect('button-press-event', (actor, event) => { + const button = event.get_button(); + if (button === Clutter.BUTTON_MIDDLE) + Util.openPreferences(); + else if (button === Clutter.BUTTON_SECONDARY) + Util.activateSearchProvider(WindowSearchProviderPrefix); + else + return Clutter.EVENT_PROPAGATE; + return Clutter.EVENT_STOP; + }); + } else if (_showAppsIconBtnPressId) { + Main.overview.dash._showAppsIcon.disconnect(_showAppsIconBtnPressId); + _showAppsIconBtnPressId = 0; + Main.overview.dash._showAppsIcon.reactive = false; + } +} + +const DashOverride = { + handleDragOver(source, actor, _x, y, _time) { + let app = Dash.getAppFromSource(source); + + // Don't allow favoriting of transient apps + if (app === null || app.is_window_backed()) + return DND.DragMotionResult.NO_DROP; + + if (!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 boxHeight = this._box.height; + + // Keep the placeholder out of the index calculation; assuming that + // the remove target has the same size as "normal" items, we don't + // need to do the same adjustment there. + if (this._dragPlaceholder) { + boxHeight -= this._dragPlaceholder.height; + numChildren--; + } + + // Same with the separator + if (this._separator) { + boxHeight -= this._separator.height; + numChildren--; + } + + let pos; + if (!this._emptyDropTarget) + pos = Math.floor(y * numChildren / boxHeight); + else + pos = 0; // always insert at the top when dash is empty + + // 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(); + this._dragPlaceholder.child.set_width(this.iconSize / 2); + this._dragPlaceholder.child.set_height(this.iconSize); + 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; + }, + + _redisplay() { + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let running = this._appSystem.get_running(); + + 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: this.iconSize, + height: 1, + }); + this._box.add_child(this._separator); + } + + // FIXME: separator placement is broken (also in original dash) + let pos = nFavorites; + 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; + indicator.x_align = opt.DASH_LEFT ? Clutter.ActorAlign.START : Clutter.ActorAlign.END; + indicator.y_align = Clutter.ActorAlign.CENTER; + + 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; + }, +}; + +const DashItemContainerCommon = { + // move labels according dash position + showLabel() { + if (!this._labelText) + return; + + this.label.set_text(this._labelText); + 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); + + let node = this.label.get_theme_node(); + let y; + + if (opt.DASH_TOP) { + const yOffset = itemHeight - labelHeight + 3 * node.get_length('-y-offset'); + 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 = 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 = 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 = { + // 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); + + if (this._showWindowsIcon) + iconChildren.push(this._showWindowsIcon); + + if (this._recentFilesIcon) + iconChildren.push(this._recentFilesIcon); + + + 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 availWidth, availHeight, maxIconSize; + 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, opt.MAX_ICON_SIZE * 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, opt.MAX_ICON_SIZE * 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; + }, +}; + +const DashIconCommon = { + after__init() { + if (opt.DASH_ICON_SCROLL) { + this._scrollConId = this.connect('scroll-event', _onScrollEvent.bind(this)); + this._leaveConId = this.connect('leave-event', _onLeaveEvent.bind(this)); + } + }, +}; + +function _onScrollEvent(source, event) { + if ((this.app && !opt.DASH_ICON_SCROLL) || (this._isSearchWindowsIcon && !opt.SEARCH_WINDOWS_ICON_SCROLL)) { + if (this._scrollConId) + this.disconnect(this._scrollConId); + if (this._leaveConId) + this.disconnect(this._leaveConId); + return Clutter.EVENT_PROPAGATE; + } + + let direction = 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(); + + _switchWindow.bind(this)(direction); + return Clutter.EVENT_STOP; +} + +function _onLeaveEvent() { + if (!this._selectedMetaWin || this.has_pointer || this.toggleButton?.has_pointer) + return; + + this._selectedPreview._activateSelected = false; + this._selectedMetaWin = null; + this._scrolledWindows = null; + _showWindowPreview.bind(this)(null); +} + +function _switchWindow(direction) { + if (!this._scrolledWindows) { + // source is app icon + if (this.app) { + this._scrolledWindows = this.app.get_windows(); + 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 = AltTab.getWindows(null); + else + this._scrolledWindows = AltTab.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 + direction; + + if (targetIdx > windows.length - 1) + targetIdx = 0; + else if (targetIdx < 0) + targetIdx = windows.length - 1; + + const metaWin = windows[targetIdx]; + _showWindowPreview.bind(this)(metaWin); + this._selectedMetaWin = metaWin; +} + +function _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()); + + 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 AppIconCommon = { + activate(button) { + const event = Clutter.get_current_event(); + const state = event ? event.get_state() : 0; + const isMiddleButton = button && button === Clutter.BUTTON_MIDDLE; + const isCtrlPressed = Util.isCtrlPressed(state); + const isShiftPressed = Util.isShiftPressed(state); + const openNewWindow = (this.app.can_open_new_window() && + this.app.state === Shell.AppState.RUNNING && + (isCtrlPressed || isMiddleButton) && !opt.DASH_CLICK_ACTION === 2) || + (opt.DASH_CLICK_ACTION === 2 && !this._selectedMetaWin && !isMiddleButton); + + const currentWS = global.workspace_manager.get_active_workspace(); + const appRecentWorkspace = _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); + } + ); + } + + 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)*/) { + // this._scroll = true; + // this._scrollTime = Date.now(); + 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_MODE && !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 && this.app.get_windows().length) { + this._moveAppToCurrentWorkspace(); + 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) { + if (shellVersion >= 42) + 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(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_actor(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++); + } + }]); + } + + if (/* opt.APP_MENU_MV_TO_WS && */this._windowsOnOtherWs()) + popupItems.push([_('Move App to Current Workspace ( Shift + Click )'), this._moveAppToCurrentWorkspace]); + } + + 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)); + 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; + }, +}; + +function _getWindowApp(metaWin) { + const tracker = Shell.WindowTracker.get_default(); + return tracker.get_window_app(metaWin); +} + +function _getAppLastUsedWindow(app) { + let recentWin; + global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => { + const winApp = _getWindowApp(metaWin); + if (!recentWin && winApp === app) + recentWin = metaWin; + }); + return recentWin; +} + +function _getAppRecentWorkspace(app) { + const recentWin = _getAppLastUsedWindow(app); + if (recentWin) + return recentWin.get_workspace(); + + return null; +} + +function _updateSearchWindowsIcon(show = opt.SHOW_WINDOWS_ICON) { + const dash = Main.overview._overview._controls.layoutManager._dash; + const dashContainer = dash._dashContainer; + + if (dash._showWindowsIcon) { + dashContainer.remove_child(dash._showWindowsIcon); + if (dash._showWindowsIconClickedId) + dash._showWindowsIcon.toggleButton.disconnect(dash._showWindowsIconClickedId); + dash._showWindowsIconClickedId = undefined; + if (dash._showWindowsIcon) + dash._showWindowsIcon.destroy(); + dash._showWindowsIcon = undefined; + } + + if (!show || !opt.WINDOW_SEARCH_PROVIDER_ENABLED) + return; + + if (!dash._showWindowsIcon) { + dash._showWindowsIcon = new ShowWindowsIcon(); + dash._showWindowsIcon.show(false); + dashContainer.add_child(dash._showWindowsIcon); + dash._hookUpLabel(dash._showWindowsIcon); + } + + dash._showWindowsIcon.icon.setIconSize(dash.iconSize); + if (opt.SHOW_WINDOWS_ICON === 1) { + dashContainer.set_child_at_index(dash._showWindowsIcon, 0); + } else if (opt.SHOW_WINDOWS_ICON === 2) { + const index = dashContainer.get_children().length - 1; + dashContainer.set_child_at_index(dash._showWindowsIcon, index); + } + + Main.overview._overview._controls.layoutManager._dash._adjustIconSize(); +} + +const ShowWindowsIcon = GObject.registerClass( +class ShowWindowsIcon extends Dash.DashItemContainer { + _init() { + super._init(); + + this._isSearchWindowsIcon = true; + this._labelText = _('Search Open Windows (Hotkey: Space)'); + this.toggleButton = new St.Button({ + style_class: 'show-apps', + track_hover: true, + can_focus: true, + toggle_mode: false, + }); + + this._iconActor = null; + this.icon = new IconGrid.BaseIcon(this.labelText, { + setSizeManually: true, + showLabel: false, + createIcon: this._createIcon.bind(this), + }); + this.icon.y_align = Clutter.ActorAlign.CENTER; + + this.toggleButton.add_actor(this.icon); + this.toggleButton._delegate = this; + + this.setChild(this.toggleButton); + + if (opt.SEARCH_WINDOWS_ICON_SCROLL) { + this.reactive = true; + this._scrollConId = this.connect('scroll-event', _onScrollEvent.bind(this)); + this._leaveConId = this.connect('leave-event', _onLeaveEvent.bind(this)); + } + } + + _createIcon(size) { + this._iconActor = new St.Icon({ + icon_name: 'focus-windows-symbolic', + icon_size: size, + style_class: 'show-apps-icon', + track_hover: true, + }); + return this._iconActor; + } +}); + +function _updateRecentFilesIcon(show = opt.SHOW_RECENT_FILES_ICON) { + const dash = Main.overview._overview._controls.layoutManager._dash; + const dashContainer = dash._dashContainer; + + if (dash._recentFilesIcon) { + dashContainer.remove_child(dash._recentFilesIcon); + if (dash._recentFilesIconClickedId) + dash._recentFilesIcon.toggleButton.disconnect(dash._recentFilesIconClickedId); + dash._recentFilesIconClickedId = undefined; + if (dash._recentFilesIcon) + dash._recentFilesIcon.destroy(); + dash._recentFilesIcon = undefined; + } + + if (!show || !opt.RECENT_FILES_SEARCH_PROVIDER_ENABLED) + return; + + if (!dash._recentFilesIcon) { + dash._recentFilesIcon = new ShowRecentFilesIcon(); + dash._recentFilesIcon.show(false); + dashContainer.add_child(dash._recentFilesIcon); + dash._hookUpLabel(dash._recentFilesIcon); + } + + dash._recentFilesIcon.icon.setIconSize(dash.iconSize); + if (opt.SHOW_RECENT_FILES_ICON === 1) { + dashContainer.set_child_at_index(dash._recentFilesIcon, 0); + } else if (opt.SHOW_RECENT_FILES_ICON === 2) { + const index = dashContainer.get_children().length - 1; + dashContainer.set_child_at_index(dash._recentFilesIcon, index); + } + + Main.overview._overview._controls.layoutManager._dash._adjustIconSize(); +} + +const ShowRecentFilesIcon = GObject.registerClass( +class ShowRecentFilesIcon extends Dash.DashItemContainer { + _init() { + super._init(); + + this._labelText = _('Search Recent Files (Hotkey: Ctrl + Space)'); + this.toggleButton = new St.Button({ + style_class: 'show-apps', + track_hover: true, + can_focus: true, + toggle_mode: false, + }); + + this._iconActor = null; + this.icon = new IconGrid.BaseIcon(this.labelText, { + setSizeManually: true, + showLabel: false, + createIcon: this._createIcon.bind(this), + }); + this.icon.y_align = Clutter.ActorAlign.CENTER; + + this.toggleButton.add_actor(this.icon); + this.toggleButton._delegate = this; + + this.setChild(this.toggleButton); + } + + _createIcon(size) { + this._iconActor = new St.Icon({ + icon_name: 'document-open-recent-symbolic', + icon_size: size, + style_class: 'show-apps-icon', + track_hover: true, + }); + return this._iconActor; + } +}); diff --git a/extensions/vertical-workspaces/lib/iconGrid.js b/extensions/vertical-workspaces/lib/iconGrid.js new file mode 100644 index 0000000..1aa980e --- /dev/null +++ b/extensions/vertical-workspaces/lib/iconGrid.js @@ -0,0 +1,314 @@ +/** + * V-Shell (Vertical Workspaces) + * iconGrid.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; +const { GLib, St, Meta } = imports.gi; +const IconGrid = imports.ui.iconGrid; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; +const shellVersion = _Util.shellVersion; + +// added sizes for better scaling +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, + 48: 48, + TINY: 32, +}; + +const PAGE_WIDTH_CORRECTION = 100; + +let opt; +let _overrides; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('appDisplayModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + + if (reset) { + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + + if (shellVersion < 43 && IconGridCommon._findBestModeForSize) { + IconGridCommon['findBestModeForSize'] = IconGridCommon._findBestModeForSize; + IconGridCommon['_findBestModeForSize'] = undefined; + } + _overrides.addOverride('IconGrid', IconGrid.IconGrid.prototype, IconGridCommon); + _overrides.addOverride('IconGridLayout', IconGrid.IconGridLayout.prototype, IconGridLayoutCommon); +} +// workaround - silence page -2 error on gnome 43 while cleaning app grid + +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); + }, + + _findBestModeForSize(width, height) { + // this function is for main grid only, folder grid calculation is in appDisplay.AppFolderDialog class + if (this._currentMode > -1 || this.layoutManager._isFolder) + return; + const { pagePadding } = this.layout_manager; + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + const iconPadding = 53 * scaleFactor; + // provided width is usually about 100px wider in horizontal orientation with prev/next page indicators + const pageIndicatorCompensation = opt.ORIENTATION ? 0 : PAGE_WIDTH_CORRECTION; + + width -= pagePadding.left + pagePadding.right + pageIndicatorCompensation; + width *= opt.APP_GRID_PAGE_WIDTH_SCALE; + height -= pagePadding.top + pagePadding.bottom; + + // store grid max dimensions for icon size algorithm + this.layoutManager._gridWidth = width; + this.layoutManager._gridHeight = height; + + 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) * scaleFactor; + // if this._gridModes.length === 1, custom grid should be used + // if (iconSize > 0 && this._gridModes.length > 1) { + let columns = opt.APP_GRID_COLUMNS; + let rows = opt.APP_GRID_ROWS; + // 0 means adaptive size + let unusedSpaceH = -1; + let unusedSpaceV = -1; + if (!columns) { + columns = Math.floor(width / (iconSize + iconPadding)) + 1; + while (unusedSpaceH < 0) { + columns -= 1; + unusedSpaceH = width - columns * (iconSize + iconPadding) - (columns - 1) * spacing; + } + } + if (!rows) { + rows = Math.floor(height / (iconSize + iconPadding)) + 1; + while (unusedSpaceV < 0) { + rows -= 1; + unusedSpaceV = height - rows * (iconSize + iconPadding) - (rows - 1) * spacing; + } + } + + this._gridModes = [{ columns, rows }]; + // } + + this._setGridMode(0); + }, +}; + +const IconGridLayoutCommon = { + _findBestIconSize() { + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + const nColumns = this.columnsPerPage; + const nRows = this.rowsPerPage; + const columnSpacingPerPage = opt.APP_GRID_SPACING * (nColumns - 1); + const rowSpacingPerPage = opt.APP_GRID_SPACING * (nRows - 1); + const iconPadding = 53 * scaleFactor; + + const paddingH = this._isFolder ? this.pagePadding.left + this.pagePadding.right : 0; + const paddingV = this._isFolder ? this.pagePadding.top + this.pagePadding.bottom : 0; + + const width = this._gridWidth ? this._gridWidth : this._pageWidth; + const height = this._gridHeight ? this._gridHeight : this._pageHeight; + if (!width || !height) + return opt.APP_GRID_ICON_SIZE_DEFAULT; + + const [firstItem] = this._container; + + if (this.fixedIconSize !== -1) + return this.fixedIconSize; + + /* if (opt.APP_GRID_ADAPTIVE && !this._isFolder) + return opt.APP_GRID_ICON_SIZE_DEFAULT;*/ + + let iconSizes = Object.values(IconSize).sort((a, b) => b - a); + + // limit max icon size for folders, the whole range is for the main grid with active folders + if (this._isFolder) + iconSizes = iconSizes.slice(iconSizes.indexOf(IconSize.LARGE), -1); + + let sizeInvalid = false; + for (const size of iconSizes) { + let usedWidth, usedHeight; + + if (firstItem) { + firstItem.icon.setIconSize(size); + const [firstItemWidth, firstItemHeight] = + firstItem.get_preferred_size(); + + const itemSize = Math.max(firstItemWidth, firstItemHeight); + if (itemSize < size) + sizeInvalid = true; + + usedWidth = itemSize * nColumns; + usedHeight = itemSize * nRows; + } + + if (!firstItem || sizeInvalid) { + usedWidth = (size + iconPadding) * nColumns; + usedHeight = (size + iconPadding) * nRows; + } + const emptyHSpace = + width - usedWidth - columnSpacingPerPage - paddingH; + // this.pagePadding.left - this.pagePadding.right; + const emptyVSpace = + height - usedHeight - rowSpacingPerPage - paddingV; + // this.pagePadding.top - this.pagePadding.bottom; + + if (emptyHSpace >= 0 && emptyVSpace >= 0) { + return size; + } + } + + return IconSize.TINY; + }, + + removeItem(item) { + if (!this._items.has(item)) { + log(`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)) { + log(`iconGrid: Item ${item} already added to IconGridLayout`); + return; + // throw new Error(`Item ${item} already added to IconGridLayout`); + } + + if (page > this._pages.length) { + log(`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; + this._container.add_child(item); + this._addItemToPage(item, page, index); + }, + + moveItem(item, newPage, newPosition) { + if (!this._items.has(item)) { + log(`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); + }, + + _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; + }, +}; diff --git a/extensions/vertical-workspaces/lib/layout.js b/extensions/vertical-workspaces/lib/layout.js new file mode 100644 index 0000000..f6562fd --- /dev/null +++ b/extensions/vertical-workspaces/lib/layout.js @@ -0,0 +1,380 @@ +/** + * V-Shell (Vertical Workspaces) + * layout.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Meta, GLib, Shell, Clutter, GObject } = imports.gi; + +const Main = imports.ui.main; +const Layout = imports.ui.layout; +const Ripples = imports.ui.ripples; +const DND = imports.ui.dnd; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +let _overrides; +let _timeouts; +let opt; +let _firstRun = true; +let _originalUpdateHotCorners; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('layoutModule', true); + const conflict = _Util.getEnabledExtensions('custom-hot-corners').length || + _Util.getEnabledExtensions('dash-to-panel').length; + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled or in conflict + if (_firstRun && (reset || conflict)) + return; + + _firstRun = false; + + if (!_originalUpdateHotCorners) + _originalUpdateHotCorners = Layout.LayoutManager.prototype._updateHotCorners; + + if (_overrides) + _overrides.removeAll(); + + if (_timeouts) { + Object.values(_timeouts).forEach(t => { + if (t) + GLib.source_remove(t); + }); + } + + if (reset) { + _overrides = null; + opt = null; + _timeouts = null; + Main.layoutManager._updateHotCorners = _originalUpdateHotCorners; + Main.layoutManager._updateHotCorners(); + return; + } + + _timeouts = {}; + + _overrides = new _Util.Overrides(); + _overrides.addOverride('LayoutManager', Layout.LayoutManager.prototype, LayoutManagerCommon); + + Main.layoutManager._updateHotCorners = LayoutManagerCommon._updateHotCorners.bind(Main.layoutManager); + + Main.layoutManager._updatePanelBarrier(); + Main.layoutManager._updateHotCorners(); +} + +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) + return; + + if (this.panelBox.height) { + let primary = this.primaryMonitor; + if ([0, 1, 3].includes(opt.HOT_CORNER_POSITION)) { + this._rightPanelBarrier = new Meta.Barrier({ + display: global.display, + 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, + }); + } + + if ([2, 4].includes(opt.HOT_CORNER_POSITION)) { + this._leftPanelBarrier = new Meta.Barrier({ + display: global.display, + x1: primary.x, y1: this.panelBox.allocation.y1, + x2: primary.x, y2: this.panelBox.allocation.y2, + directions: Meta.BarrierDirection.POSITIVE_X, + }); + } + } + }, + + _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; + + // 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 HotCorner(this, monitor, cornerX, cornerY); + corner.setBarrierSize(size); + this.hotCorners.push(corner); + } else { + this.hotCorners.push(null); + } + } + + this.emit('hot-corners-changed'); + }, +}; + +var HotCorner = GObject.registerClass( +class HotCorner extends Layout.HotCorner { + _init(layoutManager, monitor, x, y) { + super._init(layoutManager, monitor, x, y); + + 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) { + 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_EDGE && opt.DASH_VERTICAL && monitor.index === primaryMonitor; + const extendH = opt && opt.HOT_CORNER_EDGE && !opt.DASH_VERTICAL && monitor.index === primaryMonitor; + + if (opt.HOT_CORNER_POSITION <= 1) { + this._verticalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x, y1: this._y, y2: this._y + (extendV ? monitor.height : size), + directions: Meta.BarrierDirection.POSITIVE_X, + }); + this._horizontalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x + (extendH ? monitor.width : size), y1: this._y, y2: this._y, + directions: Meta.BarrierDirection.POSITIVE_Y, + }); + } else if (opt.HOT_CORNER_POSITION === 2) { + this._verticalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x, y1: this._y, y2: this._y + (extendV ? monitor.height : size), + directions: Meta.BarrierDirection.NEGATIVE_X, + }); + this._horizontalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x - size, x2: this._x, y1: this._y, y2: this._y, + directions: Meta.BarrierDirection.POSITIVE_Y, + }); + } else if (opt.HOT_CORNER_POSITION === 3) { + this._verticalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x, y1: this._y, y2: this._y - size, + directions: Meta.BarrierDirection.POSITIVE_X, + }); + this._horizontalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x + (extendH ? monitor.width : size), y1: this._y, y2: this._y, + directions: Meta.BarrierDirection.NEGATIVE_Y, + }); + } else if (opt.HOT_CORNER_POSITION === 4) { + this._verticalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x, y1: this._y, y2: this._y - size, + directions: Meta.BarrierDirection.NEGATIVE_X, + }); + this._horizontalBarrier = new Meta.Barrier({ + display: global.display, + x1: this._x, x2: this._x - size, y1: this._y, y2: this._y, + directions: Meta.BarrierDirection.NEGATIVE_Y, + }); + } + + 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 ((opt.HOT_CORNER_ACTION === 1 && !_Util.isCtrlPressed()) || (opt.HOT_CORNER_ACTION === 2 && _Util.isCtrlPressed())) + this._toggleWindowPicker(true); + else if ((opt.HOT_CORNER_ACTION === 2 && !_Util.isCtrlPressed()) || (opt.HOT_CORNER_ACTION === 1 && _Util.isCtrlPressed()) || (opt.HOT_CORNER_ACTION === 3 && _Util.isCtrlPressed())) + this._toggleApplications(true); + else if (opt.HOT_CORNER_ACTION === 3 && !_Util.isCtrlPressed()) + this._toggleWindowSearchProvider(); + if (opt.HOT_CORNER_RIPPLES && Main.overview.animationInProgress) + this._ripples.playAnimation(this._x, this._y); + } + } + + _toggleWindowPicker(leaveOverview = 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(); + + _timeouts.releaseKeyboardTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + ); + } else { + Main.overview.show(); + } + } + } + + _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._overview._controls._searchController._searchActive) { + this._toggleWindowPicker(); + const prefix = 'wq// '; + 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/vertical-workspaces/lib/messageTray.js b/extensions/vertical-workspaces/lib/messageTray.js new file mode 100644 index 0000000..b35541a --- /dev/null +++ b/extensions/vertical-workspaces/lib/messageTray.js @@ -0,0 +1,67 @@ +/** + * V-Shell (Vertical Workspaces) + * messageTray.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Clutter } = imports.gi; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Main = imports.ui.main; + +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('messageTrayModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (reset) { + opt = null; + setNotificationPosition(1); + return; + } + + setNotificationPosition(opt.NOTIFICATION_POSITION); +} + +function 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/vertical-workspaces/lib/optionsFactory.js b/extensions/vertical-workspaces/lib/optionsFactory.js new file mode 100644 index 0000000..da62dd1 --- /dev/null +++ b/extensions/vertical-workspaces/lib/optionsFactory.js @@ -0,0 +1,645 @@ +/** + * V-Shell (Vertical Workspaces) + * optionsFactory.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + */ + +'use strict'; + +const { Gtk, Gio, GObject } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Settings = Me.imports.lib.settings; + +const shellVersion = Settings.shellVersion; + +// gettext +const _ = Settings._; + +const ProfileNames = [ + _('GNOME 3'), + _('GNOME 40+ - Bottom Hot Edge'), + _('Hot Corner Centric - Top Left Hot Corner'), + _('Dock Overview - Bottom Hot Edge'), +]; + +// libadwaita is available starting with GNOME Shell 42. +let Adw = null; +try { + Adw = imports.gi.Adw; +} catch (e) {} + +function _newImageFromIconName(name) { + return Gtk.Image.new_from_icon_name(name); +} + +var ItemFactory = class ItemFactory { + constructor(gOptions) { + this._gOptions = gOptions; + this._settings = this._gOptions._gsettings; + } + + getRowWidget(text, caption, widget, variable, options = []) { + 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 && this._gOptions.options[variable]) { + const opt = this._gOptions.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); + } + + 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 = this._gOptions.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; + } + this._gOptions.connect(`changed::${key}`, () => { + widget.set_active_iter(widget._comboMap[this._gOptions.get(variable, true)]); + }); + widget.connect('changed', () => { + const [success, iter] = widget.get_active_iter(); + + if (!success) + return; + + this._gOptions.set(variable, model.get_value(iter, 1)); + }); + } + + _connectDropDown(widget, key, variable, options) { + const model = widget.get_model(); + const currentValue = this._gOptions.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(); + this._gOptions.set(variable, item.id); + }); + + this._gOptions.connect(`changed::${key}`, () => { + const newId = this._gOptions.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({ + label: shellVersion < 42 ? 'Click Me!' : '', + uri, + halign: Gtk.Align.END, + valign: Gtk.Align.CENTER, + hexpand: true, + }); + 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); + entry.connect('icon-press', e => e.set_text('')); + entry.connect('changed', e => opt.set(`profileName${profileIndex}`, e.get_text())); + + const resetProfile = this.newButton(); + resetProfile.set({ + tooltip_text: _('Reset profile to defaults'), + icon_name: 'edit-delete-symbolic', + hexpand: false, + css_classes: ['destructive-action'], + }); + + function setName() { + let name = opt.get(`profileName${profileIndex}`, true); + if (!name) + name = ProfileNames[profileIndex - 1]; + entry.set_text(name); + } + + setName(); + 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: 'edit-delete-symbolic', + }); + + btn.connect('clicked', () => { + const settings = this._settings; + settings.list_keys().forEach( + key => settings.reset(key) + ); + }); + btn._activatable = false; + return btn; + } +}; + +var AdwPrefs = class { + constructor(gOptions) { + this._gOptions = 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 = 840; + 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; + } +}; + +var LegacyPrefs = class { + constructor(gOptions) { + this._gOptions = gOptions; + } + + getPrefsWidget(pages) { + const prefsWidget = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + }); + const stack = new Gtk.Stack({ + hexpand: true, + }); + const stackSwitcher = new Gtk.StackSwitcher({ + halign: Gtk.Align.CENTER, + hexpand: true, + }); + + const context = stackSwitcher.get_style_context(); + context.add_class('caption'); + + stackSwitcher.set_stack(stack); + stack.set_transition_duration(300); + stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT); + + const pageProperties = { + hscrollbar_policy: Gtk.PolicyType.NEVER, + vscrollbar_policy: Gtk.PolicyType.AUTOMATIC, + vexpand: true, + hexpand: true, + visible: true, + }; + + const pagesBtns = []; + + for (let page of pages) { + const name = page.name; + const title = page.title; + const iconName = page.iconName; + const optionList = page.optionList; + + stack.add_named(this._getLegacyPage(optionList, pageProperties), name); + pagesBtns.push( + [new Gtk.Label({ label: title }), _newImageFromIconName(iconName, Gtk.IconSize.BUTTON)] + ); + } + + let stBtn = stackSwitcher.get_first_child ? stackSwitcher.get_first_child() : null; + for (let i = 0; i < pagesBtns.length; i++) { + const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, visible: true }); + const icon = pagesBtns[i][1]; + icon.margin_start = 30; + icon.margin_end = 30; + box.append(icon); + box.append(pagesBtns[i][0]); + if (stackSwitcher.get_children) { + stBtn = stackSwitcher.get_children()[i]; + stBtn.add(box); + } else { + stBtn.set_child(box); + stBtn.visible = true; + stBtn = stBtn.get_next_sibling(); + } + } + + if (stack.show_all) + stack.show_all(); + if (stackSwitcher.show_all) + stackSwitcher.show_all(); + + prefsWidget.append(stack); + + if (prefsWidget.show_all) + prefsWidget.show_all(); + + prefsWidget._stackSwitcher = stackSwitcher; + + return prefsWidget; + } + + _getLegacyPage(optionList, pageProperties) { + const page = new Gtk.ScrolledWindow(pageProperties); + const mainBox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + spacing: 5, + homogeneous: false, + margin_start: 30, + margin_end: 30, + margin_top: 12, + margin_bottom: 12, + }); + + let context = page.get_style_context(); + context.add_class('background'); + + let frame; + let frameBox; + 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) { + const lbl = new Gtk.Label({ + label: option, + xalign: 0, + margin_bottom: 4, + }); + + context = lbl.get_style_context(); + context.add_class('heading'); + + mainBox.append(lbl); + + frame = new Gtk.Frame({ + margin_bottom: 16, + }); + + frameBox = new Gtk.ListBox({ + selection_mode: null, + }); + + mainBox.append(frame); + frame.set_child(frameBox); + continue; + } + + 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, + }); + + grid.attach(option, 0, 0, 5, 1); + + if (widget) + grid.attach(widget, 5, 0, 2, 1); + + frameBox.append(grid); + } + page.set_child(mainBox); + + return page; + } +}; + +const DropDownItem = GObject.registerClass({ + GTypeName: 'DropdownItem', + 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, + 0, 100, 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/vertical-workspaces/lib/osdWindow.js b/extensions/vertical-workspaces/lib/osdWindow.js new file mode 100644 index 0000000..a010558 --- /dev/null +++ b/extensions/vertical-workspaces/lib/osdWindow.js @@ -0,0 +1,93 @@ +/** + * V-Shell (Vertical Workspaces) + * osdWindow.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Clutter } = imports.gi; +const Main = imports.ui.main; +const OsdWindow = imports.ui.osdWindow; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +const 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, + }, +}; + +let _overrides; +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('osdWindowModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + if (reset || !moduleEnabled) { + updateExistingOsdWindows(6); + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + _overrides.addOverride('osdWindow', OsdWindow.OsdWindow.prototype, OsdWindowCommon); +} + +function 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/vertical-workspaces/lib/overlayKey.js b/extensions/vertical-workspaces/lib/overlayKey.js new file mode 100644 index 0000000..e0fc11d --- /dev/null +++ b/extensions/vertical-workspaces/lib/overlayKey.js @@ -0,0 +1,108 @@ +/** + * V-Shell (Vertical Workspaces) + * overlayKey.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; +const { GObject, Gio, GLib, Meta, St } = imports.gi; + +const Main = imports.ui.main; +const Overview = imports.ui.overview; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +const _ = Me.imports.lib.settings._; +const shellVersion = _Util.shellVersion; +const WIN_SEARCH_PREFIX = Me.imports.lib.windowSearchProvider.prefix; +const RECENT_FILES_PREFIX = Me.imports.lib.recentFilesSearchProvider.prefix; +const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard'; + +let opt; +let _firstRun = true; + +let _originalOverlayKeyHandlerId; +let _overlayKeyHandlerId; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('overlayKeyModule', true); + reset = reset || (!_firstRun && !moduleEnabled); + + // don't even touch this module if disabled + if (_firstRun && !moduleEnabled) + return; + + _firstRun = false; + + if (reset) { + _updateOverlayKey(reset); + opt = null; + return; + } + + _updateOverlayKey(); +} + +function _updateOverlayKey(reset = false) { + if (reset) { + _restoreOverlayKeyHandler(); + } else if (!_originalOverlayKeyHandlerId) { + _originalOverlayKeyHandlerId = GObject.signal_handler_find(global.display, { signalId: 'overlay-key' }); + if (_originalOverlayKeyHandlerId !== null) + global.display.block_signal_handler(_originalOverlayKeyHandlerId); + _connectOverlayKey.bind(Main.overview._overview.controls)(); + } +} + +function _restoreOverlayKeyHandler() { + // Disconnect modified overlay key handler + if (_overlayKeyHandlerId !== null) { + global.display.disconnect(_overlayKeyHandlerId); + _overlayKeyHandlerId = null; + } + + // Unblock original overlay key handler + if (_originalOverlayKeyHandlerId !== null) { + global.display.unblock_signal_handler(_originalOverlayKeyHandlerId); + _originalOverlayKeyHandlerId = null; + } +} + +function _connectOverlayKey() { + this._a11ySettings = new Gio.Settings({ schema_id: A11Y_SCHEMA }); + + this._lastOverlayKeyTime = 0; + _overlayKeyHandlerId = global.display.connect('overlay-key', () => { + 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) { + if (mode === 1) + this._shiftState(Meta.MotionDirection.UP); + else if (mode === 2) + _Util.activateSearchProvider(WIN_SEARCH_PREFIX); + else if (mode === 3) + _Util.activateSearchProvider(RECENT_FILES_PREFIX); + } else { + Main.overview.toggle(); + } + }); +} diff --git a/extensions/vertical-workspaces/lib/overview.js b/extensions/vertical-workspaces/lib/overview.js new file mode 100644 index 0000000..2f23d05 --- /dev/null +++ b/extensions/vertical-workspaces/lib/overview.js @@ -0,0 +1,59 @@ +/** + * V-Shell (Vertical Workspaces) + * overview.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const Overview = imports.ui.overview; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +let _overrides; +let opt; + +function update(reset = false) { + if (_overrides) + _overrides.removeAll(); + + + if (reset) { + _overrides = null; + opt = null; + return; + } + + opt = Me.imports.lib.settings.opt; + _overrides = new _Util.Overrides(); + + _overrides.addOverride('Overview', Overview.Overview.prototype, OverviewCommon); +} + +const OverviewCommon = { + _showDone() { + this._animationInProgress = false; + this._coverPane.hide(); + + this.emit('shown'); + // Handle any calls to hide* while we were showing + if (!this._shown) + this._animateNotVisible(); + + this._syncGrab(); + + // 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; + } + }, +}; diff --git a/extensions/vertical-workspaces/lib/overviewControls.js b/extensions/vertical-workspaces/lib/overviewControls.js new file mode 100644 index 0000000..4959b83 --- /dev/null +++ b/extensions/vertical-workspaces/lib/overviewControls.js @@ -0,0 +1,1464 @@ +/** + * V-Shell (Vertical Workspaces) + * overviewControls.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Clutter, GLib, GObject, St } = imports.gi; +const Main = imports.ui.main; +const Util = imports.misc.util; +const OverviewControls = imports.ui.overviewControls; +const WorkspaceThumbnail = imports.ui.workspaceThumbnail; + +const ControlsState = imports.ui.overviewControls.ControlsState; +const FitMode = imports.ui.workspacesView.FitMode; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const _Util = Me.imports.lib.util; + +let _overrides; +let opt; + +const ANIMATION_TIME = imports.ui.overview.ANIMATION_TIME; +const DASH_MAX_SIZE_RATIO = 0.25; + +let _originalSearchControllerSigId; +let _searchControllerSigId; +let _timeouts; +let _startupInitComplete = false; + +function update(reset = false) { + if (_overrides) + _overrides.removeAll(); + + if (_timeouts) { + Object.values(_timeouts).forEach(id => { + if (id) + GLib.source_remove(id); + }); + } + + _replaceOnSearchChanged(reset); + + if (reset) { + _overrides = null; + opt = null; + _timeouts = null; + return; + } + + _timeouts = {}; + + opt = Me.imports.lib.settings.opt; + _overrides = new _Util.Overrides(); + + _overrides.addOverride('ControlsManager', OverviewControls.ControlsManager.prototype, ControlsManager); + + if (opt.ORIENTATION === Clutter.Orientation.VERTICAL) + _overrides.addOverride('ControlsManagerLayout', OverviewControls.ControlsManagerLayout.prototype, ControlsManagerLayoutVertical); + else + _overrides.addOverride('ControlsManagerLayout', OverviewControls.ControlsManagerLayout.prototype, ControlsManagerLayoutHorizontal); +} + +function _replaceOnSearchChanged(reset = false) { + const searchController = Main.overview._overview.controls._searchController; + if (reset) { + if (_searchControllerSigId) { + searchController.disconnect(_searchControllerSigId); + _searchControllerSigId = 0; + } + if (_originalSearchControllerSigId) { + searchController.unblock_signal_handler(_originalSearchControllerSigId); + _originalSearchControllerSigId = 0; + } + Main.overview._overview._controls.layoutManager._searchController._searchResults.translation_x = 0; + Main.overview._overview._controls.layoutManager._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) + _originalSearchControllerSigId = GObject.signal_handler_find(searchController, { signalId: 'notify', detail: 'search-active' }); + if (_originalSearchControllerSigId) + searchController.block_signal_handler(_originalSearchControllerSigId); + + _searchControllerSigId = searchController.connect('notify::search-active', ControlsManager._onSearchChanged.bind(Main.overview._overview.controls)); + } +} + +const ControlsManager = { + // 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() { + ... + }*/ + + // 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._updateWorkspacesDisplay(); + }, + + // this function is pure addition to the original code and handles wsDisp transition to APP_GRID view + _updateWorkspacesDisplay() { + 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/* && !(searchActive)*/; + + // 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/* finalState === 0 && progress === 1*/ && opt.OVERVIEW_MODE && opt.WORKSPACE_MODE) + opt.WORKSPACE_MODE = 0; + + if (currentState < 2 && currentState > 1) + WorkspaceThumbnail.RESCALE_ANIMATION_TIME = 0; + else + WorkspaceThumbnail.RESCALE_ANIMATION_TIME = 200; + + if (!opt.WS_ANIMATION || !opt.SHOW_WS_TMB) { + this._workspacesDisplay.opacity = opacity; + } else if (!opt.SHOW_WS_TMB_BG) { + // fade out ws wallpaper during transition to ws switcher if ws switcher background disabled + const ws = this._workspacesDisplay._workspacesViews[global.display.get_primary_monitor()]?._workspaces[this._workspaceAdjustment.value]; + if (ws) + ws._background.opacity = 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 = _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 finalized, giving nonsense width + const [tmbTranslationX, tmbTranslationY, dashTranslationX, dashTranslationY, searchTranslationY] = _Util.getOverviewTranslations(opt, 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; + } + 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 (!_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 (!_Util.dashNotDefault()) + this.set_child_above_sibling(this.dash, null); + + 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); + 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; + + // update App Grid after settings changed + // only if the App Grid is currently visible on the screen, the paging updates correctly + if (currentState === ControlsState.APP_GRID && this._appDisplay.visible && opt._appGridNeedsRedisplay) { + Me.imports.lib.appDisplay._updateAppGridProperties(); + opt._appGridNeedsRedisplay = 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; + }, + + _onSearchChanged() { + 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) { + this._workspacesDisplay.reactive = true; + this._workspacesDisplay.setPrimaryWorkspaceVisible(true); + } else { + this._searchController.show(); + entry.visible = true; + entry.opacity = 255; + } + + this._searchTransition = true; + + this._searchController._searchResults.translation_x = 0; + this._searchController._searchResults.translation_y = 0; + this._searchController.opacity = 255; + this._searchController.visible = true; + + if (opt.SEARCH_VIEW_ANIMATION && !this.dash.showAppsButton.checked && ![4, 8].includes(opt.WS_TMB_POSITION) /* && !opt.OVERVIEW_MODE2*/) { + this._updateAppDisplayVisibility(); + + this._searchController.opacity = searchActive ? 255 : 0; + let translationX = 0; + let translationY = 0; + const geometry = global.display.get_monitor_geometry(global.display.get_primary_monitor()); + + if (currentState < ControlsState.APP_GRID) { + 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({ + 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._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._updateAppDisplayVisibility(); + this._workspacesDisplay.setPrimaryWorkspaceVisible(true); + /* this._workspacesDisplay.ease({ + opacity: searchActive ? 0 : 255, + duration: searchActive ? SIDE_CONTROLS_ANIMATION_TIME / 2 : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._workspacesDisplay.reactive = !searchActive; + this._workspacesDisplay.setPrimaryWorkspaceVisible(!searchActive); + }, + });*/ + + this._searchController.opacity = searchActive ? 0 : 255; + this._searchController.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) { + Main.overview._overview._controls.layoutManager._searchController._searchResults._content.remove_style_class_name('search-section-content-om2'); + Main.overview.searchEntry.remove_style_class_name('search-entry-om2'); + 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 || (opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE) ? 80 : SIDE_CONTROLS_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._workspacesDisplay.setPrimaryWorkspaceVisible(!searchActive); + }, + }); + } 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 + Main.overview._overview._controls.layoutManager._searchController._searchResults._content.add_style_class_name('search-section-content-om2'); + Main.overview.searchEntry.add_style_class_name('search-entry-om2'); + } else { + Main.overview._overview._controls.layoutManager._searchController._searchResults._content.remove_style_class_name('search-section-content-om2'); + Main.overview.searchEntry.remove_style_class_name('search-entry-om2'); + } + }, + + async runStartupAnimation(callback) { + this._ignoreShowAppsButtonToggle = true; + this._searchController.prepareToEnterOverview(); + this._workspacesDisplay.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 = 0; + + // We can't run the animation before the first allocation happens + await this.layout_manager.ensureAllocation(); + + const { STARTUP_ANIMATION_TIME } = imports.ui.layout; + + // Opacity + this.ease({ + opacity: 255, + duration: STARTUP_ANIMATION_TIME, + mode: Clutter.AnimationMode.LINEAR, + onComplete: () => { + // part of the workaround for stuttering first app grid animation + this._appDisplay.visible = true; + }, + }); + + 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 [tmbTranslationX, tmbTranslationY, dashTranslationX, dashTranslationY, searchTranslationY] = + _Util.getOverviewTranslations(opt, dash, tmbBox, searchEntryBin); + + const onComplete = function () { + // running init callback again causes issues (multiple connections) + if (!_startupInitComplete) + callback(); + _startupInitComplete = true; + + // force app grid to build before the first visible animation to remove possible stuttering + this._appDisplay.opacity = 1; + + const [x, y] = this._appDisplay.get_position(); + const translationX = -x; + const translationY = -y; + this._appDisplay.translation_x = translationX; + this._appDisplay.translation_y = translationY; + GLib.idle_add(0, () => { + this._appDisplay._removeItem(this._appDisplay._orderedItems[0]); + this._appDisplay._redisplay(); + }); + + // let the main loop realize previous changes before continuing + _timeouts.startupAnim1 = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 10, + () => { + GLib.idle_add(0, () => { + this._appDisplay._removeItem(this._appDisplay._orderedItems[0]); + this._appDisplay._redisplay(); + }); + this._appDisplay.translation_x = 0; + this._appDisplay.translation_y = 0; + this._appDisplay.visible = false; + if (opt.STARTUP_STATE === 1) { + Main.overview.hide(); + } else if (opt.STARTUP_STATE === 2) { + this._appDisplay.opacity = 255; + this.dash.showAppsButton.checked = true; + } + _timeouts.startupAnim1 = 0; + return GLib.SOURCE_REMOVE; + } + ); + }.bind(this); + + if (dash.visible && !_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: () => { + 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; + + _Util.getOverviewTranslations(opt, dash, secTmbBox, searchEntryBin); + 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, + }); + } + } + } + }, + + animateToOverview(state, callback) { + this._ignoreShowAppsButtonToggle = true; + this._searchTransition = false; + + this._searchController.prepareToEnterOverview(); + this._workspacesDisplay.prepareToEnterOverview(); + + 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; + }, +}; + +const ControlsManagerLayoutVertical = { + _computeWorkspacesBoxForState(state, box, workAreaBox, dashWidth, dashHeight, thumbnailsWidth, searchHeight, startY) { + const workspaceBox = box.copy(); + let [width, height] = workspaceBox.get_size(); + // const { x1: startX/* y1: startY*/ } = workAreaBox; + const { spacing } = this; + // const { expandFraction } = this._workspacesThumbnails; + + const dash = Main.overview.dash; + // including Dash to Dock and clones properties for compatibility + + if (_Util.dashIsDashToDock()) { + // Dash to Dock also always affects workAreaBox + Main.layoutManager._trackedActors.forEach(actor => { + if (actor.affectsStruts && actor.actor.width === dash.width) { + if (dash._isHorizontal) { + // disabled inteli-hide don't needs compensation + // startY needs to be corrected in allocate() + if (dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) + height += dash.height; + } else { + width += dash.width; + } + } + }); + } + + let wWidth; + let wHeight; + let wsBoxY; + + switch (state) { + case ControlsState.HIDDEN: + // if PANEL_OVERVIEW_ONLY, the affectStruts property is set to false to avoid stuttering + // therefore we added panel height to startY for the overview allocation, + // but here we need to remove the correction because the panel will be in the hidden state + if (opt.START_Y_OFFSET) { + let [x, y] = workAreaBox.get_origin(); + y -= opt.START_Y_OFFSET; + workspaceBox.set_origin(x, y); + } else { + workspaceBox.set_origin(...workAreaBox.get_origin()); + } + workspaceBox.set_size(...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(...this._workspacesThumbnails.get_size()); + } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) { + if (opt.START_Y_OFFSET) { + let [x, y] = workAreaBox.get_origin(); + y -= opt.START_Y_OFFSET; + workspaceBox.set_origin(x, y); + } else { + workspaceBox.set_origin(...workAreaBox.get_origin()); + } + workspaceBox.set_size(...workAreaBox.get_size()); + } else { + // if PANEL_OVERVIEW_ONLY, panel doesn't affect workArea height (affectStruts === false), it is necessary to compensate + height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height; + searchHeight = opt.SHOW_SEARCH_ENTRY ? searchHeight : 0; + wWidth = width - + (opt.DASH_VERTICAL ? dash.width : 0) - + thumbnailsWidth - + 4 * spacing; + wHeight = height - + (opt.DASH_VERTICAL ? 0 : dashHeight) - + searchHeight - + 4 * spacing; + + const ratio = width / height; + let wRatio = wWidth / wHeight; + let scale = ratio / wRatio; + + if (scale > 1) { + wHeight /= scale; + wWidth = wHeight * ratio; + } else { + wWidth *= scale; + wHeight = wWidth / ratio; + } + + // height decides the actual size, ratio is given by the workarea + wHeight *= opt.WS_PREVIEW_SCALE; + wWidth *= opt.WS_PREVIEW_SCALE; + + let xOffset = 0; + let yOffset = 0; + + const yOffsetT = (opt.DASH_TOP ? dashHeight : 0) + searchHeight; + const yOffsetB = opt.DASH_BOTTOM ? dashHeight : 0; + const yAvailableSpace = (height - yOffsetT - wHeight - yOffsetB) / 2; + yOffset = yOffsetT + yAvailableSpace; + + const centeredBoxX = (width - wWidth) / 2; + + const xOffsetL = (opt.DASH_LEFT ? dashWidth : 0) + (opt.WS_TMB_LEFT ? thumbnailsWidth : 0) + 2 * spacing; + const xOffsetR = (opt.DASH_RIGHT ? dashWidth : 0) + (opt.WS_TMB_RIGHT ? thumbnailsWidth : 0) + 2 * spacing; + + this._xAlignCenter = false; + if (centeredBoxX < Math.max(xOffsetL, xOffsetR)) { + xOffset = xOffsetL + spacing + (width - xOffsetL - wWidth - xOffsetR - 2 * spacing) / 2; + } else { + xOffset = centeredBoxX; + this._xAlignCenter = true; + } + + const wsBoxX = /* startX + */xOffset; + wsBoxY = Math.round(startY + yOffset); + workspaceBox.set_origin(Math.round(wsBoxX), Math.round(wsBoxY)); + workspaceBox.set_size(Math.round(wWidth), Math.round(wHeight)); + } + } + + return workspaceBox; + }, + + _getAppDisplayBoxForState(state, box, workAreaBox, searchHeight, dashWidth, dashHeight, thumbnailsWidth, startY) { + const [width] = box.get_size(); + const { x1: startX } = workAreaBox; + // const { y1: startY } = workAreaBox; + let height = workAreaBox.get_height(); + const appDisplayBox = new Clutter.ActorBox(); + const { spacing } = this; + + searchHeight = opt.SHOW_SEARCH_ENTRY ? searchHeight : 0; + + const xOffsetL = (opt.WS_TMB_LEFT ? thumbnailsWidth : 0) + (opt.DASH_LEFT ? dashWidth : 0); + const xOffsetR = (opt.WS_TMB_RIGHT ? thumbnailsWidth : 0) + (opt.DASH_RIGHT ? dashWidth : 0); + const yOffsetT = (opt.DASH_TOP ? dashHeight : 0) + (opt.SHOW_SEARCH_ENTRY ? searchHeight : 0); + const yOffsetB = opt.DASH_BOTTOM ? dashHeight : 0; + const adWidth = opt.CENTER_APP_GRID ? width - 2 * Math.max(xOffsetL, xOffsetR) - 4 * spacing : width - xOffsetL - xOffsetR - 4 * spacing; + const adHeight = height - yOffsetT - yOffsetB - 4 * spacing; + + const appDisplayX = opt.CENTER_APP_GRID ? (width - adWidth) / 2 : xOffsetL + 2 * spacing; + const appDisplayY = startY + yOffsetT + 2 * spacing; + + 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(startX - adWidth, appDisplayY); + break; + case 3: + appDisplayBox.set_origin(appDisplayX, workAreaBox.y2); + break; + case 5: + appDisplayBox.set_origin(appDisplayX, workAreaBox.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 { spacing } = this; + + const monitor = Main.layoutManager.findMonitorForActor(this._container); + const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index); + const startX = workArea.x - monitor.x; + // if PANEL_OVERVIEW_ONLY, the affectStruts property is set to false to avoid stuttering + // therefore we need to add panel height to startY + let startY = workArea.y - monitor.y + opt.START_Y_OFFSET; + + const workAreaBox = new Clutter.ActorBox(); + workAreaBox.set_origin(startX, startY); + workAreaBox.set_size(workArea.width, workArea.height); + box.y1 += startY; + box.x1 += startX; + let [width, height] = box.get_size(); + // if panel is at bottom position, + // compensate the height of the available box (the box size is calculated for top panel) + height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height; + let availableHeight = height; + + // Dash + const maxDashHeight = Math.round(box.get_height() * DASH_MAX_SIZE_RATIO); + const maxDashWidth = maxDashHeight * 0.8; + let dashHeight = 0; + let dashWidth = 0; + + // dash cloud be overridden by the Dash to Dock clone + const dash = Main.overview.dash; + if (_Util.dashIsDashToDock()) { + // if Dash to Dock replaced the default dash and its inteli-hide id disabled we need to compensate for affected startY + if (!Main.overview.dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) { + if (Main.panel.y === monitor.y) + startY = Main.panel.height + spacing; + } + dashHeight = dash.height; + dashWidth = dash.width; + opt.DASH_VERTICAL = [1, 3].includes(dash._position); + this._dash.allocate(childBox); + } 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); + } + } + + const transitionParams = this._stateAdjustment.getStateTransitionParams(); + + // Workspace Thumbnails + let wsTmbWidth = 0; + let wsTmbHeight = 0; + + if (this._workspacesThumbnails.visible) { + // const { expandFraction } = this._workspacesThumbnails; + const dashHeightReservation = !opt.WS_TMB_FULL && !opt.DASH_VERTICAL ? dashHeight : 0; + + let maxScale = opt.MAX_THUMBNAIL_SCALE; + if (!opt.MAX_THUMBNAIL_SCALE_STABLE) { + 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; + maxScale = Util.lerp(initState, finalState, transitionParams.progress); + } + + wsTmbWidth = width * maxScale; + let totalTmbSpacing; + [totalTmbSpacing, wsTmbHeight] = this._workspacesThumbnails.get_preferred_custom_height(wsTmbWidth); + wsTmbHeight += totalTmbSpacing; + + const wsTmbHeightMax = height - dashHeightReservation; + + if (wsTmbHeight > wsTmbHeightMax) { + wsTmbHeight = wsTmbHeightMax; + wsTmbWidth = this._workspacesThumbnails.get_preferred_custom_width(wsTmbHeight)[1]; + } + + let wsTmbX; + if (opt.WS_TMB_RIGHT) + wsTmbX = Math.round(startX + width - (opt.DASH_RIGHT ? dashWidth : 0) - wsTmbWidth - spacing / 2); + else + wsTmbX = Math.round((opt.DASH_LEFT ? dashWidth : 0) + spacing / 2); + + + let wstOffset = (height - wsTmbHeight - (opt.DASH_VERTICAL ? 0 : dashHeightReservation)) / 2; + wstOffset -= opt.WS_TMB_POSITION_ADJUSTMENT * (wstOffset - spacing / 2); + let wsTmbY = Math.round(startY + (dashHeightReservation && opt.DASH_TOP ? dashHeight : 0) + wstOffset); + + childBox.set_origin(wsTmbX, wsTmbY); + childBox.set_size(Math.round(wsTmbWidth), Math.round(wsTmbHeight)); + + 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.round(Math.min(dashHeight, maxDashHeight)); + dashWidth = Math.round(Math.min(dashWidth, wMaxWidth)); + } + + let dashX, dashY, offset; + if (opt.DASH_RIGHT) + dashX = width - dashWidth; + else if (opt.DASH_LEFT) + dashX = 0; + + else if (opt.DASH_TOP) + dashY = startY; + else + dashY = startY + height - dashHeight; + + if (!opt.DASH_VERTICAL) { + offset = (width - ((opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && !this._xAlignCenter ? wsTmbWidth : 0) - dashWidth) / 2; + offset -= opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2); + dashX = offset; + + if ((opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && !this._xAlignCenter) { + if (!opt.WS_TMB_RIGHT) { + dashX = (wsTmbWidth ? wsTmbWidth : 0) + offset; + dashX = Math.max(dashX, wsTmbWidth ? wsTmbWidth + spacing : 0); + dashX = Math.min(dashX, width - dashWidth - spacing); + } + } + if (opt.WS_TMB_FULL && !opt.CENTER_DASH_WS) { + dashX = opt.WS_TMB_RIGHT + ? Math.min(width - wsTmbWidth - dashWidth, dashX + wsTmbWidth / 2 * (1 - Math.abs(opt.DASH_POSITION_ADJUSTMENT))) + : Math.max(wsTmbWidth, dashX - wsTmbWidth / 2 * (1 - Math.abs(opt.DASH_POSITION_ADJUSTMENT))); + } + } else { + offset = (height - dashHeight) / 2; + dashY = startY + (offset - opt.DASH_POSITION_ADJUSTMENT * offset); + } + + childBox.set_origin(Math.round(startX + dashX), Math.round(dashY)); + childBox.set_size(dashWidth, dashHeight); + this._dash.allocate(childBox); + } + + availableHeight -= opt.DASH_VERTICAL ? 0 : dashHeight + spacing; + + let [searchHeight] = this._searchEntry.get_preferred_height(width - wsTmbWidth); + + // Workspaces + let params = [box, workAreaBox, dashWidth, dashHeight, wsTmbWidth, searchHeight, startY]; + + // 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); + + // Search entry + const searchXoffset = (opt.DASH_LEFT ? dashWidth : 0) + spacing + (opt.WS_TMB_RIGHT ? 0 : wsTmbWidth + spacing); + + // Y position under top Dash + let searchEntryX, searchEntryY; + if (opt.DASH_TOP) + searchEntryY = startY + dashHeight - spacing; + else + searchEntryY = startY; + + + searchEntryX = searchXoffset; + let searchWidth = width - 2 * spacing - wsTmbWidth - (opt.DASH_VERTICAL ? dashWidth : 0); // xAlignCenter is given by wsBox + searchWidth = this._xAlignCenter ? width - 2 * (wsTmbWidth + spacing) : searchWidth; + + if (opt.CENTER_SEARCH_VIEW) { + childBox.set_origin(0, searchEntryY); + childBox.set_size(width, searchHeight); + } else { + childBox.set_origin(this._xAlignCenter ? 0 : searchEntryX, searchEntryY); + childBox.set_size(this._xAlignCenter ? width : searchWidth - spacing, searchHeight); + } + + this._searchEntry.allocate(childBox); + + availableHeight -= searchHeight + spacing; + + // if (this._appDisplay.visible)... ? Can cause problems + params = [box, workAreaBox, searchHeight, dashWidth, dashHeight, wsTmbWidth, startY]; // send startY, can be corrected + 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); + + // Search + if (opt.CENTER_SEARCH_VIEW) { + const dashW = (opt.DASH_VERTICAL ? dashWidth : 0) + spacing; + searchWidth = width - 2 * wsTmbWidth - 2 * dashW; + childBox.set_origin(wsTmbWidth + dashW, startY + (opt.DASH_TOP ? dashHeight : spacing) + searchHeight); + } else { + childBox.set_origin(this._xAlignCenter ? wsTmbWidth + spacing : searchXoffset, startY + (opt.DASH_TOP ? dashHeight : spacing) + searchHeight); + } + + childBox.set_size(searchWidth, availableHeight); + this._searchController.allocate(childBox); + + this._runPostAllocation(); + }, +}; + +const ControlsManagerLayoutHorizontal = { + _computeWorkspacesBoxForState(state, box, workAreaBox, dashWidth, dashHeight, thumbnailsHeight, searchHeight, startY) { + const workspaceBox = box.copy(); + let [width, height] = workspaceBox.get_size(); + // let { x1: startX/* , y1: startY*/ } = workAreaBox; + const { spacing } = this; + // const { expandFraction } = this._workspacesThumbnails; + + const dash = Main.overview.dash; + // including Dash to Dock and clones properties for compatibility + if (_Util.dashIsDashToDock()) { + // Dash to Dock always affects workAreaBox + Main.layoutManager._trackedActors.forEach(actor => { + if (actor.affectsStruts && actor.actor.width === dash.width) { + if (dash._isHorizontal) { + // disabled inteli-hide don't need compensation + // startY needs to be corrected in allocate() + if (dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) + height += dash.height; + else if (opt.DASH_TOP) + height += dash.height; + } else { + width += dash.width; + } + } + }); + } + + let wWidth, wHeight, wsBoxY, wsBoxX; + + switch (state) { + case ControlsState.HIDDEN: + // if PANEL_OVERVIEW_ONLY, the affectStruts property is set to false to avoid stuttering + // therefore we added panel height to startY for the overview allocation, + // but here we need to remove the correction since the panel will be in the hidden state + if (opt.START_Y_OFFSET) { + let [x, y] = workAreaBox.get_origin(); + y -= opt.START_Y_OFFSET; + workspaceBox.set_origin(x, y); + } else { + workspaceBox.set_origin(...workAreaBox.get_origin()); + } + workspaceBox.set_size(...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(...this._workspacesThumbnails.get_size()); + } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) { + if (opt.START_Y_OFFSET) { + let [x, y] = workAreaBox.get_origin(); + y -= opt.START_Y_OFFSET; + workspaceBox.set_origin(x, y); + } else { + workspaceBox.set_origin(...workAreaBox.get_origin()); + } + workspaceBox.set_size(...workAreaBox.get_size()); + } else { + // if PANEL_OVERVIEW_ONLY, panel doesn't affect workArea height (affectStruts === false), it is necessary to compensate + height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height; + searchHeight = opt.SHOW_SEARCH_ENTRY ? searchHeight : 0; + wWidth = width - + spacing - + (opt.DASH_VERTICAL ? dashWidth : 0) - + 4 * spacing; + wHeight = height - + (opt.DASH_VERTICAL ? spacing : dashHeight) - + thumbnailsHeight - + searchHeight - + 4 * spacing; + + const ratio = width / height; + let wRatio = wWidth / wHeight; + let scale = ratio / wRatio; + + if (scale > 1) { + wHeight /= scale; + wWidth = wHeight * ratio; + } else { + wWidth *= scale; + wHeight = wWidth / ratio; + } + + // height decides the actual size, ratio is given by the workarea + wHeight *= opt.WS_PREVIEW_SCALE; + wWidth *= opt.WS_PREVIEW_SCALE; + + let xOffset = 0; + let yOffset = 0; + + const yOffsetT = (opt.DASH_TOP ? dashHeight : 0) + (opt.WS_TMB_TOP ? thumbnailsHeight : 0) + searchHeight; + const yOffsetB = (opt.DASH_BOTTOM ? dashHeight : 0) + (opt.WS_TMB_BOTTOM ? thumbnailsHeight : 0); + + const yAvailableSpace = (height - yOffsetT - wHeight - yOffsetB) / 2; + yOffset = yOffsetT + yAvailableSpace; + + const xOffsetL = (opt.DASH_LEFT ? dashWidth : 0) + spacing; + const xOffsetR = (opt.DASH_RIGHT ? dashWidth : 0) + spacing; + const centeredBoxX = (width - wWidth) / 2; + + this._xAlignCenter = false; + if (centeredBoxX < Math.max(xOffsetL, xOffsetR)) { + xOffset = xOffsetL + spacing + (width - xOffsetL - wWidth - xOffsetR) / 2; + } else { + xOffset = centeredBoxX; + this._xAlignCenter = true; + } + + wsBoxX = /* startX + */xOffset; + wsBoxY = Math.round(startY + yOffset); + workspaceBox.set_origin(Math.round(wsBoxX), Math.round(wsBoxY)); + workspaceBox.set_size(Math.round(wWidth), Math.round(wHeight)); + } + } + + return workspaceBox; + }, + + _getAppDisplayBoxForState(state, box, workAreaBox, searchHeight, dashWidth, dashHeight, thumbnailsHeight, startY) { + const [width] = box.get_size(); + const { x1: startX } = workAreaBox; + // const { y1: startY } = workAreaBox; + let height = workAreaBox.get_height(); + const appDisplayBox = new Clutter.ActorBox(); + const { spacing } = this; + + const yOffsetT = (opt.WS_TMB_TOP ? thumbnailsHeight : 0) + (opt.DASH_TOP ? dashHeight : 0) + (opt.SHOW_SEARCH_ENTRY ? searchHeight : 0) + 2 * spacing; + const yOffsetB = (opt.WS_TMB_BOTTOM ? thumbnailsHeight : 0) + (opt.DASH_BOTTOM ? dashHeight : 0); + const xOffsetL = opt.DASH_LEFT ? dashWidth : 0; + const xOffsetR = opt.DASH_RIGHT ? dashWidth : 0; + const hSpacing = xOffsetL + xOffsetR ? 2 * spacing : 0; + const adWidth = opt.CENTER_APP_GRID ? width - 2 * Math.max(xOffsetL, xOffsetR) - 2 * hSpacing : width - xOffsetL - xOffsetR - 2 * hSpacing; + const adHeight = height - yOffsetT - yOffsetB - 4 * spacing; + + const appDisplayX = opt.CENTER_APP_GRID ? (width - adWidth) / 2 : xOffsetL + hSpacing; + const appDisplayY = startY + yOffsetT + hSpacing; + + 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(startX - adWidth, appDisplayY); + break; + case 3: + appDisplayBox.set_origin(appDisplayX, workAreaBox.y2); + break; + case 5: + appDisplayBox.set_origin(appDisplayX, workAreaBox.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 { spacing } = this; + + const monitor = Main.layoutManager.findMonitorForActor(this._container); + const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index); + const startX = workArea.x - monitor.x; + // if PANEL_OVERVIEW_ONLY, the affectStruts property is set to false to avoid stuttering + // therefore we need to add panel height to startY + let startY = workArea.y - monitor.y + opt.START_Y_OFFSET; + const workAreaBox = new Clutter.ActorBox(); + workAreaBox.set_origin(startX, startY); + workAreaBox.set_size(workArea.width, workArea.height); + box.y1 += startY; + box.x1 += startX; + let [width, height] = box.get_size(); + // if panel is at bottom position, + // compensate for the height of the available box (the box size is calculated for top panel) + height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height; + let availableHeight = height; + + // Dash + const maxDashHeight = Math.round(box.get_height() * DASH_MAX_SIZE_RATIO); + const maxDashWidth = maxDashHeight * 0.8; + let dashHeight = 0; + let dashWidth = 0; + + // dash cloud be overridden by the Dash to Dock clone + const dash = Main.overview.dash; + if (_Util.dashIsDashToDock()) { + // if Dash to Dock replaced the default dash and its inteli-hide is disabled we need to compensate for affected startY + if (!Main.overview.dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) { + // if (Main.panel.y === monitor.y) + // startY = Main.panel.height + spacing; + } + dashHeight = dash.height; + dashWidth = dash.width; + opt.DASH_TOP = dash._position === 0; + opt.DASH_VERTICAL = [1, 3].includes(dash._position); + this._dash.allocate(childBox); + } 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); + } + } + + let [searchHeight] = this._searchEntry.get_preferred_height(width); + + const transitionParams = this._stateAdjustment.getStateTransitionParams(); + + // Workspace Thumbnails + let wsTmbWidth = 0; + let wsTmbHeight = 0; + + if (this._workspacesThumbnails.visible) { + // const { expandFraction } = this._workspacesThumbnails; + const dashWidthReservation = !opt.WS_TMB_FULL && opt.DASH_VERTICAL ? dashWidth : 0; + + let maxScale = opt.MAX_THUMBNAIL_SCALE; + if (!opt.MAX_THUMBNAIL_SCALE_STABLE) { + 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; + maxScale = Util.lerp(initState, finalState, transitionParams.progress); + } + + wsTmbHeight = height * maxScale; + let totalTmbSpacing; + [totalTmbSpacing, wsTmbWidth] = this._workspacesThumbnails.get_preferred_custom_width(wsTmbHeight); + wsTmbWidth += totalTmbSpacing; + + const wsTmbWidthMax = opt.WS_TMB_FULL + ? width + : width - (opt.DASH_VERTICAL ? 0 : dashWidthReservation); + + if (wsTmbWidth > wsTmbWidthMax) { + wsTmbWidth = wsTmbWidthMax; + wsTmbHeight = this._workspacesThumbnails.get_preferred_custom_height(wsTmbWidth)[1]; + } + + let wsTmbY; + if (opt.WS_TMB_TOP) + wsTmbY = Math.round(startY + /* searchHeight + */(opt.DASH_TOP ? dashHeight : spacing / 2)); + else + wsTmbY = Math.round(startY + height - (opt.DASH_BOTTOM ? dashHeight : 0) - wsTmbHeight); + + let wstOffset = (width - wsTmbWidth) / 2; + wstOffset -= opt.WS_TMB_POSITION_ADJUSTMENT * (wstOffset - spacing / 2); + let wsTmbX = Math.round(Math.clamp( + startX + wstOffset, + startX + (opt.DASH_LEFT ? dashWidthReservation : 0), + width - wsTmbWidth - startX - (opt.DASH_RIGHT ? dashWidthReservation : 0) + )); + + childBox.set_origin(wsTmbX, wsTmbY); + childBox.set_size(Math.round(wsTmbWidth), Math.round(wsTmbHeight)); + + this._workspacesThumbnails.allocate(childBox); + + availableHeight -= wsTmbHeight + spacing; + } + + + 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.round(Math.min(dashWidth, maxDashWidth)); + dashHeight = Math.round(Math.min(dashHeight, wMaxHeight)); + } + + let dashX, dashY, offset; + if (opt.DASH_RIGHT) + dashX = width - dashWidth; + else if (opt.DASH_LEFT) + dashX = 0; + else if (opt.DASH_TOP) + dashY = startY; + else + dashY = startY + height - dashHeight; + + + if (opt.DASH_VERTICAL) { + if (opt.WS_TMB_FULL) { + offset = (height - dashHeight - wsTmbHeight) / 2; + if (opt.WS_TMB_TOP) { + offset -= opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2); + dashY = startY + offset + wsTmbHeight; + } else { + offset -= opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2); + dashY = startY + offset; + } + } else { + offset = (height - dashHeight) / 2; + offset -= opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2); + dashY = startY + offset; + } + } else { + offset = (width - dashWidth) / 2; + dashX = startX + (offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing)); + } + + childBox.set_origin(Math.round(startX + dashX), Math.round(dashY)); + childBox.set_size(dashWidth, dashHeight); + this._dash.allocate(childBox); + } + + availableHeight -= opt.DASH_VERTICAL ? 0 : dashHeight; + + // Workspaces + let params = [box, workAreaBox, dashWidth, dashHeight, wsTmbHeight, searchHeight, startY]; + + // 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); + + // Search entry + const searchXoffset = (opt.DASH_LEFT ? dashWidth : 0) + spacing; + + // Y position under top Dash + let searchEntryX, searchEntryY; + if (opt.DASH_TOP) + searchEntryY = startY + (opt.WS_TMB_TOP ? wsTmbHeight : 0) + dashHeight - spacing; + else + searchEntryY = startY + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0); + + + searchEntryX = searchXoffset; + let searchWidth = width - 2 * spacing - (opt.DASH_VERTICAL ? dashWidth : 0); // xAlignCenter is given by wsBox + searchWidth = this._xAlignCenter ? width : searchWidth; + + if (opt.CENTER_SEARCH_VIEW) { + childBox.set_origin(0, searchEntryY); + childBox.set_size(width, searchHeight); + } else { + childBox.set_origin(this._xAlignCenter ? 0 : searchEntryX, searchEntryY); + childBox.set_size(this._xAlignCenter ? width : searchWidth - spacing, searchHeight); + } + + this._searchEntry.allocate(childBox); + + availableHeight -= searchHeight + spacing; + + // if (this._appDisplay.visible)... ? Can cause problems + params = [box, workAreaBox, searchHeight, dashWidth, dashHeight, wsTmbHeight, startY]; + 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); + + // Search + if (opt.CENTER_SEARCH_VIEW) { + const dashW = (opt.DASH_VERTICAL ? dashWidth : 0) + spacing; + searchWidth = width - 2 * dashW; + childBox.set_origin(dashW, startY + (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0) + searchHeight); + } else { + childBox.set_origin(this._xAlignCenter ? spacing : searchXoffset, startY + (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0) + searchHeight); + } + + childBox.set_size(searchWidth, availableHeight); + this._searchController.allocate(childBox); + + 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/vertical-workspaces/lib/panel.js b/extensions/vertical-workspaces/lib/panel.js new file mode 100644 index 0000000..3f44ae7 --- /dev/null +++ b/extensions/vertical-workspaces/lib/panel.js @@ -0,0 +1,197 @@ +/** + * V-Shell (Vertical Workspaces) + * panel.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { GLib } = imports.gi; +const Main = imports.ui.main; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +const ANIMATION_TIME = imports.ui.overview.ANIMATION_TIME; + +let opt; +let _firstRun = true; + +let _showingOverviewConId; +let _hidingOverviewConId; +let _styleChangedConId; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('panelModule', true); + // Avoid conflict with other extensions + const conflict = _Util.getEnabledExtensions('dash-to-panel').length || + _Util.getEnabledExtensions('hidetopbar').length; + reset = reset || (!_firstRun && !moduleEnabled); + + // don't even touch this module if disabled or in potential conflict + if (_firstRun && (reset || conflict)) + return; + + _firstRun = false; + + const panelBox = Main.layoutManager.panelBox; + if (reset || !moduleEnabled) { + // _disconnectPanel(); + reset = true; + _setPanelPosition(reset); + _updateOverviewConnection(reset); + _reparentPanel(false); + + _updateStyleChangedConnection(reset); + + panelBox.translation_y = 0; + Main.panel.opacity = 255; + _setPanelStructs(true); + return; + } + + _setPanelPosition(); + _updateStyleChangedConnection(); + + if (opt.PANEL_MODE === 0) { + _updateOverviewConnection(true); + _reparentPanel(false); + panelBox.translation_y = 0; + Main.panel.opacity = 255; + _setPanelStructs(true); + } else if (opt.PANEL_MODE === 1) { + if (opt.SHOW_WS_PREVIEW_BG) { + _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); + _updateOverviewConnection(); + } else { + // otherwise move the panel below overviewGroup so it can get below workspacesDisplay + Main.layoutManager.overviewGroup.set_child_below_sibling(panelBox, Main.overview._overview); + _updateOverviewConnection(true); + } + _showPanel(true); + } else { + // if ws preview bg is disabled, panel can stay in uiGroup + _reparentPanel(false); + _showPanel(false); + _updateOverviewConnection(); + } + // _connectPanel(); + } else if (opt.PANEL_MODE === 2) { + _updateOverviewConnection(true); + _reparentPanel(false); + _showPanel(false); + // _connectPanel(); + } + _setPanelStructs(opt.PANEL_MODE === 0); + Main.layoutManager._updateHotCorners(); +} + +function _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); +} + +function _updateStyleChangedConnection(reset = false) { + if (reset) { + if (_styleChangedConId) { + Main.panel.disconnect(_styleChangedConId); + _styleChangedConId = 0; + } + } else if (!_styleChangedConId) { + Main.panel.connect('style-changed', () => { + if (opt.PANEL_MODE === 1) + Main.panel.add_style_pseudo_class('overview'); + else if (opt.OVERVIEW_MODE2) + Main.panel.remove_style_pseudo_class('overview'); + }); + } +} + +function _updateOverviewConnection(reset = false) { + if (reset) { + if (_hidingOverviewConId) { + Main.overview.disconnect(_hidingOverviewConId); + _hidingOverviewConId = 0; + } + if (_showingOverviewConId) { + Main.overview.disconnect(_showingOverviewConId); + _showingOverviewConId = 0; + } + } else { + if (!_hidingOverviewConId) { + _hidingOverviewConId = Main.overview.connect('hiding', () => { + if (!opt.SHOW_WS_PREVIEW_BG || opt.OVERVIEW_MODE2) + _showPanel(false); + }); + } + if (!_showingOverviewConId) { + _showingOverviewConId = Main.overview.connect('showing', () => { + if (!opt.SHOW_WS_PREVIEW_BG || opt.OVERVIEW_MODE2 || Main.layoutManager.panelBox.translation_y) + _showPanel(true); + }); + } + } +} + +function _reparentPanel(reparent = false) { + const panel = Main.layoutManager.panelBox; + if (reparent && panel.get_parent() === Main.layoutManager.uiGroup) { + Main.layoutManager.uiGroup.remove_child(panel); + Main.layoutManager.overviewGroup.add_child(panel); + } else if (!reparent && 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); + } +} + +function _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));*/ +} + +function _showPanel(show = true) { + if (show) { + Main.panel.opacity = 255; + Main.layoutManager.panelBox.ease({ + duration: ANIMATION_TIME, + translation_y: 0, + onComplete: () => { + _setPanelStructs(opt.PANEL_MODE === 0); + }, + }); + } else { + 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; + _setPanelStructs(opt.PANEL_MODE === 0); + }, + }); + } +} diff --git a/extensions/vertical-workspaces/lib/recentFilesSearchProvider.js b/extensions/vertical-workspaces/lib/recentFilesSearchProvider.js new file mode 100644 index 0000000..86e38f4 --- /dev/null +++ b/extensions/vertical-workspaces/lib/recentFilesSearchProvider.js @@ -0,0 +1,260 @@ +/** + * Vertical Workspaces + * recentFilesSearchProvider.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + */ + +'use strict'; + +const { GLib, Gio, Meta, St, Shell, Gtk } = imports.gi; + +const Main = imports.ui.main; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Settings = Me.imports.lib.settings; +const _Util = Me.imports.lib.util; + +// gettext +const _ = Settings._; + +const shellVersion = Settings.shellVersion; + +const ModifierType = imports.gi.Clutter.ModifierType; + +let recentFilesSearchProvider; +let _enableTimeoutId = 0; + +// prefix helps to eliminate results from other search providers +// so it needs to be something less common +// needs to be accessible from vw module +var prefix = 'fq//'; + +var opt; + +function getOverviewSearchResult() { + return Main.overview._overview.controls._searchController._searchResults; +} + + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + if (!reset && opt.RECENT_FILES_SEARCH_PROVIDER_ENABLED && !recentFilesSearchProvider) { + enable(); + } else if (reset || !opt.RECENT_FILES_SEARCH_PROVIDER_ENABLED) { + disable(); + opt = null; + } +} + +function enable() { + // delay because Fedora had problem to register a new provider soon after Shell restarts + _enableTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 2000, + () => { + if (!recentFilesSearchProvider) { + recentFilesSearchProvider = new RecentFilesSearchProvider(opt); + getOverviewSearchResult()._registerProvider(recentFilesSearchProvider); + } + _enableTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + ); +} + +function disable() { + if (recentFilesSearchProvider) { + getOverviewSearchResult()._unregisterProvider(recentFilesSearchProvider); + recentFilesSearchProvider = null; + } + if (_enableTimeoutId) { + GLib.source_remove(_enableTimeoutId); + _enableTimeoutId = 0; + } +} + +function makeResult(window, i) { + const app = Shell.WindowTracker.get_default().get_window_app(window); + const appName = app ? app.get_name() : 'Unknown'; + const windowTitle = window.get_title(); + const wsIndex = window.get_workspace().index(); + + return { + 'id': i, + // convert all accented chars to their basic form and lower case for search + 'name': `${wsIndex + 1}: ${windowTitle} ${appName}`.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(), + appName, + windowTitle, + window, + }; +} + +const closeSelectedRegex = /^\/x!$/; +const closeAllResultsRegex = /^\/xa!$/; +const moveToWsRegex = /^\/m[0-9]+$/; +const moveAllToWsRegex = /^\/ma[0-9]+$/; + +const RecentFilesSearchProvider = class RecentFilesSearchProvider { + constructor() { + this.id = 'org.gnome.Nautilus.desktop'; + this.appInfo = Gio.AppInfo.create_from_commandline('/usr/bin/nautilus -ws recent:///', 'Recent Files', null); + // this.appInfo = Shell.AppSystem.get_default().lookup_app('org.gnome.Nautilus.desktop').appInfo; + this.appInfo.get_description = () => _('Search recent files'); + this.appInfo.get_name = () => _('Recent Files'); + this.appInfo.get_id = () => this.id; + this.appInfo.get_icon = () => Gio.icon_new_for_string('document-open-recent-symbolic'); + this.appInfo.should_show = () => true; + + this.canLaunchSearch = true; + this.isRemoteProvider = false; + } + + _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 = this.files; + const _terms = [].concat(termsCopy); + // let match; + + const term = _terms.join(' '); + /* match = s => { + return fuzzyMatch(term, s); + }; */ + + const results = []; + let m; + for (let id in candidates) { + const file = this.files[id]; + const name = `${file.get_age()}d: ${file.get_display_name()} ${file.get_uri_display().replace(`/${file.get_display_name()}`, '')}`; + if (opt.SEARCH_FUZZY) + m = _Util.fuzzyMatch(term, name); + else + m = _Util.strictMatch(term, name); + + if (m !== -1) + results.push({ weight: m, id }); + } + + results.sort((a, b) => this.files[a.id].get_visited() < this.files[b.id].get_visited()); + + this.resultIds = results.map(item => item.id); + return this.resultIds; + } + + getResultMetas(resultIds, callback = null) { + const metas = resultIds.map(id => this.getResultMeta(id)); + if (shellVersion >= 43) + return new Promise(resolve => resolve(metas)); + else if (callback) + callback(metas); + return null; + } + + getResultMeta(resultId) { + const result = this.files[resultId]; + return { + 'id': resultId, + 'name': `${result.get_age()}: ${result.get_display_name()}`, + 'description': `${result.get_uri_display().replace(`/${result.get_display_name()}`, '')}`, + 'createIcon': size => { + let icon = this.getIcon(result, size); + return icon; + }, + }; + } + + getIcon(result, size) { + let file = Gio.File.new_for_uri(result.get_uri()); + let info = file.query_info(Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH, + Gio.FileQueryInfoFlags.NONE, null); + let path = info.get_attribute_byte_string( + Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH); + + let icon, gicon; + + if (path) { + gicon = Gio.FileIcon.new(Gio.File.new_for_path(path)); + } else { + const appInfo = Gio.AppInfo.get_default_for_type(result.get_mime_type(), false); + 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; + } + + launchSearch(/* terms, timeStamp */) { + this._openNautilus('recent:///'); + } + + _openNautilus(uri) { + try { + GLib.spawn_command_line_async(`nautilus -ws ${uri}`); + } catch (e) { + log(e); + } + } + + activateResult(resultId /* , terms, timeStamp */) { + const file = this.files[resultId]; + + if (_Util.isShiftPressed()) { + Main.overview.toggle(); + this._openNautilus(file.get_uri()); + } else { + const appInfo = Gio.AppInfo.get_default_for_type(file.get_mime_type(), false); + if (!(appInfo && appInfo.launch_uris([file.get_uri()], null))) + this._openNautilus(file.get_uri()); + } + } + + getInitialResultSet(terms, callback /* , cancellable = null*/) { + // In GS 43 callback arg has been removed + /* if (shellVersion >= 43) + cancellable = callback; */ + + const filesDict = {}; + const files = Gtk.RecentManager.get_default().get_items().filter(f => f.exists()); + + for (let file of files) + filesDict[file.get_uri()] = file; + + + this.files = filesDict; + + if (shellVersion >= 43) + return new Promise(resolve => resolve(this._getResultSet(terms))); + else + callback(this._getResultSet(terms)); + + return null; + } + + filterResults(results, maxResults) { + return results.slice(0, 20); + // return results.slice(0, maxResults); + } + + getSubsearchResultSet(previousResults, terms, callback /* , cancellable*/) { + // if we return previous results, quick typers get non-actual results + callback(this._getResultSet(terms)); + } + + /* createResultObject(resultMeta) { + return this.files[resultMeta.id]; + }*/ +}; diff --git a/extensions/vertical-workspaces/lib/search.js b/extensions/vertical-workspaces/lib/search.js new file mode 100644 index 0000000..8540626 --- /dev/null +++ b/extensions/vertical-workspaces/lib/search.js @@ -0,0 +1,206 @@ +/** + * V-Shell (Vertical Workspaces) + * search.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; +const { Shell, Gio, St, Clutter } = imports.gi; +const Main = imports.ui.main; + +const AppDisplay = imports.ui.appDisplay; +const Search = imports.ui.search; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +const _ = Me.imports.lib.settings._; +const shellVersion = _Util.shellVersion; + +let opt; +let _overrides; +let _firstRun = true; + +let SEARCH_MAX_WIDTH; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('searchModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + _updateSearchViewWidth(reset); + + if (reset) { + Main.overview._overview._controls.layoutManager._searchController.y_align = Clutter.ActorAlign.FILL; + opt = null; + _overrides = null; + return; + } + + _overrides = new _Util.Overrides(); + + _overrides.addOverride('AppSearchProvider', AppDisplay.AppSearchProvider.prototype, AppSearchProvider); + _overrides.addOverride('SearchResult', Search.SearchResult.prototype, SearchResult); + _overrides.addOverride('SearchResultsView', Search.SearchResultsView.prototype, SearchResultsView); + + // 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._overview._controls.layoutManager._searchController.y_align = Clutter.ActorAlign.START; +} + +function _updateSearchViewWidth(reset = false) { + const searchContent = Main.overview._overview._controls.layoutManager._searchController._searchResults._content; + if (!SEARCH_MAX_WIDTH) { // just store original value; + const themeNode = searchContent.get_theme_node(); + const width = themeNode.get_max_width(); + SEARCH_MAX_WIDTH = width; + } + + if (reset) { + searchContent.set_style(''); + } else { + let width = Math.round(SEARCH_MAX_WIDTH * opt.SEARCH_VIEW_SCALE); + searchContent.set_style(`max-width: ${width}px;`); + } +} + +// AppDisplay.AppSearchProvider +const AppSearchProvider = { + getInitialResultSet(terms, callback, _cancellable) { + // Defer until the parental controls manager is initialized, so the + // results can be filtered correctly. + if (!this._parentalControlsManager.initialized) { + let initializedId = this._parentalControlsManager.connect('app-filter-changed', () => { + if (this._parentalControlsManager.initialized) { + this._parentalControlsManager.disconnect(initializedId); + this.getInitialResultSet(terms, callback, _cancellable); + } + }); + return; + } + + + 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 dispName = appInfo.get_display_name() || ''; + let gName = appInfo.get_generic_name() || ''; + let description = appInfo.get_description() || ''; + let categories = appInfo.get_string('Categories') || ''; + let keywords = appInfo.get_string('Keywords') || ''; + name = dispName; + string = `${dispName} ${gName} ${description} ${categories} ${keywords}`; + } + } + + let m = -1; + if (shouldShow && opt.SEARCH_FUZZY) { + m = _Util.fuzzyMatch(pattern, name); + m = (m + _Util.strictMatch(pattern, string)) / 2; + } else if (shouldShow) { + m = _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) => _Util.isMoreRelevant(a.get_display_name(), b.get_display_name(), pattern)); + + let results = appInfoList.map(app => app.get_id()); + + results = results.concat(this._systemActions.getMatchingActions(terms)); + + if (shellVersion < 43) + callback(results); + else + return new Promise(resolve => resolve(results)); + }, + + // App search result size + createResultObject(resultMeta) { + if (resultMeta.id.endsWith('.desktop')) { + const icon = new AppDisplay.AppIcon(this._appSys.lookup_app(resultMeta['id']), { + expandTitleOnHover: false, + }); + icon.icon.setIconSize(opt.SEARCH_ICON_SIZE); + return icon; + } else { + const icon = new AppDisplay.SystemActionIcon(this, resultMeta); + icon.icon._setSizeManually = true; + icon.icon.setIconSize(opt.SEARCH_ICON_SIZE); + return icon; + } + }, +}; + +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 (!_Util.isShiftPressed()) + Main.overview.toggle(); + }, +}; + +const SearchResultsView = { + _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.')); + } + }, +}; diff --git a/extensions/vertical-workspaces/lib/settings.js b/extensions/vertical-workspaces/lib/settings.js new file mode 100644 index 0000000..66f3a45 --- /dev/null +++ b/extensions/vertical-workspaces/lib/settings.js @@ -0,0 +1,469 @@ +/** + * V-Shell (Vertical Workspaces) + * settings.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + */ + +'use strict'; + +const { GLib } = imports.gi; + +const Config = imports.misc.config; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +var shellVersion = parseFloat(Config.PACKAGE_VERSION); + +const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); +var _ = Gettext.gettext; +const _schema = Me.metadata['settings-schema']; + +// common instance of Options accessible from all modules +var opt; + +var Options = class Options { + constructor() { + this._gsettings = ExtensionUtils.getSettings(_schema); + 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._updateCachedSettings(); + 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: ['bool', 'secondary-ws-preview-shift'], + wsThumbnailsFull: ['bool', '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'], + dashShowWindowsIcon: ['int', 'dash-show-windows-icon'], + dashShowRecentFilesIcon: ['int', 'dash-show-recent-files-icon'], + 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'], + dashBgRadius: ['int', 'dash-bg-radius'], + 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'], + 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'], + fixUbuntuDock: ['boolean', 'fix-ubuntu-dock'], + winPreviewIconSize: ['int', 'win-preview-icon-size'], + alwaysShowWinTitles: ['boolean', 'always-show-win-titles'], + startupState: ['int', 'startup-state'], + overviewMode: ['int', 'overview-mode'], + workspaceSwitcherAnimation: ['int', 'workspace-switcher-animation'], + 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'], + appGridNamesMode: ['int', 'app-grid-names'], + appGridActivePreview: ['boolean', 'app-grid-active-preview'], + appGridFolderCenter: ['boolean', 'app-grid-folder-center'], + appGridPageWidthScale: ['int', 'app-grid-page-width-scale'], + appGridSpacing: ['int', 'app-grid-spacing'], + searchWindowsEnable: ['boolean', 'search-windows-enable'], + searchRecentFilesEnable: ['boolean', 'search-recent-files-enable'], + searchFuzzy: ['boolean', 'search-fuzzy'], + searchMaxResultsRows: ['int', 'search-max-results-rows'], + dashShowWindowsBeforeActivation: ['int', 'dash-show-windows-before-activation'], + dashIconScroll: ['int', 'dash-icon-scroll'], + 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'], + 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'], + windowIconClickSearch: ['boolean', 'window-icon-click-search'], + overlayKeySecondary: ['int', 'overlay-key-secondary'], + + workspaceThumbnailsModule: ['boolean', 'workspace-thumbnails-module'], + 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'], + winAttentionHandlerModule: ['boolean', 'win-attention-handler-module'], + swipeTrackerModule: ['boolean', 'swipe-tracker-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.shellVersion = shellVersion; + // this.storeProfile(0); + } + + 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; + } + } + + _updateCachedSettings() { + Object.keys(this.options).forEach(v => this.get(v, true)); + } + + get(option, updateCache = false) { + if (!this.options[option]) { + log(`[${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 => { + 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(); + this._gsettings.set_boolean('aaa-loading-profile', !this._gsettings.get_boolean('aaa-loading-profile')); + for (let o of Object.keys(options)) { + 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.DASH_POSITION = this.get('dashPosition', true); + 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_CLICK_ACTION = this.get('dashShowWindowsBeforeActivation', true); + this.DASH_ICON_SCROLL = this.get('dashIconScroll', true); + this.DASH_SHIFT_CLICK_MV = true; + + this.SEARCH_WINDOWS_ICON_SCROLL = this.get('searchWindowsIconScroll', true); + + this.DASH_POSITION_ADJUSTMENT = this.get('dashPositionAdjust', true); + this.DASH_POSITION_ADJUSTMENT = this.DASH_POSITION_ADJUSTMENT * -1 / 100; // range 1 to -1 + this.CENTER_DASH_WS = this.get('centerDashToWs', true); + + this.MAX_ICON_SIZE = 64; // updates from main module + this.SHOW_WINDOWS_ICON = this.get('dashShowWindowsIcon', true); + this.SHOW_RECENT_FILES_ICON = this.get('dashShowRecentFilesIcon', true); + + this.WS_TMB_POSITION = this.get('workspaceThumbnailsPosition', true); + this.ORIENTATION = this.WS_TMB_POSITION > 4 ? 0 : 1; + this.WORKSPACE_MAX_SPACING = this.get('wsMaxSpacing', true); + // 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', true); + // 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', true) * -1 / 100; // range 1 to -1 + this.SEC_WS_TMB_POSITION = this.get('secWsThumbnailsPosition', true); + 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', true) * -1 / 100; // range 1 to -1 + this.SEC_WS_PREVIEW_SHIFT = this.get('secWsPreviewShift', true); + this.SHOW_WST_LABELS = this.get('showWsTmbLabels', true); + this.SHOW_WST_LABELS_ON_HOVER = this.get('showWsTmbLabelsOnHover', true); + this.CLOSE_WS_BUTTON_MODE = this.get('closeWsButtonMode', true); + + this.MAX_THUMBNAIL_SCALE = this.get('wsThumbnailScale', true) / 100; + this.MAX_THUMBNAIL_SCALE_APPGRID = this.get('wsThumbnailScaleAppGrid', true) / 100; + if (this.MAX_THUMBNAIL_SCALE_APPGRID === 0) + this.MAX_THUMBNAIL_SCALE_APPGRID = this.MAX_THUMBNAIL_SCALE; + this.MAX_THUMBNAIL_SCALE_STABLE = this.MAX_THUMBNAIL_SCALE === this.MAX_THUMBNAIL_SCALE_APPGRID; + this.SEC_MAX_THUMBNAIL_SCALE = this.get('secWsThumbnailScale', true) / 100; + + this.WS_PREVIEW_SCALE = this.get('wsPreviewScale', true) / 100; + this.SEC_WS_PREVIEW_SCALE = this.get('secWsPreviewScale', true) / 100; + // calculate number of possibly visible neighbor previews according to ws scale + this.NUMBER_OF_VISIBLE_NEIGHBORS = Math.round(1 + (1 - this.WS_PREVIEW_SCALE) / 4); + + this.SHOW_WS_TMB_BG = this.get('showWsSwitcherBg', true) && this.SHOW_WS_TMB; + this.WS_PREVIEW_BG_RADIUS = this.get('wsPreviewBgRadius', true); + this.SHOW_WS_PREVIEW_BG = this.get('showWsPreviewBg', true); + + this.CENTER_APP_GRID = this.get('centerAppGrid', true); + + this.SHOW_SEARCH_ENTRY = this.get('showSearchEntry', true); + this.CENTER_SEARCH_VIEW = this.get('centerSearch', true); + this.APP_GRID_ANIMATION = this.get('appGridAnimation', true); + if (this.APP_GRID_ANIMATION === 4) + this.APP_GRID_ANIMATION = this._getAnimationDirection(); + + this.SEARCH_VIEW_ANIMATION = this.get('searchViewAnimation', true); + if (this.SEARCH_VIEW_ANIMATION === 4) + this.SEARCH_VIEW_ANIMATION = 3; + + this.WS_ANIMATION = this.get('workspaceAnimation', true); + + this.WIN_PREVIEW_ICON_SIZE = [64, 48, 32, 22, 8][this.get('winPreviewIconSize', true)]; + this.ALWAYS_SHOW_WIN_TITLES = this.get('alwaysShowWinTitles', true); + + this.STARTUP_STATE = this.get('startupState', true); + this.SHOW_BG_IN_OVERVIEW = this.get('showBgInOverview', true); + this.OVERVIEW_BG_BRIGHTNESS = this.get('overviewBgBrightness', true) / 100; + this.OVERVIEW_BG_BLUR_SIGMA = this.get('overviewBgBlurSigma', true); + this.APP_GRID_BG_BLUR_SIGMA = this.get('appGridBgBlurSigma', true); + this.SMOOTH_BLUR_TRANSITIONS = this.get('smoothBlurTransitions', true); + + this.OVERVIEW_MODE = this.get('overviewMode', true); + this.OVERVIEW_MODE2 = this.OVERVIEW_MODE === 2; + this.WORKSPACE_MODE = this.OVERVIEW_MODE ? 0 : 1; + + this.STATIC_WS_SWITCHER_BG = this.get('workspaceSwitcherAnimation', true); + + this.ANIMATION_TIME_FACTOR = this.get('animationSpeedFactor', true) / 100; + + this.SEARCH_ICON_SIZE = this.get('searchIconSize', true); + this.SEARCH_VIEW_SCALE = this.get('searchViewScale', true) / 100; + this.SEARCH_MAX_ROWS = this.get('searchMaxResultsRows', true); + this.SEARCH_FUZZY = this.get('searchFuzzy', true); + + this.APP_GRID_ALLOW_INCOMPLETE_PAGES = this.get('appGridIncompletePages', true); + this.APP_GRID_ICON_SIZE = this.get('appGridIconSize', true); + this.APP_GRID_COLUMNS = this.get('appGridColumns', true); + this.APP_GRID_ROWS = this.get('appGridRows', true); + this.APP_GRID_ADAPTIVE = !this.APP_GRID_COLUMNS && !this.APP_GRID_ROWS; + this.APP_GRID_ORDER = this.get('appGridOrder', true); + + this.APP_GRID_INCLUDE_DASH = this.get('appGridContent', true); + /* 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', true); + + this.APP_GRID_FOLDER_ICON_SIZE = this.get('appGridFolderIconSize', true); + this.APP_GRID_FOLDER_ICON_GRID = this.get('appGridFolderIconGrid', true); + this.APP_GRID_FOLDER_COLUMNS = this.get('appGridFolderColumns', true); + this.APP_GRID_FOLDER_ROWS = this.get('appGridFolderRows', true); + this.APP_GRID_SPACING = this.get('appGridSpacing', true); + this.APP_GRID_FOLDER_DEFAULT = this.APP_GRID_FOLDER_ROWS === 3 && this.APP_GRID_FOLDER_COLUMNS === 3; + this.APP_GRID_ACTIVE_PREVIEW = this.get('appGridActivePreview', true); + this.APP_GRID_FOLDER_CENTER = this.get('appGridFolderCenter', true); + this.APP_GRID_PAGE_WIDTH_SCALE = this.get('appGridPageWidthScale', true) / 100; + + this.APP_GRID_ICON_SIZE_DEFAULT = this.APP_GRID_ACTIVE_PREVIEW && !this.APP_GRID_ORDER ? 176 : 96; + this.APP_GRID_FOLDER_ICON_SIZE_DEFAULT = 96; + + this.WINDOW_SEARCH_PROVIDER_ENABLED = this.get('searchWindowsEnable', true); + this.RECENT_FILES_SEARCH_PROVIDER_ENABLED = this.get('searchRecentFilesEnable', true); + + this.PANEL_POSITION_TOP = this.get('panelPosition', true) === 0; + this.PANEL_MODE = this.get('panelVisibility', true); + this.PANEL_DISABLED = this.PANEL_MODE === 2; + this.PANEL_OVERVIEW_ONLY = this.PANEL_MODE === 1; + this.START_Y_OFFSET = 0; // set from main module + this.FIX_UBUNTU_DOCK = this.get('fixUbuntuDock', true); + + this.WINDOW_ATTENTION_MODE = this.get('windowAttentionMode', true); + 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', true) / 100; + this.WS_SW_POPUP_V_POSITION = this.get('wsSwPopupVPosition', true) / 100; + this.WS_SW_POPUP_MODE = this.get('wsSwPopupMode', true); + + this.SHOW_FAV_NOTIFICATION = this.get('favoritesNotify', true); + this.NOTIFICATION_POSITION = this.get('notificationPosition', true); + + this.OSD_POSITION = this.get('osdPosition', true); + + this.HOT_CORNER_ACTION = this.get('hotCornerAction', true); + this.HOT_CORNER_POSITION = this.get('hotCornerPosition', true); + 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', true); + this.HOT_CORNER_RIPPLES = this.get('hotCornerRipples', true); + + this.ALWAYS_ACTIVATE_SELECTED_WINDOW = this.get('alwaysActivateSelectedWindow', true); + this.WINDOW_ICON_CLICK_SEARCH = this.get('windowIconClickSearch', true); + + this.OVERLAY_KEY_SECONDARY = this.get('overlayKeySecondary', true); + } + + _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/vertical-workspaces/lib/swipeTracker.js b/extensions/vertical-workspaces/lib/swipeTracker.js new file mode 100644 index 0000000..d9c3407 --- /dev/null +++ b/extensions/vertical-workspaces/lib/swipeTracker.js @@ -0,0 +1,87 @@ +/** + * V-Shell (Vertical Workspaces) + * swipeTracker.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Clutter, GObject } = imports.gi; +const Main = imports.ui.main; +const SwipeTracker = imports.ui.swipeTracker; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +let opt; +let _firstRun = true; + +let _vwGestureUpdateId; +let _originalGestureUpdateId; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('swipeTrackerModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (reset || !opt.ORIENTATION) { // 1-VERTICAL, 0-HORIZONTAL + // 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 (_vwGestureUpdateId) { + Main.overview._swipeTracker._touchpadGesture.disconnect(_vwGestureUpdateId); + _vwGestureUpdateId = 0; + } + if (_originalGestureUpdateId) { + Main.overview._swipeTracker._touchpadGesture.unblock_signal_handler(_originalGestureUpdateId); + _originalGestureUpdateId = 0; + } + + opt = null; + return; + } + + if (opt.ORIENTATION) { // 1-VERTICAL, 0-HORIZONTAL + // 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 (!_originalGestureUpdateId) { + _originalGestureUpdateId = GObject.signal_handler_find(Main.overview._swipeTracker._touchpadGesture, { signalId: 'update' }); + Main.overview._swipeTracker._touchpadGesture.block_signal_handler(_originalGestureUpdateId); + Main.overview._swipeTracker._updateGesture = SwipeTrackerVertical._updateGesture; + _vwGestureUpdateId = Main.overview._swipeTracker._touchpadGesture.connect('update', SwipeTrackerVertical._updateGesture.bind(Main.overview._swipeTracker)); + } + } +} + +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/vertical-workspaces/lib/util.js b/extensions/vertical-workspaces/lib/util.js new file mode 100644 index 0000000..5f5c069 --- /dev/null +++ b/extensions/vertical-workspaces/lib/util.js @@ -0,0 +1,364 @@ +/** + * V-Shell (Vertical Workspaces) + * util.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const Gi = imports._gi; +const { Shell, Meta, Clutter } = imports.gi; + +const Config = imports.misc.config; +const Main = imports.ui.main; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +var shellVersion = parseFloat(Config.PACKAGE_VERSION); + +var Overrides = class { + constructor() { + this._overrides = {}; + } + + addOverride(name, prototype, overrideList) { + this._overrides[name] = { + originals: this.overrideProto(prototype, overrideList), + prototype, + }; + } + + removeOverride(name) { + const override = this._overrides[name]; + if (!override) + return false; + + this.overrideProto(override.prototype, override.originals); + this._overrides[name] = undefined; + return true; + } + + removeAll() { + for (let name in this._overrides) { + this.removeOverride(name); + this._overrides[name] = undefined; + } + } + + hookVfunc(proto, symbol, func) { + proto[Gi.hook_up_vfunc_symbol](symbol, func); + } + + overrideProto(proto, overrides) { + const backup = {}; + + for (let symbol in overrides) { + if (symbol.startsWith('after_')) { + const actualSymbol = symbol.slice('after_'.length); + const 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 { + backup[symbol] = proto[symbol]; + if (symbol.startsWith('vfunc')) { + if (shellVersion < 42) + this.hookVfunc(proto, symbol.slice(6), overrides[symbol]); + else + this.hookVfunc(proto[Gi.gobject_prototype_symbol], symbol.slice(6), overrides[symbol]); + } else { + proto[symbol] = overrides[symbol]; + } + } + } + return backup; + } +}; + +function getOverviewTranslations(opt, dash, tmbBox, searchEntryBin) { + // const tmbBox = Main.overview._overview._controls._thumbnailsBox; + 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) { + switch (opt.WS_TMB_POSITION) { + case 3: // left + offset = 10 + (dash?.visible && opt.DASH_LEFT ? dash.width : 0); + tmbTranslationX = -tmbBox.width - offset; + tmbTranslationY = 0; + break; + case 1: // right + offset = 10 + (dash?.visible && opt.DASH_RIGHT ? dash.width : 0); + tmbTranslationX = tmbBox.width + offset; + tmbTranslationY = 0; + break; + case 0: // top + offset = 10 + (dash?.visible && opt.DASH_TOP ? dash.height : 0) + Main.panel.height; + tmbTranslationX = 0; + tmbTranslationY = -tmbBox.height - 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 = tmbBox.height + offset; + break; + } + } + + let dashTranslationX = 0; + let dashTranslationY = 0; + let position = opt.DASH_POSITION; + // if DtD replaced the original Dash, read its position + if (dashIsDashToDock()) + position = dash._position; + + if (dash?.visible) { + switch (position) { + case 0: // top + dashTranslationX = 0; + dashTranslationY = -dash.height - dash.margin_bottom - Main.panel.height; + break; + case 1: // right + dashTranslationX = dash.width; + dashTranslationY = 0; + break; + case 2: // bottom + dashTranslationX = 0; + dashTranslationY = dash.height + dash.margin_bottom + Main.panel.height; + break; + case 3: // left + dashTranslationX = -dash.width; + dashTranslationY = 0; + break; + } + } + + return [tmbTranslationX, tmbTranslationY, dashTranslationX, dashTranslationY, searchTranslationY]; +} + +function openPreferences() { + const windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null); + let tracker = Shell.WindowTracker.get_default(); + let metaWin, isVW = null; + + for (let win of windows) { + const app = tracker.get_window_app(win); + if (win.get_title().includes(Me.metadata.name) && app.get_name() === 'Extensions') { + // this is our existing window + metaWin = win; + isVW = true; + break; + } else if (win.wm_class.includes('org.gnome.Shell.Extensions')) { + // this is prefs window of another extension + metaWin = win; + isVW = false; + } + } + + if (metaWin && !isVW) { + // other prefs window blocks opening another prefs window, so close it + metaWin.delete(global.get_current_time()); + } else if (metaWin && isVW) { + // 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 && !isVW)) { + try { + Main.extensionManager.openExtensionPrefs(Me.metadata.uuid, '', {}); + } catch (e) { + log(e); + } + } +} + +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(''); + } +} + +function dashNotDefault() { + return Main.overview.dash !== Main.overview._overview._controls.layoutManager._dash; +} + +function dashIsDashToDock() { + return Main.overview.dash._isHorizontal !== undefined; +} + +// Reorder Workspaces - callback for Dash and workspacesDisplay +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); +} + +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); + }); + } + }, + }); + } + } +} + +function isShiftPressed(state = null) { + if (state === null) + [,, state] = global.get_pointer(); + return (state & Clutter.ModifierType.SHIFT_MASK) !== 0; +} + +function isCtrlPressed(state = null) { + if (state === null) + [,, state] = global.get_pointer(); + return (state & Clutter.ModifierType.CONTROL_MASK) !== 0; +} + +function isAltPressed(state = null) { + if (state === null) + [,, state] = global.get_pointer(); + return (state & Clutter.ModifierType.MOD1_MASK) !== 0; +} + +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]; +} + +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; +} + +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; +} + +function getEnabledExtensions(uuid = '') { + let extensions = []; + Main.extensionManager._extensions.forEach(e => { + if (e.state === 1 && e.uuid.includes(uuid)) + extensions.push(e); + }); + return extensions; +} + +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; +} diff --git a/extensions/vertical-workspaces/lib/windowAttentionHandler.js b/extensions/vertical-workspaces/lib/windowAttentionHandler.js new file mode 100644 index 0000000..10703c2 --- /dev/null +++ b/extensions/vertical-workspaces/lib/windowAttentionHandler.js @@ -0,0 +1,90 @@ +/** + * V-Shell (Vertical Workspaces) + * windowAttentionHandler.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const Main = imports.ui.main; +const WindowAttentionHandler = imports.ui.windowAttentionHandler; +const MessageTray = imports.ui.messageTray; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const _Util = Me.imports.lib.util; + +let opt; +let _firstRun = false; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('winAttentionHandlerModule', true); + reset = reset || !moduleEnabled; + + if (_firstRun && reset) + return; + + _firstRun = false; + if (reset) { + reset = true; + _updateConnections(reset); + opt = null; + return; + } + + _updateConnections(); +} + +function _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); + const source = new WindowAttentionHandler.WindowAttentionSource(app, window); + Main.messageTray.add(source); + + let [title, banner] = this._getTitleAndBanner(app, window); + + const notification = new MessageTray.Notification(source, title, banner); + notification.connect('activated', () => { + source.open(); + }); + notification.setForFeedback(true); + + 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, banner] = this._getTitleAndBanner(app, window); + notification.update(title, banner); + }, source); + }, +}; diff --git a/extensions/vertical-workspaces/lib/windowManager.js b/extensions/vertical-workspaces/lib/windowManager.js new file mode 100644 index 0000000..2d46b0b --- /dev/null +++ b/extensions/vertical-workspaces/lib/windowManager.js @@ -0,0 +1,217 @@ +/** + * V-Shell (Vertical Workspaces) + * windowManager.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { GObject, Clutter, Meta } = imports.gi; + +const Main = imports.ui.main; +const WindowManager = imports.ui.windowManager; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; +let _overrides; + +const MINIMIZE_WINDOW_ANIMATION_TIME = WindowManager.MINIMIZE_WINDOW_ANIMATION_TIME; +const MINIMIZE_WINDOW_ANIMATION_MODE = WindowManager.MINIMIZE_WINDOW_ANIMATION_MODE; + +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('windowManagerModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + + _replaceMinimizeFunction(reset); + + + if (reset) { + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + + _overrides.addOverride('WindowManager', WindowManager.WindowManager.prototype, WindowManagerCommon); +} + +// ------------- Fix and adapt minimize/unminimize animations -------------------------------------- + +let _originalMinimizeSigId; +let _minimizeSigId; +let _originalUnminimizeSigId; +let _unminimizeSigId; + +function _replaceMinimizeFunction(reset = false) { + if (reset) { + Main.wm._shellwm.disconnect(_minimizeSigId); + _minimizeSigId = 0; + Main.wm._shellwm.unblock_signal_handler(_originalMinimizeSigId); + _originalMinimizeSigId = 0; + + Main.wm._shellwm.disconnect(_unminimizeSigId); + _unminimizeSigId = 0; + Main.wm._shellwm.unblock_signal_handler(_originalUnminimizeSigId); + _originalUnminimizeSigId = 0; + } else if (!_minimizeSigId) { + _originalMinimizeSigId = GObject.signal_handler_find(Main.wm._shellwm, { signalId: 'minimize' }); + if (_originalMinimizeSigId) { + Main.wm._shellwm.block_signal_handler(_originalMinimizeSigId); + _minimizeSigId = Main.wm._shellwm.connect('minimize', WindowManagerCommon._minimizeWindow.bind(Main.wm)); + } + + _originalUnminimizeSigId = GObject.signal_handler_find(Main.wm._shellwm, { signalId: 'unminimize' }); + if (_originalUnminimizeSigId) { + Main.wm._shellwm.block_signal_handler(_originalUnminimizeSigId); + _unminimizeSigId = Main.wm._shellwm.connect('unminimize', WindowManagerCommon._unminimizeWindow.bind(Main.wm)); + } + } +} + +// 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 +const WindowManagerCommon = { + _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), + }); + // } + }, +}; diff --git a/extensions/vertical-workspaces/lib/windowPreview.js b/extensions/vertical-workspaces/lib/windowPreview.js new file mode 100644 index 0000000..5d2bd61 --- /dev/null +++ b/extensions/vertical-workspaces/lib/windowPreview.js @@ -0,0 +1,379 @@ +/** + * V-Shell (Vertical Workspaces) + * windowPreview.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { Clutter, GLib, GObject, Graphene, Meta, Shell, St } = imports.gi; + +const Main = imports.ui.main; +const WindowPreview = imports.ui.windowPreview; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const _Util = Me.imports.lib.util; +const shellVersion = _Util.shellVersion; + +let _overrides; + +const WINDOW_SCALE_TIME = imports.ui.windowPreview.WINDOW_SCALE_TIME; +const WINDOW_ACTIVE_SIZE_INC = imports.ui.windowPreview.WINDOW_ACTIVE_SIZE_INC; +const WINDOW_OVERLAY_FADE_TIME = imports.ui.windowPreview.WINDOW_OVERLAY_FADE_TIME; +const SEARCH_WINDOWS_PREFIX = Me.imports.lib.windowSearchProvider.prefix; + +const ControlsState = imports.ui.overviewControls.ControlsState; + +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('windowPreviewModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + + if (reset) { + _overrides = null; + opt = null; + WindowPreview.WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750; + return; + } + + _overrides = new _Util.Overrides(); + + _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; +} + +const WindowPreviewCommon = { + // injection to _init() + after__init() { + const ICON_OVERLAP = 0.7; + + if (opt.WIN_PREVIEW_ICON_SIZE < 64) { + this.remove_child(this._icon); + this._icon.destroy(); + const tracker = Shell.WindowTracker.get_default(); + const app = tracker.get_window_app(this.metaWindow); + this._icon = app.create_icon_texture(opt.WIN_PREVIEW_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: this.windowContainer, + coordinate: Clutter.BindCoordinate.POSITION, + })); + this._icon.add_constraint(new Clutter.AlignConstraint({ + source: this.windowContainer, + align_axis: Clutter.AlignAxis.X_AXIS, + factor: 0.5, + })); + this._icon.add_constraint(new Clutter.AlignConstraint({ + source: this.windowContainer, + align_axis: Clutter.AlignAxis.Y_AXIS, + pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }), + factor: 1, + })); + this.add_child(this._icon); + if (opt.WIN_PREVIEW_ICON_SIZE < 22) { + // disable app icon + this._icon.hide(); + } + this._iconSize = opt.WIN_PREVIEW_ICON_SIZE; + } + + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + const iconOverlap = opt.WIN_PREVIEW_ICON_SIZE * ICON_OVERLAP; + // we cannot get proper title height before it gets to the stage, so 35 is estimated height + spacing + this._title.get_constraints()[1].offset = scaleFactor * (-iconOverlap - 35); + this.set_child_above_sibling(this._title, null); + // 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; + + const adjustment = this._workspace._background._stateAdjustment; + opt.WORKSPACE_MODE = 1; + _Util.exposeWindows(adjustment, false); + 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)); + } + + // replace click action with custom one + const action = this.get_actions()[0]; + + const handlerId = GObject.signal_handler_find(action, { signalId: 'clicked' }); + if (handlerId) + action.disconnect(handlerId); + + action.connect('clicked', act => { + const button = act.get_button(); + if (button === Clutter.BUTTON_PRIMARY) { + this._activate(); + return Clutter.EVENT_STOP; + } else if (button === Clutter.BUTTON_SECONDARY) { + // 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 + if (this._longPressLater) { + if (shellVersion >= 44) { + const laters = global.compositor.get_laters(); + laters.remove(this._longPressLater); + } else { + Meta.later_remove(this._longPressLater); + delete this._longPressLater; + } + } + const tracker = Shell.WindowTracker.get_default(); + const appName = tracker.get_window_app(this.metaWindow).get_name(); + _Util.activateSearchProvider(`${SEARCH_WINDOWS_PREFIX} ${appName}`); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + + if (opt.WINDOW_ICON_CLICK_SEARCH) { + const iconClickAction = new Clutter.ClickAction(); + iconClickAction.connect('clicked', act => { + if (act.get_button() === Clutter.BUTTON_PRIMARY) { + const tracker = Shell.WindowTracker.get_default(); + const appName = tracker.get_window_app(this.metaWindow).get_name(); + _Util.activateSearchProvider(`${SEARCH_WINDOWS_PREFIX} ${appName}`); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this._icon.add_action(iconClickAction); + } + }, + + _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 && 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 = opt.WORKSPACE_MODE; + else if (finalState === 1 || (finalState === 0 && !opt.WORKSPACE_MODE)) + return; + } + + if (scale === 1) { + this._icon.ease({ + duration: 50, + scale_x: scale, + scale_y: scale, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + this._title.ease({ + duration: 100, + opacity: 255, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else if (this._icon.scale_x !== 0) { + this._icon.set({ + scale_x: 0, + scale_y: 0, + }); + this._title.opacity = 0; + } + + // 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 + }, + + showOverlay(animate) { + if (!this._overlayEnabled) + return; + + if (this._overlayShown) + return; + + this._overlayShown = true; + if (!opt.ALWAYS_ACTIVATE_SELECTED_WINDOW) + 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() + ? [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.get_parent()?.set_child_above_sibling(this, null); + this._activateSelected = true; + } + + if (!opt.ALWAYS_ACTIVATE_SELECTED_WINDOW) + 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, + }); + } + }, + + _onDestroy() { + // workaround for upstream bug - hideOverlay is called after windowPreview is destroyed, from the leave event callback + // hiding the preview now avoids firing the post-mortem leave event + this.hide(); + if (this._activateSelected) + this._activate(); + + this.metaWindow._delegate = null; + this._delegate = null; + + if (this._longPressLater) { + if (shellVersion >= 44) { + const laters = global.compositor.get_laters(); + laters.remove(this._longPressLater); + delete this._longPressLater; + } else { + Meta.later_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/vertical-workspaces/lib/windowSearchProvider.js b/extensions/vertical-workspaces/lib/windowSearchProvider.js new file mode 100644 index 0000000..5f90784 --- /dev/null +++ b/extensions/vertical-workspaces/lib/windowSearchProvider.js @@ -0,0 +1,305 @@ +/** + * V-Shell (Vertical Workspaces) + * windowSearchProvider.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 -2023 + * @license GPL-3.0 + */ + +'use strict'; + +const { GLib, Gio, Meta, St, Shell } = imports.gi; + +const Main = imports.ui.main; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Settings = Me.imports.lib.settings; +const _Util = Me.imports.lib.util; + +// gettext +const _ = Settings._; + +const shellVersion = Settings.shellVersion; + +const ModifierType = imports.gi.Clutter.ModifierType; + +let windowSearchProvider; +let _enableTimeoutId = 0; + +// prefix helps to eliminate results from other search providers +// so it needs to be something less common +// needs to be accessible from vw module +var prefix = 'wq//'; + +let opt; + +const Action = { + NONE: 0, + CLOSE: 1, + CLOSE_ALL: 2, + MOVE_TO_WS: 3, + MOVE_ALL_TO_WS: 4, +}; + +function getOverviewSearchResult() { + return Main.overview._overview.controls._searchController._searchResults; +} + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + if (!reset && opt.WINDOW_SEARCH_PROVIDER_ENABLED && !windowSearchProvider) { + enable(); + } else if (reset || !opt.WINDOW_SEARCH_PROVIDER_ENABLED) { + disable(); + opt = null; + } +} + +function enable() { + // delay because Fedora had problem to register a new provider soon after Shell restarts + _enableTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 2000, + () => { + if (!windowSearchProvider) { + windowSearchProvider = new WindowSearchProvider(opt); + getOverviewSearchResult()._registerProvider( + windowSearchProvider + ); + } + _enableTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + ); +} + +function disable() { + if (windowSearchProvider) { + getOverviewSearchResult()._unregisterProvider( + windowSearchProvider + ); + windowSearchProvider = null; + } + if (_enableTimeoutId) { + GLib.source_remove(_enableTimeoutId); + _enableTimeoutId = 0; + } +} + +function makeResult(window, i) { + const app = Shell.WindowTracker.get_default().get_window_app(window); + const appName = app ? app.get_name() : 'Unknown'; + const windowTitle = window.get_title(); + const wsIndex = window.get_workspace().index(); + + return { + 'id': i, + // convert all accented chars to their basic form and lower case for search + 'name': `${wsIndex + 1}: ${windowTitle} ${appName}`.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(), + appName, + windowTitle, + window, + }; +} + +const closeSelectedRegex = /^\/x!$/; +const closeAllResultsRegex = /^\/xa!$/; +const moveToWsRegex = /^\/m[0-9]+$/; +const moveAllToWsRegex = /^\/ma[0-9]+$/; + +const WindowSearchProvider = class WindowSearchProvider { + constructor() { + this.id = `open-windows@${Me.metadata.uuid}`; + this.appInfo = Gio.AppInfo.create_from_commandline('true', _('Open Windows'), null); + this.appInfo.get_description = () => _('List of open windows'); + this.appInfo.get_name = () => _('Open Windows'); + this.appInfo.get_id = () => this.id; + this.appInfo.get_icon = () => Gio.icon_new_for_string('focus-windows-symbolic'); + this.appInfo.should_show = () => true; + + this.canLaunchSearch = true; + this.isRemoteProvider = false; + + this.action = 0; + } + + _getResultSet(terms) { + // do not modify original terms + let termsCopy = [...terms]; + // search for terms without prefix + termsCopy[0] = termsCopy[0].replace(prefix, ''); + + /* if (gOptions.get('searchWindowsCommands')) { + this.action = 0; + this.targetWs = 0; + + const lastTerm = terms[terms.length - 1]; + if (lastTerm.match(closeSelectedRegex)) { + this.action = Action.CLOSE; + } else if (lastTerm.match(closeAllResultsRegex)) { + this.action = Action.CLOSE_ALL; + } else if (lastTerm.match(moveToWsRegex)) { + this.action = Action.MOVE_TO_WS; + } else if (lastTerm.match(moveAllToWsRegex)) { + this.action = Action.MOVE_ALL_TO_WS; + } + if (this.action) { + terms.pop(); + if (this.action === Action.MOVE_TO_WS || this.action === Action.MOVE_ALL_TO_WS) { + this.targetWs = parseInt(lastTerm.replace(/^[^0-9]+/, '')) - 1; + } + } else if (lastTerm.startsWith('/')) { + terms.pop(); + } + }*/ + + const candidates = this.windows; + const _terms = [].concat(termsCopy); + // let match; + + const term = _terms.join(' '); + /* match = s => { + return fuzzyMatch(term, s); + }; */ + + const results = []; + let m; + for (let key in candidates) { + if (opt.SEARCH_FUZZY) + m = _Util.fuzzyMatch(term, candidates[key].name); + else + m = _Util.strictMatch(term, candidates[key].name); + + if (m !== -1) + results.push({ weight: m, id: key }); + } + + results.sort((a, b) => a.weight > b.weight); + const currentWs = global.workspace_manager.get_active_workspace_index(); + // prefer current workspace + results.sort((a, b) => (this.windows[a.id].window.get_workspace().index() !== currentWs) && (this.windows[b.id].window.get_workspace().index() === currentWs)); + results.sort((a, b) => (_terms !== ' ') && (a.weight > 0 && b.weight === 0)); + + this.resultIds = results.map(item => item.id); + return this.resultIds; + } + + getResultMetas(resultIds, callback = null) { + const metas = resultIds.map(id => this.getResultMeta(id)); + if (shellVersion >= 43) + return new Promise(resolve => resolve(metas)); + else + callback(metas); + return null; + } + + getResultMeta(resultId) { + const result = this.windows[resultId]; + const wsIndex = result.window.get_workspace().index(); + const app = Shell.WindowTracker.get_default().get_window_app(result.window); + return { + 'id': resultId, + 'name': `${wsIndex + 1}: ${result.windowTitle}`, + 'description': result.appName, + 'createIcon': size => { + return app + ? app.create_icon_texture(size) + : new St.Icon({ icon_name: 'icon-missing', icon_size: size }); + }, + }; + } + + launchSearch(/* terms, timeStamp*/) { + } + + activateResult(resultId/* , terms, timeStamp*/) { + const isCtrlPressed = _Util.isCtrlPressed(); + const isShiftPressed = _Util.isShiftPressed(); + + this.action = 0; + this.targetWs = 0; + + this.targetWs = global.workspaceManager.get_active_workspace().index() + 1; + if (isShiftPressed && !isCtrlPressed) + this.action = Action.MOVE_TO_WS; + else if (isShiftPressed && isCtrlPressed) + this.action = Action.MOVE_ALL_TO_WS; + + + if (!this.action) { + const result = this.windows[resultId]; + Main.activateWindow(result.window); + return; + } + + switch (this.action) { + case Action.CLOSE: + this._closeWindows([resultId]); + break; + case Action.CLOSE_ALL: + this._closeWindows(this.resultIds); + break; + case Action.MOVE_TO_WS: + this._moveWindowsToWs(resultId, [resultId]); + break; + case Action.MOVE_ALL_TO_WS: + this._moveWindowsToWs(resultId, this.resultIds); + break; + } + } + + _closeWindows(ids) { + let time = global.get_current_time(); + for (let i = 0; i < ids.length; i++) + this.windows[ids[i]].window.delete(time + i); + + Main.notify('Window Search Provider', `Closed ${ids.length} windows.`); + } + + _moveWindowsToWs(selectedId, resultIds) { + const workspace = global.workspaceManager.get_active_workspace(); + + for (let i = 0; i < resultIds.length; i++) + this.windows[resultIds[i]].window.change_workspace(workspace); + + const selectedWin = this.windows[selectedId].window; + selectedWin.activate_with_workspace(global.get_current_time(), workspace); + } + + getInitialResultSet(terms, callback/* , cancellable = null*/) { + // In GS 43 callback arg has been removed + /* if (shellVersion >= 43) + cancellable = callback;*/ + + let windows; + this.windows = windows = {}; + global.display.get_tab_list(Meta.TabList.NORMAL, null).filter(w => w.get_workspace() !== null).map( + (v, i) => { + windows[`${i}-${v.get_id()}`] = makeResult(v, `${i}-${v.get_id()}`); + return windows[`${i}-${v.get_id()}`]; + } + ); + + if (shellVersion >= 43) + return new Promise(resolve => resolve(this._getResultSet(terms))); + else + callback(this._getResultSet(terms)); + return null; + } + + filterResults(results /* , maxResults*/) { + // return results.slice(0, maxResults); + return results; + } + + getSubsearchResultSet(previousResults, terms, callback/* , cancellable*/) { + // if we return previous results, quick typers get non-actual results + callback(this._getResultSet(terms)); + } + + /* createResultObject(resultMeta) { + const app = Shell.WindowTracker.get_default().get_window_app(resultMeta.id); + return new AppIcon(app); + }*/ +}; diff --git a/extensions/vertical-workspaces/lib/workspace.js b/extensions/vertical-workspaces/lib/workspace.js new file mode 100644 index 0000000..3b61a6d --- /dev/null +++ b/extensions/vertical-workspaces/lib/workspace.js @@ -0,0 +1,152 @@ +/** + * V-Shell (Vertical Workspaces) + * workspace.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { St, Graphene } = imports.gi; + +const Main = imports.ui.main; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const _Util = Me.imports.lib.util; + +let _overrides; +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('workspaceModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + + if (reset) { + Workspace.WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95; + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + + _overrides.addOverride('WorkspaceBackground', Workspace.WorkspaceBackground.prototype, WorkspaceBackground); + + // fix overlay base for Vertical Workspaces + _overrides.addOverride('WorkspaceLayout', Workspace.WorkspaceLayout.prototype, WorkspaceLayout); +} + + +// 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) { + 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 !== this.WINDOW_PREVIEW_MAXIMUM_SCALE) { + this.WINDOW_PREVIEW_MAXIMUM_SCALE = scale; + // when transition to ws state 1 (WINDOW_PICKER) begins, replace the constant with the original one + Workspace.WINDOW_PREVIEW_MAXIMUM_SCALE = scale; + // and force recalculation of the target layout, so the transition will be smooth + this._needsLayout = true; + } + }); + } + }, + + // this fixes wrong size and position calculation of window clones while moving overview to the next (+1) workspace if vertical ws orientation is enabled in GS + _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(); + + const oversize = Math.max(topOversize, bottomOversize, leftOversize, rightOversize); + + if (rowSpacing !== null) + rowSpacing += oversize; + if (colSpacing !== null) + colSpacing += oversize; + + if (containerBox) { + const vertical = global.workspaceManager.layout_rows === -1; + + const monitor = Main.layoutManager.monitors[this._monitorIndex]; + + const bottomPoint = new Graphene.Point3D(); + if (vertical) + bottomPoint.x = containerBox.x2; + else + bottomPoint.y = containerBox.y2; + + + const transformedBottomPoint = + this._container.apply_transform_to_point(bottomPoint); + const bottomFreeSpace = vertical + ? (monitor.x + monitor.height) - transformedBottomPoint.x + : (monitor.y + monitor.height) - transformedBottomPoint.y; + + const [, bottomOverlap] = window.overlapHeights(); + + if ((bottomOverlap + oversize) > bottomFreeSpace && !vertical) + containerBox.y2 -= (bottomOverlap + oversize) - bottomFreeSpace; + } + + return [rowSpacing, colSpacing, containerBox]; + }, +}; + +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/vertical-workspaces/lib/workspaceAnimation.js b/extensions/vertical-workspaces/lib/workspaceAnimation.js new file mode 100644 index 0000000..07008c6 --- /dev/null +++ b/extensions/vertical-workspaces/lib/workspaceAnimation.js @@ -0,0 +1,184 @@ +/** + * V-Shell (Vertical Workspaces) + * workspacesAnimation.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; +const Main = imports.ui.main; +const WorkspaceSwitcherPopup = imports.ui.workspaceSwitcherPopup; +const WorkspaceAnimation = imports.ui.workspaceAnimation; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const _Util = Me.imports.lib.util; + +// first reference to constant defined using const in other module returns undefined, the MonitorGroup const will remain empty and unused +let MonitorGroupDummy = WorkspaceAnimation.MonitorGroup; +MonitorGroupDummy = null; + +let _origBaseDistance; +let _wsAnimationSwipeBeginId; +let _wsAnimationSwipeUpdateId; +let _wsAnimationSwipeEndId; + +let _overrides; +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('workspaceAnimationModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + if (reset || !moduleEnabled) { + _connectWsAnimationSwipeTracker(true); + _overrideMonitorGroupProperty(true); + _overrides = null; + opt = null; + return; + } + + if (opt.STATIC_WS_SWITCHER_BG) { + _overrides = new _Util.Overrides(); + _overrideMonitorGroupProperty(); + _overrides.addOverride('WorkspaceAnimationMonitorGroup', WorkspaceAnimation.MonitorGroup.prototype, MonitorGroup); + } + + _connectWsAnimationSwipeTracker(); +} + +// remove spacing between workspaces during transition to remove flashing wallpaper between workspaces with maximized windows +function _overrideMonitorGroupProperty(reset = false) { + if (!_origBaseDistance) + _origBaseDistance = Object.getOwnPropertyDescriptor(WorkspaceAnimation.MonitorGroup.prototype, 'baseDistance').get; + + let getter; + if (reset) { + if (_origBaseDistance) + getter = { get: _origBaseDistance }; + } else { + getter = { + get() { + // const spacing = 100 * imports.gi.St.ThemeContext.get_for_stage(global.stage).scale_factor; + const spacing = 0; + if (global.workspace_manager.layout_rows === -1) + return this._monitor.height + spacing + (opt.PANEL_MODE ? Main.panel.height : 0); // compensation for hidden panel + else + return this._monitor.width + spacing; + }, + }; + } + + if (getter) + Object.defineProperty(WorkspaceAnimation.MonitorGroup.prototype, 'baseDistance', getter); +} + +const MonitorGroup = { + // injection to _init() + after__init() { + // 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 + const stickyGroup = this.get_children()[1]; + 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._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())) { //* && !w.is_on_all_workspaces()*/) { + // 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; + }); + }); + }, +}; + +function _connectWsAnimationSwipeTracker(reset = false) { + if (reset) { + if (_wsAnimationSwipeBeginId) { + Main.wm._workspaceAnimation._swipeTracker.disconnect(_wsAnimationSwipeBeginId); + _wsAnimationSwipeBeginId = 0; + } + if (_wsAnimationSwipeEndId) { + Main.wm._workspaceAnimation._swipeTracker.disconnect(_wsAnimationSwipeEndId); + _wsAnimationSwipeEndId = 0; + } + } else if (!_wsAnimationSwipeBeginId) { + // display ws switcher popup when gesture begins and connect progress + _wsAnimationSwipeBeginId = Main.wm._workspaceAnimation._swipeTracker.connect('begin', () => _connectWsAnimationProgress(true)); + // we want to be sure that popup with the final ws index show up when gesture ends + _wsAnimationSwipeEndId = Main.wm._workspaceAnimation._swipeTracker.connect('end', (tracker, duration, endProgress) => _connectWsAnimationProgress(false, endProgress)); + } +} + +function _connectWsAnimationProgress(connect, endProgress = null) { + if (Main.overview.visible) + return; + + if (connect && !_wsAnimationSwipeUpdateId) { + _wsAnimationSwipeUpdateId = Main.wm._workspaceAnimation._swipeTracker.connect('update', (tracker, progress) => _showWsSwitcherPopup(progress)); + } else if (!connect && _wsAnimationSwipeUpdateId) { + Main.wm._workspaceAnimation._swipeTracker.disconnect(_wsAnimationSwipeUpdateId); + _wsAnimationSwipeUpdateId = 0; + _showWsSwitcherPopup(Math.round(endProgress)); + } +} + +function _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); +} diff --git a/extensions/vertical-workspaces/lib/workspaceSwitcherPopup.js b/extensions/vertical-workspaces/lib/workspaceSwitcherPopup.js new file mode 100644 index 0000000..972f35e --- /dev/null +++ b/extensions/vertical-workspaces/lib/workspaceSwitcherPopup.js @@ -0,0 +1,90 @@ +/** + * V-Shell (Vertical Workspaces) + * workspacesSwitcherPopup.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const Main = imports.ui.main; +const WorkspaceSwitcherPopup = imports.ui.workspaceSwitcherPopup; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const _Util = Me.imports.lib.util; +let _overrides; + +let opt; +let _firstRun = true; + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + const moduleEnabled = opt.get('workspaceSwitcherPopupModule', true); + reset = reset || !moduleEnabled; + + // don't even touch this module if disabled + if (_firstRun && reset) + return; + + _firstRun = false; + + if (_overrides) + _overrides.removeAll(); + + if (reset) { + _overrides = null; + opt = null; + return; + } + + _overrides = new _Util.Overrides(); + + const enabled = global.settings.get_strv('enabled-extensions'); + const allowWsPopupInjection = !(enabled.includes('workspace-switcher-manager@G-dH.github.com') || enabled.includes('WsSwitcherPopupManager@G-dH.github.com-dev')); + if (allowWsPopupInjection) { // 1-VERTICAL, 0-HORIZONTAL + _overrides.addOverride('WorkspaceSwitcherPopup', WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype, WorkspaceSwitcherPopupOverride); + } +} + +const WorkspaceSwitcherPopupOverride = { + // injection to _init() + after__init() { + if (opt.ORIENTATION) { // 1-VERTICAL, 0-HORIZONTAL + this._list.vertical = true; + } + this._list.set_style('margin: 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 = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);*/ + workArea = global.display.get_monitor_geometry(Main.layoutManager.primaryIndex); + } else { + // workArea = Main.layoutManager.getWorkAreaForMonitor(global.display.get_current_monitor()); + 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/vertical-workspaces/lib/workspaceThumbnail.js b/extensions/vertical-workspaces/lib/workspaceThumbnail.js new file mode 100644 index 0000000..d0bc206 --- /dev/null +++ b/extensions/vertical-workspaces/lib/workspaceThumbnail.js @@ -0,0 +1,1148 @@ +/** + * V-Shell (Vertical Workspaces) + * workspaceThumbnail.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { GLib, Clutter, Graphene, Meta, Shell, St } = imports.gi; +const DND = imports.ui.dnd; +const Main = imports.ui.main; +const Background = imports.ui.background; +const WorkspaceThumbnail = imports.ui.workspaceThumbnail; +const ThumbnailState = WorkspaceThumbnail.ThumbnailState; + +const ControlsState = imports.ui.overviewControls.ControlsState; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +// gettext +const _ = Me.imports.lib.settings._; + +const _Util = Me.imports.lib.util; +const shellVersion = _Util.shellVersion; + +let _overrides; + +const WORKSPACE_CUT_SIZE = 10; +const _originalMaxThumbnailScale = WorkspaceThumbnail.MAX_THUMBNAIL_SCALE; + +let opt = null; + +function update(reset = false) { + if (_overrides) + _overrides.removeAll(); + + + if (reset) { + if (_originalMaxThumbnailScale) + WorkspaceThumbnail.MAX_THUMBNAIL_SCALE = _originalMaxThumbnailScale; + _overrides = null; + opt = null; + return; + } + + opt = Me.imports.lib.settings.opt; + _overrides = new _Util.Overrides(); + + // don't limit max thumbnail scale for other clients than overview, for example AATWS. + WorkspaceThumbnail.MAX_THUMBNAIL_SCALE = 1; + + _overrides.addOverride('WorkspaceThumbnail', WorkspaceThumbnail.WorkspaceThumbnail.prototype, WorkspaceThumbnailCommon); + _overrides.addOverride('ThumbnailsBoxCommon', WorkspaceThumbnail.ThumbnailsBox.prototype, ThumbnailsBoxCommon); + + // 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; +} + +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'); + + // 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 = ExtensionUtils.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', () => { + // wait for new information + 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 && _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); + + this.connect('destroy', () => { + if (this._bgManager) + this._bgManager.destroy(); + this._bgManager = 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 && !_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._overview.controls._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 imports.ui.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 imports.ui.appDisplay.FolderIcon) { + if (shellVersion >= 44) { + for (let app of source.view._apps) { + // const app = Shell.AppSystem.get_default().lookup_app(id); + app.open_new_window(this.metaWorkspace.index()); + } + } else { + for (let id of source.view._appIds) { + 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 imports.ui.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 imports.ui.appDisplay.FolderIcon) { + if (shellVersion >= 44) { + for (let app of source.view._apps) { + // const app = Shell.AppSystem.get_default().lookup_app(id); + app.open_new_window(newWorkspaceIndex); + } + } else { + for (let id of source.view._appIds) { + 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), + WorkspaceThumbnail.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 imports.ui.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; + }, + + _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); + }, + + get_preferred_custom_width(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical.get_preferred_custom_width.bind(this)(...args); + else + return ThumbnailsBoxHorizontal.get_preferred_custom_width.bind(this)(...args); + }, + + get_preferred_custom_height(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical.get_preferred_custom_height.bind(this)(...args); + else + return ThumbnailsBoxHorizontal.get_preferred_custom_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); + }, +}; + +const ThumbnailsBoxVertical = { + _getPlaceholderTarget(index, spacing, rtl) { + this._dropPlaceholder.add_style_class_name('placeholder-vertical'); + const workspace = this._thumbnails[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]; + + 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: function(forHeight) { + // override of this vfunc doesn't work for some reason (tested on Ubuntu and Fedora), it's not reachable + get_preferred_custom_width(forHeight) { + if (!this.visible) + return [0, 0]; + + if (forHeight === -1) + return this.get_preferred_custom_height(forHeight); + + 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; + // scale = Math.min(scale, opt.MAX_THUMBNAIL_SCALE); + + const width = Math.round(this._porthole.width * scale); + return themeNode.adjust_preferred_height(width, width); + }, + + get_preferred_custom_height(_forWidth) { + if (!this.visible) + return [0, 0]; + + // Note that for getPreferredHeight/Width we cheat a bit and skip propagating + // the size request to our children because we know how big they are and know + // that the actors aren't depending on the virtual functions being called. + let 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 = 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()); + + if (shellVersion >= 44) { + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.hide(); + }); + } else { + Meta.later_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); + + if (shellVersion >= 44) { + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.show(); + }); + } else { + Meta.later_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]; + + 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]; + + 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; + }, + + get_preferred_custom_height(forWidth) { + 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; + // scale = Math.min(scale, opt.MAX_THUMBNAIL_SCALE); + + const height = Math.round(this._porthole.height * scale); + return themeNode.adjust_preferred_height(height, height); + }, + + get_preferred_custom_width(_forHeight) { + // Note that for getPreferredHeight/Width we cheat a bit and skip propagating + // the size request to our children because we know how big they are and know + // that the actors aren't depending on the virtual functions being called. + if (!this.visible) + return [0, 0]; + + 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 = 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()); + + if (shellVersion >= 44) { + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.hide(); + }); + } else { + Meta.later_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); + + if (shellVersion >= 44) { + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.show(); + }); + } else { + Meta.later_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, +}; diff --git a/extensions/vertical-workspaces/lib/workspacesView.js b/extensions/vertical-workspaces/lib/workspacesView.js new file mode 100644 index 0000000..e3575f1 --- /dev/null +++ b/extensions/vertical-workspaces/lib/workspacesView.js @@ -0,0 +1,934 @@ +/** + * V-Shell (Vertical Workspaces) + * workspacesView.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +const { GObject, Clutter, Meta, St } = imports.gi; + +const Main = imports.ui.main; +const Util = imports.misc.util; +const WorkspacesView = imports.ui.workspacesView; +// first reference to constant defined using const in other module returns undefined, the SecondaryMonitorDisplay const will remain empty and unused +const SecondaryMonitorDisplay = WorkspacesView.SecondaryMonitorDisplay; +const ControlsState = imports.ui.overviewControls.ControlsState; +const FitMode = imports.ui.workspacesView.FitMode; + +const SIDE_CONTROLS_ANIMATION_TIME = imports.ui.overview.ANIMATION_TIME; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const SEARCH_WINDOWS_PREFIX = Me.imports.lib.windowSearchProvider.prefix; +const SEARCH_RECENT_FILES_PREFIX = Me.imports.lib.recentFilesSearchProvider.prefix; + +const _Util = Me.imports.lib.util; +let _overrides; + +let opt; + + +function update(reset = false) { + opt = Me.imports.lib.settings.opt; + opt.DESKTOP_CUBE_ENABLED = Main.extensionManager._enabledExtensions.includes('desktop-cube@schneegans.github.com'); + const cubeSupported = opt.DESKTOP_CUBE_ENABLED && !opt.ORIENTATION && !opt.OVERVIEW_MODE; + + // if desktop cube extension is enabled while V-Shell is loaded, removeAll() would override its code + if (_overrides && !cubeSupported) { + _overrides.removeAll(); + global.workspace_manager.override_workspace_layout(Meta.DisplayCorner.TOPLEFT, false, 1, -1); + } + + if (reset) { + _overrides = null; + opt = null; + return; + } + + + _overrides = new _Util.Overrides(); + + if (!cubeSupported) + _overrides.addOverride('WorkspacesView', WorkspacesView.WorkspacesView.prototype, WorkspacesViewCommon); + + _overrides.addOverride('WorkspacesDisplay', WorkspacesView.WorkspacesDisplay.prototype, WorkspacesDisplay); + _overrides.addOverride('ExtraWorkspaceView', WorkspacesView.ExtraWorkspaceView.prototype, ExtraWorkspaceView); + + if (opt.ORIENTATION) { + // switch internal workspace orientation in GS + global.workspace_manager.override_workspace_layout(Meta.DisplayCorner.TOPLEFT, false, -1, 1); + _overrides.addOverride('SecondaryMonitorDisplay', WorkspacesView.SecondaryMonitorDisplay.prototype, SecondaryMonitorDisplayVertical); + } else { + _overrides.addOverride('SecondaryMonitorDisplay', WorkspacesView.SecondaryMonitorDisplay.prototype, SecondaryMonitorDisplayHorizontal); + } +} + +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() { + // replaced in _updateWorkspacesState + /* let workspaceManager = global.workspace_manager; + let active = workspaceManager.get_active_workspace_index(); + + const fitMode = this._fitModeAdjustment.value; + const singleFitMode = fitMode === FitMode.SINGLE; + + for (let w = 0; w < this._workspaces.length; w++) { + let workspace = this._workspaces[w]; + + if (this._animating || this._gestureActive || !singleFitMode) + workspace.show(); + else + workspace.visible = Math.abs(w - active) <= opt.NUMBER_OF_VISIBLE_NEIGHBORS; + }*/ + }, + + // 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; + + // define the transition values here to save time in each ws + let scaleX, scaleY; + if (opt.ORIENTATION) { // vertical 1 / horizontal 0 + scaleX = 1; + scaleY = 0.1; + } else { + scaleX = 0.1; + scaleY = 1; + } + + 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; + + const distanceToCurrentWorkspace = Math.abs(adj.value - index); + + const scaleProgress = 1 - Math.clamp(distanceToCurrentWorkspace, 0, 1); + + // 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 > 340 && 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) || + (opt.WORKSPACE_MAX_SPACING < 340 && 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, scaling animation will make impression that they move in from outside the monitor + if (!w.visible && distanceToCurrentWorkspace === 1 && initialState === ControlsState.APP_GRID && currentState === ControlsState.WINDOW_PICKER) { + w.scale_x = scaleX; + w.scale_y = scaleY; + w.visible = true; + w.ease({ + duration: 100, + scale_x: 1, + scale_y: 1, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else if (!w.visible && distanceToCurrentWorkspace <= opt.NUMBER_OF_VISIBLE_NEIGHBORS && currentState === ControlsState.WINDOW_PICKER) { + w.set({ + scale_x: 1, + scale_y: 1, + }); + } + + // 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; + }); + }, +}; + +// SecondaryMonitorDisplay Vertical +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 }; + }, + + _getThumbnailsWidth(box, spacing) { + if (opt.SEC_WS_TMB_HIDDEN) + return 0; + + const [width, height] = box.get_size(); + const { expandFraction } = this._thumbnails; + const [, thumbnailsWidth] = this._thumbnails.get_preferred_custom_width(height - 2 * spacing); + let scaledWidth; + if (opt.SEC_WS_PREVIEW_SHIFT && !opt.PANEL_DISABLED) + scaledWidth = ((height - Main.panel.height) * opt.SEC_MAX_THUMBNAIL_SCALE) * (width / height); + else + scaledWidth = width * opt.SEC_MAX_THUMBNAIL_SCALE; + + return Math.min( + thumbnailsWidth * expandFraction, + Math.round(scaledWidth)); + }, + + _getWorkspacesBoxForState(state, box, padding, thumbnailsWidth, spacing) { + // const { ControlsState } = OverviewControls; + const workspaceBox = box.copy(); + const [width, height] = workspaceBox.get_size(); + + let wWidth, wHeight, wsbX, wsbY, offset, yShift; + switch (state) { + case ControlsState.HIDDEN: + break; + case ControlsState.WINDOW_PICKER: + case ControlsState.APP_GRID: + if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) + break; + + yShift = 0; + if (opt.SEC_WS_PREVIEW_SHIFT && !opt.PANEL_DISABLED) { + if (opt.PANEL_POSITION_TOP) + yShift = Main.panel.height; + else + yShift = -Main.panel.height; + } + + wWidth = width - thumbnailsWidth - 5 * spacing; + wHeight = Math.min(wWidth / (width / height) - Math.abs(yShift), height - 4 * spacing); + wWidth = Math.round(wWidth * opt.SEC_WS_PREVIEW_SCALE); + wHeight = Math.round(wHeight * opt.SEC_WS_PREVIEW_SCALE); + + offset = Math.round(width - thumbnailsWidth - wWidth) / 2; + if (opt.SEC_WS_TMB_LEFT) + wsbX = thumbnailsWidth + offset; + else + wsbX = offset; + + wsbY = Math.round((height - wHeight - Math.abs(yShift)) / 2 + yShift); + + workspaceBox.set_origin(wsbX, wsbY); + workspaceBox.set_size(wWidth, wHeight); + break; + } + + return workspaceBox; + }, + + vfunc_allocate(box) { + this.set_allocation(box); + + const themeNode = this.get_theme_node(); + const contentBox = themeNode.get_content_box(box); + const [width, height] = contentBox.get_size(); + const { expandFraction } = this._thumbnails; + const spacing = themeNode.get_length('spacing') * expandFraction; + const padding = Math.round(0.1 * height); + + let thumbnailsWidth = this._getThumbnailsWidth(contentBox, spacing); + let [, thumbnailsHeight] = this._thumbnails.get_preferred_custom_height(thumbnailsWidth); + thumbnailsHeight = Math.min(thumbnailsHeight, height - 2 * spacing); + + this._thumbnails.visible = !opt.SEC_WS_TMB_HIDDEN; + if (this._thumbnails.visible) { + let wsTmbX; + if (opt.SEC_WS_TMB_LEFT) { // left + wsTmbX = Math.round(spacing / 4); + this._thumbnails._positionLeft = true; + } else { + wsTmbX = Math.round(width - spacing / 4 - thumbnailsWidth); + this._thumbnails._positionLeft = false; + } + + const childBox = new Clutter.ActorBox(); + const availSpace = height - thumbnailsHeight - 2 * spacing; + + let wsTmbY = availSpace / 2; + wsTmbY -= opt.SEC_WS_TMB_POSITION_ADJUSTMENT * wsTmbY - spacing; + + childBox.set_origin(Math.round(wsTmbX), Math.round(wsTmbY)); + childBox.set_size(thumbnailsWidth, thumbnailsHeight); + this._thumbnails.allocate(childBox); + } + + const { + currentState, initialState, finalState, transitioning, progress, + } = this._overviewAdjustment.getStateTransitionParams(); + + let workspacesBox; + const workspaceParams = [contentBox, padding, thumbnailsWidth, 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: SIDE_CONTROLS_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; + }, +}; + +// SecondaryMonitorDisplay Horizontal +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, padding, thumbnailsHeight, spacing) { + // const { ControlsState } = OverviewControls; + const workspaceBox = box.copy(); + const [width, height] = workspaceBox.get_size(); + + let wWidth, wHeight, wsbX, wsbY, offset, yShift; + switch (state) { + case ControlsState.HIDDEN: + break; + case ControlsState.WINDOW_PICKER: + case ControlsState.APP_GRID: + if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) + break; + + yShift = 0; + if (opt.SEC_WS_PREVIEW_SHIFT && !opt.PANEL_DISABLED) { + if (opt.PANEL_POSITION_TOP) + yShift = Main.panel.height; + else + yShift = -Main.panel.height; + } + + wHeight = height - Math.abs(yShift) - (thumbnailsHeight ? thumbnailsHeight + 4 * spacing : padding); + wWidth = Math.min(wHeight * (width / height), width - 5 * spacing); + wWidth = Math.round(wWidth * opt.SEC_WS_PREVIEW_SCALE); + wHeight = Math.round(wHeight * opt.SEC_WS_PREVIEW_SCALE); + + offset = Math.round((height - thumbnailsHeight - wHeight - Math.abs(yShift)) / 2); + if (opt.SEC_WS_TMB_TOP) + wsbY = thumbnailsHeight + offset; + else + wsbY = offset; + + wsbY += yShift; + wsbX = Math.round((width - wWidth) / 2); + + workspaceBox.set_origin(wsbX, wsbY); + workspaceBox.set_size(wWidth, wHeight); + break; + } + + return workspaceBox; + }, + + _getThumbnailsHeight(box) { + if (opt.SEC_WS_TMB_HIDDEN) + return 0; + + const [width, height] = box.get_size(); + const { expandFraction } = this._thumbnails; + const [thumbnailsHeight] = this._thumbnails.get_preferred_height(width); + return Math.min( + thumbnailsHeight * expandFraction, + height * opt.SEC_MAX_THUMBNAIL_SCALE); + }, + + vfunc_allocate(box) { + this.set_allocation(box); + + const themeNode = this.get_theme_node(); + const contentBox = themeNode.get_content_box(box); + const [width, height] = contentBox.get_size(); + const { expandFraction } = this._thumbnails; + const spacing = themeNode.get_length('spacing') * expandFraction; + const padding = Math.round(0.1 * height); + + let thumbnailsHeight = this._getThumbnailsHeight(contentBox); + let [, thumbnailsWidth] = this._thumbnails.get_preferred_custom_width(thumbnailsHeight); + thumbnailsWidth = Math.min(thumbnailsWidth, width - 2 * spacing); + + this._thumbnails.visible = !opt.SEC_WS_TMB_HIDDEN; + if (this._thumbnails.visible) { + let wsTmbY; + if (opt.SEC_WS_TMB_TOP) + wsTmbY = Math.round(spacing / 4); + else + wsTmbY = Math.round(height - spacing / 4 - thumbnailsHeight); + + + const childBox = new Clutter.ActorBox(); + const availSpace = width - thumbnailsWidth - 2 * spacing; + + let wsTmbX = availSpace / 2; + wsTmbX -= opt.SEC_WS_TMB_POSITION_ADJUSTMENT * wsTmbX - spacing; + + childBox.set_origin(Math.round(wsTmbX), Math.round(wsTmbY)); + childBox.set_size(thumbnailsWidth, thumbnailsHeight); + this._thumbnails.allocate(childBox); + } + + const { + currentState, initialState, finalState, transitioning, progress, + } = this._overviewAdjustment.getStateTransitionParams(); + + let workspacesBox; + const workspaceParams = [contentBox, padding, thumbnailsHeight, spacing]; + if (!transitioning) { + workspacesBox = + this._getWorkspacesBoxForState(currentState, ...workspaceParams); + } else { + const initialBox = + this._getWorkspacesBoxForState(initialState, ...workspaceParams); + const finalBox = + this._getWorkspacesBoxForState(finalState, ...workspaceParams); + workspacesBox = initialBox.interpolate(finalBox, progress); + } + this._workspacesView.allocate(workspacesBox); + }, + + _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 ExtraWorkspaceView = { + _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; + }, +}; + +const WorkspacesDisplay = { + _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_actor(view); + } + + 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 (_Util.isShiftPressed()) { + let direction = _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) { + _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; + } + } + + 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 (_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 (_Util.isCtrlPressed() && _Util.isShiftPressed()) { + _Util.openPreferences(); + } else if (_Util.isAltPressed()) { + Main.ctrlAltTabManager._items.forEach(i => { + if (i.sortGroup === 1 && i.name === 'Dash') + Main.ctrlAltTabManager.focusGroup(i); + }); + } else if (opt.RECENT_FILES_SEARCH_PROVIDER_ENABLED && _Util.isCtrlPressed()) { + _Util.activateSearchProvider(SEARCH_RECENT_FILES_PREFIX); + } else if (opt.WINDOW_SEARCH_PROVIDER_ENABLED) { + _Util.activateSearchProvider(SEARCH_WINDOWS_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._overview._controls._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 adjustment = this._workspacesViews[0]._workspaces[global.workspace_manager.get_active_workspace().index()]._background._stateAdjustment; + opt.WORKSPACE_MODE = 1; + _Util.exposeWindows(adjustment, true); + } else { + if (state === 2) + return Clutter.EVENT_PROPAGATE; + Main.ctrlAltTabManager._items.forEach(i => { + if (i.sortGroup === 1 && i.name === 'Windows') + Main.ctrlAltTabManager.focusGroup(i); + }); + } + + 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 (_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) + _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; + } +} |