[gnome-shell/T27795: 50/138] iconGridLayout: Add the Endless-specific class IconGridLayout



commit e549a99879d60d66c5a8b7f75e896ea65910c24c
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Wed Jan 31 14:14:19 2018 +0000

    iconGridLayout: Add the Endless-specific class IconGridLayout
    
    This will take care of reading and applying the Endless-specific
    layout of icons that we want to show in the desktop, per locale,
    based on either the value of a GSettings key or the defaults in
    the system, applying the image-specific overrides if needed.
    
    The plan is to get rid of this class in the near future and move
    to using the same GSettings-based mechanism than upstream, but
    for this release we well still be using it.
    
    2019-09-23: squashed with 58d2122de "iconGridLayout: Add the
                  Endless-specific class IconGridLayout"

 data/org.gnome.shell.gschema.xml.in |   7 +
 js/js-resources.gresource.xml       |   1 +
 js/misc/config.js.in                |   2 +
 js/ui/iconGridLayout.js             | 488 ++++++++++++++++++++++++++++++++++++
 po/POTFILES.in                      |   1 +
 5 files changed, 499 insertions(+)
---
diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in
index 829fe3f5ee..6a98124781 100644
--- a/data/org.gnome.shell.gschema.xml.in
+++ b/data/org.gnome.shell.gschema.xml.in
@@ -200,6 +200,13 @@
         key by the shell on start, and then cleared from here.
       </description>
     </key>
+    <key name="icon-grid-layout" type="a{sas}">
+      <default>{}</default>
+      <summary>Layout of application launcher icons in the grid</summary>
+      <description>
+        This key specifies the exact order of the icons shown in the applications launcher view.
+      </description>
+    </key>
     <child name="keybindings" schema="org.gnome.shell.keybindings"/>
   </schema>
 
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 160791a8b6..e0f5ee6bcd 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -146,6 +146,7 @@
     <file>ui/endlessButton.js</file>
     <file>ui/forceAppExitDialog.js</file>
     <file>ui/hotCorner.js</file>
+    <file>ui/iconGridLayout.js</file>
     <file>ui/monitor.js</file>
     <file>ui/sideComponent.js</file>
     <file>ui/status/orientation.js</file>
diff --git a/js/misc/config.js.in b/js/misc/config.js.in
index d7f16e608f..c1d6035d78 100644
--- a/js/misc/config.js.in
+++ b/js/misc/config.js.in
@@ -20,3 +20,5 @@ var VPNDIR = '@vpndir@';
 var LIBMUTTER_API_VERSION = '@LIBMUTTER_API_VERSION@'
 /* used for the watermark feature */
 var LOCALSTATEDIR = '@localstatedir@';
+/* used for the icongrid */
+var DATADIR = '@datadir@';
diff --git a/js/ui/iconGridLayout.js b/js/ui/iconGridLayout.js
new file mode 100644
index 0000000000..dde2e76f25
--- /dev/null
+++ b/js/ui/iconGridLayout.js
@@ -0,0 +1,488 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { EosMetrics, Gio, GLib, GObject, Json, Shell } = imports.gi;
+
+const Config = imports.misc.config;
+const Main = imports.ui.main;
+
+var DESKTOP_GRID_ID = 'desktop';
+
+const SCHEMA_KEY = 'icon-grid-layout';
+const DIRECTORY_EXT = '.directory';
+const FOLDER_DIR_NAME = 'desktop-directories';
+
+const DEFAULT_CONFIGS_DIR = Config.DATADIR + '/eos-shell-content/icon-grid-defaults';
+const DEFAULT_CONFIG_NAME_BASE = 'icon-grid';
+
+const OVERRIDE_CONFIGS_DIR = Config.LOCALSTATEDIR + '/lib/eos-image-defaults/icon-grid';
+const OVERRIDE_CONFIG_NAME_BASE = 'icon-grid';
+const PREPEND_CONFIG_NAME_BASE = 'icon-grid-prepend';
+const APPEND_CONFIG_NAME_BASE = 'icon-grid-append';
+
+/* Occurs when an application is uninstalled, meaning removed from the desktop's
+ * app grid. Applications can be uninstalled in the app store or via dragging
+ * and dropping to the trash.
+ */
+const SHELL_APP_REMOVED_EVENT = '683b40a7-cac0-4f9a-994c-4b274693a0a0';
+
+var IconGridLayout = GObject.registerClass({
+    Signals: { 'changed': {} },
+}, class IconGridLayout extends GObject.Object {
+    _init(params) {
+        super._init();
+
+        this._updateIconTree();
+
+        this._removeUndone = false;
+
+        global.settings.connect('changed::' + SCHEMA_KEY, () => {
+            this._updateIconTree();
+            this.emit('changed');
+        });
+    }
+
+    _getIconTreeFromVariant(allIcons) {
+        let iconTree = {};
+        let appSys = Shell.AppSystem.get_default();
+
+        for (let i = 0; i < allIcons.n_children(); i++) {
+            let context = allIcons.get_child_value(i);
+            let [folder, ] = context.get_child_value(0).get_string();
+            let children = context.get_child_value(1).get_strv();
+            iconTree[folder] = children.map(appId => {
+                // Some older versions of eos-app-store incorrectly added eos-app-*.desktop
+                // files to the icon grid layout, instead of the proper unprefixed .desktop
+                // files, which should never leak out of the Shell. Take these out of the
+                // icon layout.
+                if (appId.startsWith('eos-app-'))
+                    return appId.slice('eos-app-'.length);
+
+                // Some apps have their name superceded, for instance gedit -> org.gnome.gedit.
+                // We want the new name, not the old one.
+                let app = appSys.lookup_alias(appId);
+                if (app)
+                    return app.get_id();
+
+                return appId;
+            });
+        }
+
+        return iconTree;
+    }
+
+    _updateIconTree() {
+        let allIcons = global.settings.get_value(SCHEMA_KEY);
+        let nIcons = allIcons.n_children();
+        let iconTree = this._getIconTreeFromVariant(allIcons);
+
+        if (nIcons > 0 && !iconTree[DESKTOP_GRID_ID]) {
+            // Missing toplevel desktop ID indicates we are reading a
+            // corrupted setting. Reset grid to defaults, and let the logic
+            // below run after the GSettings notification
+            log('Corrupted icon-grid-layout detected, resetting to defaults');
+            global.settings.reset(SCHEMA_KEY);
+            return;
+        }
+
+        if (nIcons == 0) {
+            // Entirely empty indicates that we need to read in the defaults
+            allIcons = this._getDefaultIcons();
+            iconTree = this._getIconTreeFromVariant(allIcons);
+        }
+
+        this._iconTree = iconTree;
+    }
+
+    _loadConfigJsonString(dir, base) {
+        let jsonString = null;
+        let defaultFiles = GLib.get_language_names()
+            .filter(name => name.indexOf('.') == -1)
+            .map(name => {
+                let path = GLib.build_filenamev([dir, base + '-' + name + '.json']);
+                return Gio.File.new_for_path(path);
+            })
+            .some(defaultsFile => {
+                try {
+                    let [success, data] = defaultsFile.load_contents(null);
+                    jsonString = data.toString();
+                    return true;
+                } catch (e) {
+                    // Ignore errors, as we always have a fallback
+                }
+                return false;
+            });
+        return jsonString;
+    }
+
+    _mergeJsonStrings(base, override, prepend, append) {
+        let baseNode = {};
+        let prependNode = null;
+        let appendNode = null;
+        // If any image default override matches the user's locale,
+        // give that priority over the default from the base OS
+        if (override)
+            baseNode = JSON.parse(override);
+        else if (base)
+            baseNode = JSON.parse(base);
+
+        if (prepend)
+            prependNode = JSON.parse(prepend);
+
+        if (append)
+            appendNode = JSON.parse(append);
+
+        for (let key in baseNode) {
+            if (prependNode && prependNode[key])
+                baseNode[key] = prependNode[key].concat(baseNode[key]);
+
+            if (appendNode && appendNode[key])
+                baseNode[key] = baseNode[key].concat(appendNode[key]);
+        }
+        return JSON.stringify(baseNode);
+    }
+
+    _getDefaultIcons() {
+        let iconTree = null;
+
+        try {
+            let mergedJson = this._mergeJsonStrings(
+                this._loadConfigJsonString(DEFAULT_CONFIGS_DIR, DEFAULT_CONFIG_NAME_BASE),
+                this._loadConfigJsonString(OVERRIDE_CONFIGS_DIR, OVERRIDE_CONFIG_NAME_BASE),
+                this._loadConfigJsonString(OVERRIDE_CONFIGS_DIR, PREPEND_CONFIG_NAME_BASE),
+                this._loadConfigJsonString(OVERRIDE_CONFIGS_DIR, APPEND_CONFIG_NAME_BASE)
+            );
+            iconTree = Json.gvariant_deserialize_data(mergedJson, -1, 'a{sas}');
+        } catch (e) {
+            logError(e, 'Failed to read JSON config');
+        }
+
+        if (iconTree === null || iconTree.n_children() == 0) {
+            log('No icon grid defaults found!');
+            // At the minimum, put in something that avoids exceptions later
+            let fallback = {};
+            fallback[DESKTOP_GRID_ID] = [];
+            iconTree = GLib.Variant.new('a{sas}', fallback);
+        }
+
+        return iconTree;
+    }
+
+    hasIcon(id) {
+        for (let folderId in this._iconTree) {
+            let folder = this._iconTree[folderId];
+            if (folder.indexOf(id) != -1)
+                return true;
+        }
+
+        return false;
+    }
+
+    _getIconLocation(id) {
+        for (let folderId in this._iconTree) {
+            let folder = this._iconTree[folderId];
+            let nIcons = folder.length;
+
+            let itemIdx = folder.indexOf(id);
+            let nextId;
+
+            if (itemIdx < nIcons) {
+                nextId = folder[itemIdx + 1];
+            } else {
+                // append to the folder
+                nextId = null;
+            }
+
+            if (itemIdx != -1)
+                return [folderId, nextId];
+        }
+        return null;
+    }
+
+    getIcons(folder) {
+        if (this._iconTree && this._iconTree[folder])
+            return this._iconTree[folder];
+
+        return [];
+    }
+
+    iconIsFolder(id) {
+        return id && (id.endsWith(DIRECTORY_EXT));
+    }
+
+    appendIcon(id, folderId) {
+        this.repositionIcon(id, null, folderId);
+    }
+
+    removeIcon(id, interactive) {
+        if (!this.hasIcon(id))
+            return;
+
+        this._removeUndone = false;
+
+        let undoInfo = null;
+        let currentLocation = this._getIconLocation(id);
+        if (currentLocation)
+            undoInfo = { id: id,
+                         folderId: currentLocation[0],
+                         insertId: currentLocation[1] };
+
+        this.repositionIcon(id, null, null);
+
+        let info = null;
+        if (this.iconIsFolder(id)) {
+            info = Shell.DesktopDirInfo.new(id);
+        } else {
+            let appSystem = Shell.AppSystem.get_default();
+            let app = appSystem.lookup_alias(id);
+            if (app)
+                info = app.get_app_info();
+        }
+
+        if (!info)
+            return;
+
+        if (interactive)
+            Main.overview.setMessage(_("%s has been deleted").format(info.get_name()),
+                                     { forFeedback: true,
+                                       destroyCallback: (info) => {
+                                           this._onMessageDestroy (info);
+                                       },
+                                       undoCallback: (undoInfo) => {
+                                           this._undoRemoveItem(undoInfo);
+                                       },
+                                     });
+        else
+            this._onMessageDestroy(info);
+    }
+
+    _onMessageDestroy(info) {
+        if (this._removeUndone) {
+            this._removeUndone = false;
+            return;
+        }
+
+        if (!this.iconIsFolder(info.get_id())) {
+            let eventRecorder = EosMetrics.EventRecorder.get_default();
+            let appId = new GLib.Variant('s', info.get_id());
+            eventRecorder.record_event(SHELL_APP_REMOVED_EVENT, appId);
+        }
+
+        let filename = info.get_filename();
+        let userDir = GLib.get_user_data_dir();
+        if (filename && userDir && GLib.str_has_prefix(filename, userDir) &&
+            info.get_string('X-Endless-CreatedBy') === 'eos-desktop') {
+            // only delete .desktop files if they are in the user's local data
+            // folder and they were created by eos-desktop
+            info.delete();
+        }
+    }
+
+    _undoRemoveItem(undoInfo) {
+        if (undoInfo != null)
+            this.repositionIcon(undoInfo.id, undoInfo.insertId, undoInfo.folderId);
+
+        this._removeUndone = true;
+    }
+
+    listApplications() {
+        let allApplications = [];
+
+        for (let folderId in this._iconTree) {
+            let folder = this._iconTree[folderId];
+            for (let iconIdx in folder) {
+                let icon = folder[iconIdx];
+                if (!this.iconIsFolder(icon))
+                    allApplications.push(icon);
+            }
+        }
+
+        return allApplications;
+    }
+
+    repositionIcon(id, insertId, newFolderId) {
+        let icons;
+        let existing = false;
+        let isFolder = this.iconIsFolder(id);
+
+        for (let i in this._iconTree) {
+            icons = this._iconTree[i];
+            let oldPos = icons.indexOf(id);
+            if (oldPos != -1) {
+                icons.splice(oldPos, 1);
+                existing = true;
+                break;
+            }
+        }
+
+        if (newFolderId != null) {
+            // We're adding or repositioning an icon
+            icons = this._iconTree[newFolderId];
+
+            // Invalid destination folder
+            if (!icons)
+                return;
+
+            this._insertIcon(icons, id, insertId);
+
+            if (isFolder && !existing) {
+                // We're adding a folder, need to initialize an
+                // array for its contents
+                this._iconTree[id] = [];
+            }
+        } else {
+            // We're removing an entry
+            if (isFolder && existing) {
+                // We're removing a folder, need to delete the array
+                // for its contents as well
+                delete this._iconTree[id];
+            }
+        }
+
+        // Recreate GVariant from iconTree
+        let newLayout = GLib.Variant.new('a{sas}', this._iconTree);
+
+        // Store gsetting
+        global.settings.set_value(SCHEMA_KEY, newLayout);
+    }
+
+    resetDesktop() {
+        // Reset the gsetting to restore the default layout
+        global.settings.reset(SCHEMA_KEY);
+
+        let userPath = GLib.get_user_data_dir();
+        let userDir = Gio.File.new_for_path(userPath);
+
+        if (!userDir)
+            return;
+
+        // Remove any user-specified desktop files as consequence of
+        // renaming folders (only folders), to restore all default names
+        // and clean up any unused resources
+        let folderDir = userDir.get_child(FOLDER_DIR_NAME);
+        if (folderDir)
+            folderDir.enumerate_children_async(
+                Gio.FILE_ATTRIBUTE_STANDARD_NAME,
+                Gio.FileQueryInfoFlags.NONE,
+                GLib.PRIORITY_DEFAULT,
+                null,
+                this._enumerateDirectoryFiles.bind(this));
+    }
+
+    _enumerateDirectoryFiles(file, result) {
+        let enumerator = file.enumerate_children_finish(result);
+        enumerator.next_files_async(
+            GLib.MAXINT32, GLib.PRIORITY_DEFAULT, null, this._removeDirectoryFiles);
+    }
+
+    _removeDirectoryFiles(enumerator, result) {
+        let fileInfos = enumerator.next_files_finish(result);
+        for (let i = 0; i < fileInfos.length; i++) {
+            let fileInfo = fileInfos[i];
+            let fileName = fileInfo.get_name();
+            if (fileName.endsWith(DIRECTORY_EXT)) {
+                let file = enumerator.get_child(fileInfo);
+                file.delete_async(GLib.PRIORITY_DEFAULT, null, null);
+            }
+        }
+    }
+
+    // We use the insert Id instead of the index here since gsettings
+    // includes the full application list that the desktop may have.
+    // Relying on the position leads to faulty behaviour if some
+    // apps are not present on the system
+    _insertIcon(icons, id, insertId) {
+        let insertIdx = -1;
+
+        if (insertId != null)
+            insertIdx = icons.indexOf(insertId);
+
+        // We were dropped to the left of the trashcan,
+        // or we were asked to append
+        if (insertIdx == -1)
+            insertIdx = icons.length;
+
+        icons.splice(insertIdx, 0, id);
+    }
+
+    _createFolderFile(name) {
+        let keyFile = new GLib.KeyFile();
+
+        keyFile.set_value(GLib.KEY_FILE_DESKTOP_GROUP,
+                          GLib.KEY_FILE_DESKTOP_KEY_NAME, name);
+
+        keyFile.set_value(GLib.KEY_FILE_DESKTOP_GROUP,
+                          GLib.KEY_FILE_DESKTOP_KEY_TYPE, GLib.KEY_FILE_DESKTOP_TYPE_DIRECTORY);
+
+        let dir = Gio.File.new_for_path(GLib.get_user_data_dir() + '/desktop-directories');
+        try {
+            dir.make_directory_with_parents(null);
+        } catch (err) {
+            if (!err.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) {
+                logError(err, 'Error creating %s'.format(dir.get_path()));
+                return null;
+            }
+        }
+
+        let enumerator = null;
+        try {
+            enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+        } catch (err) {
+            logError(err, 'Error trying to traverse %s'.format(dir.get_path()));
+            return null;
+        }
+
+        let prefix = 'eos-folder-user-';
+        let suffix = '.directory';
+        let re = new RegExp(prefix + '([0-9]+)' + suffix);
+        let folderIndex = -1;
+
+        try {
+            for (let f = enumerator.next_file(null); f != null; f = enumerator.next_file(null)) {
+                let result = re.exec(f.get_name());
+                if (result != null) {
+                    let newFolderIndex = result[1];
+                    folderIndex = Math.max(folderIndex, parseInt(newFolderIndex));
+                }
+            }
+        } catch (err) {
+            logError(err, 'Error traversing %s'.format(dir.get_path()));
+            return null;
+        }
+
+        try {
+            enumerator.close(null);
+        } catch (err) {
+            logError(err, 'Error closing file enumerator for %s'.format(dir.get_path()));
+            return null;
+        }
+
+        ++folderIndex;
+        let filename = prefix + folderIndex + suffix;
+        let absFilename = GLib.build_filenamev([dir.get_path(), filename]);
+
+        try {
+            keyFile.save_to_file(absFilename);
+        } catch (err) {
+            logError(err, 'Failed to save key file for directory %s'.format(absFilename));
+            return null;
+        }
+
+        return filename;
+    }
+
+    addFolder(folderName) {
+        if (!folderName)
+            folderName = _("New Folder");
+
+        let id = this._createFolderFile(folderName);
+        if (!id)
+            return null;
+
+        this.appendIcon(id, DESKTOP_GRID_ID);
+
+        return id;
+    }
+});
+
+// to be used as singleton
+var layout = new IconGridLayout();
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 955765760d..eb16422faa 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -97,5 +97,6 @@ js/ui/appIconBar.js
 js/ui/endlessButton.js
 js/ui/forceAppExitDialog.js
 js/ui/hotCorner.js
+js/ui/iconGridLayout.js
 js/ui/status/orientation.js
 js/ui/userMenu.js


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]