diff --git a/extension.js b/extension.js deleted file mode 100644 index b550e07..0000000 --- a/extension.js +++ /dev/null @@ -1,694 +0,0 @@ -// ---------------------------------------------------- // -// Simple-Tiling – GNOME Shell 3.38 (X11) - Version 6 // -// © 2025 domoel – MIT // -// ---------------------------------------------------- // - -// ---------------------------------------------------- // -// Global Imports // -// ---------------------------------------------------- // -const Main = imports.ui.main; -const Meta = imports.gi.Meta; -const Shell = imports.gi.Shell; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const ExtensionUtils = imports.misc.extensionUtils; -const ByteArray = imports.byteArray; -const TILING_DELAY_MS = 20; // Change Tiling Window Delay -const CENTERING_DELAY_MS = 5; // Change Centered Window Delay - -const Me = ExtensionUtils.getCurrentExtension(); -const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel"; -const WM_SCHEMA = "org.gnome.desktop.wm.keybindings"; - -const KEYBINDINGS = { - "swap-master-window": (self) => self._swapWithMaster(), - "swap-left-window": (self) => self._swapInDirection("left"), - "swap-right-window": (self) => self._swapInDirection("right"), - "swap-up-window": (self) => self._swapInDirection("up"), - "swap-down-window": (self) => self._swapInDirection("down"), - - "focus-left": (self) => self._focusInDirection("left"), - "focus-right": (self) => self._focusInDirection("right"), - "focus-up": (self) => self._focusInDirection("up"), - "focus-down": (self) => self._focusInDirection("down"), -}; - -// ---------------------------------------------------- // -// InteractionHandler // -// ---------------------------------------------------- // -class InteractionHandler { - constructor(tiler) { - this.tiler = tiler; - this._settings = ExtensionUtils.getSettings(SCHEMA_NAME); - this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA }); - this._wmKeysToDisable = []; - this._savedWmShortcuts = {}; - this._grabOpIds = []; - this._settingsChangedId = null; - this._onSettingsChanged = this._onSettingsChanged.bind(this); - this._prepareWmShortcuts(); - } - - enable() { - if (this._wmKeysToDisable.length) { - this._wmKeysToDisable.forEach((key) => - this._wmSettings.set_value(key, new GLib.Variant("as", [])) - ); - } - this._bindAllShortcuts(); - this._settingsChangedId = this._settings.connect( - "changed", - this._onSettingsChanged - ); - this._grabOpIds.push( - global.display.connect( - "grab-op-begin", - (display, screen, window) => { - if (this.tiler.windows.includes(window)) { - this.tiler.grabbedWindow = window; - } - } - ) - ); - this._grabOpIds.push( - global.display.connect("grab-op-end", this._onGrabEnd.bind(this)) - ); - } - - disable() { - if (this._wmKeysToDisable.length) { - this._wmKeysToDisable.forEach((key) => - this._wmSettings.set_value(key, this._savedWmShortcuts[key]) - ); - } - this._unbindAllShortcuts(); - if (this._settingsChangedId) { - this._settings.disconnect(this._settingsChangedId); - this._settingsChangedId = null; - } - this._grabOpIds.forEach((id) => global.display.disconnect(id)); - this._grabOpIds = []; - } - - _bind(key, callback) { - Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, - () => callback(this)); - } - - _bindAllShortcuts() { - for (const [key, handler] of Object.entries(KEYBINDINGS)) { - this._bind(key, handler); - } - } - _unbindAllShortcuts() { - for (const key in KEYBINDINGS) { - Main.wm.removeKeybinding(key); - } - } - - _onSettingsChanged() { - this._unbindAllShortcuts(); - this._bindAllShortcuts(); - } - _prepareWmShortcuts() { - const schema = this._wmSettings.settings_schema; - const keys = []; - if (schema.has_key("toggle-tiled-left")) - keys.push("toggle-tiled-left", "toggle-tiled-right"); - else if (schema.has_key("tile-left")) - keys.push("tile-left", "tile-right"); - if (schema.has_key("toggle-maximized")) keys.push("toggle-maximized"); - else { - if (schema.has_key("maximize")) keys.push("maximize"); - if (schema.has_key("unmaximize")) keys.push("unmaximize"); - } - if (keys.length) { - this._wmKeysToDisable = keys; - keys.forEach( - (key) => - (this._savedWmShortcuts[ - key - ] = this._wmSettings.get_value(key)) - ); - } - } - - _focusInDirection(direction) { - const sourceWindow = global.display.get_focus_window(); - if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return; - - const targetWindow = this._findTargetInDirection( - sourceWindow, - direction - ); - if (targetWindow) { - targetWindow.activate(global.get_current_time()); - } - } - - _swapWithMaster() { - const windows = this.tiler.windows; - if (windows.length < 2) return; - const focusedWindow = global.display.get_focus_window(); - if (!focusedWindow || !windows.includes(focusedWindow)) return; - const focusedIndex = windows.indexOf(focusedWindow); - if (focusedIndex > 0) { - [windows[0], windows[focusedIndex]] = [ - windows[focusedIndex], - windows[0], - ]; - } else if (focusedIndex === 0) { - [windows[0], windows[1]] = [windows[1], windows[0]]; - } - this.tiler.tileNow(); - if (windows.length > 0) windows[0].activate(global.get_current_time()); - } - - _swapInDirection(direction) { - const sourceWindow = global.display.get_focus_window(); - if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return; - let targetWindow = null; - const sourceIndex = this.tiler.windows.indexOf(sourceWindow); - if ( - sourceIndex === 0 && - direction === "right" && - this.tiler.windows.length > 1 - ) { - targetWindow = this.tiler.windows[1]; - } else { - targetWindow = this._findTargetInDirection(sourceWindow, direction); - } - if (!targetWindow) return; - const targetIndex = this.tiler.windows.indexOf(targetWindow); - [this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [ - this.tiler.windows[targetIndex], - this.tiler.windows[sourceIndex], - ]; - this.tiler.tileNow(); - sourceWindow.activate(global.get_current_time()); - } - - _findTargetInDirection(source, direction) { - const sourceRect = source.get_frame_rect(); - let candidates = []; - for (const win of this.tiler.windows) { - if (win === source) continue; - const targetRect = win.get_frame_rect(); - switch (direction) { - case "left": - if (targetRect.x < sourceRect.x) candidates.push(win); - break; - case "right": - if (targetRect.x > sourceRect.x) candidates.push(win); - break; - case "up": - if (targetRect.y < sourceRect.y) candidates.push(win); - break; - case "down": - if (targetRect.y > sourceRect.y) candidates.push(win); - break; - } - } - if (candidates.length === 0) return null; - let bestTarget = null; - let minDeviation = Infinity; - for (const win of candidates) { - const targetRect = win.get_frame_rect(); - let deviation; - if (direction === "left" || direction === "right") { - deviation = Math.abs(sourceRect.y - targetRect.y); - } else { - deviation = Math.abs(sourceRect.x - targetRect.x); - } - if (deviation < minDeviation) { - minDeviation = deviation; - bestTarget = win; - } - } - return bestTarget; - } - - _onGrabEnd() { - const grabbedWindow = this.tiler.grabbedWindow; - if (!grabbedWindow) return; - const targetWindow = this._findTargetUnderPointer(grabbedWindow); - if (targetWindow) { - const sourceIndex = this.tiler.windows.indexOf(grabbedWindow); - const targetIndex = this.tiler.windows.indexOf(targetWindow); - [ - this.tiler.windows[sourceIndex], - this.tiler.windows[targetIndex], - ] = [ - this.tiler.windows[targetIndex], - this.tiler.windows[sourceIndex], - ]; - } - this.tiler.queueTile(); - this.tiler.grabbedWindow = null; - } - - _findTargetUnderPointer(excludeWindow) { - let [pointerX, pointerY] = global.get_pointer(); - let windows = global - .get_window_actors() - .map((actor) => actor.meta_window) - .filter((win) => { - if ( - !win || - win === excludeWindow || - !this.tiler.windows.includes(win) - ) - return false; - let frame = win.get_frame_rect(); - return ( - pointerX >= frame.x && - pointerX < frame.x + frame.width && - pointerY >= frame.y && - pointerY < frame.y + frame.height - ); - }); - if (windows.length > 0) { - return windows[windows.length - 1]; - } - - let bestTarget = null; - let maxOverlap = 0; - const sourceFrame = excludeWindow.get_frame_rect(); - for (const win of this.tiler.windows) { - if (win === excludeWindow) continue; - const targetFrame = win.get_frame_rect(); - const overlapX = Math.max( - 0, - Math.min( - sourceFrame.x + sourceFrame.width, - targetFrame.x + targetFrame.width - ) - Math.max(sourceFrame.x, targetFrame.x) - ); - const overlapY = Math.max( - 0, - Math.min( - sourceFrame.y + sourceFrame.height, - targetFrame.y + targetFrame.height - ) - Math.max(sourceFrame.y, targetFrame.y) - ); - const overlapArea = overlapX * overlapY; - if (overlapArea > maxOverlap) { - maxOverlap = overlapArea; - bestTarget = win; - } - } - return bestTarget; - } -} - -// ---------------------------------------------------- // -// Tiler // -// Main Classes for Tiling Logic // -// ---------------------------------------------------- // -class Tiler { - constructor() { - this.windows = []; - this.grabbedWindow = null; - this._settings = ExtensionUtils.getSettings(SCHEMA_NAME); - this._signalIds = new Map(); - this._tileInProgress = false; - - this._innerGap = this._settings.get_int("inner-gap"); - this._outerGapVertical = this._settings.get_int("outer-gap-vertical"); - this._outerGapHorizontal = this._settings.get_int( - "outer-gap-horizontal" - ); - - this._tilingDelay = TILING_DELAY_MS; - this._centeringDelay = CENTERING_DELAY_MS; - - this._exceptions = []; - this._interactionHandler = new InteractionHandler(this); - - this._tileTimeoutId = null; - this._centerTimeoutIds = []; - - this._onWindowAdded = this._onWindowAdded.bind(this); - this._onWindowRemoved = this._onWindowRemoved.bind(this); - this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind( - this - ); - this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind( - this - ); - this._onSettingsChanged = this._onSettingsChanged.bind(this); - } - - enable() { - this._loadExceptions(); - const workspaceManager = global.workspace_manager; - this._signalIds.set("workspace-changed", { - object: workspaceManager, - id: workspaceManager.connect( - "active-workspace-changed", - this._onActiveWorkspaceChanged - ), - }); - this._connectToWorkspace(); - this._interactionHandler.enable(); - this._signalIds.set("settings-changed", { - object: this._settings, - id: this._settings.connect("changed", this._onSettingsChanged), - }); - } - - disable() { - if (this._tileTimeoutId) { - GLib.source_remove(this._tileTimeoutId); - this._tileTimeoutId = null; - } - this._centerTimeoutIds.forEach(id => GLib.source_remove(id)); - this._centerTimeoutIds = []; - - this._interactionHandler.disable(); - this._disconnectFromWorkspace(); - for (const [, signal] of this._signalIds) { - try { - signal.object.disconnect(signal.id); - } catch (e) {} - } - this._signalIds.clear(); - this.windows = []; - } - - _onSettingsChanged() { - this._innerGap = this._settings.get_int("inner-gap"); - this._outerGapVertical = this._settings.get_int("outer-gap-vertical"); - this._outerGapHorizontal = this._settings.get_int( - "outer-gap-horizontal" - ); - this.queueTile(); - } - - _loadExceptions() { - const file = Gio.file_new_for_path(Me.path + "/exceptions.txt"); - if (!file.query_exists(null)) { - this._exceptions = []; - return; - } - const [ok, data] = file.load_contents(null); - this._exceptions = ok - ? ByteArray.toString(data) - .split("\n") - .map((l) => l.trim()) - .filter((l) => l && !l.startsWith("#")) - .map((l) => l.toLowerCase()) - : []; - } - - _isException(win) { - return ( - !!win && - this._exceptions.includes((win.get_wm_class() || "").toLowerCase()) - ); - } - - _isTileable(win) { - return ( - win && - !win.minimized && - !this._isException(win) && - win.get_window_type() === Meta.WindowType.NORMAL - ); - } - - _centerWindow(win) { - const timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._centeringDelay, () => { - const index = this._centerTimeoutIds.indexOf(timeoutId); - if (index > -1) { - this._centerTimeoutIds.splice(index, 1); - } - - if (!win || !win.get_display()) return GLib.SOURCE_REMOVE; - if (win.get_maximized()) { - win.unmaximize(Meta.MaximizeFlags.BOTH); - } - const monitorIndex = win.get_monitor(); - const workArea = Main.layoutManager.getWorkAreaForMonitor( - monitorIndex - ); - const frame = win.get_frame_rect(); - win.move_frame( - true, - workArea.x + Math.floor((workArea.width - frame.width) / 2), - workArea.y + Math.floor((workArea.height - frame.height) / 2) - ); - GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { - if (win.get_display()) { - if (typeof win.set_keep_above === "function") - win.set_keep_above(true); - else if (typeof win.make_above === "function") - win.make_above(); - } - return GLib.SOURCE_REMOVE; - }); - return GLib.SOURCE_REMOVE; - }); - - this._centerTimeoutIds.push(timeoutId); - } - - _onWindowMinimizedStateChanged() { - this.queueTile(); - } - - _onWindowAdded(workspace, win) { - if (this.windows.includes(win)) return; - - if (this._isException(win)) { - this._centerWindow(win); - return; - } - - if (this._isTileable(win)) { - if (this._settings.get_string("new-window-behavior") === "master") { - this.windows.unshift(win); - } else { - this.windows.push(win); - } - - const id = win.get_id(); - this._signalIds.set(`unmanaged-${id}`, { - object: win, - id: win.connect("unmanaged", () => - this._onWindowRemoved(null, win) - ), - }); - this._signalIds.set(`size-changed-${id}`, { - object: win, - id: win.connect("size-changed", () => { - if (!this.grabbedWindow) this.queueTile(); - }), - }); - this._signalIds.set(`minimized-${id}`, { - object: win, - id: win.connect( - "notify::minimized", - this._onWindowMinimizedStateChanged - ), - }); - - this.queueTile(); - } - } - - _onWindowRemoved(workspace, win) { - const index = this.windows.indexOf(win); - if (index > -1) { - this.windows.splice(index, 1); - } - - ["unmanaged", "size-changed", "minimized"].forEach((prefix) => { - const key = `${prefix}-${win.get_id()}`; - if (this._signalIds.has(key)) { - const { object, id } = this._signalIds.get(key); - try { - object.disconnect(id); - } catch (e) {} - this._signalIds.delete(key); - } - }); - this.queueTile(); - } - - _onActiveWorkspaceChanged() { - this._disconnectFromWorkspace(); - this._connectToWorkspace(); - } - - _connectToWorkspace() { - const workspace = global.workspace_manager.get_active_workspace(); - workspace - .list_windows() - .forEach((win) => this._onWindowAdded(workspace, win)); - this._signalIds.set("window-added", { - object: workspace, - id: workspace.connect("window-added", this._onWindowAdded), - }); - this._signalIds.set("window-removed", { - object: workspace, - id: workspace.connect("window-removed", this._onWindowRemoved), - }); - this.queueTile(); - } - - _disconnectFromWorkspace() { - this.windows.slice().forEach((win) => this._onWindowRemoved(null, win)); - - ["window-added", "window-removed"].forEach((key) => { - if (this._signalIds.has(key)) { - const { object, id } = this._signalIds.get(key); - try { - object.disconnect(id); - } catch (e) {} - this._signalIds.delete(key); - } - }); - } - - queueTile() { - if (this._tileInProgress || this._tileTimeoutId) return; - this._tileInProgress = true; - - this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._tilingDelay, () => { - this._tileWindows(); - this._tileInProgress = false; - this._tileTimeoutId = null; - return GLib.SOURCE_REMOVE; - }); - } - - tileNow() { - if (!this._tileInProgress) { - this._tileWindows(); - } - } - - _splitLayout(windows, area) { - if (windows.length === 0) return; - if (windows.length === 1) { - windows[0].move_resize_frame( - true, - area.x, - area.y, - area.width, - area.height - ); - return; - } - - const gap = Math.floor(this._innerGap / 2); - const primaryWindows = [windows[0]]; - const secondaryWindows = windows.slice(1); - let primaryArea, secondaryArea; - - if (area.width > area.height) { - const primaryWidth = Math.floor(area.width / 2) - gap; - primaryArea = { - x: area.x, - y: area.y, - width: primaryWidth, - height: area.height, - }; - secondaryArea = { - x: area.x + primaryWidth + this._innerGap, - y: area.y, - width: area.width - primaryWidth - this._innerGap, - height: area.height, - }; - } else { - const primaryHeight = Math.floor(area.height / 2) - gap; - primaryArea = { - x: area.x, - y: area.y, - width: area.width, - height: primaryHeight, - }; - secondaryArea = { - x: area.x, - y: area.y + primaryHeight + this._innerGap, - width: area.width, - height: area.height - primaryHeight - this._innerGap, - }; - } - - this._splitLayout(primaryWindows, primaryArea); - this._splitLayout(secondaryWindows, secondaryArea); - } - - _tileWindows() { - const windowsToTile = this.windows.filter((win) => !win.minimized); - if (windowsToTile.length === 0) return; - const monitor = Main.layoutManager.primaryMonitor; - const workArea = Main.layoutManager.getWorkAreaForMonitor( - monitor.index - ); - const innerArea = { - x: workArea.x + this._outerGapHorizontal, - y: workArea.y + this._outerGapVertical, - width: workArea.width - 2 * this._outerGapHorizontal, - height: workArea.height - 2 * this._outerGapVertical, - }; - - windowsToTile.forEach((win) => { - if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH); - }); - - if (windowsToTile.length === 1) { - windowsToTile[0].move_resize_frame( - true, - innerArea.x, - innerArea.y, - innerArea.width, - innerArea.height - ); - return; - } - - const gap = Math.floor(this._innerGap / 2); - const masterWidth = Math.floor(innerArea.width / 2) - gap; - const master = windowsToTile[0]; - master.move_resize_frame( - true, - innerArea.x, - innerArea.y, - masterWidth, - innerArea.height - ); - const stackArea = { - x: innerArea.x + masterWidth + this._innerGap, - y: innerArea.y, - width: innerArea.width - masterWidth - this._innerGap, - height: innerArea.height, - }; - this._splitLayout(windowsToTile.slice(1), stackArea); - } -} - -// ---------------------------------------------------- // -// Extension Wrapper // -// ---------------------------------------------------- // -class SimpleTilingExtension { - constructor() { - this.tiler = null; - } - enable() { - this.tiler = new Tiler(); - this.tiler.enable(); - } - disable() { - if (this.tiler) { - this.tiler.disable(); - this.tiler = null; - } - } -} - -function init() { - return new SimpleTilingExtension(); -} diff --git a/legacy.js b/legacy.js new file mode 100644 index 0000000..bf65493 --- /dev/null +++ b/legacy.js @@ -0,0 +1,554 @@ +/////////////////////////////////////////////////////////////// +// Simple‑Tiling – LEGACY (GNOME Shell 3.38 ‑ 44) // +// © 2025 domoel – MIT // +///////////////////////////////////////////////////////////// + +'use strict'; + +// ── GLOBAL IMPORTS ──────────────────────────────────────── +const Main = imports.ui.main; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const ExtensionUtils= imports.misc.extensionUtils; +const ByteArray = imports.byteArray; + +const Me = ExtensionUtils.getCurrentExtension(); + +// ── CONST ──────────────────────────────────────────── +const SCHEMA_NAME = 'org.gnome.shell.extensions.simple-tiling.domoel'; +const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings'; + +const TILING_DELAY_MS = 20; // Change Tiling Window Delay +const CENTERING_DELAY_MS = 5; // Change Centered Window Delay + +const KEYBINDINGS = { + 'swap-master-window': (self) => self._swapWithMaster(), + 'swap-left-window': (self) => self._swapInDirection('left'), + 'swap-right-window': (self) => self._swapInDirection('right'), + 'swap-up-window': (self) => self._swapInDirection('up'), + 'swap-down-window': (self) => self._swapInDirection('down'), + 'focus-left': (self) => self._focusInDirection('left'), + 'focus-right': (self) => self._focusInDirection('right'), + 'focus-up': (self) => self._focusInDirection('up'), + 'focus-down': (self) => self._focusInDirection('down'), +}; + +// ── HELPER‑FUNCTION ──────────────────────────────────────── +function addKeybinding(name, settings, flags, mode, handler) { + if (Main.wm?.addKeybinding) + Main.wm.addKeybinding(name, settings, flags, mode, handler); + else + global.display.add_keybinding(name, settings, flags, mode, handler); +} +function removeKeybinding(name) { + if (Main.wm?.removeKeybinding) + Main.wm.removeKeybinding(name); + else + global.display.remove_keybinding(name); +} + +function getWorkAreaForMonitor(monitorIndex) { + if (Main.layoutManager?.getWorkAreaForMonitor) + return Main.layoutManager.getWorkAreaForMonitor(monitorIndex); + + return global.workspace_manager + .get_active_workspace() + .get_work_area_for_monitor(monitorIndex); +} + +function decodeUtf8(bytes) { + if (typeof ByteArray !== 'undefined') + return ByteArray.toString(bytes); + return new TextDecoder('utf-8').decode(bytes); +} + +function getPointer() { + return global.get_pointer ? global.get_pointer() + : global.display.get_pointer(); +} + +// ── INTERACTIONHANDLER ─────────────────────────────────── +class InteractionHandler { + constructor(tiler) { + this.tiler = tiler; + this._settings = ExtensionUtils.getSettings(SCHEMA_NAME); + this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA }); + this._wmKeysToDisable = []; + this._savedWmShortcuts= {}; + this._grabOpIds = []; + this._settingsChangedId = null; + + this._onSettingsChanged = this._onSettingsChanged.bind(this); + + this._prepareWmShortcuts(); + } + + enable() { + if (this._wmKeysToDisable.length) + this._wmKeysToDisable.forEach(k => + this._wmSettings.set_value(k, new GLib.Variant('as', []))); + + this._bindAllShortcuts(); + this._settingsChangedId = + this._settings.connect('changed', this._onSettingsChanged); + + this._grabOpIds.push( + global.display.connect('grab-op-begin', + (display, screen, win) => { + if (this.tiler.windows.includes(win)) + this.tiler.grabbedWindow = win; + })); + this._grabOpIds.push( + global.display.connect('grab-op-end', this._onGrabEnd.bind(this))); + } + + disable() { + if (this._wmKeysToDisable.length) + this._wmKeysToDisable.forEach(k => + this._wmSettings.set_value(k, this._savedWmShortcuts[k])); + + this._unbindAllShortcuts(); + + if (this._settingsChangedId) { + this._settings.disconnect(this._settingsChangedId); + this._settingsChangedId = null; + } + this._grabOpIds.forEach(id => global.display.disconnect(id)); + this._grabOpIds = []; + } + + _bind(key, callback) { + addKeybinding(key, this._settings, + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.NORMAL, + () => callback(this)); + } + _bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k,h); } + _unbindAllShortcuts(){ for (const k in KEYBINDINGS) removeKeybinding(k); } + + _onSettingsChanged() { + this._unbindAllShortcuts(); + this._bindAllShortcuts(); + } + + _prepareWmShortcuts() { + const schema = this._wmSettings.settings_schema; + const keys = []; + + if (schema.has_key('toggle-tiled-left')) + keys.push('toggle-tiled-left','toggle-tiled-right'); + else if (schema.has_key('tile-left')) + keys.push('tile-left','tile-right'); + + if (schema.has_key('toggle-maximized')) + keys.push('toggle-maximized'); + else { + if (schema.has_key('maximize')) keys.push('maximize'); + if (schema.has_key('unmaximize')) keys.push('unmaximize'); + } + + if (keys.length) { + this._wmKeysToDisable = keys; + keys.forEach(k => this._savedWmShortcuts[k] = + this._wmSettings.get_value(k)); + } + } + + _focusInDirection(direction) { + const src = global.display.get_focus_window(); + if (!src || !this.tiler.windows.includes(src)) return; + const tgt = this._findTargetInDirection(src, direction); + if (tgt) tgt.activate(global.get_current_time()); + } + + _swapWithMaster() { + const w = this.tiler.windows; + if (w.length < 2) return; + const foc = global.display.get_focus_window(); + if (!foc || !w.includes(foc)) return; + const idx = w.indexOf(foc); + if (idx > 0) + [w[0], w[idx]] = [w[idx], w[0]]; + else + [w[0], w[1]] = [w[1], w[0]]; + this.tiler.tileNow(); + w[0]?.activate(global.get_current_time()); + } + _swapInDirection(direction) { + const src = global.display.get_focus_window(); + if (!src || !this.tiler.windows.includes(src)) return; + + let tgt = null; + const srcIdx = this.tiler.windows.indexOf(src); + if (srcIdx === 0 && direction === 'right' && this.tiler.windows.length>1) + tgt = this.tiler.windows[1]; + else + tgt = this._findTargetInDirection(src, direction); + + if (!tgt) return; + const tgtIdx = this.tiler.windows.indexOf(tgt); + [this.tiler.windows[srcIdx], this.tiler.windows[tgtIdx]] = + [this.tiler.windows[tgtIdx], this.tiler.windows[srcIdx]]; + + this.tiler.tileNow(); + src.activate(global.get_current_time()); + } + + _findTargetInDirection(src, direction) { + const sRect = src.get_frame_rect(); + const cands = []; + + for (const win of this.tiler.windows) { + if (win === src) continue; + const tRect = win.get_frame_rect(); + switch (direction) { + case 'left': if (tRect.x < sRect.x) cands.push(win); break; + case 'right': if (tRect.x > sRect.x) cands.push(win); break; + case 'up': if (tRect.y < sRect.y) cands.push(win); break; + case 'down': if (tRect.y > sRect.y) cands.push(win); break; + } + } + if (!cands.length) return null; + + let best=null, min=Infinity; + for (const win of cands) { + const tRect = win.get_frame_rect(); + const dev = (direction==='left'||direction==='right') + ? Math.abs(sRect.y - tRect.y) + : Math.abs(sRect.x - tRect.x); + if (dev < min) { min=dev; best=win; } + } + return best; + } + + _onGrabEnd() { + const grabbed = this.tiler.grabbedWindow; + if (!grabbed) return; + + const tgt = this._findTargetUnderPointer(grabbed); + if (tgt) { + const a = this.tiler.windows.indexOf(grabbed); + const b = this.tiler.windows.indexOf(tgt); + [this.tiler.windows[a], this.tiler.windows[b]] = + [this.tiler.windows[b], this.tiler.windows[a]]; + } + this.tiler.queueTile(); + this.tiler.grabbedWindow = null; + } + + _findTargetUnderPointer(exclude) { + const [x,y] = getPointer(); + const wins = global.get_window_actors() + .map(a => a.meta_window) + .filter(w => w && w!==exclude && + this.tiler.windows.includes(w) && + ((()=>{ const f=w.get_frame_rect(); + return x>=f.x && x=f.y && ymax){ max=area; best=w; } + } + return best; + } +} + +// ── TILER ──────────────────────────────────────────────── +class Tiler { + constructor() { + this.windows = []; + this.grabbedWindow = null; + this._settings = ExtensionUtils.getSettings(SCHEMA_NAME); + + this._signalIds = new Map(); + this._tileTimeoutId = null; + this._centerTimeoutIds= []; + this._tileInProgress = false; + + this._innerGap = this._settings.get_int('inner-gap'); + this._outerGapVertical= this._settings.get_int('outer-gap-vertical'); + this._outerGapHorizontal = this._settings.get_int('outer-gap-horizontal'); + + this._tilingDelay = TILING_DELAY_MS; + this._centeringDelay= CENTERING_DELAY_MS; + + this._exceptions = []; + this._interactionHandler = new InteractionHandler(this); + + this._onWindowAdded = this._onWindowAdded.bind(this); + this._onWindowRemoved= this._onWindowRemoved.bind(this); + this._onActiveWorkspaceChanged = + this._onActiveWorkspaceChanged.bind(this); + this._onWindowMinimizedStateChanged = + this._onWindowMinimizedStateChanged.bind(this); + this._onSettingsChanged = this._onSettingsChanged.bind(this); + } + + enable() { + this._loadExceptions(); + + const wm = global.workspace_manager; + this._signalIds.set('workspace-changed', { + object: wm, + id: wm.connect('active-workspace-changed', + this._onActiveWorkspaceChanged), + }); + + this._connectToWorkspace(); + + this._interactionHandler.enable(); + + this._signalIds.set('settings-changed', { + object: this._settings, + id: this._settings.connect('changed', this._onSettingsChanged), + }); + } + + disable() { + if (this._tileTimeoutId) { + GLib.source_remove(this._tileTimeoutId); + this._tileTimeoutId = null; + } + this._centerTimeoutIds.forEach(id => GLib.source_remove(id)); + this._centerTimeoutIds = []; + + this._interactionHandler.disable(); + this._disconnectFromWorkspace(); + + for (const [,sig] of this._signalIds) { + try { sig.object.disconnect(sig.id); } catch {} + } + this._signalIds.clear(); + this.windows = []; + } + + _onSettingsChanged() { + this._innerGap = this._settings.get_int('inner-gap'); + this._outerGapVertical = this._settings.get_int('outer-gap-vertical'); + this._outerGapHorizontal= this._settings.get_int('outer-gap-horizontal'); + this.queueTile(); + } + + _loadExceptions() { + const file = Gio.File.new_for_path(Me.path + '/exceptions.txt'); + if (!file.query_exists(null)) { this._exceptions=[]; return; } + + const [ok, data] = file.load_contents(null); + this._exceptions = ok ? decodeUtf8(data) + .split('\n') + .map(l => l.trim()) + .filter(l => l && !l.startsWith('#')) + .map(l => l.toLowerCase()) + : []; + } + + _isException(win) { + if (!win) return false; + const wmClass = (win.get_wm_class() || '').toLowerCase(); + const appId = (win.get_gtk_application_id() || '').toLowerCase(); + return this._exceptions.includes(wmClass) || this._exceptions.includes(appId); + } + _isTileable(win) { + return win && !win.minimized && !this._isException(win) && + win.get_window_type() === Meta.WindowType.NORMAL; + } + + _centerWindow(win) { + const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + this._centeringDelay, () => { + const idx = this._centerTimeoutIds.indexOf(id); + if (idx>-1) this._centerTimeoutIds.splice(idx,1); + + if (!win || !win.get_display()) return GLib.SOURCE_REMOVE; + if (win.get_maximized()) + win.unmaximize(Meta.MaximizeFlags.BOTH); + + const monitorIndex = win.get_monitor(); + const workArea = getWorkAreaForMonitor(monitorIndex); + const frame = win.get_frame_rect(); + win.move_frame(true, + workArea.x + Math.floor((workArea.width - frame.width )/2), + workArea.y + Math.floor((workArea.height - frame.height)/2)); + + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + if (win.get_display()) { + if (typeof win.set_keep_above === 'function') + win.set_keep_above(true); + else if (typeof win.make_above === 'function') + win.make_above(); + } + return GLib.SOURCE_REMOVE; + }); + return GLib.SOURCE_REMOVE; + }); + this._centerTimeoutIds.push(id); + } + + _onWindowMinimizedStateChanged(){ this.queueTile(); } + + _onWindowAdded(workspace, win) { + if (this.windows.includes(win)) return; + + if (this._isException(win)) { this._centerWindow(win); return; } + + if (this._isTileable(win)) { + if (this._settings.get_string('new-window-behavior') === 'master') + this.windows.unshift(win); + else + this.windows.push(win); + + const id = win.get_id(); + this._signalIds.set(`unmanaged-${id}`, { + object: win, id: win.connect('unmanaged', + ()=>this._onWindowRemoved(null, win))}); + this._signalIds.set(`size-${id}`, { + object: win, id: win.connect('size-changed', + ()=>{ if (!this.grabbedWindow) this.queueTile(); })}); + this._signalIds.set(`min-${id}`, { + object: win, id: win.connect('notify::minimized', + this._onWindowMinimizedStateChanged)}); + this.queueTile(); + } + } + + _onWindowRemoved(workspace, win) { + const idx = this.windows.indexOf(win); + if (idx>-1) this.windows.splice(idx,1); + + ['unmanaged','size','min'].forEach(pref=>{ + const key = `${pref}-${win.get_id()}`; + if (this._signalIds.has(key)) { + const {object,id} = this._signalIds.get(key); + try{ object.disconnect(id);}catch{} + this._signalIds.delete(key); + } + }); + this.queueTile(); + } + + _onActiveWorkspaceChanged() { + this._disconnectFromWorkspace(); + this._connectToWorkspace(); + } + + _connectToWorkspace() { + const ws = global.workspace_manager.get_active_workspace(); + ws.list_windows().forEach(w=>this._onWindowAdded(ws,w)); + this._signalIds.set('win-add', { + object: ws, id: ws.connect('window-added', this._onWindowAdded)}); + this._signalIds.set('win-rem', { + object: ws, id: ws.connect('window-removed', this._onWindowRemoved)}); + this.queueTile(); + } + _disconnectFromWorkspace() { + this.windows.slice().forEach(w=>this._onWindowRemoved(null,w)); + ['win-add','win-rem'].forEach(k=>{ + if (this._signalIds.has(k)) { + const {object,id}=this._signalIds.get(k); + try{ object.disconnect(id);}catch{} + this._signalIds.delete(k); + } + }); + } + + queueTile() { + if (this._tileInProgress || this._tileTimeoutId) return; + this._tileInProgress = true; + this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + this._tilingDelay, () => { + this._tileWindows(); + this._tileInProgress = false; + this._tileTimeoutId = null; + return GLib.SOURCE_REMOVE; + }); + } + tileNow() { if (!this._tileInProgress) this._tileWindows(); } + + _splitLayout(windows, area) { + if (!windows.length) return; + if (windows.length === 1) { + windows[0].move_resize_frame(true, + area.x, area.y, area.width, area.height); + return; + } + + const gap = Math.floor(this._innerGap/2); + const prim = [windows[0]]; + const sec = windows.slice(1); + + let primArea, secArea; + if (area.width > area.height) { + const pW = Math.floor(area.width/2) - gap; + primArea = {x: area.x, y: area.y, + width: pW, height: area.height}; + secArea = {x: area.x+pW+this._innerGap, y: area.y, + width: area.width-pW-this._innerGap, + height: area.height}; + } else { + const pH = Math.floor(area.height/2) - gap; + primArea = {x: area.x, y: area.y, + width: area.width, height: pH}; + secArea = {x: area.x, y: area.y+pH+this._innerGap, + width: area.width, + height: area.height-pH-this._innerGap}; + } + this._splitLayout(prim, primArea); + this._splitLayout(sec, secArea); + } + + _tileWindows() { + const wins = this.windows.filter(w=>!w.minimized); + if (!wins.length) return; + + const monitor = Main.layoutManager.primaryMonitor; + const work = getWorkAreaForMonitor(monitor.index); + const inner = { x: work.x + this._outerGapHorizontal, + y: work.y + this._outerGapVertical, + width: work.width - 2*this._outerGapHorizontal, + height: work.height - 2*this._outerGapVertical }; + + wins.forEach(w=>{ if (w.get_maximized()) + w.unmaximize(Meta.MaximizeFlags.BOTH); }); + + if (wins.length===1) { + wins[0].move_resize_frame(true, + inner.x, inner.y, inner.width, inner.height); + return; + } + + const gap = Math.floor(this._innerGap/2); + const masterW = Math.floor(inner.width/2) - gap; + const master = wins[0]; + master.move_resize_frame(true, + inner.x, inner.y, masterW, inner.height); + + const stack = { x: inner.x + masterW + this._innerGap, + y: inner.y, + width: inner.width - masterW - this._innerGap, + height: inner.height }; + this._splitLayout(wins.slice(1), stack); + } +} + +// ── EXTENSION‑WRAPPER ─────────────────────────────────── +class SimpleTilingExtension { + enable() { this.tiler = new Tiler(); this.tiler.enable(); } + disable() { this.tiler?.disable(); this.tiler = null; } +} + +function init() { + return new SimpleTilingExtension(); +}