summaryrefslogtreecommitdiffstats
path: root/js/ui/appDisplay.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/ui/appDisplay.js3273
1 files changed, 3273 insertions, 0 deletions
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
new file mode 100644
index 0000000..61fd0bc
--- /dev/null
+++ b/js/ui/appDisplay.js
@@ -0,0 +1,3273 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported AppDisplay, AppSearchProvider */
+
+const {
+ Clutter, Gio, GLib, GObject, Graphene, Pango, Shell, St,
+} = imports.gi;
+
+const AppFavorites = imports.ui.appFavorites;
+const { AppMenu } = imports.ui.appMenu;
+const BoxPointer = imports.ui.boxpointer;
+const DND = imports.ui.dnd;
+const GrabHelper = imports.ui.grabHelper;
+const IconGrid = imports.ui.iconGrid;
+const Layout = imports.ui.layout;
+const Main = imports.ui.main;
+const PageIndicators = imports.ui.pageIndicators;
+const ParentalControlsManager = imports.misc.parentalControlsManager;
+const PopupMenu = imports.ui.popupMenu;
+const Search = imports.ui.search;
+const SwipeTracker = imports.ui.swipeTracker;
+const Params = imports.misc.params;
+const SystemActions = imports.misc.systemActions;
+
+var MENU_POPUP_TIMEOUT = 600;
+var POPDOWN_DIALOG_TIMEOUT = 500;
+
+var FOLDER_SUBICON_FRACTION = .4;
+
+var VIEWS_SWITCH_TIME = 400;
+var VIEWS_SWITCH_ANIMATION_DELAY = 100;
+
+var SCROLL_TIMEOUT_TIME = 150;
+
+var APP_ICON_SCALE_IN_TIME = 500;
+var APP_ICON_SCALE_IN_DELAY = 700;
+
+var APP_ICON_TITLE_EXPAND_TIME = 200;
+var APP_ICON_TITLE_COLLAPSE_TIME = 100;
+
+const FOLDER_DIALOG_ANIMATION_TIME = 200;
+
+const PAGE_PREVIEW_ANIMATION_TIME = 150;
+const PAGE_INDICATOR_FADE_TIME = 200;
+const PAGE_PREVIEW_RATIO = 0.20;
+
+const OVERSHOOT_THRESHOLD = 20;
+const OVERSHOOT_TIMEOUT = 1000;
+
+const DELAYED_MOVE_TIMEOUT = 200;
+
+const DIALOG_SHADE_NORMAL = Clutter.Color.from_pixel(0x000000cc);
+const DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000055);
+
+const DEFAULT_FOLDERS = {
+ 'Utilities': {
+ name: 'X-GNOME-Utilities.directory',
+ categories: ['X-GNOME-Utilities'],
+ apps: [
+ 'gnome-abrt.desktop',
+ 'gnome-system-log.desktop',
+ 'nm-connection-editor.desktop',
+ 'org.gnome.baobab.desktop',
+ 'org.gnome.Connections.desktop',
+ 'org.gnome.DejaDup.desktop',
+ 'org.gnome.Dictionary.desktop',
+ 'org.gnome.DiskUtility.desktop',
+ 'org.gnome.eog.desktop',
+ 'org.gnome.Evince.desktop',
+ 'org.gnome.FileRoller.desktop',
+ 'org.gnome.fonts.desktop',
+ 'org.gnome.seahorse.Application.desktop',
+ 'org.gnome.tweaks.desktop',
+ 'org.gnome.Usage.desktop',
+ 'vinagre.desktop',
+ ],
+ },
+ 'YaST': {
+ name: 'suse-yast.directory',
+ categories: ['X-SuSE-YaST'],
+ },
+};
+
+function _getCategories(info) {
+ let categoriesStr = info.get_categories();
+ if (!categoriesStr)
+ return [];
+ return categoriesStr.split(';');
+}
+
+function _listsIntersect(a, b) {
+ for (let itemA of a) {
+ if (b.includes(itemA))
+ return true;
+ }
+ return false;
+}
+
+function _getFolderName(folder) {
+ let name = folder.get_string('name');
+
+ if (folder.get_boolean('translate')) {
+ let translated = Shell.util_get_translated_folder_name(name);
+ if (translated !== null)
+ return translated;
+ }
+
+ return name;
+}
+
+function _getViewFromIcon(icon) {
+ for (let parent = icon.get_parent(); parent; parent = parent.get_parent()) {
+ if (parent instanceof BaseAppView)
+ return parent;
+ }
+ return null;
+}
+
+function _findBestFolderName(apps) {
+ let appInfos = apps.map(app => app.get_app_info());
+
+ let categoryCounter = {};
+ let commonCategories = [];
+
+ appInfos.reduce((categories, appInfo) => {
+ for (let category of _getCategories(appInfo)) {
+ if (!(category in categoryCounter))
+ categoryCounter[category] = 0;
+
+ categoryCounter[category] += 1;
+
+ // If a category is present in all apps, its counter will
+ // reach appInfos.length
+ if (category.length > 0 &&
+ categoryCounter[category] == appInfos.length)
+ categories.push(category);
+ }
+ return categories;
+ }, commonCategories);
+
+ for (let category of commonCategories) {
+ const directory = `${category}.directory`;
+ const translated = Shell.util_get_translated_folder_name(directory);
+ if (translated !== null)
+ return translated;
+ }
+
+ return null;
+}
+
+const AppGrid = GObject.registerClass({
+ Properties: {
+ 'indicators-padding': GObject.ParamSpec.boxed('indicators-padding',
+ 'Indicators padding', 'Indicators padding',
+ GObject.ParamFlags.READWRITE,
+ Clutter.Margin.$gtype),
+ },
+}, class AppGrid extends IconGrid.IconGrid {
+ _init(layoutParams) {
+ super._init(layoutParams);
+
+ this._indicatorsPadding = new Clutter.Margin();
+ }
+
+ _updatePadding() {
+ const node = this.get_theme_node();
+ const {rowSpacing, columnSpacing} = this.layoutManager;
+
+ const padding = this._indicatorsPadding.copy();
+ padding.left += rowSpacing;
+ padding.right += rowSpacing;
+ padding.top += columnSpacing;
+ padding.bottom += columnSpacing;
+ ['top', 'right', 'bottom', 'left'].forEach(side => {
+ padding[side] += node.get_length(`page-padding-${side}`);
+ });
+
+ this.layoutManager.pagePadding = padding;
+ }
+
+ vfunc_style_changed() {
+ super.vfunc_style_changed();
+ this._updatePadding();
+ }
+
+ get indicatorsPadding() {
+ return this._indicatorsPadding;
+ }
+
+ set indicatorsPadding(v) {
+ if (this._indicatorsPadding === v)
+ return;
+
+ this._indicatorsPadding = v ? v : new Clutter.Margin();
+ this._updatePadding();
+ }
+});
+
+const BaseAppViewGridLayout = GObject.registerClass(
+class BaseAppViewGridLayout extends Clutter.BinLayout {
+ _init(grid, scrollView, nextPageIndicator, nextPageArrow,
+ previousPageIndicator, previousPageArrow) {
+ if (!(grid instanceof AppGrid))
+ throw new Error('Grid must be an AppGrid subclass');
+
+ super._init();
+
+ this._grid = grid;
+ this._scrollView = scrollView;
+ this._previousPageIndicator = previousPageIndicator;
+ this._previousPageArrow = previousPageArrow;
+ this._nextPageIndicator = nextPageIndicator;
+ this._nextPageArrow = nextPageArrow;
+
+ grid.connect('pages-changed', () => this._syncPageIndicatorsVisibility());
+
+ this._pageIndicatorsAdjustment = new St.Adjustment({
+ lower: 0,
+ upper: 1,
+ });
+ this._pageIndicatorsAdjustment.connect(
+ 'notify::value', () => this._syncPageIndicators());
+
+ this._showIndicators = false;
+ this._currentPage = 0;
+ this._pageWidth = 0;
+ }
+
+ _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 * PAGE_PREVIEW_RATIO) / 2;
+
+ return Math.max(idealIndicatorWidth, minArrowsWidth);
+ }
+
+ _syncPageIndicatorsVisibility(animate = true) {
+ const previousIndicatorsVisible =
+ this._currentPage > 0 && this._showIndicators;
+
+ if (previousIndicatorsVisible)
+ this._previousPageIndicator.show();
+
+ this._previousPageIndicator.ease({
+ opacity: previousIndicatorsVisible ? 255 : 0,
+ duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
+ onComplete: () => {
+ if (!previousIndicatorsVisible)
+ this._previousPageIndicator.hide();
+ },
+ });
+
+ const previousArrowVisible =
+ this._currentPage > 0 && !previousIndicatorsVisible;
+
+ if (previousArrowVisible)
+ this._previousPageArrow.show();
+
+ this._previousPageArrow.ease({
+ opacity: previousArrowVisible ? 255 : 0,
+ duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
+ onComplete: () => {
+ if (!previousArrowVisible)
+ this._previousPageArrow.hide();
+ },
+ });
+
+ // Always show the next page indicator to allow dropping
+ // icons into new pages
+ const {allowIncompletePages, nPages} = this._grid.layoutManager;
+ const nextIndicatorsVisible = this._showIndicators &&
+ (allowIncompletePages ? true : this._currentPage < nPages - 1);
+
+ if (nextIndicatorsVisible)
+ this._nextPageIndicator.show();
+
+ this._nextPageIndicator.ease({
+ opacity: nextIndicatorsVisible ? 255 : 0,
+ duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
+ onComplete: () => {
+ if (!nextIndicatorsVisible)
+ this._nextPageIndicator.hide();
+ },
+ });
+
+ const nextArrowVisible =
+ this._currentPage < nPages - 1 &&
+ !nextIndicatorsVisible;
+
+ if (nextArrowVisible)
+ this._nextPageArrow.show();
+
+ this._nextPageArrow.ease({
+ opacity: nextArrowVisible ? 255 : 0,
+ duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
+ onComplete: () => {
+ if (!nextArrowVisible)
+ this._nextPageArrow.hide();
+ },
+ });
+ }
+
+ _getEndIcon(icons) {
+ const {columnsPerPage} = this._grid.layoutManager;
+ const index = Math.min(icons.length, columnsPerPage);
+ return icons[Math.max(index - 1, 0)];
+ }
+
+ _translatePreviousPageIcons(value, ltr) {
+ if (this._currentPage === 0)
+ return;
+
+ const previousPage = this._currentPage - 1;
+ const icons = this._grid.getItemsAtPage(previousPage).filter(i => i.visible);
+ if (icons.length === 0)
+ return;
+
+ const {left, right} = this._grid.indicatorsPadding;
+ const {columnSpacing} = this._grid.layoutManager;
+ const endIcon = this._getEndIcon(icons);
+ let iconOffset;
+
+ if (ltr) {
+ const currentPageOffset = this._pageWidth * this._currentPage;
+ iconOffset = currentPageOffset - endIcon.allocation.x2 + left - columnSpacing;
+ } else {
+ const rtlPage = this._grid.nPages - previousPage - 1;
+ const pageOffset = this._pageWidth * rtlPage;
+ iconOffset = pageOffset - endIcon.allocation.x1 - right + columnSpacing;
+ }
+
+ for (const icon of icons)
+ icon.translationX = iconOffset * value;
+ }
+
+ _translateNextPageIcons(value, ltr) {
+ if (this._currentPage >= this._grid.nPages - 1)
+ return;
+
+ const nextPage = this._currentPage + 1;
+ const icons = this._grid.getItemsAtPage(nextPage).filter(i => i.visible);
+ if (icons.length === 0)
+ return;
+
+ const {left, right} = this._grid.indicatorsPadding;
+ const {columnSpacing} = this._grid.layoutManager;
+ let iconOffset;
+
+ if (ltr) {
+ const pageOffset = this._pageWidth * nextPage;
+ iconOffset = pageOffset - icons[0].allocation.x1 - right + columnSpacing;
+ } else {
+ const rtlPage = this._grid.nPages - this._currentPage - 1;
+ const currentPageOffset = this._pageWidth * rtlPage;
+ iconOffset = currentPageOffset - icons[0].allocation.x2 + left - columnSpacing;
+ }
+
+ for (const icon of icons)
+ icon.translationX = iconOffset * value;
+ }
+
+ _syncPageIndicators() {
+ if (!this._container)
+ return;
+
+ const {value} = this._pageIndicatorsAdjustment;
+
+ const ltr = this._container.get_text_direction() !== Clutter.TextDirection.RTL;
+ const {left, right} = this._grid.indicatorsPadding;
+ const leftIndicatorOffset = -left * (1 - value);
+ const rightIndicatorOffset = right * (1 - value);
+
+ this._previousPageIndicator.translationX =
+ ltr ? leftIndicatorOffset : rightIndicatorOffset;
+ this._nextPageIndicator.translationX =
+ ltr ? rightIndicatorOffset : leftIndicatorOffset;
+
+ const leftArrowOffset = -left * value;
+ const rightArrowOffset = right * value;
+
+ this._previousPageArrow.translationX =
+ ltr ? leftArrowOffset : rightArrowOffset;
+ this._nextPageArrow.translationX =
+ ltr ? rightArrowOffset : leftArrowOffset;
+
+ // Page icons
+ this._translatePreviousPageIcons(value, ltr);
+ this._translateNextPageIcons(value, ltr);
+
+ if (this._grid.nPages > 0) {
+ this._grid.getItemsAtPage(this._currentPage).forEach(icon => {
+ icon.translationX = 0;
+ });
+ }
+ }
+
+ vfunc_set_container(container) {
+ this._container = container;
+ this._pageIndicatorsAdjustment.actor = container;
+ this._syncPageIndicators();
+ }
+
+ vfunc_allocate(container, box) {
+ const ltr = container.get_text_direction() !== Clutter.TextDirection.RTL;
+ const indicatorsWidth = this._getIndicatorsWidth(box);
+
+ this._grid.indicatorsPadding = new Clutter.Margin({
+ left: indicatorsWidth,
+ right: indicatorsWidth,
+ });
+
+ this._scrollView.allocate(box);
+
+ const leftBox = box.copy();
+ leftBox.x2 = leftBox.x1 + indicatorsWidth;
+
+ const rightBox = box.copy();
+ rightBox.x1 = rightBox.x2 - indicatorsWidth;
+
+ this._previousPageIndicator.allocate(ltr ? leftBox : rightBox);
+ this._previousPageArrow.allocate_align_fill(ltr ? leftBox : rightBox,
+ 0.5, 0.5, false, false);
+ this._nextPageIndicator.allocate(ltr ? rightBox : leftBox);
+ this._nextPageArrow.allocate_align_fill(ltr ? rightBox : leftBox,
+ 0.5, 0.5, false, false);
+
+ this._pageWidth = box.get_width();
+ }
+
+ goToPage(page, animate = true) {
+ if (this._currentPage === page)
+ return;
+
+ this._currentPage = page;
+ this._syncPageIndicatorsVisibility(animate);
+ this._syncPageIndicators();
+ }
+
+ showPageIndicators() {
+ if (this._showIndicators)
+ return;
+
+ this._pageIndicatorsAdjustment.ease(1, {
+ duration: PAGE_PREVIEW_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
+ });
+
+ this._grid.clipToView = false;
+ this._showIndicators = true;
+ this._syncPageIndicatorsVisibility();
+ }
+
+ hidePageIndicators() {
+ if (!this._showIndicators)
+ return;
+
+ this._pageIndicatorsAdjustment.ease(0, {
+ duration: PAGE_PREVIEW_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
+ onComplete: () => {
+ this._grid.clipToView = true;
+ },
+ });
+
+ this._showIndicators = false;
+ this._syncPageIndicatorsVisibility();
+ }
+});
+
+var BaseAppView = GObject.registerClass({
+ GTypeFlags: GObject.TypeFlags.ABSTRACT,
+ Properties: {
+ 'gesture-modes': GObject.ParamSpec.flags(
+ 'gesture-modes', 'gesture-modes', 'gesture-modes',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ Shell.ActionMode, Shell.ActionMode.OVERVIEW),
+ },
+ Signals: {
+ 'view-loaded': {},
+ },
+}, class BaseAppView extends St.Widget {
+ _init(params = {}) {
+ super._init(params);
+
+ this._grid = this._createGrid();
+ this._grid._delegate = this;
+ // Standard hack for ClutterBinLayout
+ this._grid.x_expand = true;
+ this._grid.connect('pages-changed', () => {
+ this.goToPage(this._grid.currentPage);
+ this._pageIndicators.setNPages(this._grid.nPages);
+ this._pageIndicators.setCurrentPosition(this._grid.currentPage);
+ });
+
+ // Scroll View
+ this._scrollView = new St.ScrollView({
+ style_class: 'apps-scroll-view',
+ clip_to_allocation: true,
+ x_expand: true,
+ y_expand: true,
+ reactive: true,
+ enable_mouse_scrolling: false,
+ });
+ this._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER);
+
+ this._canScroll = true; // limiting scrolling speed
+ this._scrollTimeoutId = 0;
+ this._scrollView.connect('scroll-event', this._onScroll.bind(this));
+
+ this._scrollView.add_actor(this._grid);
+
+ const scroll = this._scrollView.hscroll;
+ this._adjustment = scroll.adjustment;
+ this._adjustment.connect('notify::value', adj => {
+ const value = adj.value / adj.page_size;
+ this._pageIndicators.setCurrentPosition(value);
+ });
+
+ // Page Indicators
+ this._pageIndicators =
+ new PageIndicators.PageIndicators(Clutter.Orientation.HORIZONTAL);
+
+ this._pageIndicators.y_expand = false;
+ this._pageIndicators.connect('page-activated',
+ (indicators, pageIndex) => {
+ this.goToPage(pageIndex);
+ });
+ this._pageIndicators.connect('scroll-event', (actor, event) => {
+ this._scrollView.event(event, false);
+ });
+
+ // Navigation indicators
+ this._nextPageIndicator = new St.Widget({
+ style_class: 'page-navigation-hint next',
+ opacity: 0,
+ visible: false,
+ reactive: true,
+ x_expand: true,
+ y_expand: true,
+ x_align: Clutter.ActorAlign.FILL,
+ y_align: Clutter.ActorAlign.FILL,
+ });
+
+ this._prevPageIndicator = new St.Widget({
+ style_class: 'page-navigation-hint previous',
+ opacity: 0,
+ visible: false,
+ reactive: true,
+ x_expand: true,
+ y_expand: true,
+ x_align: Clutter.ActorAlign.FILL,
+ y_align: Clutter.ActorAlign.FILL,
+ });
+
+ // Next/prev page arrows
+ const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
+ this._nextPageArrow = new St.Button({
+ style_class: 'page-navigation-arrow',
+ icon_name: rtl
+ ? 'carousel-arrow-previous-symbolic'
+ : 'carousel-arrow-next-symbolic',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this._nextPageArrow.connect('clicked',
+ () => this.goToPage(this._grid.currentPage + 1));
+
+ this._prevPageArrow = new St.Button({
+ style_class: 'page-navigation-arrow',
+ icon_name: rtl
+ ? 'carousel-arrow-next-symbolic'
+ : 'carousel-arrow-previous-symbolic',
+ opacity: 0,
+ visible: false,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this._prevPageArrow.connect('clicked',
+ () => this.goToPage(this._grid.currentPage - 1));
+
+ const scrollContainer = new St.Widget({
+ clip_to_allocation: true,
+ y_expand: true,
+ });
+ scrollContainer.add_child(this._scrollView);
+ scrollContainer.add_child(this._prevPageIndicator);
+ scrollContainer.add_child(this._nextPageIndicator);
+ scrollContainer.add_child(this._nextPageArrow);
+ scrollContainer.add_child(this._prevPageArrow);
+ scrollContainer.layoutManager = new BaseAppViewGridLayout(
+ this._grid,
+ this._scrollView,
+ this._nextPageIndicator,
+ this._nextPageArrow,
+ this._prevPageIndicator,
+ this._prevPageArrow);
+ this._appGridLayout = scrollContainer.layoutManager;
+ scrollContainer._delegate = this;
+
+ this._box = new St.BoxLayout({
+ vertical: true,
+ x_expand: true,
+ y_expand: true,
+ });
+ this._box.add_child(scrollContainer);
+ this._box.add_child(this._pageIndicators);
+
+ // Swipe
+ this._swipeTracker = new SwipeTracker.SwipeTracker(this._scrollView,
+ Clutter.Orientation.HORIZONTAL, this.gestureModes);
+ this._swipeTracker.orientation = Clutter.Orientation.HORIZONTAL;
+ this._swipeTracker.connect('begin', this._swipeBegin.bind(this));
+ this._swipeTracker.connect('update', this._swipeUpdate.bind(this));
+ this._swipeTracker.connect('end', this._swipeEnd.bind(this));
+
+ this._orientation = Clutter.Orientation.HORIZONTAL;
+
+ this._items = new Map();
+ this._orderedItems = [];
+
+ // Filter the apps through the user’s parental controls.
+ this._parentalControlsManager = ParentalControlsManager.getDefault();
+ this._parentalControlsManager.connectObject('app-filter-changed',
+ () => this._redisplay(), this);
+
+ // Don't duplicate favorites
+ this._appFavorites = AppFavorites.getAppFavorites();
+ this._appFavorites.connectObject('changed',
+ () => this._redisplay(), this);
+
+ // Drag n' Drop
+ this._overshootTimeoutId = 0;
+ this._delayedMoveData = null;
+
+ this._dragBeginId = 0;
+ this._dragEndId = 0;
+ this._dragCancelledId = 0;
+
+ this.connect('destroy', this._onDestroy.bind(this));
+
+ this._previewedPages = new Map();
+ }
+
+ _onDestroy() {
+ if (this._swipeTracker) {
+ this._swipeTracker.destroy();
+ delete this._swipeTracker;
+ }
+
+ this._removeDelayedMove();
+ this._disconnectDnD();
+ }
+
+ _createGrid() {
+ return new AppGrid({allow_incomplete_pages: true});
+ }
+
+ _onScroll(actor, event) {
+ if (this._swipeTracker.canHandleScrollEvent(event))
+ return Clutter.EVENT_PROPAGATE;
+
+ if (!this._canScroll)
+ return Clutter.EVENT_STOP;
+
+ const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
+ const vertical = this._orientation === Clutter.Orientation.VERTICAL;
+
+ let nextPage = this._grid.currentPage;
+ switch (event.get_scroll_direction()) {
+ case Clutter.ScrollDirection.UP:
+ nextPage -= 1;
+ break;
+
+ case Clutter.ScrollDirection.DOWN:
+ nextPage += 1;
+ break;
+
+ case Clutter.ScrollDirection.LEFT:
+ if (vertical)
+ return Clutter.EVENT_STOP;
+ nextPage += rtl ? 1 : -1;
+ break;
+
+ case Clutter.ScrollDirection.RIGHT:
+ if (vertical)
+ return Clutter.EVENT_STOP;
+ nextPage += rtl ? -1 : 1;
+ break;
+
+ default:
+ return Clutter.EVENT_STOP;
+ }
+
+ this.goToPage(nextPage);
+
+ this._canScroll = false;
+ this._scrollTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
+ SCROLL_TIMEOUT_TIME, () => {
+ this._canScroll = true;
+ this._scrollTimeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+
+ return Clutter.EVENT_STOP;
+ }
+
+ _swipeBegin(tracker, monitor) {
+ if (monitor !== Main.layoutManager.primaryIndex)
+ return;
+
+ if (this._dragFocus) {
+ this._dragFocus.cancelActions();
+ this._dragFocus = null;
+ }
+
+ const adjustment = this._adjustment;
+ adjustment.remove_transition('value');
+
+ const progress = adjustment.value / adjustment.page_size;
+ const points = Array.from({ length: this._grid.nPages }, (v, i) => i);
+ const size = tracker.orientation === Clutter.Orientation.VERTICAL
+ ? this._grid.allocation.get_height() : this._grid.allocation.get_width();
+
+ tracker.confirmSwipe(size, points, progress, Math.round(progress));
+ }
+
+ _swipeUpdate(tracker, progress) {
+ const adjustment = this._adjustment;
+ adjustment.value = progress * adjustment.page_size;
+ }
+
+ _swipeEnd(tracker, duration, endProgress) {
+ const adjustment = this._adjustment;
+ const value = endProgress * adjustment.page_size;
+
+ adjustment.ease(value, {
+ mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
+ duration,
+ onComplete: () => this.goToPage(endProgress, false),
+ });
+ }
+
+ _connectDnD() {
+ this._dragBeginId =
+ Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this));
+ this._dragEndId =
+ Main.overview.connect('item-drag-end', this._onDragEnd.bind(this));
+ this._dragCancelledId =
+ Main.overview.connect('item-drag-cancelled', this._onDragCancelled.bind(this));
+ }
+
+ _disconnectDnD() {
+ if (this._dragBeginId > 0) {
+ Main.overview.disconnect(this._dragBeginId);
+ this._dragBeginId = 0;
+ }
+
+ if (this._dragEndId > 0) {
+ Main.overview.disconnect(this._dragEndId);
+ this._dragEndId = 0;
+ }
+
+ if (this._dragCancelledId > 0) {
+ Main.overview.disconnect(this._dragCancelledId);
+ this._dragCancelledId = 0;
+ }
+
+ if (this._dragMonitor) {
+ DND.removeDragMonitor(this._dragMonitor);
+ this._dragMonitor = null;
+ }
+ }
+
+ _maybeMoveItem(dragEvent) {
+ const [success, x, y] =
+ this._grid.transform_stage_point(dragEvent.x, dragEvent.y);
+
+ if (!success)
+ return;
+
+ const { source } = dragEvent;
+ const [page, position, dragLocation] =
+ this._getDropTarget(x, y, source);
+ const item = position !== -1
+ ? this._grid.getItemAt(page, position) : null;
+
+
+ // Dragging over invalid parts of the grid cancels the timeout
+ if (item === source ||
+ dragLocation === IconGrid.DragLocation.INVALID ||
+ dragLocation === IconGrid.DragLocation.ON_ICON) {
+ this._removeDelayedMove();
+ return;
+ }
+
+ if (!this._delayedMoveData ||
+ this._delayedMoveData.page !== page ||
+ this._delayedMoveData.position !== position) {
+ // Update the item with a small delay
+ this._removeDelayedMove();
+ this._delayedMoveData = {
+ page,
+ position,
+ source,
+ destroyId: source.connect('destroy', () => this._removeDelayedMove()),
+ timeoutId: GLib.timeout_add(GLib.PRIORITY_DEFAULT,
+ DELAYED_MOVE_TIMEOUT, () => {
+ this._moveItem(source, page, position);
+ this._delayedMoveData.timeoutId = 0;
+ this._removeDelayedMove();
+ return GLib.SOURCE_REMOVE;
+ }),
+ };
+ }
+ }
+
+ _removeDelayedMove() {
+ if (!this._delayedMoveData)
+ return;
+
+ const { source, destroyId, timeoutId } = this._delayedMoveData;
+
+ if (timeoutId > 0)
+ GLib.source_remove(timeoutId);
+
+ if (destroyId > 0)
+ source.disconnect(destroyId);
+
+ this._delayedMoveData = null;
+ }
+
+ _resetOvershoot() {
+ if (this._overshootTimeoutId)
+ GLib.source_remove(this._overshootTimeoutId);
+ this._overshootTimeoutId = 0;
+ }
+
+ _dragWithinOvershootRegion(dragEvent) {
+ const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
+ const {x, y, targetActor: indicator} = dragEvent;
+ const [indicatorX, indicatorY] = indicator.get_transformed_position();
+ const [indicatorWidth, indicatorHeight] = indicator.get_transformed_size();
+
+ let overshootX = indicatorX;
+ if (indicator === this._nextPageIndicator || rtl)
+ overshootX += indicatorWidth - OVERSHOOT_THRESHOLD;
+
+ const overshootBox = new Clutter.ActorBox();
+ overshootBox.set_origin(overshootX, indicatorY);
+ overshootBox.set_size(OVERSHOOT_THRESHOLD, indicatorHeight);
+
+ return overshootBox.contains(x, y);
+ }
+
+ _handleDragOvershoot(dragEvent) {
+ // Already animating
+ if (this._adjustment.get_transition('value') !== null)
+ return;
+
+ const {targetActor} = dragEvent;
+
+ if (targetActor !== this._prevPageIndicator &&
+ targetActor !== this._nextPageIndicator) {
+ this._resetOvershoot();
+ return;
+ }
+
+ if (this._overshootTimeoutId > 0)
+ return;
+
+ let targetPage;
+ if (dragEvent.targetActor === this._prevPageIndicator)
+ targetPage = this._grid.currentPage - 1;
+ else
+ targetPage = this._grid.currentPage + 1;
+
+ if (targetPage < 0 || targetPage >= this._grid.nPages)
+ return; // don't go beyond first/last page
+
+ // If dragging over the drag overshoot threshold region, immediately
+ // switch pages
+ if (this._dragWithinOvershootRegion(dragEvent)) {
+ this._resetOvershoot();
+ this.goToPage(targetPage);
+ }
+
+ this._overshootTimeoutId =
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, OVERSHOOT_TIMEOUT, () => {
+ this._resetOvershoot();
+ this.goToPage(targetPage);
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._overshootTimeoutId,
+ '[gnome-shell] this._overshootTimeoutId');
+ }
+
+ _onDragBegin() {
+ this._dragMonitor = {
+ dragMotion: this._onDragMotion.bind(this),
+ dragDrop: this._onDragDrop.bind(this),
+ };
+ DND.addDragMonitor(this._dragMonitor);
+ this._appGridLayout.showPageIndicators();
+ this._dragFocus = null;
+ this._swipeTracker.enabled = false;
+ }
+
+ _onDragMotion(dragEvent) {
+ if (!(dragEvent.source instanceof AppViewItem))
+ return DND.DragMotionResult.CONTINUE;
+
+ const appIcon = dragEvent.source;
+
+ // Handle the drag overshoot. When dragging to above the
+ // icon grid, move to the page above; when dragging below,
+ // move to the page below.
+ if (appIcon instanceof AppViewItem)
+ this._handleDragOvershoot(dragEvent);
+
+ this._maybeMoveItem(dragEvent);
+
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ _onDragDrop(dropEvent) {
+ // Because acceptDrop() does not receive the target actor, store it
+ // here and use this value in the acceptDrop() implementation below.
+ this._dropTarget = dropEvent.targetActor;
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ _onDragEnd() {
+ if (this._dragMonitor) {
+ DND.removeDragMonitor(this._dragMonitor);
+ this._dragMonitor = null;
+ }
+
+ this._resetOvershoot();
+ this._appGridLayout.hidePageIndicators();
+ this._swipeTracker.enabled = true;
+ }
+
+ _onDragCancelled() {
+ // At this point, the positions aren't stored yet, thus _redisplay()
+ // will move all items to their original positions
+ this._redisplay();
+ this._appGridLayout.hidePageIndicators();
+ this._swipeTracker.enabled = true;
+ }
+
+ _canAccept(source) {
+ return source instanceof AppViewItem;
+ }
+
+ handleDragOver(source) {
+ if (!this._canAccept(source))
+ return DND.DragMotionResult.NO_DROP;
+
+ return DND.DragMotionResult.MOVE_DROP;
+ }
+
+ acceptDrop(source) {
+ const dropTarget = this._dropTarget;
+ delete this._dropTarget;
+
+ if (!this._canAccept(source))
+ return false;
+
+ if (dropTarget === this._prevPageIndicator ||
+ dropTarget === this._nextPageIndicator) {
+ const 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;
+
+ this._moveItem(source, page, position);
+ this._removeDelayedMove();
+ }
+
+ return true;
+ }
+
+ _findBestPageToAppend(startPage = 1) {
+ for (let i = startPage; i < this._grid.nPages; i++) {
+ const pageItems =
+ this._grid.getItemsAtPage(i).filter(c => c.visible);
+
+ if (pageItems.length < this._grid.itemsPerPage)
+ return i;
+ }
+
+ return -1;
+ }
+
+ _getLinearPosition(page, position) {
+ let itemIndex = 0;
+
+ if (this._grid.nPages > 0) {
+ const realPage = page === -1 ? this._grid.nPages - 1 : page;
+
+ itemIndex = position === -1
+ ? this._grid.getItemsAtPage(realPage).filter(c => c.visible).length - 1
+ : position;
+
+ for (let i = 0; i < realPage; i++) {
+ const pageItems = this._grid.getItemsAtPage(i).filter(c => c.visible);
+ itemIndex += pageItems.length;
+ }
+ }
+
+ return itemIndex;
+ }
+
+ _addItem(item, page, position) {
+ // Append icons to the first page with empty slot, starting from
+ // the second page
+ if (this._grid.nPages > 1 && page === -1 && position === -1)
+ page = this._findBestPageToAppend();
+
+ const itemIndex = this._getLinearPosition(page, position);
+
+ this._orderedItems.splice(itemIndex, 0, item);
+ this._items.set(item.id, item);
+ this._grid.addItem(item, page, position);
+ }
+
+ _removeItem(item) {
+ const iconIndex = this._orderedItems.indexOf(item);
+
+ this._orderedItems.splice(iconIndex, 1);
+ this._items.delete(item.id);
+ this._grid.removeItem(item);
+ }
+
+ _getItemPosition(item) {
+ const { itemsPerPage } = this._grid;
+
+ let iconIndex = this._orderedItems.indexOf(item);
+ if (iconIndex === -1)
+ iconIndex = this._orderedItems.length - 1;
+
+ const page = Math.floor(iconIndex / itemsPerPage);
+ const position = iconIndex % itemsPerPage;
+
+ return [page, position];
+ }
+
+ _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);
+ });
+
+ this.emit('view-loaded');
+ }
+
+ getAllItems() {
+ return this._orderedItems;
+ }
+
+ _compareItems(a, b) {
+ return a.name.localeCompare(b.name);
+ }
+
+ _selectAppInternal(id) {
+ if (this._items.has(id))
+ this._items.get(id).navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
+ else
+ log(`No such application ${id}`);
+ }
+
+ selectApp(id) {
+ if (this._items.has(id)) {
+ let item = this._items.get(id);
+
+ if (item.mapped) {
+ this._selectAppInternal(id);
+ } else {
+ // Need to wait until the view is mapped
+ let signalId = item.connect('notify::mapped', actor => {
+ if (actor.mapped) {
+ actor.disconnect(signalId);
+ this._selectAppInternal(id);
+ }
+ });
+ }
+ } else {
+ // Need to wait until the view is built
+ let signalId = this.connect('view-loaded', () => {
+ this.disconnect(signalId);
+ this.selectApp(id);
+ });
+ }
+ }
+
+ _getDropTarget(x, y, source) {
+ const { currentPage } = this._grid;
+
+ let [item, dragLocation] = this._grid.getDropTarget(x, y);
+
+ const [sourcePage, sourcePosition] = this._grid.getItemPosition(source);
+ const targetPage = currentPage;
+ let targetPosition = item
+ ? this._grid.getItemPosition(item)[1] : -1;
+
+ // In case we're hovering over the edge of an item but the
+ // reflow will happen in the opposite direction (the drag
+ // can't "naturally push the item away"), we instead set the
+ // drop target to the adjacent item that can be pushed away
+ // in the reflow-direction.
+ //
+ // We must avoid doing that if we're hovering over the first
+ // or last column though, in that case there is no adjacent
+ // icon we could push away.
+ if (dragLocation === IconGrid.DragLocation.START_EDGE &&
+ targetPosition > sourcePosition &&
+ targetPage === sourcePage) {
+ const nColumns = this._grid.layout_manager.columns_per_page;
+ const targetColumn = targetPosition % nColumns;
+
+ if (targetColumn > 0) {
+ targetPosition -= 1;
+ dragLocation = IconGrid.DragLocation.END_EDGE;
+ }
+ } else if (dragLocation === IconGrid.DragLocation.END_EDGE &&
+ (targetPosition < sourcePosition ||
+ targetPage !== sourcePage)) {
+ const nColumns = this._grid.layout_manager.columns_per_page;
+ const targetColumn = targetPosition % nColumns;
+
+ if (targetColumn < nColumns - 1) {
+ targetPosition += 1;
+ dragLocation = IconGrid.DragLocation.START_EDGE;
+ }
+ }
+
+ // Append to the page if dragging over empty area
+ if (dragLocation === IconGrid.DragLocation.EMPTY_SPACE) {
+ const pageItems =
+ this._grid.getItemsAtPage(currentPage).filter(c => c.visible);
+
+ targetPosition = pageItems.length;
+ }
+
+ return [targetPage, targetPosition, dragLocation];
+ }
+
+ _moveItem(item, newPage, newPosition) {
+ const [page, position] = this._grid.getItemPosition(item);
+ if (page === newPage && position === newPosition)
+ return;
+
+ // Update the _orderedItems array
+ let index = this._orderedItems.indexOf(item);
+ this._orderedItems.splice(index, 1);
+
+ index = this._getLinearPosition(newPage, newPosition);
+ this._orderedItems.splice(index, 0, item);
+
+ this._grid.moveItem(item, newPage, newPosition);
+ }
+
+ vfunc_map() {
+ this._swipeTracker.enabled = true;
+ this._connectDnD();
+ super.vfunc_map();
+ }
+
+ vfunc_unmap() {
+ if (this._swipeTracker)
+ this._swipeTracker.enabled = false;
+ this._disconnectDnD();
+ super.vfunc_unmap();
+ }
+
+ animateSwitch(animationDirection) {
+ this.remove_all_transitions();
+ this._grid.remove_all_transitions();
+
+ let params = {
+ duration: VIEWS_SWITCH_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ };
+ if (animationDirection == IconGrid.AnimationDirection.IN) {
+ this.show();
+ params.opacity = 255;
+ params.delay = VIEWS_SWITCH_ANIMATION_DELAY;
+ } else {
+ params.opacity = 0;
+ params.delay = 0;
+ params.onComplete = () => this.hide();
+ }
+
+ this._grid.ease(params);
+ }
+
+ goToPage(pageNumber, animate = true) {
+ pageNumber = Math.clamp(pageNumber, 0, Math.max(this._grid.nPages - 1, 0));
+
+ if (this._grid.currentPage === pageNumber)
+ return;
+
+ this._appGridLayout.goToPage(pageNumber, animate);
+ this._grid.goToPage(pageNumber, animate);
+ }
+
+ updateDragFocus(dragFocus) {
+ this._dragFocus = dragFocus;
+ }
+});
+
+var PageManager = GObject.registerClass({
+ Signals: { 'layout-changed': {} },
+}, class PageManager extends GObject.Object {
+ _init() {
+ super._init();
+
+ this._updatingPages = false;
+ this._loadPages();
+
+ global.settings.connect('changed::app-picker-layout',
+ this._loadPages.bind(this));
+ }
+
+ _loadPages() {
+ const layout = global.settings.get_value('app-picker-layout');
+ this._pages = layout.recursiveUnpack();
+ if (!this._updatingPages)
+ this.emit('layout-changed');
+ }
+
+ getAppPosition(appId) {
+ let position = -1;
+ let page = -1;
+
+ for (let pageIndex = 0; pageIndex < this._pages.length; pageIndex++) {
+ const pageData = this._pages[pageIndex];
+
+ if (appId in pageData) {
+ page = pageIndex;
+ position = pageData[appId].position;
+ break;
+ }
+ }
+
+ return [page, position];
+ }
+
+ set pages(p) {
+ const packedPages = [];
+
+ // Pack the icon properties as a GVariant
+ for (const page of p) {
+ const pageData = {};
+ for (const [appId, properties] of Object.entries(page))
+ pageData[appId] = new GLib.Variant('a{sv}', properties);
+ packedPages.push(pageData);
+ }
+
+ this._updatingPages = true;
+
+ const variant = new GLib.Variant('aa{sv}', packedPages);
+ global.settings.set_value('app-picker-layout', variant);
+
+ this._updatingPages = false;
+ }
+
+ get pages() {
+ return this._pages;
+ }
+});
+
+var AppDisplay = GObject.registerClass(
+class AppDisplay extends BaseAppView {
+ _init() {
+ super._init({
+ layout_manager: new Clutter.BinLayout(),
+ x_expand: true,
+ y_expand: true,
+ });
+
+ this._pageManager = new PageManager();
+ this._pageManager.connect('layout-changed', () => this._redisplay());
+
+ this.add_child(this._box);
+
+ this._folderIcons = [];
+
+ this._currentDialog = null;
+ this._displayingDialog = false;
+
+ this._placeholder = null;
+
+ this._overviewHiddenId = 0;
+ this._redisplayWorkId = Main.initializeDeferredWork(this, () => {
+ this._redisplay();
+ if (this._overviewHiddenId === 0)
+ this._overviewHiddenId = Main.overview.connect('hidden', () => this.goToPage(0));
+ });
+
+ Shell.AppSystem.get_default().connect('installed-changed', () => {
+ Main.queueDeferredWork(this._redisplayWorkId);
+ });
+ this._folderSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' });
+ this._ensureDefaultFolders();
+ this._folderSettings.connect('changed::folder-children', () => {
+ Main.queueDeferredWork(this._redisplayWorkId);
+ });
+ }
+
+ _onDestroy() {
+ super._onDestroy();
+
+ if (this._scrollTimeoutId !== 0) {
+ GLib.source_remove(this._scrollTimeoutId);
+ this._scrollTimeoutId = 0;
+ }
+ }
+
+ vfunc_map() {
+ this._keyPressEventId =
+ global.stage.connect('key-press-event',
+ this._onKeyPressEvent.bind(this));
+ super.vfunc_map();
+ }
+
+ vfunc_unmap() {
+ if (this._keyPressEventId) {
+ global.stage.disconnect(this._keyPressEventId);
+ this._keyPressEventId = 0;
+ }
+ super.vfunc_unmap();
+ }
+
+ _redisplay() {
+ this._folderIcons.forEach(icon => {
+ icon.view._redisplay();
+ });
+
+ super._redisplay();
+ }
+
+ _savePages() {
+ const pages = [];
+
+ for (let i = 0; i < this._grid.nPages; i++) {
+ const pageItems =
+ this._grid.getItemsAtPage(i).filter(c => c.visible);
+ const pageData = {};
+
+ pageItems.forEach((item, index) => {
+ pageData[item.id] = {
+ position: GLib.Variant.new_int32(index),
+ };
+ });
+ pages.push(pageData);
+ }
+
+ this._pageManager.pages = pages;
+ }
+
+ _ensureDefaultFolders() {
+ if (this._folderSettings.get_strv('folder-children').length > 0)
+ return;
+
+ const folders = Object.keys(DEFAULT_FOLDERS);
+ this._folderSettings.set_strv('folder-children', folders);
+
+ const { path } = this._folderSettings;
+ for (const folder of folders) {
+ const { name, categories, apps } = DEFAULT_FOLDERS[folder];
+ const child = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.app-folders.folder',
+ path: `${path}folders/${folder}/`,
+ });
+ child.set_string('name', name);
+ child.set_boolean('translate', true);
+ child.set_strv('categories', categories);
+ if (apps)
+ child.set_strv('apps', apps);
+ }
+ }
+
+ _ensurePlaceholder(source) {
+ if (this._placeholder)
+ return;
+
+ 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 AppIcon(app, { isDraggable });
+ this._placeholder.connect('notify::pressed', icon => {
+ if (icon.pressed)
+ this.updateDragFocus(icon);
+ });
+ this._placeholder.scaleAndFade();
+ this._redisplay();
+ }
+
+ _removePlaceholder() {
+ if (this._placeholder) {
+ this._placeholder.undoScaleAndFade();
+ this._placeholder = null;
+ this._redisplay();
+ }
+ }
+
+ getAppInfos() {
+ return this._appInfoList;
+ }
+
+ _getItemPosition(item) {
+ if (item === this._placeholder) {
+ let [page, position] = this._grid.getItemPosition(item);
+
+ if (page === -1)
+ page = this._findBestPageToAppend(this._grid.currentPage);
+
+ return [page, position];
+ }
+
+ return this._pageManager.getAppPosition(item.id);
+ }
+
+ _compareItems(a, b) {
+ const [aPage, aPosition] = this._getItemPosition(a);
+ const [bPage, bPosition] = this._getItemPosition(b);
+
+ if (aPage === -1 && bPage === -1)
+ return a.name.localeCompare(b.name);
+ else if (aPage === -1)
+ return 1;
+ else if (bPage === -1)
+ return -1;
+
+ if (aPage !== bPage)
+ return aPage - bPage;
+
+ return aPosition - bPosition;
+ }
+
+ _loadApps() {
+ let appIcons = [];
+ this._appInfoList = Shell.AppSystem.get_default().get_installed().filter(appInfo => {
+ try {
+ appInfo.get_id(); // catch invalid file encodings
+ } catch (e) {
+ return false;
+ }
+ return !this._appFavorites.isFavorite(appInfo.get_id()) &&
+ this._parentalControlsManager.shouldShowApp(appInfo);
+ });
+
+ let apps = this._appInfoList.map(app => app.get_id());
+
+ let appSys = Shell.AppSystem.get_default();
+
+ const appsInsideFolders = new Set();
+ this._folderIcons = [];
+
+ 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 FolderIcon(id, path, this);
+ icon.connect('apps-changed', () => {
+ this._redisplay();
+ this._savePages();
+ });
+ icon.connect('notify::pressed', () => {
+ if (icon.pressed)
+ this.updateDragFocus(icon);
+ });
+ }
+
+ // Don't try to display empty folders
+ if (!icon.visible) {
+ icon.destroy();
+ return;
+ }
+
+ appIcons.push(icon);
+ this._folderIcons.push(icon);
+
+ icon.getAppIds().forEach(appId => appsInsideFolders.add(appId));
+ });
+
+ // 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 (appsInsideFolders.has(appId))
+ return;
+
+ let icon = this._items.get(appId);
+ if (!icon) {
+ let app = appSys.lookup_app(appId);
+
+ icon = new 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;
+ }
+
+ animateSwitch(animationDirection) {
+ super.animateSwitch(animationDirection);
+
+ if (this._currentDialog && this._displayingDialog &&
+ animationDirection == IconGrid.AnimationDirection.OUT) {
+ this._currentDialog.ease({
+ opacity: 0,
+ duration: VIEWS_SWITCH_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => (this.opacity = 255),
+ });
+ }
+ }
+
+ goToPage(pageNumber, animate = true) {
+ pageNumber = Math.clamp(pageNumber, 0, Math.max(this._grid.nPages - 1, 0));
+
+ if (this._grid.currentPage === pageNumber &&
+ this._displayingDialog &&
+ this._currentDialog)
+ return;
+ if (this._displayingDialog && this._currentDialog)
+ this._currentDialog.popdown();
+
+ super.goToPage(pageNumber, animate);
+ }
+
+ _onScroll(actor, event) {
+ if (this._displayingDialog || !this._scrollView.reactive)
+ return Clutter.EVENT_STOP;
+
+ return super._onScroll(actor, event);
+ }
+
+ _onKeyPressEvent(actor, event) {
+ if (this._displayingDialog)
+ return Clutter.EVENT_STOP;
+
+ if (event.get_key_symbol() === Clutter.KEY_Page_Up) {
+ this.goToPage(this._grid.currentPage - 1);
+ return Clutter.EVENT_STOP;
+ } else if (event.get_key_symbol() === Clutter.KEY_Page_Down) {
+ this.goToPage(this._grid.currentPage + 1);
+ return Clutter.EVENT_STOP;
+ } else if (event.get_key_symbol() === Clutter.KEY_Home) {
+ this.goToPage(0);
+ return Clutter.EVENT_STOP;
+ } else if (event.get_key_symbol() === Clutter.KEY_End) {
+ this.goToPage(this._grid.nPages - 1);
+ return Clutter.EVENT_STOP;
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ addFolderDialog(dialog) {
+ Main.layoutManager.overviewGroup.add_child(dialog);
+ dialog.connect('open-state-changed', (o, isOpen) => {
+ this._currentDialog?.disconnectObject(this);
+
+ this._currentDialog = null;
+
+ if (isOpen) {
+ this._currentDialog = dialog;
+ this._currentDialog.connectObject('destroy',
+ () => (this._currentDialog = null), this);
+ }
+ this._displayingDialog = isOpen;
+ });
+ }
+
+ _maybeMoveItem(dragEvent) {
+ const clonedEvent = {
+ ...dragEvent,
+ source: this._placeholder ? this._placeholder : dragEvent.source,
+ };
+
+ super._maybeMoveItem(clonedEvent);
+ }
+
+ _onDragBegin(overview, source) {
+ super._onDragBegin(overview, source);
+
+ // 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 (_getViewFromIcon(source) instanceof FolderView ||
+ this._appFavorites.isFavorite(source.id))
+ this._ensurePlaceholder(source);
+ }
+
+ _onDragMotion(dragEvent) {
+ if (this._currentDialog)
+ return DND.DragMotionResult.CONTINUE;
+
+ return super._onDragMotion(dragEvent);
+ }
+
+ _onDragEnd() {
+ super._onDragEnd();
+ this._removePlaceholder();
+ this._savePages();
+ }
+
+ _onDragCancelled(overview, source) {
+ const view = _getViewFromIcon(source);
+
+ if (view instanceof FolderView)
+ return;
+
+ super._onDragCancelled(overview, source);
+ }
+
+ acceptDrop(source) {
+ if (!super.acceptDrop(source))
+ return false;
+
+ this._savePages();
+
+ let view = _getViewFromIcon(source);
+ if (view instanceof FolderView)
+ view.removeApp(source.app);
+
+ if (this._currentDialog)
+ this._currentDialog.popdown();
+
+ if (this._appFavorites.isFavorite(source.id))
+ this._appFavorites.removeFavorite(source.id);
+
+ return true;
+ }
+
+ createFolder(apps) {
+ let newFolderId = GLib.uuid_string_random();
+
+ let folders = this._folderSettings.get_strv('folder-children');
+ folders.push(newFolderId);
+ this._folderSettings.set_strv('folder-children', folders);
+
+ // Create the new folder
+ let newFolderPath = this._folderSettings.path.concat('folders/', newFolderId, '/');
+ let newFolderSettings;
+ try {
+ newFolderSettings = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.app-folders.folder',
+ path: newFolderPath,
+ });
+ } catch (e) {
+ log('Error creating new folder');
+ return false;
+ }
+
+ // The hovered AppIcon always passes its own id as the first
+ // one, and this is where we want the folder to be created
+ let [folderPage, folderPosition] =
+ this._grid.getItemPosition(this._items.get(apps[0]));
+
+ // Adjust the final position
+ folderPosition -= apps.reduce((counter, appId) => {
+ const [page, position] =
+ this._grid.getItemPosition(this._items.get(appId));
+ if (page === folderPage && position < folderPosition)
+ counter++;
+ return counter;
+ }, 0);
+
+ let appItems = apps.map(id => this._items.get(id).app);
+ let folderName = _findBestFolderName(appItems);
+ if (!folderName)
+ folderName = _("Unnamed Folder");
+
+ newFolderSettings.delay();
+ newFolderSettings.set_string('name', folderName);
+ newFolderSettings.set_strv('apps', apps);
+ newFolderSettings.apply();
+
+ this._redisplay();
+
+ // Move the folder to where the icon target icon was
+ const folderItem = this._items.get(newFolderId);
+ this._moveItem(folderItem, folderPage, folderPosition);
+ this._savePages();
+
+ return true;
+ }
+});
+
+var AppSearchProvider = class AppSearchProvider {
+ constructor() {
+ this._appSys = Shell.AppSystem.get_default();
+ this.id = 'applications';
+ this.isRemoteProvider = false;
+ this.canLaunchSearch = false;
+
+ this._systemActions = new SystemActions.getDefault();
+
+ this._parentalControlsManager = ParentalControlsManager.getDefault();
+ }
+
+ getResultMetas(apps) {
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ let metas = [];
+ for (let id of apps) {
+ if (id.endsWith('.desktop')) {
+ let app = this._appSys.lookup_app(id);
+
+ metas.push({
+ id: app.get_id(),
+ name: app.get_name(),
+ createIcon: size => app.create_icon_texture(size),
+ });
+ } else {
+ let name = this._systemActions.getName(id);
+ let iconName = this._systemActions.getIconName(id);
+
+ const createIcon = size => new St.Icon({
+ icon_name: iconName,
+ width: size * scaleFactor,
+ height: size * scaleFactor,
+ style_class: 'system-action-icon',
+ });
+
+ metas.push({ id, name, createIcon });
+ }
+ }
+
+ return new Promise(resolve => resolve(metas));
+ }
+
+ filterResults(results, maxNumber) {
+ return results.slice(0, maxNumber);
+ }
+
+ getInitialResultSet(terms, cancellable) {
+ // Defer until the parental controls manager is initialised, so the
+ // results can be filtered correctly.
+ if (!this._parentalControlsManager.initialized) {
+ return new Promise(resolve => {
+ let initializedId = this._parentalControlsManager.connect('app-filter-changed', async () => {
+ if (this._parentalControlsManager.initialized) {
+ this._parentalControlsManager.disconnect(initializedId);
+ resolve(await this.getInitialResultSet(terms, cancellable));
+ }
+ });
+ });
+ }
+
+ let query = terms.join(' ');
+ let groups = Shell.AppSystem.search(query);
+ let usage = Shell.AppUsage.get_default();
+ let results = [];
+
+ groups.forEach(group => {
+ group = group.filter(appID => {
+ const app = this._appSys.lookup_app(appID);
+ return app && this._parentalControlsManager.shouldShowApp(app.app_info);
+ });
+ results = results.concat(group.sort(
+ (a, b) => usage.compare(a, b)));
+ });
+
+ results = results.concat(this._systemActions.getMatchingActions(terms));
+ return new Promise(resolve => resolve(results));
+ }
+
+ getSubsearchResultSet(previousResults, terms, cancellable) {
+ return this.getInitialResultSet(terms, cancellable);
+ }
+
+ createResultObject(resultMeta) {
+ if (resultMeta.id.endsWith('.desktop')) {
+ return new AppIcon(this._appSys.lookup_app(resultMeta['id']), {
+ expandTitleOnHover: false,
+ });
+ } else {
+ return new SystemActionIcon(this, resultMeta);
+ }
+ }
+};
+
+var AppViewItem = GObject.registerClass(
+class AppViewItem extends St.Button {
+ _init(params = {}, isDraggable = true, expandTitleOnHover = true) {
+ super._init({
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ reactive: true,
+ button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
+ can_focus: true,
+ ...params,
+ });
+
+ this._delegate = this;
+
+ if (isDraggable) {
+ this._draggable = DND.makeDraggable(this, { timeoutThreshold: 200 });
+
+ this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
+ this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
+ this._draggable.connect('drag-end', this._onDragEnd.bind(this));
+ }
+
+ this._otherIconIsHovering = false;
+ this._expandTitleOnHover = expandTitleOnHover;
+
+ if (expandTitleOnHover)
+ this.connect('notify::hover', this._onHover.bind(this));
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ if (this._dragMonitor) {
+ DND.removeDragMonitor(this._dragMonitor);
+ this._dragMonitor = null;
+ }
+
+ if (this._draggable) {
+ if (this._dragging)
+ Main.overview.endItemDrag(this);
+ this._draggable = null;
+ }
+ }
+
+ _updateMultiline() {
+ if (!this._expandTitleOnHover || !this.icon.label)
+ return;
+
+ const { label } = this.icon;
+ const { clutterText } = label;
+ 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 = this._forcedHighlight || this.hover || this.has_key_focus();
+ label.save_easing_state();
+ label.set_easing_duration(expand
+ ? APP_ICON_TITLE_EXPAND_TIME
+ : APP_ICON_TITLE_COLLAPSE_TIME);
+ clutterText.set({
+ line_wrap: expand,
+ line_wrap_mode: expand ? Pango.WrapMode.WORD_CHAR : Pango.WrapMode.NONE,
+ ellipsize: expand ? Pango.EllipsizeMode.NONE : Pango.EllipsizeMode.END,
+ });
+ }
+
+ _onHover() {
+ this._updateMultiline();
+ }
+
+ _onDragBegin() {
+ this._dragging = true;
+ this.scaleAndFade();
+ Main.overview.beginItemDrag(this);
+ }
+
+ _onDragCancelled() {
+ this._dragging = false;
+ Main.overview.cancelledItemDrag(this);
+ }
+
+ _onDragEnd() {
+ this._dragging = false;
+ this.undoScaleAndFade();
+ Main.overview.endItemDrag(this);
+ }
+
+ scaleIn() {
+ this.scale_x = 0;
+ this.scale_y = 0;
+
+ this.ease({
+ scale_x: 1,
+ scale_y: 1,
+ duration: APP_ICON_SCALE_IN_TIME,
+ delay: APP_ICON_SCALE_IN_DELAY,
+ mode: Clutter.AnimationMode.EASE_OUT_QUINT,
+ });
+ }
+
+ scaleAndFade() {
+ this.reactive = false;
+ this.ease({
+ scale_x: 0.5,
+ scale_y: 0.5,
+ opacity: 0,
+ });
+ }
+
+ undoScaleAndFade() {
+ this.reactive = true;
+ this.ease({
+ scale_x: 1.0,
+ scale_y: 1.0,
+ opacity: 255,
+ });
+ }
+
+ _canAccept(source) {
+ return source !== this;
+ }
+
+ _setHoveringByDnd(hovering) {
+ if (this._otherIconIsHovering === hovering)
+ return;
+
+ this._otherIconIsHovering = hovering;
+
+ if (hovering) {
+ this._dragMonitor = {
+ dragMotion: this._onDragMotion.bind(this),
+ };
+ DND.addDragMonitor(this._dragMonitor);
+ } else {
+ DND.removeDragMonitor(this._dragMonitor);
+ }
+ }
+
+ _onDragMotion(dragEvent) {
+ if (!this.contains(dragEvent.targetActor))
+ this._setHoveringByDnd(false);
+
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ _withinLeeways(x) {
+ return x < IconGrid.LEFT_DIVIDER_LEEWAY ||
+ x > this.width - IconGrid.RIGHT_DIVIDER_LEEWAY;
+ }
+
+ vfunc_key_focus_in() {
+ this._updateMultiline();
+ super.vfunc_key_focus_in();
+ }
+
+ vfunc_key_focus_out() {
+ this._updateMultiline();
+ super.vfunc_key_focus_out();
+ }
+
+ handleDragOver(source, _actor, x) {
+ if (source === this)
+ return DND.DragMotionResult.NO_DROP;
+
+ if (!this._canAccept(source))
+ return DND.DragMotionResult.CONTINUE;
+
+ if (this._withinLeeways(x)) {
+ this._setHoveringByDnd(false);
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ this._setHoveringByDnd(true);
+
+ return DND.DragMotionResult.MOVE_DROP;
+ }
+
+ acceptDrop(source, _actor, x) {
+ this._setHoveringByDnd(false);
+
+ if (!this._canAccept(source))
+ return false;
+
+ if (this._withinLeeways(x))
+ return false;
+
+ return true;
+ }
+
+ cancelActions() {
+ if (this._draggable)
+ this._draggable.fakeRelease();
+ this.fake_release();
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get name() {
+ return this._name;
+ }
+
+ setForcedHighlight(highlighted) {
+ this._forcedHighlight = highlighted;
+ this.set({
+ track_hover: !highlighted,
+ hover: highlighted,
+ });
+ }
+});
+
+var FolderGrid = GObject.registerClass(
+class FolderGrid extends AppGrid {
+ _init() {
+ super._init({
+ allow_incomplete_pages: false,
+ columns_per_page: 3,
+ rows_per_page: 3,
+ page_halign: Clutter.ActorAlign.CENTER,
+ page_valign: Clutter.ActorAlign.CENTER,
+ });
+
+ this.setGridModes([
+ {
+ rows: 3,
+ columns: 3,
+ },
+ ]);
+ }
+});
+
+var FolderView = GObject.registerClass(
+class FolderView extends BaseAppView {
+ _init(folder, id, parentView) {
+ super._init({
+ layout_manager: new Clutter.BinLayout(),
+ x_expand: true,
+ y_expand: true,
+ gesture_modes: Shell.ActionMode.POPUP,
+ });
+
+ // If it not expand, the parent doesn't take into account its preferred_width when allocating
+ // the second time it allocates, so we apply the "Standard hack for ClutterBinLayout"
+ this._grid.x_expand = true;
+ this._id = id;
+ this._folder = folder;
+ this._parentView = parentView;
+ this._grid._delegate = this;
+
+ this.add_child(this._box);
+
+ let action = new Clutter.PanAction({ interpolate: true });
+ action.connect('pan', this._onPan.bind(this));
+ this._scrollView.add_action(action);
+
+ this._deletingFolder = false;
+ this._appIds = [];
+ this._redisplay();
+ }
+
+ _createGrid() {
+ return new FolderGrid();
+ }
+
+ _getFolderApps() {
+ const appIds = [];
+ const excludedApps = this._folder.get_strv('excluded-apps');
+ const appSys = Shell.AppSystem.get_default();
+ const addAppId = appId => {
+ if (excludedApps.includes(appId))
+ return;
+
+ if (this._appFavorites.isFavorite(appId))
+ return;
+
+ const app = appSys.lookup_app(appId);
+ if (!app)
+ return;
+
+ if (!this._parentalControlsManager.shouldShowApp(app.get_app_info()))
+ return;
+
+ if (appIds.indexOf(appId) !== -1)
+ return;
+
+ appIds.push(appId);
+ };
+
+ 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 = _getCategories(appInfo);
+ if (!_listsIntersect(folderCategories, appCategories))
+ return;
+
+ addAppId(appInfo.get_id());
+ });
+
+ return appIds;
+ }
+
+ _getItemPosition(item) {
+ const appIndex = this._appIds.indexOf(item.id);
+
+ if (appIndex === -1)
+ return [-1, -1];
+
+ const { itemsPerPage } = this._grid;
+ return [Math.floor(appIndex / itemsPerPage), appIndex % itemsPerPage];
+ }
+
+ _compareItems(a, b) {
+ const aPosition = this._appIds.indexOf(a.id);
+ const bPosition = this._appIds.indexOf(b.id);
+
+ if (aPosition === -1 && bPosition === -1)
+ return a.name.localeCompare(b.name);
+ else if (aPosition === -1)
+ return 1;
+ else if (bPosition === -1)
+ return -1;
+
+ return aPosition - bPosition;
+ }
+
+ 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;`,
+ });
+
+ let subSize = Math.floor(FOLDER_SUBICON_FRACTION * size);
+
+ let numItems = this._orderedItems.length;
+ let rtl = icon.get_text_direction() == Clutter.TextDirection.RTL;
+ for (let i = 0; i < 4; i++) {
+ const style = `width: ${subSize}px; height: ${subSize}px;`;
+ let bin = new St.Bin({ style });
+ if (i < numItems)
+ bin.child = this._orderedItems[i].app.create_icon_texture(subSize);
+ layout.attach(bin, rtl ? (i + 1) % 2 : i % 2, Math.floor(i / 2), 1, 1);
+ }
+
+ return icon;
+ }
+
+ _onPan(action) {
+ let [dist_, dx_, dy] = action.get_motion_delta(0);
+ let adjustment = this._scrollView.vscroll.adjustment;
+ adjustment.value -= (dy / this._scrollView.height) * adjustment.page_size;
+ return false;
+ }
+
+ _loadApps() {
+ let apps = [];
+ let appSys = Shell.AppSystem.get_default();
+
+ this._appIds.forEach(appId => {
+ const app = appSys.lookup_app(appId);
+
+ let icon = this._items.get(appId);
+ if (!icon)
+ icon = new AppIcon(app);
+
+ apps.push(icon);
+ });
+
+ return apps;
+ }
+
+ _redisplay() {
+ // Keep the app ids list cached
+ this._appIds = this._getFolderApps();
+
+ super._redisplay();
+ }
+
+ acceptDrop(source) {
+ if (!super.acceptDrop(source))
+ return false;
+
+ const folderApps = this._orderedItems.map(item => item.id);
+ this._folder.set_strv('apps', folderApps);
+
+ return true;
+ }
+
+ addApp(app) {
+ let folderApps = this._folder.get_strv('apps');
+ folderApps.push(app.id);
+
+ this._folder.set_strv('apps', folderApps);
+
+ // Also remove from 'excluded-apps' if the app id is listed
+ // there. This is only possible on categories-based folders.
+ let excludedApps = this._folder.get_strv('excluded-apps');
+ let index = excludedApps.indexOf(app.id);
+ if (index >= 0) {
+ excludedApps.splice(index, 1);
+ this._folder.set_strv('excluded-apps', excludedApps);
+ }
+ }
+
+ removeApp(app) {
+ let folderApps = this._folder.get_strv('apps');
+ let index = folderApps.indexOf(app.id);
+ if (index >= 0)
+ folderApps.splice(index, 1);
+
+ // Remove the folder if this is the last app icon; otherwise,
+ // just remove the icon
+ if (folderApps.length == 0) {
+ this._deletingFolder = true;
+
+ // Resetting all keys deletes the relocatable schema
+ let keys = this._folder.settings_schema.list_keys();
+ for (const key of keys)
+ this._folder.reset(key);
+
+ let settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' });
+ let folders = settings.get_strv('folder-children');
+ folders.splice(folders.indexOf(this._id), 1);
+ settings.set_strv('folder-children', folders);
+
+ this._deletingFolder = false;
+ } else {
+ // If this is a categories-based folder, also add it to
+ // the list of excluded apps
+ const categories = this._folder.get_strv('categories');
+ if (categories.length > 0) {
+ const excludedApps = this._folder.get_strv('excluded-apps');
+ excludedApps.push(app.id);
+ this._folder.set_strv('excluded-apps', excludedApps);
+ }
+
+ this._folder.set_strv('apps', folderApps);
+ }
+ }
+
+ get deletingFolder() {
+ return this._deletingFolder;
+ }
+});
+
+var FolderIcon = GObject.registerClass({
+ Signals: {
+ 'apps-changed': {},
+ },
+}, class FolderIcon extends AppViewItem {
+ _init(id, path, parentView) {
+ super._init({
+ style_class: 'app-well-app app-folder',
+ button_mask: St.ButtonMask.ONE,
+ toggle_mode: true,
+ can_focus: true,
+ }, global.settings.is_writable('app-picker-layout'));
+ this._id = id;
+ this._name = '';
+ this._parentView = parentView;
+
+ this._folder = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.app-folders.folder',
+ path,
+ });
+
+ this.icon = new IconGrid.BaseIcon('', {
+ createIcon: this._createIcon.bind(this),
+ setSizeManually: true,
+ });
+ this.set_child(this.icon);
+ this.label_actor = this.icon.label;
+
+ this.view = new FolderView(this._folder, id, parentView);
+
+ this._folder.connectObject(
+ 'changed', this._sync.bind(this), this);
+ this._sync();
+ }
+
+ _onDestroy() {
+ super._onDestroy();
+
+ if (this._dialog)
+ this._dialog.destroy();
+ else
+ this.view.destroy();
+ }
+
+ vfunc_clicked() {
+ this.open();
+ }
+
+ vfunc_unmap() {
+ if (this._dialog)
+ this._dialog.popdown();
+
+ super.vfunc_unmap();
+ }
+
+ open() {
+ this._ensureFolderDialog();
+ this.view._scrollView.vscroll.adjustment.value = 0;
+ this._dialog.popup();
+ }
+
+ getAppIds() {
+ return this.view.getAllItems().map(item => item.id);
+ }
+
+ _setHoveringByDnd(hovering) {
+ if (this._otherIconIsHovering == hovering)
+ return;
+
+ super._setHoveringByDnd(hovering);
+
+ if (hovering)
+ this.add_style_pseudo_class('drop');
+ else
+ this.remove_style_pseudo_class('drop');
+ }
+
+ _onDragMotion(dragEvent) {
+ if (!this._canAccept(dragEvent.source))
+ this._setHoveringByDnd(false);
+
+ return super._onDragMotion(dragEvent);
+ }
+
+ getDragActor() {
+ const iconParams = {
+ createIcon: this._createIcon.bind(this),
+ showLabel: this.icon.label !== null,
+ setSizeManually: false,
+ };
+
+ const icon = new IconGrid.BaseIcon(this.name, iconParams);
+ icon.style_class = this.style_class;
+
+ return icon;
+ }
+
+ getDragActorSource() {
+ return this;
+ }
+
+ _canAccept(source) {
+ if (!(source instanceof AppIcon))
+ return false;
+
+ let view = _getViewFromIcon(source);
+ if (!view || !(view instanceof AppDisplay))
+ return false;
+
+ if (this._folder.get_strv('apps').includes(source.id))
+ return false;
+
+ return true;
+ }
+
+ acceptDrop(source) {
+ const accepted = super.acceptDrop(source);
+
+ if (!accepted)
+ return false;
+
+ this.view.addApp(source.app);
+
+ return true;
+ }
+
+ _updateName() {
+ let name = _getFolderName(this._folder);
+ if (this.name == name)
+ return;
+
+ this._name = name;
+ this.icon.label.text = this.name;
+ }
+
+ _sync() {
+ if (this.view.deletingFolder)
+ return;
+
+ this.emit('apps-changed');
+ this._updateName();
+ this.visible = this.view.getAllItems().length > 0;
+ this.icon.update();
+ }
+
+ _createIcon(iconSize) {
+ return this.view.createFolderIcon(iconSize, this);
+ }
+
+ _ensureFolderDialog() {
+ if (this._dialog)
+ return;
+ if (!this._dialog) {
+ this._dialog = new AppFolderDialog(this, this._folder,
+ this._parentView);
+ this._parentView.addFolderDialog(this._dialog);
+ this._dialog.connect('open-state-changed', (popup, isOpen) => {
+ const duration = FOLDER_DIALOG_ANIMATION_TIME / 2;
+ const mode = isOpen
+ ? Clutter.AnimationMode.EASE_OUT_QUAD
+ : Clutter.AnimationMode.EASE_IN_QUAD;
+
+ this.ease({
+ opacity: isOpen ? 0 : 255,
+ duration,
+ mode,
+ delay: isOpen ? 0 : FOLDER_DIALOG_ANIMATION_TIME - duration,
+ });
+
+ if (!isOpen)
+ this.checked = false;
+ });
+ }
+ }
+});
+
+var AppFolderDialog = GObject.registerClass({
+ Signals: {
+ 'open-state-changed': { param_types: [GObject.TYPE_BOOLEAN] },
+ },
+}, class AppFolderDialog extends St.Bin {
+ _init(source, folder, appDisplay) {
+ super._init({
+ visible: false,
+ x_expand: true,
+ y_expand: true,
+ reactive: true,
+ });
+
+ this.add_constraint(new Layout.MonitorConstraint({ primary: true }));
+
+ const clickAction = new Clutter.ClickAction();
+ clickAction.connect('clicked', () => {
+ const [x, y] = clickAction.get_coords();
+ const actor =
+ global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
+
+ if (actor === this)
+ this.popdown();
+ });
+ this.add_action(clickAction);
+
+ this._source = source;
+ this._folder = folder;
+ this._view = source.view;
+ this._appDisplay = appDisplay;
+ this._delegate = this;
+
+ this._isOpen = false;
+
+ this._viewBox = new St.BoxLayout({
+ style_class: 'app-folder-dialog',
+ x_expand: true,
+ y_expand: true,
+ x_align: Clutter.ActorAlign.FILL,
+ y_align: Clutter.ActorAlign.FILL,
+ vertical: true,
+ });
+
+ this.child = new St.Bin({
+ style_class: 'app-folder-dialog-container',
+ child: this._viewBox,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ this._addFolderNameEntry();
+ this._viewBox.add_child(this._view);
+
+ global.focus_manager.add_group(this);
+
+ this._grabHelper = new GrabHelper.GrabHelper(this, {
+ actionMode: Shell.ActionMode.POPUP,
+ });
+ this.connect('destroy', this._onDestroy.bind(this));
+
+ this._dragMonitor = null;
+ this._sourceMappedId = 0;
+ this._popdownTimeoutId = 0;
+ this._needsZoomAndFade = false;
+
+ this._popdownCallbacks = [];
+ }
+
+ _addFolderNameEntry() {
+ this._entryBox = new St.BoxLayout({
+ style_class: 'folder-name-container',
+ });
+ this._viewBox.add_child(this._entryBox);
+
+ // Empty actor to center the title
+ let ghostButton = new Clutter.Actor();
+ this._entryBox.add_child(ghostButton);
+
+ let stack = new Shell.Stack({
+ x_expand: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this._entryBox.add_child(stack);
+
+ // Folder name label
+ this._folderNameLabel = new St.Label({
+ style_class: 'folder-name-label',
+ x_expand: true,
+ y_expand: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ stack.add_child(this._folderNameLabel);
+
+ // Folder name entry
+ this._entry = new St.Entry({
+ style_class: 'folder-name-entry',
+ opacity: 0,
+ reactive: false,
+ });
+ this._entry.clutter_text.set({
+ x_expand: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+
+ this._entry.clutter_text.connect('activate', () => {
+ this._showFolderLabel();
+ });
+
+ stack.add_child(this._entry);
+
+ // Edit button
+ this._editButton = new St.Button({
+ style_class: 'edit-folder-button',
+ button_mask: St.ButtonMask.ONE,
+ toggle_mode: true,
+ reactive: true,
+ can_focus: true,
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.CENTER,
+ icon_name: 'document-edit-symbolic',
+ });
+
+ this._editButton.connect('notify::checked', () => {
+ if (this._editButton.checked)
+ this._showFolderEntry();
+ else
+ this._showFolderLabel();
+ });
+
+ this._entryBox.add_child(this._editButton);
+
+ ghostButton.add_constraint(new Clutter.BindConstraint({
+ source: this._editButton,
+ coordinate: Clutter.BindCoordinate.SIZE,
+ }));
+
+ this._folder.connect('changed::name', () => this._syncFolderName());
+ this._syncFolderName();
+ }
+
+ _syncFolderName() {
+ let newName = _getFolderName(this._folder);
+
+ this._folderNameLabel.text = newName;
+ this._entry.text = newName;
+ }
+
+ _switchActor(from, to) {
+ to.reactive = true;
+ to.ease({
+ opacity: 255,
+ duration: 300,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ from.ease({
+ opacity: 0,
+ duration: 300,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ from.reactive = false;
+ },
+ });
+ }
+
+ _showFolderLabel() {
+ if (this._editButton.checked)
+ this._editButton.checked = false;
+
+ this._maybeUpdateFolderName();
+ this._switchActor(this._entry, this._folderNameLabel);
+ }
+
+ _showFolderEntry() {
+ this._switchActor(this._folderNameLabel, this._entry);
+
+ this._entry.clutter_text.set_selection(0, -1);
+ this._entry.clutter_text.grab_key_focus();
+ }
+
+ _maybeUpdateFolderName() {
+ let folderName = _getFolderName(this._folder);
+ let newFolderName = this._entry.text.trim();
+
+ if (newFolderName.length === 0 || newFolderName === folderName)
+ return;
+
+ this._folder.set_string('name', newFolderName);
+ this._folder.set_boolean('translate', false);
+ }
+
+ _zoomAndFadeIn() {
+ let [sourceX, sourceY] =
+ this._source.get_transformed_position();
+ let [dialogX, dialogY] =
+ this.child.get_transformed_position();
+
+ 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: 0,
+ translation_y: 0,
+ scale_x: 1,
+ scale_y: 1,
+ opacity: 255,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ this._needsZoomAndFade = false;
+
+ if (this._sourceMappedId === 0) {
+ this._sourceMappedId = this._source.connect(
+ 'notify::mapped', this._zoomAndFadeOut.bind(this));
+ }
+ }
+
+ _zoomAndFadeOut() {
+ if (!this._isOpen)
+ return;
+
+ if (!this._source.mapped) {
+ this.hide();
+ return;
+ }
+
+ 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,
+ translation_y: sourceY - dialogY,
+ 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;
+ }
+
+ _removeDragMonitor() {
+ if (!this._dragMonitor)
+ return;
+
+ DND.removeDragMonitor(this._dragMonitor);
+ this._dragMonitor = null;
+ }
+
+ _removePopdownTimeout() {
+ if (this._popdownTimeoutId === 0)
+ return;
+
+ GLib.source_remove(this._popdownTimeoutId);
+ this._popdownTimeoutId = 0;
+ }
+
+ _onDestroy() {
+ if (this._isOpen) {
+ this._isOpen = false;
+ this._grabHelper.ungrab({ actor: this });
+ this._grabHelper = null;
+ }
+
+ if (this._sourceMappedId) {
+ this._source.disconnect(this._sourceMappedId);
+ this._sourceMappedId = 0;
+ }
+
+ this._removePopdownTimeout();
+ this._removeDragMonitor();
+ }
+
+ vfunc_allocate(box) {
+ super.vfunc_allocate(box);
+
+ // We can only start zooming after receiving an allocation
+ if (this._needsZoomAndFade)
+ this._zoomAndFadeIn();
+ }
+
+ vfunc_key_press_event(keyEvent) {
+ if (global.stage.get_key_focus() != this)
+ return Clutter.EVENT_PROPAGATE;
+
+ // Since we need to only grab focus on one item child when the user
+ // actually press a key we don't use navigate_focus when opening
+ // the popup.
+ // Instead of that, grab the focus on the AppFolderPopup actor
+ // and actually moves the focus to a child only when the user
+ // actually press a key.
+ // It should work with just grab_key_focus on the AppFolderPopup
+ // actor, but since the arrow keys are not wrapping_around the focus
+ // is not grabbed by a child when the widget that has the current focus
+ // is the same that is requesting focus, so to make it works with arrow
+ // keys we need to connect to the key-press-event and navigate_focus
+ // when that happens using TAB_FORWARD or TAB_BACKWARD instead of arrow
+ // keys
+
+ // Use TAB_FORWARD for down key and right key
+ // and TAB_BACKWARD for up key and left key on ltr
+ // languages
+ let direction;
+ let isLtr = Clutter.get_default_text_direction() == Clutter.TextDirection.LTR;
+ switch (keyEvent.keyval) {
+ case Clutter.KEY_Down:
+ direction = St.DirectionType.TAB_FORWARD;
+ break;
+ case Clutter.KEY_Right:
+ direction = isLtr
+ ? St.DirectionType.TAB_FORWARD
+ : St.DirectionType.TAB_BACKWARD;
+ break;
+ case Clutter.KEY_Up:
+ direction = St.DirectionType.TAB_BACKWARD;
+ break;
+ case Clutter.KEY_Left:
+ direction = isLtr
+ ? St.DirectionType.TAB_BACKWARD
+ : St.DirectionType.TAB_FORWARD;
+ break;
+ default:
+ return Clutter.EVENT_PROPAGATE;
+ }
+ return this.navigate_focus(null, direction, 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,
+ });
+ }
+
+ _withinDialog(x, y) {
+ const childExtents = this.child.get_transformed_extents();
+ return childExtents.contains_point(new Graphene.Point({ x, y }));
+ }
+
+ _setupDragMonitor() {
+ if (this._dragMonitor)
+ return;
+
+ this._dragMonitor = {
+ dragMotion: dragEvent => {
+ const withinDialog =
+ this._withinDialog(dragEvent.x, dragEvent.y);
+
+ this._setLighterBackground(!withinDialog);
+
+ if (withinDialog) {
+ this._removePopdownTimeout();
+ this._removeDragMonitor();
+ }
+ return DND.DragMotionResult.CONTINUE;
+ },
+ };
+ DND.addDragMonitor(this._dragMonitor);
+ }
+
+ _setupPopdownTimeout() {
+ if (this._popdownTimeoutId > 0)
+ return;
+
+ this._popdownTimeoutId =
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, POPDOWN_DIALOG_TIMEOUT, () => {
+ this._popdownTimeoutId = 0;
+ this.popdown();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ handleDragOver(source, actor, x, y) {
+ if (this._withinDialog(x, y)) {
+ this._setLighterBackground(false);
+ this._removePopdownTimeout();
+ this._removeDragMonitor();
+ } else {
+ this._setupPopdownTimeout();
+ this._setupDragMonitor();
+ }
+
+ return DND.DragMotionResult.MOVE_DROP;
+ }
+
+ acceptDrop(source) {
+ const appId = source.id;
+
+ this.popdown(() => {
+ this._view.removeApp(source);
+ this._appDisplay.selectApp(appId);
+ });
+
+ return true;
+ }
+
+ toggle() {
+ if (this._isOpen)
+ this.popdown();
+ else
+ this.popup();
+ }
+
+ popup() {
+ if (this._isOpen)
+ return;
+
+ 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);
+ }
+
+ popdown(callback) {
+ // Either call the callback right away, or wait for the zoom out
+ // animation to finish
+ if (callback) {
+ if (this.visible)
+ this._popdownCallbacks.push(callback);
+ else
+ callback();
+ }
+
+ if (!this._isOpen)
+ return;
+
+ this._zoomAndFadeOut();
+ this._showFolderLabel();
+
+ this._isOpen = false;
+ this._grabHelper.ungrab({ actor: this });
+ this.emit('open-state-changed', false);
+ }
+});
+
+var AppIcon = GObject.registerClass({
+ Signals: {
+ 'menu-state-changed': { param_types: [GObject.TYPE_BOOLEAN] },
+ 'sync-tooltip': {},
+ },
+}, class AppIcon extends AppViewItem {
+ _init(app, iconParams = {}) {
+ // Get the isDraggable property without passing it on to the BaseIcon:
+ const appIconParams = Params.parse(iconParams, { isDraggable: true }, true);
+ const isDraggable = appIconParams['isDraggable'];
+ delete iconParams['isDraggable'];
+ const expandTitleOnHover = appIconParams['expandTitleOnHover'];
+ delete iconParams['expandTitleOnHover'];
+
+ super._init({ style_class: 'app-well-app' }, isDraggable, expandTitleOnHover);
+
+ this.app = app;
+ this._id = app.get_id();
+ this._name = app.get_name();
+
+ this._iconContainer = new St.Widget({
+ layout_manager: new Clutter.BinLayout(),
+ x_expand: true,
+ y_expand: true,
+ });
+
+ this.set_child(this._iconContainer);
+
+ this._folderPreviewId = 0;
+
+ iconParams['createIcon'] = this._createIcon.bind(this);
+ iconParams['setSizeManually'] = true;
+ this.icon = new IconGrid.BaseIcon(app.get_name(), iconParams);
+ this._iconContainer.add_child(this.icon);
+
+ this._dot = new St.Widget({
+ style_class: 'app-well-app-running-dot',
+ layout_manager: new Clutter.BinLayout(),
+ x_expand: true,
+ y_expand: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.END,
+ });
+ this._iconContainer.add_child(this._dot);
+
+ this.label_actor = this.icon.label;
+
+ this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this));
+
+ this._menu = null;
+ this._menuManager = new PopupMenu.PopupMenuManager(this);
+
+ this._menuTimeoutId = 0;
+ this.app.connectObject('notify::state',
+ () => this._updateRunningStyle(), this);
+ this._updateRunningStyle();
+ }
+
+ _onDestroy() {
+ super._onDestroy();
+
+ if (this._folderPreviewId > 0) {
+ GLib.source_remove(this._folderPreviewId);
+ this._folderPreviewId = 0;
+ }
+
+ this._removeMenuTimeout();
+ }
+
+ _onDragBegin() {
+ if (this._menu)
+ this._menu.close(true);
+ this._removeMenuTimeout();
+ super._onDragBegin();
+ }
+
+ _createIcon(iconSize) {
+ return this.app.create_icon_texture(iconSize);
+ }
+
+ _removeMenuTimeout() {
+ if (this._menuTimeoutId > 0) {
+ GLib.source_remove(this._menuTimeoutId);
+ this._menuTimeoutId = 0;
+ }
+ }
+
+ _updateRunningStyle() {
+ if (this.app.state != Shell.AppState.STOPPED)
+ this._dot.show();
+ else
+ this._dot.hide();
+ }
+
+ _setPopupTimeout() {
+ this._removeMenuTimeout();
+ this._menuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MENU_POPUP_TIMEOUT, () => {
+ this._menuTimeoutId = 0;
+ this.popupMenu();
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._menuTimeoutId, '[gnome-shell] this.popupMenu');
+ }
+
+ vfunc_leave_event(crossingEvent) {
+ const ret = super.vfunc_leave_event(crossingEvent);
+
+ this.fake_release();
+ this._removeMenuTimeout();
+ return ret;
+ }
+
+ vfunc_button_press_event(buttonEvent) {
+ const ret = super.vfunc_button_press_event(buttonEvent);
+ if (buttonEvent.button == 1) {
+ this._setPopupTimeout();
+ } else if (buttonEvent.button == 3) {
+ this.popupMenu();
+ return Clutter.EVENT_STOP;
+ }
+ return ret;
+ }
+
+ vfunc_touch_event(touchEvent) {
+ const ret = super.vfunc_touch_event(touchEvent);
+ if (touchEvent.type == Clutter.EventType.TOUCH_BEGIN)
+ this._setPopupTimeout();
+
+ return ret;
+ }
+
+ vfunc_clicked(button) {
+ this._removeMenuTimeout();
+ this.activate(button);
+ }
+
+ _onKeyboardPopupMenu() {
+ this.popupMenu();
+ this._menu.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
+ }
+
+ getId() {
+ return this.app.get_id();
+ }
+
+ popupMenu(side = St.Side.LEFT) {
+ this.setForcedHighlight(true);
+ this._removeMenuTimeout();
+ this.fake_release();
+
+ if (!this._menu) {
+ this._menu = new AppMenu(this, side, {
+ favoritesSection: true,
+ showSingleWindows: true,
+ });
+ this._menu.setApp(this.app);
+ this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
+ if (!isPoppedUp)
+ this._onMenuPoppedDown();
+ });
+ Main.overview.connectObject('hiding',
+ () => this._menu.close(), this);
+
+ Main.uiGroup.add_actor(this._menu.actor);
+ this._menuManager.addMenu(this._menu);
+ }
+
+ this.emit('menu-state-changed', true);
+
+ this._menu.open(BoxPointer.PopupAnimation.FULL);
+ this._menuManager.ignoreRelease();
+ this.emit('sync-tooltip');
+
+ return false;
+ }
+
+ _onMenuPoppedDown() {
+ this.setForcedHighlight(false);
+ this.emit('menu-state-changed', false);
+ }
+
+ activate(button) {
+ let event = Clutter.get_current_event();
+ let modifiers = event ? event.get_state() : 0;
+ let isMiddleButton = button && button == Clutter.BUTTON_MIDDLE;
+ let isCtrlPressed = (modifiers & Clutter.ModifierType.CONTROL_MASK) != 0;
+ let openNewWindow = this.app.can_open_new_window() &&
+ this.app.state == Shell.AppState.RUNNING &&
+ (isCtrlPressed || isMiddleButton);
+
+ if (this.app.state == Shell.AppState.STOPPED || openNewWindow)
+ this.animateLaunch();
+
+ if (openNewWindow)
+ this.app.open_new_window(-1);
+ else
+ this.app.activate();
+
+ Main.overview.hide();
+ }
+
+ animateLaunch() {
+ this.icon.animateZoomOut();
+ }
+
+ animateLaunchAtPos(x, y) {
+ this.icon.animateZoomOutAtPos(x, y);
+ }
+
+ shellWorkspaceLaunch(params) {
+ let { stack } = new Error();
+ log(`shellWorkspaceLaunch is deprecated, use app.open_new_window() instead\n${stack}`);
+
+ params = Params.parse(params, {
+ workspace: -1,
+ timestamp: 0,
+ });
+
+ this.app.open_new_window(params.workspace);
+ }
+
+ getDragActor() {
+ return this.app.create_icon_texture(Main.overview.dash.iconSize);
+ }
+
+ // Returns the original actor that should align with the actor
+ // we show as the item is being dragged.
+ getDragActorSource() {
+ return this.icon.icon;
+ }
+
+ shouldShowTooltip() {
+ return this.hover && (!this._menu || !this._menu.isOpen);
+ }
+
+ _showFolderPreview() {
+ this.icon.label.opacity = 0;
+ this.icon.icon.ease({
+ scale_x: FOLDER_SUBICON_FRACTION,
+ scale_y: FOLDER_SUBICON_FRACTION,
+ });
+ }
+
+ _hideFolderPreview() {
+ this.icon.label.opacity = 255;
+ this.icon.icon.ease({
+ scale_x: 1.0,
+ scale_y: 1.0,
+ });
+ }
+
+ _canAccept(source) {
+ let view = _getViewFromIcon(source);
+
+ return source != this &&
+ (source instanceof this.constructor) &&
+ (view instanceof AppDisplay);
+ }
+
+ _setHoveringByDnd(hovering) {
+ if (this._otherIconIsHovering == hovering)
+ return;
+
+ super._setHoveringByDnd(hovering);
+
+ if (hovering) {
+ if (this._folderPreviewId > 0)
+ return;
+
+ this._folderPreviewId =
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
+ this.add_style_pseudo_class('drop');
+ this._showFolderPreview();
+ this._folderPreviewId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ } else {
+ if (this._folderPreviewId > 0) {
+ GLib.source_remove(this._folderPreviewId);
+ this._folderPreviewId = 0;
+ }
+ this._hideFolderPreview();
+ this.remove_style_pseudo_class('drop');
+ }
+ }
+
+ acceptDrop(source, actor, x) {
+ const accepted = super.acceptDrop(source, actor, x);
+ if (!accepted)
+ return false;
+
+ let view = _getViewFromIcon(this);
+ let apps = [this.id, source.id];
+
+ return view?.createFolder(apps);
+ }
+
+ cancelActions() {
+ if (this._menu)
+ this._menu.close(true);
+ this._removeMenuTimeout();
+ super.cancelActions();
+ }
+});
+
+var SystemActionIcon = GObject.registerClass(
+class SystemActionIcon extends Search.GridSearchResult {
+ activate() {
+ SystemActions.getDefault().activateAction(this.metaInfo['id']);
+ Main.overview.hide();
+ }
+});