From 488d98289c0dd971dac8485ceeceeb3da74c8599 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 16 Jul 2019 17:51:25 -0300 Subject: [PATCH] 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/671 --- js/ui/appDisplay.js | 99 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index ec1cb84c7..8882336ff 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -91,6 +91,44 @@ function _getViewFromIcon(icon) { return parent._delegate; } +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) @@ -791,6 +829,37 @@ var AllView = class AllView extends BaseAppView { this._nEventBlockerInhibits--; this._eventBlocker.visible = this._nEventBlockerInhibits == 0; } + + createFolder(apps) { + let newFolderId = GLib.uuid_string_random(); + + let folders = this._folderSettings.get_strv('folder-children'); + folders.push(newFolderId); + this._folderSettings.set_strv('folder-children', folders); + + // Create the new folder + let newFolderPath = this._folderSettings.path.concat('folders/', newFolderId, '/'); + let newFolderSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.app-folders.folder', + path: 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); @@ -1149,11 +1218,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; this._grid._delegate = this; @@ -1304,10 +1374,8 @@ var FolderView = class FolderView extends BaseAppView { removeApp(app) { let folderApps = this._folder.get_strv('apps'); let index = folderApps.indexOf(app.id); - if (index >= 0) { + if (index >= 0) folderApps.splice(index, 1); - this._folder.set_strv('apps', folderApps); - } // If this is a categories-based folder, also add it to // the list of excluded apps @@ -1318,6 +1386,22 @@ var FolderView = class FolderView extends BaseAppView { this._folder.set_strv('excluded-apps', excludedApps); } + // 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); + } else { + this._folder.set_strv('apps', folderApps); + } + return true; } }; @@ -1347,7 +1431,7 @@ var FolderIcon = class FolderIcon { this.actor.set_child(this.icon); this.actor.label_actor = this.icon.label; - this.view = new FolderView(this._folder, parentView); + this.view = new FolderView(this._folder, id, parentView); Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this)); @@ -2091,7 +2175,10 @@ var AppIcon = class AppIcon { if (!this._canAccept(source)) return false; - return true; + let view = _getViewFromIcon(this); + let apps = [this.id, source.id]; + + return view.createFolder(apps); } }; Signals.addSignalMethods(AppIcon.prototype);