[gnome-shell/T27795: 50/138] iconGridLayout: Add the Endless-specific class IconGridLayout
- From: Georges Basile Stavracas Neto <gbsneto src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell/T27795: 50/138] iconGridLayout: Add the Endless-specific class IconGridLayout
- Date: Tue, 1 Oct 2019 23:33:32 +0000 (UTC)
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]