From 06a7ab871f82d285ee00ffaf53443837189f7a3a Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 1 Aug 2019 20:54:42 -0300 Subject: [PATCH 01/36] st/texture-cache: Monitor GtkIconTheme changes The texture cache, right now, only monitors for complete theme changes. If the contents of the icon theme change, however, the texture cache isn't properly invalidated. This manifests itself as a randomly reproducible bug when installing an app; the app icon may be the fallback forever, or as long as something else updates the icon theme. Watch for the GtkIconTheme:changed signal, and evict the texture cache when the theme changes. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/661 --- src/st/st-texture-cache.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/st/st-texture-cache.c b/src/st/st-texture-cache.c index eec28007b3..bf54c65140 100644 --- a/src/st/st-texture-cache.c +++ b/src/st/st-texture-cache.c @@ -150,6 +150,14 @@ on_icon_theme_changed (StSettings *settings, g_signal_emit (cache, signals[ICON_THEME_CHANGED], 0); } +static void +on_gtk_icon_theme_changed (GtkIconTheme *icon_theme, + StTextureCache *self) +{ + st_texture_cache_evict_icons (self); + g_signal_emit (self, signals[ICON_THEME_CHANGED], 0); +} + static void st_texture_cache_init (StTextureCache *self) { @@ -160,6 +168,8 @@ st_texture_cache_init (StTextureCache *self) self->priv->icon_theme = gtk_icon_theme_new (); gtk_icon_theme_add_resource_path (self->priv->icon_theme, "/org/gnome/shell/theme/icons"); + g_signal_connect (self->priv->icon_theme, "changed", + G_CALLBACK (on_gtk_icon_theme_changed), self); settings = st_settings_get (); g_signal_connect (settings, "notify::gtk-icon-theme", -- GitLab From 6c85bd6aebec24abc7065a16ba2db9208aa04678 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 1 Aug 2019 20:58:20 -0300 Subject: [PATCH 02/36] shell/app-system: Monitor for icon theme changes Whenever an app is installed, the usual routine is to run 'gtk-update-icon-cache' after installing all of the app's files. The side effect of that is that the .desktop file of the application is installed before the icon theme is updated. By the time GAppInfoMonitor emits the 'changed' signal, the icon theme is not yet updated, leading to StIcon use the fallback icon. Under some circumstances (e.g. on very slow spinning disks) the app icon is never actually loaded, and we see the fallback icon forever. Monitor the icon theme for changes when an app is installed. Try as many as 6 times before giving up on detecting an icon theme update. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/661 --- src/shell-app-system.c | 54 +++++++++++++++++++++++++++++++++++++++ src/st/st-texture-cache.c | 8 ++++++ src/st/st-texture-cache.h | 2 ++ 3 files changed, 64 insertions(+) diff --git a/src/shell-app-system.c b/src/shell-app-system.c index f632cbe545..127f29ef02 100644 --- a/src/shell-app-system.c +++ b/src/shell-app-system.c @@ -14,6 +14,14 @@ #include "shell-app-system-private.h" #include "shell-global.h" #include "shell-util.h" +#include "st.h" + +/* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That + * should be plenty of time for even a slow spinning drive to update + * the icon cache. + */ +#define RESCAN_TIMEOUT_MS 2500 +#define MAX_RESCAN_RETRIES 6 /* Vendor prefixes are something that can be preprended to a .desktop * file name. Undo this. @@ -51,6 +59,9 @@ struct _ShellAppSystemPrivate { GHashTable *id_to_app; GHashTable *startup_wm_class_to_id; GList *installed_apps; + + guint rescan_icons_timeout_id; + guint n_rescan_retries; }; static void shell_app_system_finalize (GObject *object); @@ -157,12 +168,54 @@ stale_app_remove_func (gpointer key, return app_is_stale (value); } +static gboolean +rescan_icon_theme_cb (gpointer user_data) +{ + ShellAppSystemPrivate *priv; + ShellAppSystem *self; + StTextureCache *texture_cache; + gboolean rescanned; + + self = (ShellAppSystem *) user_data; + priv = self->priv; + + texture_cache = st_texture_cache_get_default (); + rescanned = st_texture_cache_rescan_icon_theme (texture_cache); + + priv->n_rescan_retries++; + + if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES) + { + priv->n_rescan_retries = 0; + priv->rescan_icons_timeout_id = 0; + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +rescan_icon_theme (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv = self->priv; + + priv->n_rescan_retries = 0; + + if (priv->rescan_icons_timeout_id > 0) + return; + + priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS, + rescan_icon_theme_cb, + self); +} + static void installed_changed (GAppInfoMonitor *monitor, gpointer user_data) { ShellAppSystem *self = user_data; + rescan_icon_theme (self); scan_startup_wm_class_to_id (self); g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL); @@ -200,6 +253,7 @@ shell_app_system_finalize (GObject *object) g_hash_table_destroy (priv->id_to_app); g_hash_table_destroy (priv->startup_wm_class_to_id); g_list_free_full (priv->installed_apps, g_object_unref); + g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove); G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object); } diff --git a/src/st/st-texture-cache.c b/src/st/st-texture-cache.c index bf54c65140..53df7195b8 100644 --- a/src/st/st-texture-cache.c +++ b/src/st/st-texture-cache.c @@ -1567,3 +1567,11 @@ st_texture_cache_get_default (void) instance = g_object_new (ST_TYPE_TEXTURE_CACHE, NULL); return instance; } + +gboolean +st_texture_cache_rescan_icon_theme (StTextureCache *cache) +{ + StTextureCachePrivate *priv = cache->priv; + + return gtk_icon_theme_rescan_if_needed (priv->icon_theme); +} diff --git a/src/st/st-texture-cache.h b/src/st/st-texture-cache.h index 11d1c4e640..a99316da8f 100644 --- a/src/st/st-texture-cache.h +++ b/src/st/st-texture-cache.h @@ -113,4 +113,6 @@ CoglTexture * st_texture_cache_load (StTextureCache *cache, void *data, GError **error); +gboolean st_texture_cache_rescan_icon_theme (StTextureCache *cache); + #endif /* __ST_TEXTURE_CACHE_H__ */ -- GitLab From abe9b82c484606d0c574494355b4d809c50f52f7 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sat, 29 Jun 2019 12:58:26 -0300 Subject: [PATCH 03/36] folderIcon: Move app icon loading to FolderView Future patches will diff the old and new icons of views, in order to animate them when necessary (e.g. adding an app icon to a folder, or from a folder back to the app grid). In order to do that, all views must be streamlined in how they load app icons. Currently, FrequentView and AllView are already following the behavior expected by BaseAppView, but FolderView isn't. Its icons are loaded by FolderIcon, and FolderView doesn't implement BaseView._loadApps(), which makes it impossible to diff old and new apps. Move the app icon loading routine from FolderIcon to FolderView, by implementing the _loadApps() method. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 83 ++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index a027dc86b1..0cc3d9faa4 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1020,11 +1020,13 @@ var AppSearchProvider = class AppSearchProvider { }; var FolderView = class FolderView extends BaseAppView { - constructor() { + constructor(folder, parentView) { super(null, null); // 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._folder = folder; + this._parentView = parentView; this.actor = new St.ScrollView({ overlay_scrollbars: true }); this.actor.set_policy(St.PolicyType.NEVER, St.PolicyType.AUTOMATIC); @@ -1035,6 +1037,9 @@ var FolderView = class FolderView extends BaseAppView { let action = new Clutter.PanAction({ interpolate: true }); action.connect('pan', this._onPan.bind(this)); this.actor.add_action(action); + + this._folder.connect('changed', this._redisplay.bind(this)); + this._redisplay(); } _childFocused(actor) { @@ -1127,6 +1132,43 @@ var FolderView = class FolderView extends BaseAppView { setPaddingOffsets(offset) { this._offsetForEachSide = offset; } + + _loadApps() { + let excludedApps = this._folder.get_strv('excluded-apps'); + let appSys = Shell.AppSystem.get_default(); + let addAppId = appId => { + if (this.hasItem(appId)) + return; + + if (excludedApps.includes(appId)) + return; + + let app = appSys.lookup_app(appId); + if (!app) + return; + + if (!app.get_app_info().should_show()) + return; + + let icon = new AppIcon(app); + this.addItem(icon); + }; + + let folderApps = this._folder.get_strv('apps'); + folderApps.forEach(addAppId); + + let folderCategories = this._folder.get_strv('categories'); + let appInfos = this._parentView.getAppInfos(); + appInfos.forEach(appInfo => { + let appCategories = _getCategories(appInfo); + if (!_listsIntersect(folderCategories, appCategories)) + return; + + addAppId(appInfo.get_id()); + }); + + this.loadGrid(); + } }; var FolderIcon = class FolderIcon { @@ -1154,7 +1196,7 @@ var FolderIcon = class FolderIcon { this.actor.set_child(this.icon); this.actor.label_actor = this.icon.label; - this.view = new FolderView(); + this.view = new FolderView(this._folder, parentView); this.actor.connect('clicked', this.open.bind(this)); this.actor.connect('destroy', this.onDestroy.bind(this)); @@ -1201,44 +1243,7 @@ var FolderIcon = class FolderIcon { _redisplay() { this._updateName(); - - this.view.removeAll(); - - let excludedApps = this._folder.get_strv('excluded-apps'); - let appSys = Shell.AppSystem.get_default(); - let addAppId = appId => { - if (this.view.hasItem(appId)) - return; - - if (excludedApps.includes(appId)) - return; - - let app = appSys.lookup_app(appId); - if (!app) - return; - - if (!app.get_app_info().should_show()) - return; - - let icon = new AppIcon(app); - this.view.addItem(icon); - }; - - let folderApps = this._folder.get_strv('apps'); - folderApps.forEach(addAppId); - - let folderCategories = this._folder.get_strv('categories'); - let appInfos = this._parentView.getAppInfos(); - appInfos.forEach(appInfo => { - let appCategories = _getCategories(appInfo); - if (!_listsIntersect(folderCategories, appCategories)) - return; - - addAppId(appInfo.get_id()); - }); - this.actor.visible = this.view.getAllItems().length > 0; - this.view.loadGrid(); this.emit('apps-changed'); } -- GitLab From ce78e8ae54043e4d7af7c464c66dc071ef6298e1 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sat, 29 Jun 2019 13:15:20 -0300 Subject: [PATCH 04/36] frequentView: Use BaseAppView.addItem() and loadGrid() FrequentView is another view that is slightly not unified with how BaseAppView expects subclasses to load app icons. Instead of using BaseAppView.addItem() and then calling BaseAppview.loadGrid(), it adds the app icons directly to the icon grid. Make FrequentView add icons using BaseAppview.addItem(), and load the icons using BaseAppView.loadGrid(). https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 0cc3d9faa4..714f6d1481 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -706,6 +706,11 @@ var FrequentView = class FrequentView extends BaseAppView { return this._usage.get_most_used().length >= MIN_FREQUENT_APPS_COUNT; } + _compareItems() { + // The FrequentView does not need to be sorted alphabetically + return 0; + } + _loadApps() { let mostUsed = this._usage.get_most_used(); let hasUsefulData = this.hasUsefulData(); @@ -726,8 +731,10 @@ var FrequentView = class FrequentView extends BaseAppView { continue; let appIcon = new AppIcon(mostUsed[i], { isDraggable: favoritesWritable }); - this._grid.addItem(appIcon, -1); + this.addItem(appIcon); } + + this.loadGrid(); } // Called before allocation to calculate dynamic spacing -- GitLab From 48a2b6cb0bb4a615fd273c9256019786fe17baf8 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sat, 29 Jun 2019 13:22:08 -0300 Subject: [PATCH 05/36] baseAppView: Call loadGrid() directly Now that the three views follow the exact same loading routine (remove all + load apps + load grid), we don't need each view call loadGrid() directly anymore. This is an important step in order to animate adding and removing icons, since now we can diff old and new app icons properly. Move all calls to BaseAppView.loadGrid() to a single one after BaseAppView._loadApps(). Also add the underscore prefix, since this is now considered a protected function. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 714f6d1481..6bdd2ee518 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -123,6 +123,7 @@ class BaseAppView { _redisplay() { this.removeAll(); this._loadApps(); + this._loadGrid(); } getAllItems() { @@ -146,7 +147,7 @@ class BaseAppView { return a.name.localeCompare(b.name); } - loadGrid() { + _loadGrid() { this._allItems.sort(this._compareItems); this._allItems.forEach(item => this._grid.addItem(item)); this.emit('view-loaded'); @@ -417,8 +418,10 @@ var AllView = class AllView extends BaseAppView { { isDraggable: favoritesWritable }); this.addItem(icon); }); + } - this.loadGrid(); + _loadGrid() { + super._loadGrid(); this._refilterApps(); } @@ -733,8 +736,6 @@ var FrequentView = class FrequentView extends BaseAppView { { isDraggable: favoritesWritable }); this.addItem(appIcon); } - - this.loadGrid(); } // Called before allocation to calculate dynamic spacing @@ -1173,8 +1174,6 @@ var FolderView = class FolderView extends BaseAppView { addAppId(appInfo.get_id()); }); - - this.loadGrid(); } }; -- GitLab From 47d2f4dbeb471e4cb97ea50f59fb8373876adeff Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 1 Jul 2019 23:01:59 -0300 Subject: [PATCH 06/36] baseAppView: Add only non-added icons, in order In the close future, BaseAppView will only add new icons (compared to the remove all + readd all approach that is now). With that, the items in the this._allItems array will be iterated multiple times, but items can only be added once, and in the order they're in the array. Add the items in the this._allItems array passing the index they should be added, and don't add icons with a parent already set. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 6bdd2ee518..d760deed99 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -149,7 +149,15 @@ class BaseAppView { _loadGrid() { this._allItems.sort(this._compareItems); - this._allItems.forEach(item => this._grid.addItem(item)); + + this._allItems.forEach((item, index) => { + // Don't readd already added items + if (item.actor.get_parent()) + return; + + this._grid.addItem(item, index); + }); + this.emit('view-loaded'); } -- GitLab From c13efe96dc38253520838b8c951ae4f3396ddc40 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 1 Jul 2019 23:13:07 -0300 Subject: [PATCH 07/36] baseAppView: Only add and remove when necessary BaseAppView currently removes all icons, and readds them, every time the list of app icons needs to be redisplayed. In order to allow animating app icon positions in the future, however, we cannot destroy the actors of the app icons. Previous commits paved the way for us to do differential loading, i.e. add only the icons that were added, and remove only what was removed. Make the BaseAppView effectively implement differential loading. The BaseAppView.removeAll() method is removed, since we do not remove all icons anymore. BaseAppView._loadApps() now returns an array with the new apps, instead of putting them directly at the BaseAppView lists. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 79 +++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index d760deed99..dc75792640 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -114,15 +114,33 @@ class BaseAppView { // Nothing by default } - removeAll() { - this._grid.destroyAll(); - this._items = {}; - this._allItems = []; - } - _redisplay() { - this.removeAll(); - this._loadApps(); + let oldApps = this._allItems.slice(); + let oldAppIds = oldApps.map(icon => icon.id); + + let newApps = this._loadApps(); + 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 => { + let iconIndex = this._allItems.indexOf(icon); + + this._allItems.splice(iconIndex, 1); + this._grid.removeItem(icon); + delete this._items[icon.id]; + }); + + // Add new app icons + addedApps.forEach(icon => { + let iconIndex = newApps.indexOf(icon); + + this._allItems.splice(iconIndex, 0, icon); + this._items[icon.id] = icon; + }); + this._loadGrid(); } @@ -335,26 +353,6 @@ var AllView = class AllView extends BaseAppView { }); } - removeAll() { - this.folderIcons = []; - super.removeAll(); - } - - _redisplay() { - let openFolderId = null; - if (this._displayingPopup && this._currentPopup) - openFolderId = this._currentPopup._source.id; - - super._redisplay(); - - if (openFolderId) { - let [folderToReopen] = this.folderIcons.filter(folder => folder.id == openFolderId); - - if (folderToReopen) - folderToReopen.open(); - } - } - _itemNameChanged(item) { // If an item's name changed, we can pluck it out of where it's // supposed to be and reinsert it where it's sorted. @@ -386,6 +384,7 @@ var AllView = class AllView extends BaseAppView { } _loadApps() { + let newApps = []; this._appInfoList = Shell.AppSystem.get_default().get_installed().filter(appInfo => { try { (appInfo.get_id()); // catch invalid file encodings @@ -399,6 +398,8 @@ var AllView = class AllView extends BaseAppView { let appSys = Shell.AppSystem.get_default(); + this.folderIcons = []; + let folders = this._folderSettings.get_strv('folder-children'); folders.forEach(id => { if (this.hasItem(id)) @@ -407,7 +408,7 @@ var AllView = class AllView extends BaseAppView { let icon = new FolderIcon(id, path, this); icon.connect('name-changed', this._itemNameChanged.bind(this)); icon.connect('apps-changed', this._refilterApps.bind(this)); - this.addItem(icon); + newApps.push(icon); this.folderIcons.push(icon); }); @@ -424,8 +425,10 @@ var AllView = class AllView extends BaseAppView { let icon = new AppIcon(app, { isDraggable: favoritesWritable }); - this.addItem(icon); + newApps.push(icon); }); + + return newApps; } _loadGrid() { @@ -723,11 +726,12 @@ var FrequentView = class FrequentView extends BaseAppView { } _loadApps() { + let apps = []; let mostUsed = this._usage.get_most_used(); let hasUsefulData = this.hasUsefulData(); this._noFrequentAppsLabel.visible = !hasUsefulData; if (!hasUsefulData) - return; + return []; // 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 @@ -742,8 +746,10 @@ var FrequentView = class FrequentView extends BaseAppView { continue; let appIcon = new AppIcon(mostUsed[i], { isDraggable: favoritesWritable }); - this.addItem(appIcon); + apps.push(appIcon); } + + return apps; } // Called before allocation to calculate dynamic spacing @@ -1150,11 +1156,12 @@ var FolderView = class FolderView extends BaseAppView { } _loadApps() { + let apps = []; let excludedApps = this._folder.get_strv('excluded-apps'); let appSys = Shell.AppSystem.get_default(); let addAppId = appId => { - if (this.hasItem(appId)) - return; + if (this.hasItem(id)) + return; if (excludedApps.includes(appId)) return; @@ -1167,7 +1174,7 @@ var FolderView = class FolderView extends BaseAppView { return; let icon = new AppIcon(app); - this.addItem(icon); + apps.push(icon); }; let folderApps = this._folder.get_strv('apps'); @@ -1182,6 +1189,8 @@ var FolderView = class FolderView extends BaseAppView { addAppId(appInfo.get_id()); }); + + return apps; } }; -- GitLab From 9da49606f7ded69e901d5922de7af7ceb31cee60 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 30 Jul 2019 12:15:51 -0300 Subject: [PATCH 08/36] allView, folderView: Only add icons once FolderView and AllView currently check if the item is present in the BaseAppView._items map, in order to avoid adding the same icon multiple times. Now that BaseAppView._loadGrid() has a different role -- it returns a list with all app icons, and BaseAppView diffs with the current list of app icons -- checking the BaseAppView._items map is wrong. Make sure there are no duplicated items in the temporary array returned by all _loadGrid() implementations. Remove the now unused BaseAppView.hasItem() method. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index dc75792640..3b9e68fcdf 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -148,15 +148,8 @@ class BaseAppView { return this._allItems; } - hasItem(id) { - return this._items[id] !== undefined; - } - addItem(icon) { let id = icon.id; - if (this.hasItem(id)) - throw new Error(`icon with id ${id} already added to view`); - this._allItems.push(icon); this._items[id] = icon; } @@ -402,12 +395,13 @@ var AllView = class AllView extends BaseAppView { let folders = this._folderSettings.get_strv('folder-children'); folders.forEach(id => { - if (this.hasItem(id)) - return; let path = this._folderSettings.path + 'folders/' + id + '/'; - let icon = new FolderIcon(id, path, this); - icon.connect('name-changed', this._itemNameChanged.bind(this)); - icon.connect('apps-changed', this._refilterApps.bind(this)); + let icon = this._items[id]; + if (!icon) { + icon = new FolderIcon(id, path, this); + icon.connect('name-changed', this._itemNameChanged.bind(this)); + icon.connect('apps-changed', this._refilterApps.bind(this)); + } newApps.push(icon); this.folderIcons.push(icon); }); @@ -1160,9 +1154,6 @@ var FolderView = class FolderView extends BaseAppView { let excludedApps = this._folder.get_strv('excluded-apps'); let appSys = Shell.AppSystem.get_default(); let addAppId = appId => { - if (this.hasItem(id)) - return; - if (excludedApps.includes(appId)) return; @@ -1173,6 +1164,9 @@ var FolderView = class FolderView extends BaseAppView { if (!app.get_app_info().should_show()) return; + if (apps.some(appIcon => appIcon.id == appId)) + return; + let icon = new AppIcon(app); apps.push(icon); }; -- GitLab From f214c5b572cf073bce481e29276dfa099365b7bb Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 10:12:08 -0300 Subject: [PATCH 09/36] baseAppView: Remove unused BaseAppView.addItem Now that BaseAppView does not allow for subclasses to add and remove items directly, the addItem() method can be removed. Remove BaseAppView.addItem(). https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 3b9e68fcdf..21d4912e3e 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -148,12 +148,6 @@ class BaseAppView { return this._allItems; } - addItem(icon) { - let id = icon.id; - this._allItems.push(icon); - this._items[id] = icon; - } - _compareItems(a, b) { return a.name.localeCompare(b.name); } -- GitLab From 038917e5f1fb24f8f6dd232bb5f346087a72d137 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 13:33:07 -0300 Subject: [PATCH 10/36] allView: Redisplay on folder changes Now that redisplaying is a lightweight operation that only adds and removes what changed, we don't need to just refilter the app icons in AllView when a folder changes. Call _redisplay() in AllView when folders change. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/645 --- js/ui/appDisplay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 21d4912e3e..3cb71f1d26 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -394,7 +394,7 @@ var AllView = class AllView extends BaseAppView { if (!icon) { icon = new FolderIcon(id, path, this); icon.connect('name-changed', this._itemNameChanged.bind(this)); - icon.connect('apps-changed', this._refilterApps.bind(this)); + icon.connect('apps-changed', this._redisplay.bind(this)); } newApps.push(icon); this.folderIcons.push(icon); -- GitLab From 23191ec2390c504e204b4e03b97b0744c2f3dc0f Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 28 Jun 2019 19:44:20 -0300 Subject: [PATCH 11/36] appDisplay: Add event blocker inhibition API The event blocker is an actor that is added in between the icon grid and the app folder popup in order to guarantee that clicking outside the app folder will collapse it. However, the next patch will require allowing dragging events to be passed to folder icons, and the event blocker gets in our way here, preventing drag n' drop to work properly. Add an API to inhibit the event blocker. This API will be used by the app folders while an item is dragged. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 3cb71f1d26..94a7c7474e 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -338,6 +338,8 @@ var AllView = class AllView extends BaseAppView { this._folderSettings.connect('changed::folder-children', () => { Main.queueDeferredWork(this._redisplayWorkId); }); + + this._nEventBlockerInhibits = 0; } _itemNameChanged(item) { @@ -672,6 +674,16 @@ var AllView = class AllView extends BaseAppView { for (let i = 0; i < this.folderIcons.length; i++) this.folderIcons[i].adaptToSize(availWidth, availHeight); } + + inhibitEventBlocker() { + this._nEventBlockerInhibits++; + this._eventBlocker.visible = this._nEventBlockerInhibits == 0; + } + + uninhibitEventBlocker() { + this._nEventBlockerInhibits--; + this._eventBlocker.visible = this._nEventBlockerInhibits == 0; + } }; Signals.addSignalMethods(AllView.prototype); -- GitLab From cc9f949b653d7a21ad0f1c77d830222b845f41f5 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 28 Jun 2019 19:47:32 -0300 Subject: [PATCH 12/36] folderIcon: Allow dropping application icons Connect to the overview signals related to Drag n' Drop, and allow dropping application icons in it. Dropping an icon appends the application id to the folder's GSettings key. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 94a7c7474e..f62b94176b 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1221,6 +1221,11 @@ var FolderIcon = class FolderIcon { this.view = new FolderView(this._folder, parentView); + Main.overview.connect('item-drag-begin', + this._onDragBegin.bind(this)); + Main.overview.connect('item-drag-end', + this._onDragEnd.bind(this)); + this.actor.connect('clicked', this.open.bind(this)); this.actor.connect('destroy', this.onDestroy.bind(this)); this.actor.connect('notify::mapped', () => { @@ -1254,6 +1259,47 @@ var FolderIcon = class FolderIcon { return this.view.getAllItems().map(item => item.id); } + _onDragBegin() { + this._parentView.inhibitEventBlocker(); + } + + _onDragEnd() { + this._parentView.uninhibitEventBlocker(); + } + + _canDropAt(source) { + if (!(source instanceof AppIcon)) + return false; + + if (!global.settings.is_writable('favorite-apps')) + return false; + + if (this._folder.get_strv('apps').includes(source.id)) + return false + + return true; + } + + handleDragOver(source, actor, x, y, time) { + if (!this._canDropAt(source)) + return DND.DragMotionResult.NO_DROP; + + return DND.DragMotionResult.MOVE_DROP; + } + + acceptDrop(source, actor, x, y, time) { + if (!this._canDropAt(source)) + return true; + + let app = source.app; + let folderApps = this._folder.get_strv('apps'); + folderApps.push(app.id); + + this._folder.set_strv('apps', folderApps); + + return true; + } + _updateName() { let name = _getFolderName(this._folder); if (this.name == name) -- GitLab From bb9f05843f55c89a40a37e87654ac2e8be5fd7af Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 28 Jun 2019 19:49:18 -0300 Subject: [PATCH 13/36] folderIcon: Update folder icon after dropping After dropping an application into the folder icon, the list of applications is updated but the folder icon itself is not. Introduce BaseIcon.update() and call it from FolderIcon when redisplaying. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 1 + js/ui/iconGrid.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index f62b94176b..bbca2ed792 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1313,6 +1313,7 @@ var FolderIcon = class FolderIcon { _redisplay() { this._updateName(); this.actor.visible = this.view.getAllItems().length > 0; + this.icon.update(); this.emit('apps-changed'); } diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index c74ff4361a..7f5abed018 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -142,6 +142,10 @@ class BaseIcon extends St.Bin { // animating. zoomOutActor(this.child); } + + update() { + this._createIconTexture(this.iconSize); + } }); function clamp(value, min, max) { -- GitLab From 6da23c8d4d9e1f34dac0f3727e68f83393b14636 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 28 Jun 2019 21:11:10 -0300 Subject: [PATCH 14/36] appIcon: Always pass parent view We will soon need to know which view this icon belongs to, so add an extra parameter to the constructor to store the parent view. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 15 ++++++++++----- js/ui/dash.js | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index bbca2ed792..a1eb2256af 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -413,7 +413,7 @@ var AllView = class AllView extends BaseAppView { apps.forEach(appId => { let app = appSys.lookup_app(appId); - let icon = new AppIcon(app, + let icon = new AppIcon(app, this, { isDraggable: favoritesWritable }); newApps.push(icon); }); @@ -744,7 +744,7 @@ var FrequentView = class FrequentView extends BaseAppView { for (let i = 0; i < mostUsed.length; i++) { if (!mostUsed[i].get_app_info().should_show()) continue; - let appIcon = new AppIcon(mostUsed[i], + let appIcon = new AppIcon(mostUsed[i], this, { isDraggable: favoritesWritable }); apps.push(appIcon); } @@ -1035,7 +1035,7 @@ var AppSearchProvider = class AppSearchProvider { createResultObject(resultMeta) { if (resultMeta.id.endsWith('.desktop')) - return new AppIcon(this._appSys.lookup_app(resultMeta['id'])); + return new AppIcon(this._appSys.lookup_app(resultMeta['id']), null); else return new SystemActionIcon(this, resultMeta); } @@ -1173,7 +1173,7 @@ var FolderView = class FolderView extends BaseAppView { if (apps.some(appIcon => appIcon.id == appId)) return; - let icon = new AppIcon(app); + let icon = new AppIcon(app, this); apps.push(icon); }; @@ -1560,10 +1560,11 @@ var AppFolderPopup = class AppFolderPopup { Signals.addSignalMethods(AppFolderPopup.prototype); var AppIcon = class AppIcon { - constructor(app, iconParams = {}) { + constructor(app, view, iconParams = {}) { this.app = app; this.id = app.get_id(); this.name = app.get_name(); + this._view = view; this.actor = new St.Button({ style_class: 'app-well-app', reactive: true, @@ -1802,6 +1803,10 @@ var AppIcon = class AppIcon { shouldShowTooltip() { return this.actor.hover && (!this._menu || !this._menu.isOpen); } + + get view() { + return this._view; + } }; Signals.addSignalMethods(AppIcon.prototype); diff --git a/js/ui/dash.js b/js/ui/dash.js index 7da335bfb8..a96b954012 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -475,7 +475,7 @@ var Dash = class Dash { } _createAppItem(app) { - let appIcon = new AppDisplay.AppIcon(app, + let appIcon = new AppDisplay.AppIcon(app, null, { setSizeManually: true, showLabel: false }); if (appIcon._draggable) { -- GitLab From 79f9391c002a15a75fa987c59aca6ace7200dbf8 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 28 Jun 2019 20:48:22 -0300 Subject: [PATCH 15/36] allView: Switch pages when dragging above or below the grid This is necessary for being able to drag application icons to folders in different pages. Add a drag motion handler to AllView and handle overshoots when dragging. Only handle it when dragging from AllView. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index a1eb2256af..83fd577457 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -339,6 +339,9 @@ var AllView = class AllView extends BaseAppView { Main.queueDeferredWork(this._redisplayWorkId); }); + Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this)); + Main.overview.connect('item-drag-end', this._onDragEnd.bind(this)); + this._nEventBlockerInhibits = 0; } @@ -675,6 +678,58 @@ var AllView = class AllView extends BaseAppView { this.folderIcons[i].adaptToSize(availWidth, availHeight); } + _handleDragOvershoot(dragEvent) { + let [gridX, gridY] = this.actor.get_transformed_position(); + let [gridWidth, gridHeight] = this.actor.get_transformed_size(); + let gridBottom = gridY + gridHeight; + + // Within the grid boundaries, or already animating + if (dragEvent.y > gridY && dragEvent.y < gridBottom || + Tweener.isTweening(this._adjustment)) { + return; + } + + // Moving above the grid + let currentY = this._adjustment.value; + if (dragEvent.y <= gridY && currentY > 0) { + this.goToPage(this._grid.currentPage - 1); + return; + } + + // Moving below the grid + let maxY = this._adjustment.upper - this._adjustment.page_size; + if (dragEvent.y >= gridBottom && currentY < maxY) { + this.goToPage(this._grid.currentPage + 1); + return; + } + } + + _onDragBegin() { + this._dragMonitor = { + dragMotion: this._onDragMotion.bind(this) + }; + DND.addDragMonitor(this._dragMonitor); + } + + _onDragMotion(dragEvent) { + let 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.view == this) + this._handleDragOvershoot(dragEvent); + + return DND.DragMotionResult.CONTINUE; + } + + _onDragEnd() { + if (this._dragMonitor) { + DND.removeDragMonitor(this._dragMonitor); + this._dragMonitor = null; + } + } + inhibitEventBlocker() { this._nEventBlockerInhibits++; this._eventBlocker.visible = this._nEventBlockerInhibits == 0; -- GitLab From 241b4cd1c851f748dd423c8eea2c4cbda7c5c0bb Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sat, 29 Jun 2019 00:44:27 -0300 Subject: [PATCH 16/36] allView: Set delegate field DnD still relies on the _delegate field being set, so set it. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 83fd577457..4aa889072a 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -252,6 +252,7 @@ var AllView = class AllView extends BaseAppView { this.actor = new St.Widget({ layout_manager: new Clutter.BinLayout(), x_expand: true, y_expand: true }); this.actor.add_actor(this._scrollView); + this._grid._delegate = this; this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); -- GitLab From 74077b0f6ea8dc68b5cdcdfe0d9ec3e78bfd09c9 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sat, 29 Jun 2019 01:26:08 -0300 Subject: [PATCH 17/36] allView: Remove icon from folder when dropping outside When dropping an app icon to outside the folder, remove the app from that folder. GNOME Shell is already smart enough to figure out the setting changes and update the icons. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 4aa889072a..8709d30de0 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -731,6 +731,38 @@ var AllView = class AllView extends BaseAppView { } } + _canDropAt(source) { + if (!(source instanceof AppIcon)) + return false; + + if (!global.settings.is_writable('favorite-apps')) + return false; + + if (!(source.view instanceof FolderView)) + return false; + + return true; + } + + handleDragOver(source, actor, x, y, time) { + if (!this._canDropAt(source)) + return DND.DragMotionResult.NO_DROP; + + return DND.DragMotionResult.MOVE_DROP; + } + + acceptDrop(source, actor, x, y, time) { + if (!this._canDropAt(source)) + return false; + + source.view.removeApp(source.app); + + if (this._currentPopup) + this._currentPopup.popdown(); + + return true; + } + inhibitEventBlocker() { this._nEventBlockerInhibits++; this._eventBlocker.visible = this._nEventBlockerInhibits == 0; @@ -1248,6 +1280,19 @@ var FolderView = class FolderView extends BaseAppView { return apps; } + + removeApp(app) { + let folderApps = this._folder.get_strv('apps'); + let index = folderApps.indexOf(app.id); + if (index < 0) + return false; + + folderApps.splice(index, 1); + + this._folder.set_strv('apps', folderApps); + + return true; + } }; var FolderIcon = class FolderIcon { -- GitLab From 52257f513703a1f36974850515ca6efdce0c0dcd Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 1 Jul 2019 21:37:35 -0300 Subject: [PATCH 18/36] folderIcon: Add visual drag-over feedback WIP: This is not exactly what was discussed on IRC, but it's looking alright as a first iteration. Design feedback welcomed. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- data/theme/gnome-shell-sass/_common.scss | 3 +++ js/ui/appDisplay.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss index 5e377df153..8731ba48d9 100644 --- a/data/theme/gnome-shell-sass/_common.scss +++ b/data/theme/gnome-shell-sass/_common.scss @@ -1514,6 +1514,9 @@ StScrollBar { border-image: none; background-image: none; } + &:drop .overview-icon { + background-color: transparentize($selected_bg_color,.15); + } &:active .overview-icon, &:checked .overview-icon { background-color: transparentize(darken($osd_bg_color,10%), 0.5); diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 8709d30de0..9ff61ed95b 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1361,11 +1361,29 @@ var FolderIcon = class FolderIcon { } _onDragBegin() { + this._dragMonitor = { + dragMotion: this._onDragMotion.bind(this), + }; + DND.addDragMonitor(this._dragMonitor); + this._parentView.inhibitEventBlocker(); } + _onDragMotion(dragEvent) { + let target = dragEvent.targetActor; + + if (!this.actor.contains(target) || !this._canDropAt(dragEvent.source)) + this.actor.remove_style_pseudo_class('drop'); + else + this.actor.add_style_pseudo_class('drop'); + + return DND.DragMotionResult.CONTINUE; + } + _onDragEnd() { + this.actor.remove_style_pseudo_class('drop'); this._parentView.uninhibitEventBlocker(); + DND.removeDragMonitor(this._dragMonitor); } _canDropAt(source) { -- GitLab From 11ce7829bc0b5f2763db40e090e04ded60f5a90c Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sat, 29 Jun 2019 14:09:32 -0300 Subject: [PATCH 19/36] allView: Scale in when moving icons from folders App icons inside folders are already animated when the folder is opened, but moving an app icon from a folder doesn't, making the transition abrupt. Fortunately, it's easy to detect icons that were previously hidden but are not anymore. Add an animation to these icons when showing. WIP: tentatively using the Tweener parameters from Endless OS. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 9ff61ed95b..5c8d05bb2d 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -39,6 +39,9 @@ var VIEWS_SWITCH_ANIMATION_DELAY = 0.1; var PAGE_SWITCH_TIME = 0.3; +var APP_ICON_SCALE_IN_TIME = 0.5; +var APP_ICON_SCALE_IN_DELAY = 0.7; + const SWITCHEROO_BUS_NAME = 'net.hadess.SwitcherooControl'; const SWITCHEROO_OBJECT_PATH = '/net/hadess/SwitcherooControl'; @@ -358,6 +361,8 @@ var AllView = class AllView extends BaseAppView { } _refilterApps() { + let filteredApps = this._allItems.filter(icon => !icon.actor.visible); + this._allItems.forEach(icon => { if (icon instanceof AppIcon) icon.actor.visible = true; @@ -370,6 +375,14 @@ var AllView = class AllView extends BaseAppView { appIcon.actor.visible = false; }); }); + + // Scale in app icons that weren't visible, but now are + this._allItems.filter(icon => { + return icon.actor.visible && filteredApps.includes(icon); + }).forEach(icon => { + if (icon instanceof AppIcon) + icon.scheduleScaleIn(); + }); } getAppInfos() { @@ -1705,6 +1718,7 @@ var AppIcon = class AppIcon { this._iconContainer.add_child(this._dot); this.actor._delegate = this; + this._scaleInId = 0; // Get the isDraggable property without passing it on to the BaseIcon: let appIconParams = Params.parse(iconParams, { isDraggable: true }, true); @@ -1902,6 +1916,46 @@ var AppIcon = class AppIcon { this.icon.animateZoomOut(); } + _scaleIn() { + this.actor.scale_x = 0; + this.actor.scale_y = 0; + this.actor.pivot_point = new Clutter.Point({ x: 0.5, y: 0.5 }); + + Tweener.addTween(this.actor, { + scale_x: 1, + scale_y: 1, + time: APP_ICON_SCALE_IN_TIME, + delay: APP_ICON_SCALE_IN_DELAY, + transition: (t, b, c, d) => { + // Similar to easeOutElastic, but less aggressive. + t /= d; + let p = 0.5; + return b + c * (Math.pow(2, -11 * t) * Math.sin(2 * Math.PI * (t - p / 4) / p) + 1); + } + }); + } + + _unscheduleScaleIn() { + if (this._scaleInId != 0) { + this.actor.disconnect(this._scaleInId); + this._scaleInId = 0; + } + } + + scheduleScaleIn() { + if (this._scaleInId != 0) + return; + + if (this.actor.mapped) { + this._scaleIn(); + } else { + this._scaleInId = this.actor.connect('notify::mapped', () => { + this._unscheduleScaleIn(); + this._scaleIn(); + }) + } + } + shellWorkspaceLaunch(params) { params = Params.parse(params, { workspace: -1, timestamp: 0 }); -- GitLab From 2e4fb404a7e89f9a79bc99a0b762a5c571e69d6d Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Wed, 3 Jul 2019 14:53:09 -0300 Subject: [PATCH 20/36] controlsManager: Don't fade icon grid while dragging As pointed out by designers, fading it signals that the icon grid is not a drop target, when now it actually is. Remove the fade effect applied to the icon grid when dragging. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/overviewControls.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/js/ui/overviewControls.js b/js/ui/overviewControls.js index e5bb384797..378cc1b320 100644 --- a/js/ui/overviewControls.js +++ b/js/ui/overviewControls.js @@ -424,17 +424,6 @@ var ControlsManager = class { layout.connect('allocation-changed', this._updateWorkspacesGeometry.bind(this)); Main.overview.connect('showing', this._updateSpacerVisibility.bind(this)); - Main.overview.connect('item-drag-begin', () => { - let activePage = this.viewSelector.getActivePage(); - if (activePage != ViewSelector.ViewPage.WINDOWS) - this.viewSelector.fadeHalf(); - }); - Main.overview.connect('item-drag-end', () => { - this.viewSelector.fadeIn(); - }); - Main.overview.connect('item-drag-cancelled', () => { - this.viewSelector.fadeIn(); - }); } _updateWorkspacesGeometry() { -- GitLab From 37aa5d0faf2ac36b066c5d9dafb0cc90875dc1ca Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 13:36:30 -0300 Subject: [PATCH 21/36] gschema: Add the 'icons-data' key This is the key that will store the icons of the icon grid. It is a sorted array of application ids, and the order of items saved in this key corresponds to the icon order. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- data/org.gnome.shell.gschema.xml.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in index 9c3e42c944..b8ff914fcb 100644 --- a/data/org.gnome.shell.gschema.xml.in +++ b/data/org.gnome.shell.gschema.xml.in @@ -109,6 +109,10 @@ the shell. + + [] + Data about the icons in the icon grid + -- GitLab From ca01b0287ebb66fb35778f41004d8fb59002dc14 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 13:31:43 -0300 Subject: [PATCH 22/36] allView: Don't sort icons We are moving towards being able to move icons to custom positions. To achieve that, the icon grid should stop sort its icons. Remove the sorting code from AllView and BaseAppView. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 5c8d05bb2d..9ad947fbc9 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -151,13 +151,7 @@ class BaseAppView { return this._allItems; } - _compareItems(a, b) { - return a.name.localeCompare(b.name); - } - _loadGrid() { - this._allItems.sort(this._compareItems); - this._allItems.forEach((item, index) => { // Don't readd already added items if (item.actor.get_parent()) @@ -349,17 +343,6 @@ var AllView = class AllView extends BaseAppView { this._nEventBlockerInhibits = 0; } - _itemNameChanged(item) { - // If an item's name changed, we can pluck it out of where it's - // supposed to be and reinsert it where it's sorted. - let oldIdx = this._allItems.indexOf(item); - this._allItems.splice(oldIdx, 1); - let newIdx = Util.insertSorted(this._allItems, item, this._compareItems); - - this._grid.removeItem(item); - this._grid.addItem(item, newIdx); - } - _refilterApps() { let filteredApps = this._allItems.filter(icon => !icon.actor.visible); @@ -412,7 +395,6 @@ var AllView = class AllView extends BaseAppView { let icon = this._items[id]; if (!icon) { icon = new FolderIcon(id, path, this); - icon.connect('name-changed', this._itemNameChanged.bind(this)); icon.connect('apps-changed', this._redisplay.bind(this)); } newApps.push(icon); @@ -821,11 +803,6 @@ var FrequentView = class FrequentView extends BaseAppView { return this._usage.get_most_used().length >= MIN_FREQUENT_APPS_COUNT; } - _compareItems() { - // The FrequentView does not need to be sorted alphabetically - return 0; - } - _loadApps() { let apps = []; let mostUsed = this._usage.get_most_used(); -- GitLab From 0dd430f2f47193d9c9b2b946a14356ababecda7a Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 17:41:15 -0300 Subject: [PATCH 23/36] appDisplay: Add moveItem() This is a handy function to implement changing an icon's position in the grid. WIP: better commit message https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 22 ++++++++++++++++++++++ js/ui/iconGrid.js | 16 ++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 9ad947fbc9..3459ab5af4 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -163,6 +163,28 @@ class BaseAppView { this.emit('view-loaded'); } + moveItem(item, newPosition) { + let itemIndex = this._allItems.indexOf(item); + + if (itemIndex == -1) { + log('Trying to move item %s that is not in this app view'.format(item.id)); + return; + } + + let visibleItems = this._allItems.filter(item => item.actor.visible); + let visibleIndex = visibleItems.indexOf(item); + if (newPosition > visibleIndex) + newPosition -= 1; + + // Remove from the old position + this._allItems.splice(itemIndex, 1); + + let realPosition = this._grid.moveItem(item, newPosition); + this._allItems.splice(realPosition, 0, item); + + return realPosition; + } + _selectAppInternal(id) { if (this._items[id]) this._items[id].actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index 7f5abed018..1020d835e8 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -695,6 +695,22 @@ var IconGrid = GObject.registerClass({ this.add_actor(item.actor); } + moveItem(item, newPosition) { + if (!this.contains(item.actor)) { + log('Cannot move item not contained by the IconGrid'); + return; + } + + let children = this.get_children(); + let visibleChildren = children.filter(c => c.is_visible()); + let visibleChildAtPosition = visibleChildren[newPosition]; + let realPosition = children.indexOf(visibleChildAtPosition); + + this.set_child_at_index(item.actor, realPosition); + + return realPosition; + } + removeItem(item) { this.remove_child(item.actor); } -- GitLab From a9e7b7853ae9162ae6fb6491897ad971af77fadb Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 17:39:59 -0300 Subject: [PATCH 24/36] iconGrid: Add item nudge API Nudging an item can be done via one side, or both. This is essentially a set of helpers for Drag n' Drop to use. The x and y values are relative to the icon grid itself. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 12 +++ js/ui/iconGrid.js | 253 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 3459ab5af4..c29d884ce0 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -255,6 +255,18 @@ class BaseAppView { Tweener.addTween(this._grid, params); } + + canDropAt(x, y) { + return this._grid.canDropAt(x, y); + } + + nudgeItemsAtIndex(index, dragLocation) { + this._grid.nudgeItemsAtIndex(index, dragLocation); + } + + removeNudges() { + this._grid.removeNudges(); + } } Signals.addSignalMethods(BaseAppView.prototype); diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index 1020d835e8..5b07ea8712 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -29,6 +29,25 @@ var AnimationDirection = { var APPICON_ANIMATION_OUT_SCALE = 3; var APPICON_ANIMATION_OUT_TIME = 0.25; +const LEFT_DIVIDER_LEEWAY = 30; +const RIGHT_DIVIDER_LEEWAY = 30; + +const NUDGE_ANIMATION_TYPE = Clutter.AnimationMode.EASE_OUT_ELASTIC; +const NUDGE_DURATION = 800; + +const NUDGE_RETURN_ANIMATION_TYPE = Clutter.AnimationMode.EASE_OUT_QUINT; +const NUDGE_RETURN_DURATION = 300; + +const NUDGE_FACTOR = 0.33; + +var DragLocation = { + DEFAULT: 0, + ON_ICON: 1, + START_EDGE: 2, + END_EDGE: 3, + EMPTY_AREA: 4, +} + var BaseIcon = GObject.registerClass( class BaseIcon extends St.Bin { _init(label, params) { @@ -807,6 +826,223 @@ var IconGrid = GObject.registerClass({ } return GLib.SOURCE_REMOVE; } + + // Drag n' Drop methods + + nudgeItemsAtIndex(index, dragLocation) { + // No nudging when the cursor is in an empty area + if (dragLocation == DragLocation.EMPTY_AREA || dragLocation == DragLocation.ON_ICON) + return; + + let children = this.get_children().filter(c => c.is_visible()); + let nudgeIndex = index; + let rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); + + if (dragLocation != DragLocation.START_EDGE) { + let leftItem = children[nudgeIndex - 1]; + let offset = rtl ? Math.floor(this._hItemSize * NUDGE_FACTOR) : Math.floor(-this._hItemSize * NUDGE_FACTOR); + this._animateNudge(leftItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset); + } + + // Nudge the icon to the right if we are the first item or not at the + // end of row + if (dragLocation != DragLocation.END_EDGE) { + let rightItem = children[nudgeIndex]; + let offset = rtl ? Math.floor(-this._hItemSize * NUDGE_FACTOR) : Math.floor(this._hItemSize * NUDGE_FACTOR); + this._animateNudge(rightItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset); + } + } + + removeNudges() { + let children = this.get_children().filter(c => c.is_visible()); + for (let index = 0; index < children.length; index++) { + this._animateNudge(children[index], + NUDGE_RETURN_ANIMATION_TYPE, + NUDGE_RETURN_DURATION, + 0); + } + } + + _animateNudge(item, animationType, duration, offset) { + if (!item) + return; + + item.save_easing_state(); + item.set_easing_mode(animationType); + item.set_easing_duration(duration); + item.translation_x = offset; + item.restore_easing_state(); + } + + // This function is overriden by the PaginatedIconGrid subclass so we can + // take into account the extra space when dragging from a folder + _calculateDndRow(y) { + let rowHeight = this._getVItemSize() + this._getSpacing(); + return Math.floor(y / rowHeight); + } + + // Returns the drop point index or -1 if we can't drop there + canDropAt(x, y) { + // This is an complex calculation, but in essence, we divide the grid + // as: + // + // left empty space + // | left padding right padding + // | | width without padding | + // +--------+---+---------------------------------------+-----+ + // | | | | | | | | + // | | | | | | | | + // | | |--------+-----------+----------+-------| | + // | | | | | | | | + // | | | | | | | | + // | | |--------+-----------+----------+-------| | + // | | | | | | | | + // | | | | | | | | + // | | |--------+-----------+----------+-------| | + // | | | | | | | | + // | | | | | | | | + // +--------+---+---------------------------------------+-----+ + // + // The left empty space is immediately discarded, and ignored in all + // calculations. + // + // The width (with paddings) is used to determine if we're dragging + // over the left or right padding, and which column is being dragged + // on. + // + // Finally, the width without padding is used to figure out where in + // the icon (start edge, end edge, on it, etc) the cursor is. + + let [nColumns, usedWidth] = this._computeLayout(this.width); + + let leftEmptySpace; + switch (this._xAlign) { + case St.Align.START: + leftEmptySpace = 0; + break; + case St.Align.MIDDLE: + leftEmptySpace = Math.floor((this.width - usedWidth) / 2); + break; + case St.Align.END: + leftEmptySpace = availWidth - usedWidth; + } + + x -= leftEmptySpace; + y -= this.topPadding; + + let row = this._calculateDndRow(y); + + // Correct sx to handle the left padding to correctly calculate + // the column + let rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); + let gridX = x - this.leftPadding; + + let widthWithoutPadding = usedWidth - this.leftPadding - this.rightPadding; + let columnWidth = widthWithoutPadding / nColumns; + + let column; + if (x < this.leftPadding) + column = 0; + else if (x > usedWidth - this.rightPadding) + column = nColumns - 1; + else + column = Math.floor(gridX / columnWidth); + + let isFirstIcon = column == 0; + let isLastIcon = column == nColumns - 1; + + // If we're outside of the grid, we are in an invalid drop location + if (x < 0 || x > usedWidth) + return [-1, DragLocation.DEFAULT]; + + let children = this.get_children().filter(c => c.is_visible()); + let childIndex = Math.min((row * nColumns) + column, children.length); + + // If we're above the grid vertically, we are in an invalid + // drop location + if (childIndex < 0) + return [-1, DragLocation.DEFAULT]; + + // If we're past the last visible element in the grid, + // we might be allowed to drop there. + if (childIndex >= children.length) + return [children.length, DragLocation.EMPTY_AREA]; + + let child = children[childIndex]; + let [childMinWidth, childMinHeight, childNaturalWidth, childNaturalHeight] = child.get_preferred_size(); + + // This is the width of the cell that contains the icon + // (excluding spacing between cells) + let childIconWidth = Math.max(this._getHItemSize(), childNaturalWidth); + + // Calculate the original position of the child icon (prior to nudging) + let childX; + if (rtl) + childX = widthWithoutPadding - (column * columnWidth) - childIconWidth; + else + childX = column * columnWidth; + + let iconLeftX = childX + LEFT_DIVIDER_LEEWAY; + let iconRightX = childX + childIconWidth - RIGHT_DIVIDER_LEEWAY + + let dropIndex; + let dragLocation; + + x -= this.leftPadding; + + if (x < iconLeftX) { + // We are to the left of the icon target + if (isFirstIcon || x < 0) { + // We are before the leftmost icon on the grid + if (rtl) { + dropIndex = childIndex + 1; + dragLocation = DragLocation.END_EDGE; + } else { + dropIndex = childIndex; + dragLocation = DragLocation.START_EDGE; + } + } else { + // We are between the previous icon (next in RTL) and this one + if (rtl) + dropIndex = childIndex + 1; + else + dropIndex = childIndex; + + dragLocation = DragLocation.DEFAULT; + } + } else if (x >= iconRightX) { + // We are to the right of the icon target + if (childIndex >= children.length) { + // We are beyond the last valid icon + // (to the right of the app store / trash can, if present) + dropIndex = -1; + dragLocation = DragLocation.DEFAULT; + } else if (isLastIcon || x >= widthWithoutPadding) { + // We are beyond the rightmost icon on the grid + if (rtl) { + dropIndex = childIndex; + dragLocation = DragLocation.START_EDGE; + } else { + dropIndex = childIndex + 1; + dragLocation = DragLocation.END_EDGE; + } + } else { + // We are between this icon and the next one (previous in RTL) + if (rtl) + dropIndex = childIndex; + else + dropIndex = childIndex + 1; + + dragLocation = DragLocation.DEFAULT; + } + } else { + // We are over the icon target area + dropIndex = childIndex; + dragLocation = DragLocation.ON_ICON; + } + + return [dropIndex, dragLocation]; + } }); var PaginatedIconGrid = GObject.registerClass({ @@ -881,6 +1117,23 @@ var PaginatedIconGrid = GObject.registerClass({ } // Overridden from IconGrid + _calculateDndRow(y) { + let row = super._calculateDndRow(y); + + // If there's no extra space, just return the current value and maintain + // the same behavior when without a folder opened. + if (!this._extraSpaceData) + return row; + + let [ baseRow, nRowsUp, nRowsDown ] = this._extraSpaceData; + let newRow = row + nRowsUp; + + if (row > baseRow) + newRow -= nRowsDown; + + return newRow; + } + _getChildrenToAnimate() { let children = this._getVisibleChildren(); let firstIndex = this._childrenPerPage * this.currentPage; -- GitLab From ebf26101406cc59336b6db37fa942918aa050650 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 16:27:18 -0300 Subject: [PATCH 25/36] folderView: Move DnD functions to BaseAppView It will be much easier to handle Drag n' Drop with BaseAppView implementing default handlers for it. Move handleDragOver() and acceptDrop() to BaseAppView. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index c29d884ce0..931f69e120 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -192,6 +192,14 @@ class BaseAppView { log(`No such application ${id}`); } + handleDragOver(source, actor, x, y, time) { + return DND.DragMotionResult.NO_DROP; + } + + acceptDrop(source, actor, x, y, time) { + return false; + } + selectApp(id) { if (this._items[id] && this._items[id].actor.mapped) { this._selectAppInternal(id); -- GitLab From 4056c56800c7073105bb1a328d49a0b8ec53b736 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 13:38:11 -0300 Subject: [PATCH 26/36] allView: Add support for custom positioning Use the new 'icon-grid-layout' key to sort the applications and folders as it is saved. Applications that are not in the key are appended, and when compared to each other, fallback to alphabetical order. Because the 'icon-grid-layout' key is, by default, empty, this means that all icons and folders are sorted alphabetically the first time they are displayed, preserving the current behavior. This commit does not add any way to change the order without modifying the GSettings key directly. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 57 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 931f69e120..8ca63384e4 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -160,6 +160,10 @@ class BaseAppView { this._grid.addItem(item, index); }); + this._allItems.forEach((item, index) => { + this._grid.set_child_at_index(item.actor, index); + }); + this.emit('view-loaded'); } @@ -379,6 +383,12 @@ var AllView = class AllView extends BaseAppView { Main.queueDeferredWork(this._redisplayWorkId); }); + this._gridSettings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); + this._gridChangedId = this._gridSettings.connect('changed::icons-data', () => { + if (!this._blockGridSettings) + Main.queueDeferredWork(this._redisplayWorkId); + }); + Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this)); Main.overview.connect('item-drag-end', this._onDragEnd.bind(this)); @@ -430,6 +440,10 @@ var AllView = class AllView extends BaseAppView { let appSys = Shell.AppSystem.get_default(); this.folderIcons = []; + let appsInFolder = []; + + let iconsData = this._gridSettings.get_value('icons-data').deep_unpack(); + let customPositionedIcons = []; let folders = this._folderSettings.get_strv('folder-children'); folders.forEach(id => { @@ -439,8 +453,14 @@ var AllView = class AllView extends BaseAppView { icon = new FolderIcon(id, path, this); icon.connect('apps-changed', this._redisplay.bind(this)); } - newApps.push(icon); this.folderIcons.push(icon); + + if (iconsData[id]) + customPositionedIcons.push(icon); + else + newApps.push(icon); + + icon.getAppIds().forEach(appId => appsInFolder.push(appId)); }); // Allow dragging of the icon only if the Dash would accept a drop to @@ -451,12 +471,45 @@ var AllView = class AllView extends BaseAppView { // but we hope that is not used much. let favoritesWritable = global.settings.is_writable('favorite-apps'); + // First, add only the app icons that do not have a custom position + // set. These icons will be sorted alphabetically. apps.forEach(appId => { let app = appSys.lookup_app(appId); let icon = new AppIcon(app, this, { isDraggable: favoritesWritable }); - newApps.push(icon); + + if (iconsData[appId]) + customPositionedIcons.push(icon); + else + newApps.push(icon); + }); + newApps.sort((a, b) => a.name.localeCompare(b.name)); + + // The stored position is final. That means we need to add the custom + // icons in order (first to last) otherwise they end up with in the + // wrong position + customPositionedIcons.sort((a, b) => { + let indexA = iconsData[a.id].deep_unpack()['position'].deep_unpack(); + let indexB = iconsData[b.id].deep_unpack()['position'].deep_unpack(); + + return indexA - indexB; + }); + + // Now add the icons with a custom position set. Because 'newApps' has + // literally all apps -- including the ones that will be hidden -- we + // need to translate from visible position to the real position. + let visibleApps = newApps.filter(app => !appsInFolder.includes(app.id)); + + customPositionedIcons.forEach((icon, index) => { + let iconData = iconsData[icon.id].deep_unpack(); + let position = iconData['position'].deep_unpack(); + + // Because we are modifying 'newApps' here, compensate the number + // of added items by subtracting 'index' + let visibleAppAtPosition = visibleApps[position - index]; + let realPosition = newApps.indexOf(visibleAppAtPosition); + newApps.splice(realPosition, 0, icon); }); return newApps; -- GitLab From 6e3696baad81d778d2a9ec295a29e52ccab3b28f Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 2 Jul 2019 17:42:29 -0300 Subject: [PATCH 27/36] allView, folderView: Implement moving icons This makes use of the new BaseAppIcon.moveItem() API. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 111 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 8ca63384e4..80a85674ae 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -515,6 +515,37 @@ var AllView = class AllView extends BaseAppView { return newApps; } + moveItem(item, position) { + let visibleApps = this._allItems.filter(icon => icon.actor.visible); + let oldPosition = visibleApps.indexOf(item); + + if (oldPosition == position) + return; + + super.moveItem(item, position); + + if (position > oldPosition) + position -= 1; + + // Update all custom icon positions to match what's visible + visibleApps = this._allItems.filter(icon => icon.actor.visible); + let iconsData = this._gridSettings.get_value('icons-data').deep_unpack(); + visibleApps.forEach((icon, index) => { + if (!iconsData[icon.id] || icon.id == item.id) + return; + + iconsData[icon.id] = new GLib.Variant('a{sv}', { + 'position': GLib.Variant.new_uint32(index), + }); + }); + + iconsData[item.id] = new GLib.Variant('a{sv}', { + 'position': GLib.Variant.new_uint32(position), + }); + this._gridSettings.set_value('icons-data', + new GLib.Variant('a{sv}', iconsData)); + } + _loadGrid() { super._loadGrid(); this._refilterApps(); @@ -811,45 +842,60 @@ var AllView = class AllView extends BaseAppView { if (appIcon.view == this) this._handleDragOvershoot(dragEvent); + if (dragEvent.targetActor != this._grid) + this.removeNudges(); + return DND.DragMotionResult.CONTINUE; } _onDragEnd() { + this.removeNudges(); + if (this._dragMonitor) { DND.removeDragMonitor(this._dragMonitor); this._dragMonitor = null; } } - _canDropAt(source) { - if (!(source instanceof AppIcon)) - return false; + handleDragOver(source, actor, x, y, time) { + let sourceIndex = -1; + if (source.view == this) { + let visibleItems = this._allItems.filter(item => item.actor.visible); + sourceIndex = visibleItems.indexOf(source); + } - if (!global.settings.is_writable('favorite-apps')) - return false; + let [index, dragLocation] = this.canDropAt(x, y); - if (!(source.view instanceof FolderView)) - return false; + this.removeNudges(); + if (source.view && source.view != this) + source.view.removeNudges(); - return true; - } + if (index != -1) { + if (sourceIndex == -1 || (index != sourceIndex && index != sourceIndex + 1)) + this.nudgeItemsAtIndex(index, dragLocation); - handleDragOver(source, actor, x, y, time) { - if (!this._canDropAt(source)) - return DND.DragMotionResult.NO_DROP; + return DND.DragMotionResult.MOVE_DROP; + } - return DND.DragMotionResult.MOVE_DROP; + return DND.DragMotionResult.NO_DROP; } acceptDrop(source, actor, x, y, time) { - if (!this._canDropAt(source)) + let [index, dragLocation] = this.canDropAt(x, y); + + if (index == -1) return false; - source.view.removeApp(source.app); + if (source.view instanceof FolderView) { + source.view.removeApp(source.app); + source = this._items[source.id]; - if (this._currentPopup) - this._currentPopup.popdown(); + if (this._currentPopup) + this._currentPopup.popdown(); + } + this.moveItem(source, index); + this.removeNudges(); return true; } @@ -1228,6 +1274,7 @@ var FolderView = class FolderView extends BaseAppView { let scrollableContainer = new St.BoxLayout({ vertical: true, reactive: true }); scrollableContainer.add_actor(this._grid); this.actor.add_actor(scrollableContainer); + this._grid._delegate = this; let action = new Clutter.PanAction({ interpolate: true }); action.connect('pan', this._onPan.bind(this)); @@ -1297,6 +1344,29 @@ var FolderView = class FolderView extends BaseAppView { this.actor.set_height(this.usedHeight()); } + handleDragOver(source, actor, x, y, time) { + let [index, dragLocation] = this.canDropAt(x, y); + let sourceIndex = this._allItems.indexOf(source); + + this._parentView.removeNudges(); + this.removeNudges(); + if (index != -1 && index != sourceIndex && index != sourceIndex + 1) + this.nudgeItemsAtIndex(index, dragLocation); + + return DND.DragMotionResult.MOVE_DROP; + } + + acceptDrop(source, actor, x, y, time) { + let [index, dragLocation] = this.canDropAt(x, y); + let success = index != -1; + + if (success) + this.moveItem(source, index); + + this.removeNudges(); + return success; + } + _getPageAvailableSize() { let pageBox = new Clutter.ActorBox(); pageBox.x1 = pageBox.y1 = 0; @@ -1378,6 +1448,13 @@ var FolderView = class FolderView extends BaseAppView { return true; } + + moveItem(item, newPosition) { + super.moveItem(item, newPosition); + + let appIds = this._allItems.map(icon => icon.id); + this._folder.set_strv('apps', appIds); + } }; var FolderIcon = class FolderIcon { -- GitLab From bf322cd51a887b6f3fe3dc8ead6a6a41641beca1 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 4 Jul 2019 18:39:13 -0300 Subject: [PATCH 28/36] appIcon: Scale and fade itself when starting drag As per design direction, scale and fade the app icon when starting dragging it, and show it again if the drop is accepted. Clutter takes care of animating the rest of icon positions through implicit animations. Scale and fade the dragged icon while it's being dragged. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 30 +++++++++++++++++++++++++++++- js/ui/dash.js | 10 ---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 80a85674ae..c6a8b27e93 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -894,6 +894,8 @@ var AllView = class AllView extends BaseAppView { this._currentPopup.popdown(); } + source.undoScaleAndFade(); + this.moveItem(source, index); this.removeNudges(); return true; @@ -1358,8 +1360,11 @@ var FolderView = class FolderView extends BaseAppView { acceptDrop(source, actor, x, y, time) { let [index, dragLocation] = this.canDropAt(x, y); + let sourceIndex = this._allItems.indexOf(source); let success = index != -1; + source.undoScaleAndFade(); + if (success) this.moveItem(source, index); @@ -1569,8 +1574,10 @@ var FolderIcon = class FolderIcon { } acceptDrop(source, actor, x, y, time) { - if (!this._canDropAt(source)) + if (!this._canDropAt(source)) { + source.undoScaleAndFade(); return true; + } let app = source.app; let folderApps = this._folder.get_strv('apps'); @@ -1848,6 +1855,7 @@ var AppIcon = class AppIcon { this._view = view; this.actor = new St.Button({ style_class: 'app-well-app', + pivot_point: new Clutter.Point({x: 0.5, y: 0.5}), reactive: true, button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO, can_focus: true, @@ -1894,6 +1902,7 @@ var AppIcon = class AppIcon { this._draggable = DND.makeDraggable(this.actor); this._draggable.connect('drag-begin', () => { this._dragging = true; + this.scaleAndFade(); this._removeMenuTimeout(); Main.overview.beginItemDrag(this); }); @@ -1903,6 +1912,7 @@ var AppIcon = class AppIcon { }); this._draggable.connect('drag-end', () => { this._dragging = false; + this.undoScaleAndFade(); Main.overview.endItemDrag(this); }); } @@ -2126,6 +2136,24 @@ var AppIcon = class AppIcon { return this.actor.hover && (!this._menu || !this._menu.isOpen); } + scaleAndFade() { + this.actor.save_easing_state(); + this.actor.reactive = false; + this.actor.scale_x = 0.75; + this.actor.scale_y = 0.75; + this.actor.opacity = 128; + this.actor.restore_easing_state(); + } + + undoScaleAndFade() { + this.actor.save_easing_state(); + this.actor.reactive = true; + this.actor.scale_x = 1.0; + this.actor.scale_y = 1.0; + this.actor.opacity = 255; + this.actor.restore_easing_state(); + } + get view() { return this._view; } diff --git a/js/ui/dash.js b/js/ui/dash.js index a96b954012..c22cb3e8b4 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -478,16 +478,6 @@ var Dash = class Dash { let appIcon = new AppDisplay.AppIcon(app, null, { setSizeManually: true, showLabel: false }); - if (appIcon._draggable) { - appIcon._draggable.connect('drag-begin', - () => { - appIcon.actor.opacity = 50; - }); - appIcon._draggable.connect('drag-end', - () => { - appIcon.actor.opacity = 255; - }); - } appIcon.connect('menu-state-changed', (appIcon, opened) => { -- GitLab From ac3bc03f3fd48b06f6179c055cf37b34c21038d4 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 1 Jul 2019 23:25:19 -0300 Subject: [PATCH 29/36] iconGrid: Implicitly animate icon positions Add a proper easing state, and animate icon positions using Clutter implicit animations. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/iconGrid.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index 5b07ea8712..3cf84f65b5 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -374,7 +374,11 @@ var IconGrid = GObject.registerClass({ } else { if (!animating) children[i].opacity = 255; + + children[i].save_easing_state(); + children[i].set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD); children[i].allocate(childBox, flags); + children[i].restore_easing_state(); } columnIndex++; @@ -1098,7 +1102,12 @@ var PaginatedIconGrid = GObject.registerClass({ for (let i = 0; i < children.length; i++) { let childBox = this._calculateChildBox(children[i], x, y, box); + + children[i].save_easing_state(); + children[i].set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD); children[i].allocate(childBox, flags); + children[i].restore_easing_state(); + children[i].show(); columnIndex++; -- GitLab From 0bdcf2958f2a96a9407a9730e44fc9dd08c5e90d Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 4 Jul 2019 19:23:52 -0300 Subject: [PATCH 30/36] iconGrid: Apply delay to easing state Also following design suggestion, add a small delay to the icons moving so as to give the impression that they're moving in order. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/iconGrid.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index 3cf84f65b5..435cdf697d 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -40,6 +40,8 @@ const NUDGE_RETURN_DURATION = 300; const NUDGE_FACTOR = 0.33; +const ICON_POSITION_DELAY = 25; + var DragLocation = { DEFAULT: 0, ON_ICON: 1, @@ -365,6 +367,7 @@ var IconGrid = GObject.registerClass({ let y = box.y1 + this.topPadding; let columnIndex = 0; let rowIndex = 0; + let nChanged = 0; for (let i = 0; i < children.length; i++) { let childBox = this._calculateChildBox(children[i], x, y, box); @@ -375,8 +378,13 @@ var IconGrid = GObject.registerClass({ if (!animating) children[i].opacity = 255; + // Figure out how much delay to apply + if (!childBox.equal(children[i].get_allocation_box())) + nChanged++; + children[i].save_easing_state(); children[i].set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD); + children[i].set_easing_delay(ICON_POSITION_DELAY * nChanged); children[i].allocate(childBox, flags); children[i].restore_easing_state(); } @@ -1099,12 +1107,18 @@ var PaginatedIconGrid = GObject.registerClass({ let x = box.x1 + leftEmptySpace + this.leftPadding; let y = box.y1 + this.topPadding; let columnIndex = 0; + let nChanged = 0; for (let i = 0; i < children.length; i++) { let childBox = this._calculateChildBox(children[i], x, y, box); + // Figure out how much delay to apply + if (!childBox.equal(children[i].get_allocation_box())) + nChanged++; + children[i].save_easing_state(); children[i].set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD); + children[i].set_easing_delay(ICON_POSITION_DELAY * nChanged); children[i].allocate(childBox, flags); children[i].restore_easing_state(); -- GitLab From 0596848c272a665e28374c5b132816b6dbb898a7 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Wed, 3 Jul 2019 13:29:20 -0300 Subject: [PATCH 31/36] appIcon: Use a real BaseIcon as the drag actor Moving an app icon to other positions is semantically different to dragging an actor to the dash; the act of moving should itself be semantic, in that we should feel like we are moving the actual icon. Currently, AppIcon gives the DnD code a simplified version of itself, with just its icon, instead of a complete copy with the label. Make AppIcon create a new IconGrid.BaseIcon and use it as the drag actor. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index c6a8b27e93..d89a41593a 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -2123,7 +2123,12 @@ var AppIcon = class AppIcon { } getDragActor() { - return this.app.create_icon_texture(Main.overview.dashIconSize); + let iconParams = { createIcon: this._createIcon.bind(this), + showLabel: (this.icon.label != null), + setSizeManually: true }; + let icon = new IconGrid.BaseIcon(this.name, iconParams); + icon.setIconSize(this.icon.iconSize); + return icon; } // Returns the original actor that should align with the actor -- GitLab From 55eb949defc285ea7651ee0fb773cd287c5c06b2 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 5 Jul 2019 12:32:51 -0300 Subject: [PATCH 32/36] baseViewIcon: Introduce base class for view icons Right now, only AppIcon supports being dragged. In the future, however, both app and folder icons will be reorderable, and to avoid copying the same code between FolderIcon and AppIcon, add a new base class BaseViewIcon that contains the shared code between them. Adding this new base class also has the side effect that it already allows for folder icons to be dragged, although full support for that will come in next commits. Because the Dash icons are not drop targets themselves, add a tiny DashIcon class, which is an AppDisplay.AppIcon subclass, and disable all DND drop code from it. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 443 ++++++++++++++++++++++++++------------------ js/ui/dash.js | 30 ++- 2 files changed, 292 insertions(+), 181 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index d89a41593a..6a722498be 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -886,7 +886,8 @@ var AllView = class AllView extends BaseAppView { if (index == -1) return false; - if (source.view instanceof FolderView) { + if ((source instanceof AppIcon) && + (source.view instanceof FolderView)) { source.view.removeApp(source.app); source = this._items[source.id]; @@ -1462,37 +1463,243 @@ var FolderView = class FolderView extends BaseAppView { } }; -var FolderIcon = class FolderIcon { +var BaseViewIcon = class BaseViewIcon { + constructor(params, buttonParams) { + buttonParams = Params.parse(buttonParams, { + pivot_point: new Clutter.Point({x: 0.5, y: 0.5}), + reactive: true, + can_focus: true, + x_fill: true, + y_fill: true + }, true); + + this.actor = new St.Button(buttonParams); + this.actor._delegate = this; + + // Get the isDraggable property without passing it on to the BaseIcon: + params = Params.parse(params, { + isDraggable: true, + hideWhileDragging: false + }, true); + let isDraggable = params['isDraggable']; + delete params['isDraggable']; + + this._hasDndHover = false; + + if (isDraggable) { + this._draggable = DND.makeDraggable(this.actor); + this._draggable.connect('drag-begin', () => { + this._dragging = true; + this.scaleAndFade(); + Main.overview.beginItemDrag(this); + }); + this._draggable.connect('drag-cancelled', () => { + this._dragging = false; + Main.overview.cancelledItemDrag(this); + }); + this._draggable.connect('drag-end', () => { + this._dragging = false; + this.undoScaleAndFade(); + Main.overview.endItemDrag(this); + }); + } + + Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this)); + Main.overview.connect('item-drag-end', this._onDragEnd.bind(this)); + + this.actor.connect('destroy', this._onDestroy.bind(this)); + } + + _onDestroy() { + if (this._draggable && this._dragging) { + Main.overview.endItemDrag(this); + this.draggable = null; + } + } + + _createIcon(iconSize) { + throw new GObject.NotImplementedError(`_createIcon in ${this.constructor.name}`); + } + + _canDropAt(source) { + return false; + } + + // Should be overriden by subclasses + _setHoveringByDnd(isHovering) { + if (isHovering) + this.actor.add_style_pseudo_class('drop'); + else + this.actor.remove_style_pseudo_class('drop'); + } + + _onDragBegin() { + this._dragMonitor = { + dragMotion: this._onDragMotion.bind(this), + }; + DND.addDragMonitor(this._dragMonitor); + } + + _onDragMotion(dragEvent) { + let target = dragEvent.targetActor; + let hoveringActor = target == this.actor || this.actor.contains(target); + let canDrop = this._canDropAt(dragEvent.source); + let hasDndHover = hoveringActor && canDrop; + + if (this._hasDndHover != hasDndHover) { + this._setHoveringByDnd(hasDndHover); + this._hasDndHover = hasDndHover; + } + + return DND.DragMotionResult.CONTINUE; + } + + _onDragEnd() { + this.actor.remove_style_pseudo_class('drop'); + DND.removeDragMonitor(this._dragMonitor); + } + + handleDragOver(source, actor, x, y, time) { + if (source == this) + return DND.DragMotionResult.NO_DROP; + + if (!this._canDropAt(source)) + return DND.DragMotionResult.CONTINUE; + + return DND.DragMotionResult.MOVE_DROP; + } + + acceptDrop(source, actor, x, y, time) { + source.undoScaleAndFade(); + + this._setHoveringByDnd(false); + + if (!this._canDropAt(source)) + return false; + + return true; + } + + getDragActor() { + let iconParams = { + createIcon: this._createIcon.bind(this), + showLabel: (this._icon.label != null), + setSizeManually: true + }; + + let icon = new IconGrid.BaseIcon(this.name, iconParams); + icon.setIconSize(this.icon.iconSize); + + let bin = new St.Bin({ style_class: this.actor.style_class }); + bin.set_child(icon); + + return bin; + } + + getDragActorSource() { + return this._icon.icon; + } + + _scaleIn() { + this.actor.scale_x = 0; + this.actor.scale_y = 0; + this.actor.pivot_point = new Clutter.Point({ x: 0.5, y: 0.5 }); + + Tweener.addTween(this.actor, { + scale_x: 1, + scale_y: 1, + time: APP_ICON_SCALE_IN_TIME, + delay: APP_ICON_SCALE_IN_DELAY, + transition: (t, b, c, d) => { + // Similar to easeOutElastic, but less aggressive. + t /= d; + let p = 0.5; + return b + c * (Math.pow(2, -11 * t) * Math.sin(2 * Math.PI * (t - p / 4) / p) + 1); + } + }); + } + + _unscheduleScaleIn() { + if (this._scaleInId != 0) { + this.actor.disconnect(this._scaleInId); + this._scaleInId = 0; + } + } + + scheduleScaleIn() { + if (this._scaleInId != 0) + return; + + if (this.actor.mapped) { + this._scaleIn(); + } else { + this._scaleInId = this.actor.connect('notify::mapped', () => { + this._unscheduleScaleIn(); + this._scaleIn(); + }) + } + } + + scaleAndFade() { + this.actor.save_easing_state(); + this.actor.reactive = false; + this.actor.scale_x = 0.75; + this.actor.scale_y = 0.75; + this.actor.opacity = 128; + this.actor.restore_easing_state(); + } + + undoScaleAndFade() { + this.actor.save_easing_state(); + this.actor.reactive = true; + this.actor.scale_x = 1.0; + this.actor.scale_y = 1.0; + this.actor.opacity = 255; + this.actor.restore_easing_state(); + } + + get icon() { + return this._icon; + } + + get id() { + return this._id; + } + + get name() { + return this._name; + } + + get view() { + return this._view; + } +} + +var FolderIcon = class FolderIcon extends BaseViewIcon { constructor(id, path, parentView) { - this.id = id; - this.name = ''; - this._parentView = parentView; + super({ hideWhileDragging: true }, { + style_class: 'app-well-app app-folder', + toggle_mode: true + }); + + this._id = id; + this._name = ''; + this._view = parentView; this._folder = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders.folder', path: path }); - this.actor = new St.Button({ style_class: 'app-well-app app-folder', - button_mask: St.ButtonMask.ONE, - toggle_mode: true, - can_focus: true, - x_fill: true, - y_fill: true }); - this.actor._delegate = this; + // whether we need to update arrow side, position etc. this._popupInvalidated = false; - this.icon = new IconGrid.BaseIcon('', { + this._icon = new IconGrid.BaseIcon('', { createIcon: this._createIcon.bind(this), setSizeManually: true }); this.actor.set_child(this.icon); this.actor.label_actor = this.icon.label; - this.view = new FolderView(this._folder, parentView); - - Main.overview.connect('item-drag-begin', - this._onDragBegin.bind(this)); - Main.overview.connect('item-drag-end', - this._onDragEnd.bind(this)); + this._folderView = new FolderView(this._folder, parentView); this.actor.connect('clicked', this.open.bind(this)); this.actor.connect('destroy', this.onDestroy.bind(this)); @@ -1506,10 +1713,10 @@ var FolderIcon = class FolderIcon { } onDestroy() { - this.view.actor.destroy(); + this._folderView.actor.destroy(); if (this._spaceReadySignalId) { - this._parentView.disconnect(this._spaceReadySignalId); + this.view.disconnect(this._spaceReadySignalId); this._spaceReadySignalId = 0; } @@ -1519,38 +1726,22 @@ var FolderIcon = class FolderIcon { open() { this._ensurePopup(); - this.view.actor.vscroll.adjustment.value = 0; + this._folderView.actor.vscroll.adjustment.value = 0; this._openSpaceForPopup(); } getAppIds() { - return this.view.getAllItems().map(item => item.id); + return this._folderView.getAllItems().map(item => item.id); } _onDragBegin() { - this._dragMonitor = { - dragMotion: this._onDragMotion.bind(this), - }; - DND.addDragMonitor(this._dragMonitor); - - this._parentView.inhibitEventBlocker(); - } - - _onDragMotion(dragEvent) { - let target = dragEvent.targetActor; - - if (!this.actor.contains(target) || !this._canDropAt(dragEvent.source)) - this.actor.remove_style_pseudo_class('drop'); - else - this.actor.add_style_pseudo_class('drop'); - - return DND.DragMotionResult.CONTINUE; + super._onDragBegin(); + this.view.inhibitEventBlocker(); } _onDragEnd() { - this.actor.remove_style_pseudo_class('drop'); - this._parentView.uninhibitEventBlocker(); - DND.removeDragMonitor(this._dragMonitor); + super._onDragEnd(); + this.view.uninhibitEventBlocker(); } _canDropAt(source) { @@ -1590,57 +1781,57 @@ var FolderIcon = class FolderIcon { _updateName() { let name = _getFolderName(this._folder); - if (this.name == name) + if (this._name == name) return; - this.name = name; - this.icon.label.text = this.name; + this._name = name; + this.icon.label.text = name; this.emit('name-changed'); } _redisplay() { this._updateName(); - this.actor.visible = this.view.getAllItems().length > 0; + this.actor.visible = this._folderView.getAllItems().length > 0; this.icon.update(); this.emit('apps-changed'); } _createIcon(iconSize) { - return this.view.createFolderIcon(iconSize, this); + return this._folderView.createFolderIcon(iconSize, this); } _popupHeight() { - let usedHeight = this.view.usedHeight() + this._popup.getOffset(St.Side.TOP) + this._popup.getOffset(St.Side.BOTTOM); + let usedHeight = this._folderView.usedHeight() + this._popup.getOffset(St.Side.TOP) + this._popup.getOffset(St.Side.BOTTOM); return usedHeight; } _openSpaceForPopup() { - this._spaceReadySignalId = this._parentView.connect('space-ready', () => { - this._parentView.disconnect(this._spaceReadySignalId); + this._spaceReadySignalId = this.view.connect('space-ready', () => { + this.view.disconnect(this._spaceReadySignalId); this._spaceReadySignalId = 0; this._popup.popup(); this._updatePopupPosition(); }); - this._parentView.openSpaceForPopup(this, this._boxPointerArrowside, this.view.nRowsDisplayedAtOnce()); + this.view.openSpaceForPopup(this, this._boxPointerArrowside, this._folderView.nRowsDisplayedAtOnce()); } _calculateBoxPointerArrowSide() { - let spaceTop = this.actor.y - this._parentView.getCurrentPageY(); - let spaceBottom = this._parentView.actor.height - (spaceTop + this.actor.height); + let spaceTop = this.actor.y - this.view.getCurrentPageY(); + let spaceBottom = this.view.actor.height - (spaceTop + this.actor.height); return spaceTop > spaceBottom ? St.Side.BOTTOM : St.Side.TOP; } _updatePopupSize() { // StWidget delays style calculation until needed, make sure we use the correct values - this.view._grid.ensure_style(); + this._folderView._grid.ensure_style(); let offsetForEachSide = Math.ceil((this._popup.getOffset(St.Side.TOP) + this._popup.getOffset(St.Side.BOTTOM) - this._popup.getCloseButtonOverlap()) / 2); // Add extra padding to prevent boxpointer decorations and close button being cut off - this.view.setPaddingOffsets(offsetForEachSide); - this.view.adaptToSize(this._parentAvailableWidth, this._parentAvailableHeight); + this._folderView.setPaddingOffsets(offsetForEachSide); + this._folderView.adaptToSize(this._parentAvailableWidth, this._parentAvailableHeight); } _updatePopupPosition() { @@ -1659,7 +1850,7 @@ var FolderIcon = class FolderIcon { this._boxPointerArrowside = this._calculateBoxPointerArrowSide(); if (!this._popup) { this._popup = new AppFolderPopup(this, this._boxPointerArrowside); - this._parentView.addFolderPopup(this._popup); + this.view.addFolderPopup(this._popup); this._popup.connect('open-state-changed', (popup, isOpen) => { if (!isOpen) this.actor.checked = false; @@ -1676,7 +1867,7 @@ var FolderIcon = class FolderIcon { this._parentAvailableWidth = width; this._parentAvailableHeight = height; if (this._popup) - this.view.adaptToSize(width, height); + this._folderView.adaptToSize(width, height); this._popupInvalidated = true; } }; @@ -1685,7 +1876,7 @@ Signals.addSignalMethods(FolderIcon.prototype); var AppFolderPopup = class AppFolderPopup { constructor(source, side) { this._source = source; - this._view = source.view; + this._view = source._folderView; this._arrowSide = side; this._isOpen = false; @@ -1847,20 +2038,16 @@ var AppFolderPopup = class AppFolderPopup { }; Signals.addSignalMethods(AppFolderPopup.prototype); -var AppIcon = class AppIcon { - constructor(app, view, iconParams = {}) { +var AppIcon = class AppIcon extends BaseViewIcon { + constructor(app, parentView, iconParams = {}) { + super(iconParams, { + button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO, + style_class: 'app-well-app' + }); this.app = app; - this.id = app.get_id(); - this.name = app.get_name(); - this._view = view; - - this.actor = new St.Button({ style_class: 'app-well-app', - pivot_point: new Clutter.Point({x: 0.5, y: 0.5}), - reactive: true, - button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO, - can_focus: true, - x_fill: true, - y_fill: true }); + this._id = app.get_id(); + this._name = app.get_name(); + this._view = parentView; this._dot = new St.Widget({ style_class: 'app-well-app-running-dot', layout_manager: new Clutter.BinLayout(), @@ -1877,14 +2064,11 @@ var AppIcon = class AppIcon { this.actor._delegate = this; this._scaleInId = 0; - // Get the isDraggable property without passing it on to the BaseIcon: - let appIconParams = Params.parse(iconParams, { isDraggable: true }, true); - let isDraggable = appIconParams['isDraggable']; delete iconParams['isDraggable']; iconParams['createIcon'] = this._createIcon.bind(this); iconParams['setSizeManually'] = true; - this.icon = new IconGrid.BaseIcon(app.get_name(), iconParams); + this._icon = new IconGrid.BaseIcon(app.get_name(), iconParams); this._iconContainer.add_child(this.icon); this.actor.label_actor = this.icon.label; @@ -1898,26 +2082,8 @@ var AppIcon = class AppIcon { this._menu = null; this._menuManager = new PopupMenu.PopupMenuManager(this.actor); - if (isDraggable) { - this._draggable = DND.makeDraggable(this.actor); - this._draggable.connect('drag-begin', () => { - this._dragging = true; - this.scaleAndFade(); - this._removeMenuTimeout(); - Main.overview.beginItemDrag(this); - }); - this._draggable.connect('drag-cancelled', () => { - this._dragging = false; - Main.overview.cancelledItemDrag(this); - }); - this._draggable.connect('drag-end', () => { - this._dragging = false; - this.undoScaleAndFade(); - Main.overview.endItemDrag(this); - }); - } - - this.actor.connect('destroy', this._onDestroy.bind(this)); + if (this._draggable) + this._draggable.connect('drag-begin', this._removeMenuTimeout.bind(this)); this._menuTimeoutId = 0; this._stateChangedId = this.app.connect('notify::state', () => { @@ -1927,12 +2093,10 @@ var AppIcon = class AppIcon { } _onDestroy() { + super._onDestroy(); + if (this._stateChangedId > 0) this.app.disconnect(this._stateChangedId); - if (this._draggable && this._dragging) { - Main.overview.endItemDrag(this); - this.draggable = null; - } this._stateChangedId = 0; this._removeMenuTimeout(); } @@ -2075,46 +2239,6 @@ var AppIcon = class AppIcon { this.icon.animateZoomOut(); } - _scaleIn() { - this.actor.scale_x = 0; - this.actor.scale_y = 0; - this.actor.pivot_point = new Clutter.Point({ x: 0.5, y: 0.5 }); - - Tweener.addTween(this.actor, { - scale_x: 1, - scale_y: 1, - time: APP_ICON_SCALE_IN_TIME, - delay: APP_ICON_SCALE_IN_DELAY, - transition: (t, b, c, d) => { - // Similar to easeOutElastic, but less aggressive. - t /= d; - let p = 0.5; - return b + c * (Math.pow(2, -11 * t) * Math.sin(2 * Math.PI * (t - p / 4) / p) + 1); - } - }); - } - - _unscheduleScaleIn() { - if (this._scaleInId != 0) { - this.actor.disconnect(this._scaleInId); - this._scaleInId = 0; - } - } - - scheduleScaleIn() { - if (this._scaleInId != 0) - return; - - if (this.actor.mapped) { - this._scaleIn(); - } else { - this._scaleInId = this.actor.connect('notify::mapped', () => { - this._unscheduleScaleIn(); - this._scaleIn(); - }) - } - } - shellWorkspaceLaunch(params) { params = Params.parse(params, { workspace: -1, timestamp: 0 }); @@ -2122,46 +2246,9 @@ var AppIcon = class AppIcon { this.app.open_new_window(params.workspace); } - getDragActor() { - let iconParams = { createIcon: this._createIcon.bind(this), - showLabel: (this.icon.label != null), - setSizeManually: true }; - let icon = new IconGrid.BaseIcon(this.name, iconParams); - icon.setIconSize(this.icon.iconSize); - return icon; - } - - // 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.actor.hover && (!this._menu || !this._menu.isOpen); } - - scaleAndFade() { - this.actor.save_easing_state(); - this.actor.reactive = false; - this.actor.scale_x = 0.75; - this.actor.scale_y = 0.75; - this.actor.opacity = 128; - this.actor.restore_easing_state(); - } - - undoScaleAndFade() { - this.actor.save_easing_state(); - this.actor.reactive = true; - this.actor.scale_x = 1.0; - this.actor.scale_y = 1.0; - this.actor.opacity = 255; - this.actor.restore_easing_state(); - } - - get view() { - return this._view; - } }; Signals.addSignalMethods(AppIcon.prototype); diff --git a/js/ui/dash.js b/js/ui/dash.js index c22cb3e8b4..136a9837ef 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -25,6 +25,32 @@ function getAppFromSource(source) { } } +var DashIcon = class DashIcon extends AppDisplay.AppIcon { + constructor(app) { + super(app, null, { + setSizeManually: true, + showLabel: false + }); + + + } + + // Disable all DnD methods + _onDragBegin() { + } + + _onDragEnd() { + } + + handleDragOver() { + return DND.DragMotionResult.CONTINUE; + } + + acceptDrop() { + return false; + } +} + // A container like StBin, but taking the child's scale into account // when requesting a size var DashItemContainer = GObject.registerClass( @@ -475,9 +501,7 @@ var Dash = class Dash { } _createAppItem(app) { - let appIcon = new AppDisplay.AppIcon(app, null, - { setSizeManually: true, - showLabel: false }); + let appIcon = new DashIcon(app); appIcon.connect('menu-state-changed', (appIcon, opened) => { -- GitLab From 40ad9ab18c6bfc054c902222c62a90986a093725 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 12 Jul 2019 19:32:54 -0300 Subject: [PATCH 33/36] appIcon: Show folder preview when dragging over WIP https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 6a722498be..cf71d7b10a 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -2063,6 +2063,7 @@ var AppIcon = class AppIcon extends BaseViewIcon { this.actor._delegate = this; this._scaleInId = 0; + this._folderPreviewId = 0; delete iconParams['isDraggable']; @@ -2095,6 +2096,11 @@ var AppIcon = class AppIcon extends BaseViewIcon { _onDestroy() { super._onDestroy(); + if (this._folderPreviewId > 0) { + GLib.source_remove(this._folderPreviewId); + this._folderPreviewId = 0; + } + if (this._stateChangedId > 0) this.app.disconnect(this._stateChangedId); this._stateChangedId = 0; @@ -2249,6 +2255,57 @@ var AppIcon = class AppIcon extends BaseViewIcon { shouldShowTooltip() { return this.actor.hover && (!this._menu || !this._menu.isOpen); } + + _showFolderPreview() { + this.icon.label.opacity = 0; + + // HACK!!! + this.icon._iconBin.save_easing_state(); + this.icon._iconBin.scale_x = FOLDER_SUBICON_FRACTION; + this.icon._iconBin.scale_y = FOLDER_SUBICON_FRACTION; + this.icon._iconBin.restore_easing_state(); + } + + _hideFolderPreview() { + this.icon.label.opacity = 255; + + // HACK!!! + this.icon._iconBin.save_easing_state(); + this.icon._iconBin.scale_x = 1.0; + this.icon._iconBin.scale_y = 1.0; + this.icon._iconBin.restore_easing_state(); + } + + _setHoveringByDnd(hovering) { + if (hovering) { + if (this._folderPreviewId > 0) + return; + + this._folderPreviewId = + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + this._folderPreviewId = 0; + + super._setHoveringByDnd(true); + this._showFolderPreview(); + + return GLib.SOURCE_REMOVE; + }); + } else { + if (this._folderPreviewId > 0) { + GLib.source_remove(this._folderPreviewId); + this._folderPreviewId = 0; + } + + super._setHoveringByDnd(false); + this._hideFolderPreview(); + } + } + + _canDropAt(source) { + return source != this && + (source instanceof AppIcon) && + (this.view instanceof AllView); + } }; Signals.addSignalMethods(AppIcon.prototype); -- GitLab From 854922866bc07a6ed21f134dce0cea2c63bba466 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 16 Jul 2019 17:51:25 -0300 Subject: [PATCH 34/36] appIcon: Create and delete folders with DnD Create a new folder when dropping an icon over another icon. Try and find a good folder name by looking into the categories of the applications. Delete the folder when removing the last icon. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 122 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index cf71d7b10a..79437e24a8 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -85,6 +85,44 @@ function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } +function _findBestFolderName(apps) { + let appInfos = apps.map(app => app.get_app_info()); + + let categoryCounter = {}; + let commonCategories = []; + + appInfos.reduce((categories, appInfo) => { + for (let category of appInfo.get_categories().split(';')) { + 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) { + let keyfile = new GLib.KeyFile(); + let path = 'desktop-directories/%s.directory'.format(category); + + try { + keyfile.load_from_data_dirs(path, GLib.KeyFileFlags.NONE); + return keyfile.get_locale_string('Desktop Entry', 'Name', null); + } catch (e) { + continue; + } + } + + return null; +} + class BaseAppView { constructor(params, gridParams) { if (this.constructor === BaseAppView) @@ -911,6 +949,46 @@ var AllView = class AllView extends BaseAppView { this._nEventBlockerInhibits--; this._eventBlocker.visible = this._nEventBlockerInhibits == 0; } + + createFolder(apps, position=-1) { + let newFolderId = GLib.uuid_string_random(); + + let folders = this._folderSettings.get_strv('folder-children'); + folders.push(newFolderId); + this._folderSettings.set_strv('folder-children', folders); + + // Position the new folder before creating it + if (position >= 0) { + let iconsData = this._gridSettings.get_value('icons-data').deep_unpack(); + iconsData[newFolderId] = new GLib.Variant('a{sv}', { + 'position': GLib.Variant.new_uint32(position), + }); + this._gridSettings.set_value('icons-data', + new GLib.Variant('a{sv}', iconsData)); + } + + // Create the new folder. We are cannot use but Gio.Settings.new_with_path() + // for that. + let newFolderPath = this._folderSettings.path.concat('folders/', newFolderId, '/'); + let newFolderSettings = Gio.Settings.new_with_path('org.gnome.desktop.app-folders.folder', + newFolderPath); + if (!newFolderSettings) { + log('Error creating new folder'); + return false; + } + + let appItems = apps.map(id => this._items[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(); + + return true; + } }; Signals.addSignalMethods(AllView.prototype); @@ -1264,11 +1342,12 @@ var AppSearchProvider = class AppSearchProvider { }; var FolderView = class FolderView extends BaseAppView { - constructor(folder, parentView) { + constructor(folder, id, parentView) { super(null, null); // 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; @@ -1450,7 +1529,30 @@ var FolderView = class FolderView extends BaseAppView { folderApps.splice(index, 1); - this._folder.set_strv('apps', folderApps); + // Remove the folder if this is the last app icon; otherwise, + // just remove the icon + if (folderApps.length == 0) { + 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); + + // Resetting all keys deletes the relocatable schema + let keys = this._folder.settings_schema.list_keys(); + for (let key of keys) + this._folder.reset(key); + + // Remove the folder from the custom position list too + settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); + let iconsData = settings.get_value('icons-data').deep_unpack(); + if (iconsData[this._id]) { + delete iconsData[this._id]; + settings.set_value('icons-data', + new GLib.Variant('a{sv}', iconsData)); + } + } else { + this._folder.set_strv('apps', folderApps); + } return true; } @@ -1699,7 +1801,7 @@ var FolderIcon = class FolderIcon extends BaseViewIcon { this.actor.set_child(this.icon); this.actor.label_actor = this.icon.label; - this._folderView = new FolderView(this._folder, parentView); + this._folderView = new FolderView(this._folder, id, parentView); this.actor.connect('clicked', this.open.bind(this)); this.actor.connect('destroy', this.onDestroy.bind(this)); @@ -2306,6 +2408,20 @@ var AppIcon = class AppIcon extends BaseViewIcon { (source instanceof AppIcon) && (this.view instanceof AllView); } + + acceptDrop(source, actor, x, y, time) { + if (!super.acceptDrop(source, actor, x, y, time)) + return false; + + let apps = [this.id, source.id]; + let visibleItems = this.view.getAllItems().filter(item => item.actor.visible); + let position = visibleItems.indexOf(this); + + if (visibleItems.indexOf(source) < position) + position -= 1; + + return this.view.createFolder(apps, position); + } }; Signals.addSignalMethods(AppIcon.prototype); -- GitLab From 717ec0f8a4667b024c8c8344cc4e905b8fb76a71 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 5 Jul 2019 11:05:29 -0300 Subject: [PATCH 35/36] folderView: Allow moving to specific position As of now, the only way to add an app icon to a folder is by dragging it to the folder icon itself. Even though we allow opening the folder popup when hovering the icon, dropping an app icon there wouldn't work. Make the folder view add the app icon to it's GSettings key (which will trigger _redisplay() and will show the new icon) when dropping to a specific position. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 79437e24a8..7e9b558e55 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1440,13 +1440,28 @@ var FolderView = class FolderView extends BaseAppView { acceptDrop(source, actor, x, y, time) { let [index, dragLocation] = this.canDropAt(x, y); - let sourceIndex = this._allItems.indexOf(source); let success = index != -1; source.undoScaleAndFade(); - if (success) - this.moveItem(source, index); + if (success) { + // If we're dragging from another folder, remove from the old folder + if (source.view != this && + (source instanceof AppIcon) && + (source.view instanceof FolderView)) { + source.view.removeApp(source.app); + } + + // If the new app icon is not in this folder yet, add it; otherwise, + // just move the icon to the new position + let folderApps = this._folder.get_strv('apps'); + if (!folderApps.includes(source.id)) { + folderApps.splice(index, 0, source.id); + this._folder.set_strv('apps', folderApps); + } else { + this.moveItem(source, index); + } + } this.removeNudges(); return success; -- GitLab From 87a76a5757519207ddf0aa334345a405a3ddeb45 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 4 Jul 2019 16:03:20 -0300 Subject: [PATCH 36/36] appDisplay: Close popup when dragging When a drag starts inside a folder, and the cursor moves to outside it, close the currently opened folder popup. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603 --- js/ui/appDisplay.js | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 7e9b558e55..4bf87d0041 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -431,6 +431,7 @@ var AllView = class AllView extends BaseAppView { Main.overview.connect('item-drag-end', this._onDragEnd.bind(this)); this._nEventBlockerInhibits = 0; + this._popdownId = 0; } _refilterApps() { @@ -864,6 +865,25 @@ var AllView = class AllView extends BaseAppView { } } + _schedulePopdown() { + if (this._popdownId > 0) + return; + + this._popdownId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { + if (this._currentPopup) + this._currentPopup.popdown(); + this._popdownId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _unschedulePopdown() { + if (this._popdownId > 0) { + GLib.source_remove(this._popdownId); + this._popdownId = 0; + } + } + _onDragBegin() { this._dragMonitor = { dragMotion: this._onDragMotion.bind(this) @@ -874,11 +894,22 @@ var AllView = class AllView extends BaseAppView { _onDragMotion(dragEvent) { let 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.view == this) + // When dragging from a folder, don't nudge items; instead, + // prevent DND entirely by returning NO_DROP + if (this._currentPopup) { + if (dragEvent.targetActor == this._grid || + this._grid.contains(dragEvent.targetActor)) { + this._schedulePopdown(); + return DND.DragMotionResult.NO_DROP; + } else { + this._unschedulePopdown(); + } + } else { + // 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); + } if (dragEvent.targetActor != this._grid) this.removeNudges(); -- GitLab