1475 lines
53 KiB
JavaScript
1475 lines
53 KiB
JavaScript
|
/**
|
||
|
* 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);
|
||
|
}
|
||
|
});
|