diff options
Diffstat (limited to 'extensions/vertical-workspaces/lib/appDisplay.js')
-rw-r--r-- | extensions/vertical-workspaces/lib/appDisplay.js | 1474 |
1 files changed, 1474 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); + } +}); |