import Meta from 'gi://Meta'; import GLib from 'gi://GLib'; import {Orientation, Tile, TileState} from './tile.js'; import {Position} from './position.js'; import * as Resize from './resize.js'; import Gio from 'gi://Gio'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import {Monitor} from './monitor.js'; import Shell from 'gi://Shell'; import Mtk from 'gi://Mtk'; import {TopBarSearchEntry} from './topBarSearchEntry.js'; export var Direction; (function (Direction) { Direction[Direction['North'] = 1] = 'North'; Direction[Direction['South'] = 2] = 'South'; Direction[Direction['West'] = 3] = 'West'; Direction[Direction['East'] = 4] = 'East'; })(Direction || (Direction = {})); export class TileWindowManager { /** ************************************************/ // Store all signals to be restored when extension is disabled _wrappedWindows; _nMonitors; _windowCreatedSignal; _windowGrabSignal; _workspaceAddedSignal; _workspaceRemovedSignal; _activeWorkspaceSignal; _grabBeginSignal; _monitorChangedSignal; _workareasChangedSignal; /** ************************************************/ _settings; _userResize; static locked = false; // Alternate windows rotation static rotateEven = [0, 0]; // Search bar widgets _topBarSearchEntry; _modalSearchEntry; _sourceId; // Tiles structures static _workspaces = new Map(); static _main_monitor; constructor(extension) { if (!TileWindowManager._workspaces) TileWindowManager._workspaces = new Map(); TileWindowManager._main_monitor = global.display.get_primary_monitor(); this._settings = extension._settings; this._nMonitors = global.display.get_n_monitors(); for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { let _monitors = new Array(this._nMonitors); for (let j = 0; j < _monitors.length; j++) _monitors[j] = new Monitor(j); TileWindowManager._workspaces.set(i, _monitors); } this._wrappedWindows = new Map(); this._userResize = new Set(); this._sourceId = null; if (TileWindowManager.locked) this._loadAfterSessionLock(); global.get_window_actors().forEach(actor => { if (actor.meta_window && actor.meta_window.get_window_type() === Meta.WindowType.NORMAL) this._addNewWindow(actor.meta_window); }); this.updateMonitors(); this._windowCreatedSignal = global.display.connect('window-created', (display, obj) => this._onWindowCreated(display, obj)); this._workareasChangedSignal = global.display.connect('workareas-changed', () => { TileWindowManager._workspaces.forEach(val => { val.forEach(m => m.updateSize()); }); this.updateMonitors(); }); this._grabBeginSignal = global.display.connect('grab-op-begin', (_, w) => this._userResize.add(w)); this._windowGrabSignal = global.display.connect('grab-op-end', (_, window, op) => { this._onGrab(window, op); this._userResize.delete(window); }); this._workspaceAddedSignal = global.workspace_manager.connect('workspace-added', (_, index) => { this._onWorkspaceCreated(index); }); this._workspaceRemovedSignal = global.workspace_manager.connect('workspace-removed', (_, index) => { this._onWorkspaceRemoved(index); }); this._activeWorkspaceSignal = global.workspace_manager.connect('active-workspace-changed', () => { this.updateMonitors(); this.updateAdjacents(); }); this._monitorChangedSignal = global.backend.get_monitor_manager().connect('monitors-changed', () => { const n = global.display.get_n_monitors(); if (n !== this._nMonitors) { let diff = n - this._nMonitors; this._nMonitors = n; if (diff > 0) this._addMonitors(); else this._removeMonitors(); } }); } static getMonitors(workspaceIndex = undefined) { let wk; if (!workspaceIndex) wk = TileWindowManager._workspaces.get(global.workspace_manager.get_active_workspace_index()); else wk = TileWindowManager._workspaces.get(workspaceIndex); if (wk) return wk; else return []; } _addMonitors() { let newPrimMonitor = global.display.get_primary_monitor(); TileWindowManager._workspaces.forEach((val, _, __) => { let currPrim = null; if (newPrimMonitor !== TileWindowManager._main_monitor) currPrim = val.filter(e => e.index === TileWindowManager._main_monitor)[0]; while (val.length !== this._nMonitors) { const index = val.length; val.push(new Monitor(index)); // Move content on the new primary screen if (index === newPrimMonitor) { val[index].fullscreen = currPrim?.fullscreen ?? false; val[index].root = currPrim?.root ?? null; val[index].root?.forEach(el => { el.monitor = index; }); if (currPrim?.fullscreen) currPrim.fullscreen = false; if (currPrim?.root) currPrim.root = null; } } }); TileWindowManager._main_monitor = newPrimMonitor; } _removeMonitors() { TileWindowManager._workspaces.forEach((val, _, __) => { let windows = []; while (val.length !== this._nMonitors) windows.push(val.pop()); for (let i = 0; i < windows.length; i++) { windows[i]?.root?.forEach(t => { if (t.window) this._insertWindow(t.window, null, 0); }); } }); TileWindowManager._main_monitor = global.display.get_primary_monitor(); this.updateMonitors(); this.updateAdjacents(); } /** * Refresh **ALL** existing tiles. */ updateMonitors() { TileWindowManager._workspaces.forEach((value, _) => { value.forEach(el => el.root?.update()); }); } updateAdjacents() { TileWindowManager._workspaces.forEach((value, _) => { value.forEach(el => el.root?.forEach(t => t.findAdjacents())); }); } destroy() { global.display.disconnect(this._windowCreatedSignal); global.display.disconnect(this._windowGrabSignal); global.display.disconnect(this._grabBeginSignal); global.display.disconnect(this._workareasChangedSignal); global.workspace_manager.disconnect(this._workspaceAddedSignal); global.workspace_manager.disconnect(this._workspaceRemovedSignal); global.workspace_manager.disconnect(this._activeWorkspaceSignal); global.backend.disconnect(this._monitorChangedSignal); if (Resize.resizeSourceId !== null) GLib.Source.remove(Resize.resizeSourceId); // Disconnect each window this._wrappedWindows.forEach((value, key) => { key.minimize = value[0]; key.maximize = value[1]; key.disconnect(value[2]); key.disconnect(value[3]); key.disconnect(value[4]); key.disconnect(value[5]); key.disconnect(value[6]); key.disconnect(value[7]); this._wrappedWindows.delete(key); }); this._wrappedWindows.clear(); this._topBarSearchEntry?.destroy(); TileWindowManager._main_monitor = null; TileWindowManager._workspaces = null; } /** Check if the window is a `valid` window. * A `valid` window is a window created by user and * running an app. * It is tricky to filter windows correctly. Here we exclude * windows that don't have app id (the id is just the app number). * This method must be called when we're sure the window is **fully** * created (basically when wait for first-frame signal). Otherwise * we may badly filter some windows. * * @param {Meta.Window} window * @returns boolean */ _isValidWindow(window) { if (!window) return false; if (window.get_window_type() !== Meta.WindowType.NORMAL) return false; let app = Shell.WindowTracker.get_default().get_window_app(window); if (!app) return false; if (app.get_id().startsWith('window:')) return false; for (var [_, value] of TileWindowManager._workspaces) { let containsWindow = value.reduce((acc, val) => val.root ? acc || val.root.contains(window) : acc, false); if (containsWindow) return false; } return true; } _onWorkspaceCreated(index) { let _monitors = new Array(global.display.get_n_monitors()); for (let i = 0; i < _monitors.length; i++) _monitors[i] = new Monitor(i); if (!TileWindowManager._workspaces.has(index)) TileWindowManager._workspaces.set(index, _monitors); } _onWorkspaceRemoved(index) { TileWindowManager._workspaces.delete(index); let newMap = new Map(); TileWindowManager._workspaces.forEach((value, key) => { if (key > index) { value.forEach(el => el.root?.forEach(t => { t.workspace = key - 1; })); newMap.set(key - 1, value); } else { newMap.set(key, value); } }); TileWindowManager._workspaces = newMap; this.updateMonitors(); this.updateAdjacents(); } _onWindowCreated(_, window) { // Wait for first frame to be sure window is fully created window.get_compositor_private().connect('first-frame', () => { this._addNewWindow(window); }); } _windowWorkspaceChanged(window) { let tile = window.tile; if (tile) { let w = window.get_workspace()?.index(); if (w !== null) { window.change_workspace_by_index(w, false); if (w !== window.tile.workspace) { this._removeWindow(window); this._insertWindow(window, w); tile.workspace = w; } this.updateMonitors(); this.updateAdjacents(); } } } /** Connect to signals and remove some functions * * @param {Meta.Window} window */ configureWindowSignals(window) { let minimizeSignal = window.connect('notify::minimized', () => { if (!window.minimized) { let tile = window.tile; if (tile.state === TileState.MINIMIZED) { if (TileWindowManager.getMonitors()[tile.monitor].fullscreen) { if (this._settings?.get_int('fullscreen-switch') === 0) this.maximizeTile(window, true); else this.maximizeTile(window); } } } else { if (window.tile.state === TileState.MINIMIZED) return; window.unminimize(); } }); let maximizeSignal1 = window.connect('notify::maximized-horizontally', () => { if (window.tile.state === TileState.MAXIMIZED) return; if (window.maximized_horizontally || window.maximized_vertically) window.unmaximize(Meta.MaximizeFlags.BOTH); }); let maximizeSignal2 = window.connect('notify::maximized-vertically', () => { if (window.tile.state === TileState.MAXIMIZED) return; if (window.maximized_horizontally || window.maximized_vertically) window.unmaximize(Meta.MaximizeFlags.BOTH); }); let unmanagedSignal = window.connect('unmanaged', () => { this._removeWindow(window); this._removeWindowSignals(window); }); let workspaceChangedSignal = window.connect('workspace-changed', w => this._windowWorkspaceChanged(w)); let sizeChangedSignal = window.connect('size-changed', w => { let tile = w.tile; if (!this._userResize.has(w) && (tile.position.width !== w.get_frame_rect().width || tile.position.height !== w.get_frame_rect().height)) tile.update(); }); window._originalMaximize = window.maximize; window._originalMinimize = window.minimize; this._wrappedWindows.set(window, [window.minimize, window.maximize, minimizeSignal, maximizeSignal1, maximizeSignal2, unmanagedSignal, workspaceChangedSignal, sizeChangedSignal]); window.minimize = () => { }; window.maximize = () => { }; } _addNewWindow(window) { if (!this._isValidWindow(window)) return; this.configureWindowSignals(window); this._insertWindow(window); } _insertWindow(window, workspace = null, monitor = null) { let _monitors = TileWindowManager._workspaces.get(workspace != null ? workspace : window.get_workspace()?.index()); if (!_monitors) return; let selectedMonitor; // Select monitor if (monitor !== null) { selectedMonitor = _monitors[monitor]; } else if (this._settings?.get_int('monitor-tile-insertion-behavior') === 0) { selectedMonitor = Monitor.bestFitMonitor(_monitors); } else { let m = global.display.get_current_monitor(); selectedMonitor = _monitors[m]; } // Selected monitor index let index = selectedMonitor.index; // Now insert tile on selected monitor if (selectedMonitor.size() === 0) { let tile = Tile.createTileLeaf(window, new Position(1.0, 0, 0, 0, 0), index); tile.workspace = window.get_workspace().index(); window.tile = tile; _monitors[index].root = tile; _monitors[index].root?.update(); } else { _monitors[index].root?.addWindowOnBlock(window); window.tile.workspace = window.get_workspace().index(); if (_monitors[index].fullscreen) window.tile.state = TileState.MINIMIZED; _monitors[index].root?.update(); } } _removeWindowSignals(window) { window.tile?.destroy(); // Disconnect signals let s = this._wrappedWindows.get(window); if (s) { window.minimize = s[0]; window.maximize = s[1]; window.disconnect(s[2]); window.disconnect(s[3]); window.disconnect(s[4]); window.disconnect(s[5]); window.disconnect(s[6]); window.disconnect(s[7]); } this._wrappedWindows.delete(window); } _removeWindow(window) { // get Tile from window let tile = window.tile; // Not found if (!tile) return; let m = tile.monitor; if (TileWindowManager.getMonitors()[m].fullscreen) { TileWindowManager.getMonitors()[m].fullscreen = false; let focus = null; TileWindowManager.getMonitors()[m].root?.forEach(el => { el.state = TileState.DEFAULT; el.window?.unminimize(); if (!focus && el.window && el.window !== window) { el.window?.focus(0); focus = el; } }); } if (tile.removeTile() === null) TileWindowManager.getMonitors(tile.workspace)[m].root = null; else TileWindowManager.getMonitors()[m].root?.update(); } _onGrab(window, op) { if (!window) return; let tile = window.tile; if (!tile) return; let m = tile.monitor; let rect; switch (op) { case Meta.GrabOp.MOVING: if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { TileWindowManager.getMonitors()[m].root?.update(); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_E: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeE(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_W: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeW(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_N: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeN(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_S: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeS(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_NE: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeN(tile, rect); Resize.resizeE(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_NW: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeN(tile, rect); Resize.resizeW(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_SE: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeS(tile, rect); Resize.resizeE(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; case Meta.GrabOp.RESIZING_SW: rect = window.get_frame_rect(); if (this._sourceId !== null) GLib.Source.remove(this._sourceId); this._sourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { Resize.resizeS(tile, rect); Resize.resizeW(tile, rect); this._sourceId = null; return GLib.SOURCE_REMOVE; }); break; default: break; } } /** Resize operation with keyboard. This operation is * different from the one operated with the mouse (grab operation) * because we handle left/right resize differently. * * @param {Meta.GrabOp} op * @param {number} resizeGap * @returns void */ resizeFocusedWindow(op, resizeGap = 10) { let window = global.display.focusWindow; if (!window) return; let tile = window.tile; if (op === Meta.GrabOp.RESIZING_E) { if (tile.adjacents[1]) { let r = new Mtk.Rectangle({ x: tile.position.x, y: tile.position.y, width: tile.position.width + resizeGap, height: tile.position.height, }); Resize.resizeE(tile, r); } else if (tile.adjacents[0]) { let r = new Mtk.Rectangle({ x: tile.position.x + resizeGap, y: tile.position.y, width: tile.position.width - resizeGap, height: tile.position.height, }); Resize.resizeW(tile, r); } } else if (op === Meta.GrabOp.RESIZING_W) { if (tile.adjacents[0]) { let r = new Mtk.Rectangle({ x: tile.position.x - resizeGap, y: tile.position.y, width: tile.position.width + resizeGap, height: tile.position.height, }); Resize.resizeW(tile, r); } else if (tile.adjacents[1]) { let r = new Mtk.Rectangle({ x: tile.position.x, y: tile.position.y, width: tile.position.width - resizeGap, height: tile.position.height, }); Resize.resizeE(tile, r); } } else if (op === Meta.GrabOp.RESIZING_N) { if (tile.adjacents[2]) { let r = new Mtk.Rectangle({ x: tile.position.x, y: tile.position.y - resizeGap, width: tile.position.width, height: tile.position.height + resizeGap, }); Resize.resizeN(tile, r); } else if (tile.adjacents[3]) { let r = new Mtk.Rectangle({ x: tile.position.x, y: tile.position.y, width: tile.position.width, height: tile.position.height - resizeGap, }); Resize.resizeS(tile, r); } } else if (op === Meta.GrabOp.RESIZING_S) { if (tile.adjacents[3]) { let r = new Mtk.Rectangle({ x: tile.position.x, y: tile.position.y, width: tile.position.width, height: tile.position.height + resizeGap, }); Resize.resizeS(tile, r); } else if (tile.adjacents[2]) { let r = new Mtk.Rectangle({ x: tile.position.x, y: tile.position.y + resizeGap, width: tile.position.width, height: tile.position.height - resizeGap, }); Resize.resizeN(tile, r); } } } /** Clock wise windows rotation. * Rotates the parent tile (if existing). * * @param {Meta.Window} window * @returns */ rotateWindow(window) { let tile = window.tile; if (!tile) return; let parent = tile.parent; if (!parent) return; let newPositions; if (parent.orientation === Orientation.Horizontal) { newPositions = parent.position.split(Orientation.Vertical); if (parent.child1 && parent.child2) { newPositions[TileWindowManager.rotateEven[0] === 0 ? 0 : 1].splitProportion = parent.child1.position.splitProportion; newPositions[TileWindowManager.rotateEven[0] === 0 ? 1 : 0].splitProportion = parent.child2.position.splitProportion; parent.child1.resize(newPositions[TileWindowManager.rotateEven[0] === 0 ? 0 : 1]); parent.child2.resize(newPositions[TileWindowManager.rotateEven[0] === 0 ? 1 : 0]); parent.forEach(el => el.findAdjacents()); TileWindowManager.rotateEven[0] = (TileWindowManager.rotateEven[0] + 1) % 2; parent.update(); } else { return; } parent.orientation = Orientation.Vertical; } else if (parent.orientation === Orientation.Vertical) { newPositions = parent.position.split(Orientation.Horizontal); if (parent.child1 && parent.child2) { newPositions[TileWindowManager.rotateEven[1] === 0 ? 1 : 0].splitProportion = parent.child1.position.splitProportion; newPositions[TileWindowManager.rotateEven[1] === 0 ? 0 : 1].splitProportion = parent.child2.position.splitProportion; parent.child1.resize(newPositions[TileWindowManager.rotateEven[1] === 0 ? 1 : 0]); parent.child2.resize(newPositions[TileWindowManager.rotateEven[1] === 0 ? 0 : 1]); parent.forEach(el => el.findAdjacents()); TileWindowManager.rotateEven[1] = (TileWindowManager.rotateEven[1] + 1) % 2; parent.update(); } else { return; } parent.orientation = Orientation.Horizontal; } } /** Maximize the currently focused window. * Others windows are reduced using tile specific state. * * @param {Meta.Window} window * @param {boolean} replace true to stay in fullscreen and just replace the window otherwise false * @returns */ maximizeTile(window, replace = false) { let tile = window.tile; if (!tile) return; let m = tile.monitor; if (TileWindowManager.getMonitors()[tile.monitor].fullscreen) { TileWindowManager.getMonitors()[tile.monitor].fullscreen = false; TileWindowManager.getMonitors()[m].root?.forEach(el => { el.state = TileState.DEFAULT; if (el.window?.minimized) el.window?.unminimize(); }); if (replace) { TileWindowManager.getMonitors()[m].fullscreen = true; TileWindowManager.getMonitors()[m].root?.forEach(el => { if (el.id === tile.id) el.state = TileState.MAXIMIZED; else el.state = TileState.MINIMIZED; }); } } else { TileWindowManager.getMonitors()[tile.monitor].fullscreen = true; TileWindowManager.getMonitors()[m].root?.forEach(el => { if (el.id === tile.id) el.state = TileState.MAXIMIZED; else el.state = TileState.MINIMIZED; }); } TileWindowManager.getMonitors()[m].root?.update(); } moveTile(dir) { let window = global.display.get_focus_window(); if (!window) return; let tile = window.tile; if (!tile.window) return; let exchangeTile = TileWindowManager.getMonitors()[tile.monitor].closestTile(tile, dir); if (!exchangeTile || !exchangeTile.window) return; let tmpWindow = exchangeTile.window; exchangeTile.window = tile.window; tile.window = tmpWindow; tile.window.tile = tile; exchangeTile.window.tile = exchangeTile; TileWindowManager.getMonitors()[tile.monitor].root?.update(); } changeFocus(dir) { let window = global.display.get_focus_window(); if (!window) return; let tile = window.tile; if (!tile.window) return; if (TileWindowManager.getMonitors()[tile.monitor].fullscreen === true) { let mon = TileWindowManager.getMonitors()[tile.monitor].closestMonitor(dir); if (mon === null) return; let newFocus; switch (dir) { case Direction.North: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.South); if (newFocus) newFocus?.window?.focus(0); break; case Direction.South: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.North); if (newFocus) newFocus?.window?.focus(0); break; case Direction.East: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.West); if (newFocus) newFocus?.window?.focus(0); break; case Direction.West: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.East); if (newFocus) newFocus?.window?.focus(0); break; } } else { let newFocus = TileWindowManager.getMonitors()[tile.monitor].closestTile(tile, dir); if (newFocus === null) { let mon = TileWindowManager.getMonitors()[tile.monitor].closestMonitor(dir); if (mon === null) return; switch (dir) { case Direction.North: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.South); if (newFocus) newFocus?.window?.focus(0); break; case Direction.South: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.North); if (newFocus) newFocus?.window?.focus(0); break; case Direction.East: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.West); if (newFocus) newFocus?.window?.focus(0); break; case Direction.West: newFocus = TileWindowManager.getMonitors()[mon].getTile(Direction.East); if (newFocus) newFocus?.window?.focus(0); break; default: } } else { newFocus?.window?.focus(0); } } } createSearchBar() { if (this._topBarSearchEntry && this._topBarSearchEntry.isAlive()) { this._topBarSearchEntry.destroy(); this._topBarSearchEntry = undefined; return; } if (this._settings) this._topBarSearchEntry = new TopBarSearchEntry(this._settings); } refresh() { TileWindowManager.getMonitors().forEach(el => el.root ? el.root.update() : null); } moveToWorkspace(next) { let window = global.display.get_focus_window(); let tile = window.tile; let current = tile.workspace; if (next) { if (current < global.workspaceManager.get_n_workspaces() - 1) { tile.workspace = current + 1; window.change_workspace_by_index(tile.workspace, false); this._removeWindow(window); this._insertWindow(window, current + 1); } } else if (current > 0) { tile.workspace = current - 1; window.change_workspace_by_index(tile.workspace, false); this._removeWindow(window); this._insertWindow(window, current - 1); } this.updateMonitors(); this.updateAdjacents(); } moveToNextMonitor() { let window = global.display.get_focus_window(); this._removeWindow(window); let tile = window.tile; let monitors = TileWindowManager.getMonitors(); let newMonitorIndex = (tile.monitor + 1) % monitors.length; let newMonitor = monitors[newMonitorIndex]; if (newMonitor.size() === 0) { let newTile = Tile.createTileLeaf(window, new Position(1.0, 0, 0, 0, 0), newMonitorIndex); newTile.workspace = window.get_workspace().index(); window.tile = newTile; newMonitor.root = newTile; newMonitor.root?.update(); } else { // Easier to create a new tile for insertion newMonitor.root?.addWindowOnBlock(window); window.tile.workspace = window.get_workspace().index(); if (newMonitor.fullscreen) window.tile.state = TileState.MINIMIZED; newMonitor.root?.update(); } } /** Extension is disabled on screen lock. * We save the state of the Monitors before we quit. * * @returns */ _saveBeforeSessionLock() { if (!Main.sessionMode.isLocked) return; TileWindowManager.locked = true; const userPath = GLib.get_user_config_dir(); const parentPath = GLib.build_filenamev([userPath, '/grimble']); const parent = Gio.File.new_for_path(parentPath); try { parent.make_directory_with_parents(null); } catch (e) { if (e.code !== Gio.IOErrorEnum.EXISTS) throw e; } const path = GLib.build_filenamev([parentPath, '/tilingWmSession2.json']); const file = Gio.File.new_for_path(path); try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) { if (e.code !== Gio.IOErrorEnum.EXISTS) throw e; } file.replace_contents(JSON.stringify({ windows: Array.from(TileWindowManager._workspaces.entries()), }, (key, value) => { if (value instanceof Meta.Window) return value.get_id(); else if (key === '_parent') // remove cyclic references return undefined; else return value; }), null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); } _loadAfterSessionLock() { if (!TileWindowManager.locked) return; TileWindowManager.locked = false; const userPath = GLib.get_user_config_dir(); const path = GLib.build_filenamev([userPath, '/grimble/tilingWmSession2.json']); const file = Gio.File.new_for_path(path); if (!file.query_exists(null)) return; try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) { if (e.code !== Gio.IOErrorEnum.EXISTS) throw e; } const [success, contents] = file.load_contents(null); if (!success || !contents.length) return; const openWindows = global.display.list_all_windows(); const states = JSON.parse(new TextDecoder().decode(contents), (key, value) => key === '_window' && typeof value === 'number' ? openWindows.find(val => val.get_id() === value) : value); let map = new Map(states.windows); map.forEach((mapValue, mapKey, _) => { mapValue.forEach((value, index, array) => { // We have to rebuild correct types of each object array[index] = Monitor.fromObject(value); array[index].root?.forEach(el => { if (el.window) { this.configureWindowSignals(el.window); el.window.change_workspace_by_index(mapKey, false); } }); }); TileWindowManager._workspaces.set(mapKey, mapValue); }); this.updateMonitors(); } }