/* extension.js * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-2.0-or-later */ import { Gio, GLib, Meta } from './src/dependencies/gi.js'; import { Extension, Main } from './src/dependencies/shell.js'; import MoveHandler from './src/extension/moveHandler.js'; import ResizeHandler from './src/extension/resizeHandler.js'; import KeybindingHandler from './src/extension/keybindingHandler.js'; import LayoutsManager from './src/extension/layoutsManager.js'; import ActiveWindowHint from './src/extension/activeWindowHint.js'; import AltTabOverride from './src/extension/altTab.js'; import { Rect } from './src/extension/utility.js'; /** * 2 entry points: * 1. keyboard shortcuts: * => keybindingHandler.js * 2. Grabbing a window: * => moveHandler.js (when moving a window) * => resizeHandler.js (when resizing a window) */ class SettingsOverrider { constructor(settingsSingleton) { this._settings = settingsSingleton; this._overrides = new Map(); this._originalSettings = new Map(); this._maybeNullValue = GLib.Variant.new_maybe( new GLib.VariantType('b'), null); const savedSettings = this._settings.getUserValue('overridden-settings'); this._wasOverridden = savedSettings !== null; } _maybeUpdateOverriden(schemaId, key, value) { if (this._wasOverridden) return undefined; const savedSettings = this._settings.getValue( 'overridden-settings').deepUnpack(); const prefKey = `${schemaId}.${key}`; const oldValue = savedSettings[prefKey]; if (value !== undefined) savedSettings[prefKey] = value ?? this._maybeNullValue; else delete savedSettings[prefKey]; this._settings.setValue('overridden-settings', new GLib.Variant('a{sv}', savedSettings)); return oldValue; } add(settings, key, value) { this._originalSettings.set(settings.schemaId, settings); const userValue = settings.get_user_value(key); const values = this._overrides.get(settings.schemaId) ?? new Map(); if (!values.size) this._overrides.set(settings.schemaId, values); values.set(key, userValue); settings.set_value(key, value); this._maybeUpdateOverriden(settings.schemaId, key, userValue ?? this._maybeNullValue); } remove(schema, key) { const settings = this._originalSettings.get(schema); if (!settings) return; const values = this._overrides.get(settings.schemaId); const value = values?.get(key); if (value === undefined) return; if (value) settings.set_value(key, value); else settings.reset(key); values.delete(key); this._maybeUpdateOverriden(settings.schemaId, key, undefined); } _clear() { if (this._wasOverridden) { const savedSettings = this._settings.getValue( 'overridden-settings').unpack(); Object.entries(savedSettings).forEach(([path, value]) => { const splits = path.split('.'); const key = splits.at(-1); const schemaId = splits.slice(0, -1).join('.'); const settings = this._originalSettings.get(schemaId) ?? new Gio.Settings({ schema_id: schemaId }); value = value.get_variant(); if (value.equal(this._maybeNullValue)) settings.reset(key); else settings.set_value(key, value); }); } else { this._originalSettings.forEach(settings => { this._overrides.get(settings.schemaId).forEach((value, key) => { if (value) settings.set_value(key, value); else settings.reset(key); }); }); } this._settings.reset('overridden-settings'); } destroy() { this._clear(); this._maybeNullValue = null; this._originalSettings = null; this._overrides = null; this._settings = null; } } export default class TilingAssistantExtension extends Extension { async enable() { this.settings = (await import('./src/common.js')).Settings; this.settings.initialize(this.getSettings()); this._settingsOverrider = new SettingsOverrider(this.settings); const twmModule = await import('./src/extension/tilingWindowManager.js'); this._twm = twmModule.TilingWindowManager; this._twm.initialize(); this._moveHandler = new MoveHandler(); this._resizeHandler = new ResizeHandler(); this._keybindingHandler = new KeybindingHandler(); this._layoutsManager = new LayoutsManager(); this._activeWindowHintHandler = new ActiveWindowHint(); this._altTabOverride = new AltTabOverride(); // Disable native tiling. this._settingsOverrider.add(new Gio.Settings({ schema_id: 'org.gnome.mutter' }), 'edge-tiling', new GLib.Variant('b', false)); // Disable native keybindings for Super+Up/Down/Left/Right const gnomeMutterKeybindings = new Gio.Settings({ schema_id: 'org.gnome.mutter.keybindings' }); const gnomeDesktopKeybindings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.keybindings' }); const sc = (await import('./src/common.js')).Shortcuts; const emptyStrvVariant = new GLib.Variant('as', []); if (gnomeDesktopKeybindings.get_strv('maximize').includes('Up') && this.settings.getStrv(sc.MAXIMIZE).includes('Up')) { this._settingsOverrider.add(gnomeDesktopKeybindings, 'maximize', emptyStrvVariant); } if (gnomeDesktopKeybindings.get_strv('unmaximize').includes('Down') && this.settings.getStrv(sc.RESTORE_WINDOW).includes('Down')) { this._settingsOverrider.add(gnomeDesktopKeybindings, 'unmaximize', emptyStrvVariant); } if (gnomeMutterKeybindings.get_strv('toggle-tiled-left').includes('Left') && this.settings.getStrv(sc.LEFT).includes('Left')) { this._settingsOverrider.add(gnomeMutterKeybindings, 'toggle-tiled-left', emptyStrvVariant); } if (gnomeMutterKeybindings.get_strv('toggle-tiled-right').includes('Right') && this.settings.getStrv(sc.RIGHT).includes('Right')) { this._settingsOverrider.add(gnomeMutterKeybindings, 'toggle-tiled-right', emptyStrvVariant); } // Include tiled windows when dragging from the top panel. this._getDraggableWindowForPosition = Main.panel._getDraggableWindowForPosition; Main.panel._getDraggableWindowForPosition = function (stageX) { const workspaceManager = global.workspace_manager; const windows = workspaceManager.get_active_workspace().list_windows(); const allWindowsByStacking = global.display.sort_windows_by_stacking(windows).reverse(); return allWindowsByStacking.find(w => { const rect = w.get_frame_rect(); const workArea = w.get_work_area_current_monitor(); return w.is_on_primary_monitor() && w.showing_on_its_workspace() && w.get_window_type() !== Meta.WindowType.DESKTOP && (w.maximized_vertically || w.tiledRect?.y === workArea.y) && stageX > rect.x && stageX < rect.x + rect.width; }); }; // Restore tiled window properties after session was unlocked. this._loadAfterSessionLock(); // Setting used for detection of a fresh install and do compatibility // changes if necessary... this.settings.setInt('last-version-installed', this.metadata.version); } disable() { // Save tiled window properties, if the session was locked to restore // them after the session is unlocked again. this._saveBeforeSessionLock(); this._settingsOverrider.destroy(); this._settingsOverrider = null; this._moveHandler.destroy(); this._moveHandler = null; this._resizeHandler.destroy(); this._resizeHandler = null; this._keybindingHandler.destroy(); this._keybindingHandler = null; this._layoutsManager.destroy(); this._layoutsManager = null; this._activeWindowHintHandler.destroy(); this._activeWindowHintHandler = null; this._altTabOverride.destroy(); this._altTabOverride = null; this._twm.destroy(); this._twm = null; this.settings.destroy(); this.settings = null; // Restore old functions. Main.panel._getDraggableWindowForPosition = this._getDraggableWindowForPosition; this._getDraggableWindowForPosition = null; // Delete custom tiling properties. const openWindows = global.display.get_tab_list(Meta.TabList.NORMAL, null); openWindows.forEach(w => { delete w.isTiled; delete w.tiledRect; delete w.untiledRect; }); } /** * Extensions are disabled when the screen is locked. So save the custom tiling * properties of windows before locking the screen. */ _saveBeforeSessionLock() { if (!Main.sessionMode.isLocked) return; this._wasLocked = true; const rectToJsObj = rect => rect && { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; // can't just check for isTiled because maximized windows may // have an untiledRect as well in case window gaps are used const openWindows = this._twm.getWindows(true); const savedWindows = openWindows.filter(w => w.untiledRect).map(w => { return { windowId: w.get_stable_sequence(), isTiled: w.isTiled, tiledRect: rectToJsObj(w.tiledRect), untiledRect: rectToJsObj(w.untiledRect) }; }); const saveObj = { 'windows': savedWindows, 'tileGroups': Array.from(this._twm.getTileGroups()) }; const userPath = GLib.get_user_config_dir(); const parentPath = GLib.build_filenamev([userPath, '/tiling-assistant']); const parent = Gio.File.new_for_path(parentPath); try { parent.make_directory_with_parents(null); } catch (e) {} const path = GLib.build_filenamev([parentPath, '/tiledSessionRestore.json']); const file = Gio.File.new_for_path(path); try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) {} file.replace_contents(JSON.stringify(saveObj), null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); } /** * Extensions are disabled when the screen is locked. After having saved them, * reload them here. */ _loadAfterSessionLock() { if (!this._wasLocked) return; this._wasLocked = false; const userPath = GLib.get_user_config_dir(); const path = GLib.build_filenamev([userPath, '/tiling-assistant/tiledSessionRestore.json']); const file = Gio.File.new_for_path(path); if (!file.query_exists(null)) return; try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) {} const [success, contents] = file.load_contents(null); if (!success || !contents.length) return; const openWindows = this._twm.getWindows(true); const saveObj = JSON.parse(new TextDecoder().decode(contents)); const windowObjects = saveObj['windows']; windowObjects.forEach(wObj => { const { windowId, isTiled, tiledRect, untiledRect } = wObj; const window = openWindows.find(w => w.get_stable_sequence() === windowId); if (!window) return; const jsToRect = jsRect => jsRect && new Rect( jsRect.x, jsRect.y, jsRect.width, jsRect.height ); window.isTiled = isTiled; window.tiledRect = jsToRect(tiledRect); window.untiledRect = jsToRect(untiledRect); }); const tileGroups = new Map(saveObj['tileGroups']); this._twm.setTileGroups(tileGroups); openWindows.forEach(w => { if (tileGroups.has(w.get_id())) { const group = this._twm.getTileGroupFor(w); this._twm.updateTileGroup(group); } }); } }