[gnome-shell/eos3.8: 38/255] iconGridLayout: Add the Endless-specific class IconGridLayout



commit a0aafa78c2666a528f02976b1185803104d275d9
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"
     * 2020-03-13: Code style cleanup
          + Add LOCALSTATEDIR to js/misc/config.h.in
          + Fix logic error in IconGridLayout.findInArray()
          + Partially squashed with 5222f5c2b

 data/org.gnome.shell.gschema.xml.in |   8 +-
 js/js-resources.gresource.xml       |   1 +
 js/misc/config.js.in                |   3 +
 js/misc/meson.build                 |   1 +
 js/ui/iconGridLayout.js             | 526 ++++++++++++++++++++++++++++++++++++
 meson.build                         |   1 +
 po/POTFILES.in                      |   1 +
 7 files changed, 540 insertions(+), 1 deletion(-)
---
diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in
index 0f044ad3f5..6211310ebd 100644
--- a/data/org.gnome.shell.gschema.xml.in
+++ b/data/org.gnome.shell.gschema.xml.in
@@ -168,7 +168,13 @@
         when a window is minimized.
       </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 1baea14d28..9e8914a4a6 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/sideComponent.js</file>
     <file>ui/userMenu.js</file>
     <file>ui/workspaceMonitor.js</file>
diff --git a/js/misc/config.js.in b/js/misc/config.js.in
index e54e280441..6bea2c06da 100644
--- a/js/misc/config.js.in
+++ b/js/misc/config.js.in
@@ -17,3 +17,6 @@ var LIBEXECDIR = '@libexecdir@';
 var PKGDATADIR = '@datadir@/@PACKAGE_NAME@';
 /* g-i package versions */
 var LIBMUTTER_API_VERSION = '@LIBMUTTER_API_VERSION@'
+/* used for the icongrid */
+var DATADIR = '@datadir@';
+var LOCALSTATEDIR = '@localstatedir@';
diff --git a/js/misc/meson.build b/js/misc/meson.build
index 2702c3dbc9..6b56c9ef69 100644
--- a/js/misc/meson.build
+++ b/js/misc/meson.build
@@ -7,6 +7,7 @@ jsconf.set10('HAVE_BLUETOOTH', bt_dep.found())
 jsconf.set10('HAVE_NETWORKMANAGER', have_networkmanager)
 jsconf.set('datadir', datadir)
 jsconf.set('libexecdir', libexecdir)
+jsconf.set('localstatedir', localstatedir)
 
 config_js = configure_file(
   input: 'config.js.in',
diff --git a/js/ui/iconGridLayout.js b/js/ui/iconGridLayout.js
new file mode 100644
index 0000000000..b9d87f81bb
--- /dev/null
+++ b/js/ui/iconGridLayout.js
@@ -0,0 +1,526 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+/* exported getDefault */
+
+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 = '%s/eos-shell-content/icon-grid-defaults'.format(Config.DATADIR);
+const DEFAULT_CONFIG_NAME_BASE = 'icon-grid';
+
+const OVERRIDE_CONFIGS_DIR = '%s/lib/eos-image-defaults/icon-grid'.format(Config.LOCALSTATEDIR);
+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';
+
+function findInArray(array, func) {
+    for (let item of array) {
+        if (func(item))
+            return item;
+    }
+
+    return null;
+}
+
+let _singleton = null;
+
+function getDefault() {
+    if (_singleton === null)
+        _singleton = new IconGridLayout();
+
+    return _singleton;
+}
+
+var IconGridLayout = GObject.registerClass({
+    Signals: { 'layout-changed': {} },
+}, class IconGridLayout extends GObject.Object {
+    _init() {
+        super._init();
+
+        this._updateIconTree();
+
+        this._removeUndone = false;
+
+        global.settings.connect('changed::%s'.format(SCHEMA_KEY), () => {
+            this._updateIconTree();
+            this.emit('layout-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;
+        GLib.get_language_names()
+            .filter(name => name.indexOf('.') === -1)
+            .map(name => {
+                let path = GLib.build_filenamev([dir, '%s-%s.json'.format(base, name)]);
+                return Gio.File.new_for_path(path);
+            })
+            .some(defaultsFile => {
+                try {
+                    let [, 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);
+    }
+
+    // Two operations, first insert the new icon
+    // to the left of the old one, then remove
+    // the old one
+    //
+    // defaultFolderId here refers to the folder id
+    // to insert the icon into if the icon is not already
+    // in a folder. Otherwise, we use the folder that
+    // the icon is in already.
+    replaceIcon(originalId, replacementId, defaultFolderId) {
+        let folderId = findInArray(Object.keys(this._iconTree), key => {
+            return this._iconTree[key].indexOf(originalId) !== -1;
+        }) || defaultFolderId;
+
+        this.repositionIcon(replacementId, originalId, folderId);
+        this.removeIcon(originalId, false);
+    }
+
+    removeIcon(id, interactive) {
+        if (!this.hasIcon(id))
+            return;
+
+        this._removeUndone = false;
+
+        let undoInfo = null;
+        let currentLocation = this._getIconLocation(id);
+        if (currentLocation) {
+            undoInfo = {
+                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 removed').format(info.get_name()), {
+                    forFeedback: true,
+                    destroyCallback: () => this._onMessageDestroy(info),
+                    undoCallback: () => 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 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('%s/desktop-directories'.format(GLib.get_user_data_dir()));
+        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('%s([0-9]+)%s'.format(prefix, 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) {
+                    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, positionId = null) {
+        if (!folderName)
+            folderName = _('New Folder');
+
+        let id = this._createFolderFile(folderName);
+        if (!id)
+            return null;
+
+        this.repositionIcon(id, positionId, DESKTOP_GRID_ID);
+
+        return id;
+    }
+});
diff --git a/meson.build b/meson.build
index 3e4472704d..d1e6e75535 100644
--- a/meson.build
+++ b/meson.build
@@ -46,6 +46,7 @@ bindir = join_paths(prefix, get_option('bindir'))
 datadir = join_paths(prefix, get_option('datadir'))
 libdir = join_paths(prefix, get_option('libdir'))
 libexecdir = join_paths(prefix, get_option('libexecdir'))
+localstatedir = join_paths(prefix, get_option('localstatedir'))
 mandir = join_paths(prefix, get_option('mandir'))
 sysconfdir = join_paths(prefix, get_option('sysconfdir'))
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2d39b74683..7824ba5b2d 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -101,4 +101,5 @@ js/ui/appIconBar.js
 js/ui/endlessButton.js
 js/ui/forceAppExitDialog.js
 js/ui/hotCorner.js
+js/ui/iconGridLayout.js
 js/ui/userMenu.js


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