/** Settings utility shared between the running extension and the preference UI. */ var Extension; if (imports.misc.extensionUtils.extensions) { Extension = imports.misc.extensionUtils.extensions["paperwm@hedning:matrix.org"]; } else { // Cannot relaiably test for imports.ui in the preference ui try { Extension = imports.ui.main.extensionManager.lookup("paperwm@hedning:matrix.org"); } catch(e) { Extension = imports.misc.extensionUtils.getCurrentExtension(); } } var Gio = imports.gi.Gio; var GLib = imports.gi.GLib; var Gtk = imports.gi.Gtk; var Convenience = Extension.imports.convenience; var settings = Convenience.getSettings(); var workspaceSettingsCache = {}; var WORKSPACE_KEY = 'org.gnome.Shell.Extensions.PaperWM.Workspace'; var WORKSPACE_LIST_KEY = 'org.gnome.Shell.Extensions.PaperWM.WorkspaceList'; var KEYBINDINGS_KEY = 'org.gnome.Shell.Extensions.PaperWM.Keybindings'; // This is the value mutter uses for the keyvalue of above_tab var META_KEY_ABOVE_TAB = 0x2f7259c9; var prefs = {}; ['window-gap', 'vertical-margin', 'vertical-margin-bottom', 'horizontal-margin', 'workspace-colors', 'default-background', 'animation-time', 'use-workspace-name', 'pressure-barrier', 'default-show-top-bar', 'swipe-sensitivity', 'swipe-friction', 'cycle-width-steps', 'cycle-height-steps'] .forEach((k) => setState(null, k)); function setVerticalMargin() { let vMargin = settings.get_int('vertical-margin'); let gap = settings.get_int('window-gap'); prefs.vertical_margin = Math.max(Math.round(gap/2), vMargin); } let timerId; function onWindowGapChanged() { setVerticalMargin(); if (timerId) imports.mainloop.source_remove(timerId); timerId = imports.mainloop.timeout_add(500, () => { Extension.imports.tiling.spaces.mru().forEach(space => { space.layout(); }); timerId = null; }); } function setState($, key) { let value = settings.get_value(key); let name = key.replace(/-/g, '_'); prefs[name] = value.deep_unpack(); } var schemaSource, workspaceList, conflictSettings; function setSchemas() { // Schemas that may contain conflicting keybindings // It's possible to inject or remove settings here on `user.init`. conflictSettings = [ new Gio.Settings({schema_id: 'org.gnome.mutter.keybindings'}), new Gio.Settings({schema_id: 'org.gnome.mutter.wayland.keybindings'}), new Gio.Settings({schema_id: "org.gnome.desktop.wm.keybindings"}), new Gio.Settings({schema_id: "org.gnome.shell.keybindings"}) ]; schemaSource = Gio.SettingsSchemaSource.new_from_directory( GLib.build_filenamev([Extension.path, "schemas"]), Gio.SettingsSchemaSource.get_default(), false ); workspaceList = new Gio.Settings({ settings_schema: schemaSource.lookup(WORKSPACE_LIST_KEY, true) }); } setSchemas(); // Initialize imediately so prefs.js can import properly function init() { settings.connect('changed', setState); settings.connect('changed::vertical-margin', onWindowGapChanged); settings.connect('changed::vertical-margin-bottom', onWindowGapChanged); settings.connect('changed::window-gap', onWindowGapChanged); setVerticalMargin(); // A intermediate window is created before the prefs dialog is created. // Prevent it from being inserted into the tiling causing flickering and general disorder defwinprop({ wm_class: "Gnome-shell-extension-prefs", scratch_layer: true, focus: true, }); defwinprop({ wm_class: /gnome-screenshot/i, scratch_layer: true, focus: true, }); } var id; function enable() { setSchemas(); } function disable() { workspaceSettingsCache = {}; } /// Workspaces function getWorkspaceSettings(index) { let list = workspaceList.get_strv('list'); for (let uuid of list) { let settings = getWorkspaceSettingsByUUID(uuid); if (settings.get_int('index') === index) { return [uuid, settings]; } } return getNewWorkspaceSettings(index); } function getNewWorkspaceSettings(index) { let uuid = GLib.uuid_string_random(); let settings = getWorkspaceSettingsByUUID(uuid); let list = workspaceList.get_strv('list'); list.push(uuid); workspaceList.set_strv('list', list); settings.set_int('index', index); return [uuid, settings]; } function getWorkspaceSettingsByUUID(uuid) { if (!workspaceSettingsCache[uuid]) { let settings = new Gio.Settings({ settings_schema: schemaSource.lookup(WORKSPACE_KEY, true), path: `/org/gnome/shell/extensions/paperwm/workspaces/${uuid}/` }); workspaceSettingsCache[uuid] = settings; } return workspaceSettingsCache[uuid]; } /** Returns [[uuid, settings, name], ...] (Only used for debugging/development atm.) */ function findWorkspaceSettingsByName(regex) { let list = workspaceList.get_strv('list'); let settingss = list.map(getWorkspaceSettingsByUUID); return Extension.imports.utils.zip(list, settingss, settingss.map(s => s.get_string('name'))) .filter(([uuid, s, name]) => name.match(regex)); } /** Only used for debugging/development atm. */ function deleteWorkspaceSettingsByName(regex, dryrun=true) { let out = "" function rprint(...args) { print(...args); out += args.join(" ") + "\n"; } let n = global.workspace_manager.get_n_workspaces(); for (let [uuid, s, name] of findWorkspaceSettingsByName(regex)) { let index = s.get_int('index'); if (index < n) { rprint("Skipping in-use settings", name, index); continue; } rprint(dryrun ? "[dry]" : "", `Delete settings for '${name}' (${uuid})`); if (!dryrun) { deleteWorkspaceSettings(uuid); } } return out; } /** Only used for debugging/development atm. */ function deleteWorkspaceSettings(uuid) { // NB! Does not check if the settings is currently in use. Does not reindex subsequent settings. let list = workspaceList.get_strv('list'); let i = list.indexOf(uuid); let settings = getWorkspaceSettingsByUUID(list[i]); for (let key of settings.list_keys()) { // Hopefully resetting all keys will delete the relocatable settings from dconf? settings.reset(key); } list.splice(i, 1); workspaceList.set_strv('list', list); } // Useful for debugging function printWorkspaceSettings() { let list = workspaceList.get_strv('list'); let settings = list.map(getWorkspaceSettingsByUUID); let zipped = Extension.imports.utils.zip(list, settings); const key = s => s[1].get_int('index'); zipped.sort((a,b) => key(a) - key(b)); for (let [uuid, s] of zipped) { print('index:', s.get_int('index'), s.get_string('name'), s.get_string('color'), uuid); } } /// Keybindings /** * Two keystrings can represent the same key combination */ function keystrToKeycombo(keystr) { // Above_Tab is a fake keysymbol provided by mutter let aboveTab = false; if (keystr.match(/Above_Tab/)) { // Gtk bails out if provided with an unknown keysymbol keystr = keystr.replace('Above_Tab', 'A'); aboveTab = true; } let [key, mask] = Gtk.accelerator_parse(keystr); if (aboveTab) key = META_KEY_ABOVE_TAB; return `${key}|${mask}`; // Since js doesn't have a mapable tuple type } function keycomboToKeystr(combo) { let [mutterKey, mods] = combo.split('|').map(s => Number.parseInt(s)); let key = mutterKey; if (mutterKey === META_KEY_ABOVE_TAB) key = 97; // a let keystr = Gtk.accelerator_name(key, mods); if (mutterKey === META_KEY_ABOVE_TAB) keystr = keystr.replace(/a$/, 'Above_Tab'); return keystr; } function generateKeycomboMap(settings) { let map = {}; for (let name of settings.list_keys()) { let value = settings.get_value(name); if (value.get_type_string() !== 'as') continue; for (let combo of value.deep_unpack().map(keystrToKeycombo)) { if (combo === '0|0') continue; if (map[combo]) { map[combo].push(name); } else { map[combo] = [name]; } } } return map; } function findConflicts(schemas) { schemas = schemas || conflictSettings; let conflicts = []; const paperMap = generateKeycomboMap(Convenience.getSettings(KEYBINDINGS_KEY)); for (let settings of schemas) { const against = generateKeycomboMap(settings); for (let combo in paperMap) { if (against[combo]) { conflicts.push({ name: paperMap[combo][0], conflicts: against[combo], settings, combo }); } } } return conflicts; } /// Winprops /** Modelled after notion/ion3's system Examples: defwinprop({ wm_class: "Riot", scratch_layer: true }) */ var winprops = []; function winprop_match_p(meta_window, prop) { let wm_class = meta_window.wm_class || ""; let title = meta_window.title; if (prop.wm_class.constructor === RegExp) { if (!wm_class.match(prop.wm_class)) return false; } else if (prop.wm_class !== wm_class) { return false; } if (prop.title) { if (prop.title.constructor === RegExp) { if (!title.match(prop.title)) return false; } else { if (prop.title !== title) return false; } } return true; } function find_winprop(meta_window) { let props = winprops.filter( winprop_match_p.bind(null, meta_window)); return props[0]; } function defwinprop(spec) { winprops.push(spec); }