%PDF- %PDF-
| Direktori : /usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/prefs/ |
| Current File : //usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/prefs/layoutsPrefs.js |
import { Gio, GLib } from '../dependencies/prefs/gi.js';
import { LayoutRow } from './layoutRow.js';
/**
* This class takes care of everything related to layouts (at least on the
* preference side). It's only being instanced by prefs.js. After that, it
* loads / saves layouts from / to the disk and loads the gui for managing
* layouts. The gui is created by instancing a bunch of Gtk.ListBoxRows from
* layoutGui.js for each layout and putting them into a Gtk.ListBox from the
* prefs.ui file.
*
* A popup layout has a name (String) and an array of LayoutItems (JS Objects).
* A LayoutItem has a rect (JS Objects), an optional (String) appId and optional
* loopType (String). Only the rect is a mandatory. The name lets the user
* search for a layout with the 'Search popup layout' keybinding. The rectangle's
* properties range from 0 to 1 (-> relative scale to the monitor). After a layout
* is activated by the user, the 'Tiling Popup' will appear at every LayoutItem's
* rect and ask the user which of the open windows they want to tile to that rect.
* If a loopType is set, the Tiling Popup will keep spawning at that spot and
* all tiled windows will evenly share that rect until the user cancels the tiling
* popup. Only then will we jump to the next LayoutItem. Possible loopTypes:
* horizontal ('h') or vertical (any other non-empty string). This allows the
* user to create 'Master and Stack' type of layouts. If an appId is defined,
* instead of the Tiling Popup appearing, a new instance of the app will be
* opened and tiled to that rect (or at least I tried to do that).
*
* By default, the settings for layouts are hidden behind the 'Advanced /
* Experimental' switch because I used a lot of hacks / assumptions... and
* I am not even using the layouts myself. However, I don't want to remove
* an existing feature... thus it's hidden
*/
export default class {
constructor(settings, builder, path) {
// Keep a reference to the settings for the shortcuts
this._settings = settings;
// The Gtk.ListBox, which LayoutRows are added to
this._layoutsListBox = builder.get_object('layouts_listbox');
// Unique button to save changes made to all layouts to the disk. For
// simplicity, reload from file after saving to get rid of invalid input.
this._saveLayoutsButton = builder.get_object('save_layouts_button');
this._saveLayoutsButton.connect('clicked', () => {
this._saveLayouts();
this._loadLayouts();
});
// Unique button to load layouts from the disk
// (discarding all tmp changes) without any user prompt
this._reloadLayoutsButton = builder.get_object('reload_layouts_button');
this._reloadLayoutsButton.connect('clicked', () => {
this._loadLayouts();
});
// Unique button to add a new *tmp* LayoutRow
this._addLayoutButton = builder.get_object('add_layout_button');
this._addLayoutButton.connect('clicked', () => {
const row = this._createLayoutRow(LayoutRow.getInstanceCount());
row.toggleReveal();
});
// Bind the general layouts keyboard shortcuts.
['search-popup-layout'].forEach(key => {
const shortcut = builder.get_object(key.replaceAll('-', '_'));
shortcut.initialize(key, this._settings);
});
// Finally, load the existing settings.
this._loadLayouts(path);
}
_loadLayouts(path) {
this._applySaveButtonStyle('');
this._forEachLayoutRow(row => row.destroy());
LayoutRow.resetInstanceCount();
// Try to load layouts file.
const saveFile = this._makeFile();
const [success, contents] = saveFile.load_contents(null);
if (!success)
return;
let layouts = [];
// Custom layouts are already defined in the file.
if (contents.length) {
layouts = JSON.parse(new TextDecoder().decode(contents));
// Ensure at least 1 empty row otherwise the listbox won't have
// a height but a weird looking shadow only.
layouts.length
? layouts.forEach((layout, idx) => this._createLayoutRow(idx, layout))
: this._createLayoutRow(0);
// Otherwise import the examples... but only do it once!
// Use a setting as a flag.
} else {
const importExamples = 'import-layout-examples';
if (!this._settings.get_boolean(importExamples))
return;
this._settings.set_boolean(importExamples, false);
const exampleFile = this._makeFile(`${path}/src`, 'layouts_example.json');
const [succ, c] = exampleFile.load_contents(null);
if (!succ)
return;
layouts = c.length ? JSON.parse(new TextDecoder().decode(c)) : [];
layouts.forEach((layout, idx) => this._createLayoutRow(idx, layout));
this._saveLayouts();
}
}
_saveLayouts() {
this._applySaveButtonStyle('');
const layouts = [];
this._forEachLayoutRow(layoutRow => {
const lay = layoutRow.getLayout();
if (lay) {
layouts.push(lay);
// Check, if all layoutRows were valid so far. Use getIdx()
// instead of forEach's idx because a layoutRow may have been
// deleted by the user.
if (layoutRow.getIdx() === layouts.length - 1)
return;
// Invalid or empty layouts are ignored. For example, the user
// defined a valid layout with a keybinding on row idx 3 but left
// the row at idx 2 empty. When saving, the layout at idx 2 gets
// removed and layout at idx 3 takes its place (i. e. becomes
// idx 2). We need to update the keybindings to reflect that.
const keys = this._settings.get_strv(`activate-layout${layoutRow.getIdx()}`);
this._settings.set_strv(`activate-layout${layouts.length - 1}`, keys);
this._settings.set_strv(`activate-layout${layoutRow.getIdx()}`, []);
} else {
// Remove keyboard shortcuts, if they aren't assigned to a
// valid layout, because they won't be visible to the user
// since invalid layouts get removed
this._settings.set_strv(`activate-layout${layoutRow.getIdx()}`, []);
}
});
const saveFile = this._makeFile();
saveFile.replace_contents(
JSON.stringify(layouts),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null
);
}
/**
* @param {string} [parentPath=''] path to the parent directory.
* @param {string} [fileName=''] name of the layouts file.
* @returns {object} the Gio.File.
*/
_makeFile(parentPath = '', fileName = '') {
// Create directory structure, if it doesn't exist.
const userConfigDir = GLib.get_user_config_dir();
const dirLocation = parentPath ||
GLib.build_filenamev([userConfigDir, '/tiling-assistant']);
const parentDir = Gio.File.new_for_path(dirLocation);
try { parentDir.make_directory_with_parents(null); } catch (e) {}
// Create file, if it doesn't exist.
const fName = fileName || 'layouts.json';
const filePath = GLib.build_filenamev([dirLocation, '/', fName]);
const file = Gio.File.new_for_path(filePath);
try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) {}
return file;
}
/**
* @param {string} [actionName=''] possible styles: 'suggested-action'
* or 'destructive-action'
*/
_applySaveButtonStyle(actionName = '') {
// The suggested-action is used to indicate that the user made
// changes; the destructive-action, if saving will drop changes
// (e. g. when changes were invalid)
const actions = ['suggested-action', 'destructive-action'];
const context = this._saveLayoutsButton.get_style_context();
actions.forEach(a => a === actionName
? context.add_class(a)
: context.remove_class(a));
}
/**
* @param {number} index the index of the new layouts row.
* @param {Layout} layout the parsed JS Object from the layouts file.
*/
_createLayoutRow(index, layout = null) {
// Layouts are limited to 20 since there are only
// that many keybindings in the schemas.xml file
if (index >= 20)
return;
const layoutRow = new LayoutRow(layout, this._settings);
layoutRow.connect('changed', (row, ok) => {
// Un / Highlight the save button, if the user made in / valid changes.
this._applySaveButtonStyle(ok ? 'suggested-action' : 'destructive-action');
});
this._layoutsListBox.append(layoutRow);
return layoutRow;
}
_forEachLayoutRow(callback) {
for (let i = 0, child = this._layoutsListBox.get_first_child(); !!child; i++) {
// Get a ref to the next widget in case the curr widget
// gets destroyed during the function call.
const nxtSibling = child.get_next_sibling();
callback.call(this, child, i);
child = nxtSibling;
}
}
}