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] 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 cf71d7b10..79437e24a 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);