diff --git a/extension.ts b/extension.ts index 1573223..c6b391d 100644 --- a/extension.ts +++ b/extension.ts @@ -63,6 +63,11 @@ export default class aerospike extends Extension { this.refreshKeybinding('toggle-orientation'); }); + this.settings.connect('changed::reset-ratios', () => { + log(`Reset ratios keybinding changed to: ${this.settings.get_strv('reset-ratios')}`); + this.refreshKeybinding('reset-ratios'); + }); + this.settings.connect('changed::dropdown-option', () => { log(`Dropdown option changed to: ${this.settings.get_string('dropdown-option')}`); }); @@ -108,6 +113,11 @@ export default class aerospike extends Extension { this.windowManager.toggleActiveContainerOrientation(); }); break; + case 'reset-ratios': + this.bindKeybinding('reset-ratios', () => { + this.windowManager.resetActiveContainerRatios(); + }); + break; } } @@ -142,6 +152,10 @@ export default class aerospike extends Extension { this.bindKeybinding('toggle-orientation', () => { this.windowManager.toggleActiveContainerOrientation(); }); + + this.bindKeybinding('reset-ratios', () => { + this.windowManager.resetActiveContainerRatios(); + }); } private bindKeybinding(settingName: string, callback: () => void) { diff --git a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml index 2c31914..1fdda35 100644 --- a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml +++ b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml @@ -49,5 +49,18 @@ Toggles the orientation of the container holding the active window between horizontal and vertical + + z']]]> + Reset container ratios to equal splits + Resets all window size ratios in the active window's container to equal splits + + + + 0.10 + + Minimum window size percentage + Minimum fraction of a container that any single window may occupy when resizing boundaries + + \ No newline at end of file diff --git a/src/utils/events.ts b/src/utils/events.ts index 0332280..6329129 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -6,15 +6,23 @@ export type QueuedEvent = { callback: () => void; } -const queuedEvents: QueuedEvent[] = []; +// Pending events indexed by name so that duplicate events collapse into one. +// Only the most-recently-queued callback for a given name is kept. +const pendingEvents: Map = new Map(); export default function queueEvent(event: QueuedEvent, interval = 200) { - queuedEvents.push(event); + // Overwrite any earlier pending event with the same name — the latest + // callback is always the most up-to-date one. + pendingEvents.set(event.name, event); + GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => { - const e = queuedEvents.pop() - if (e) { + const e = pendingEvents.get(event.name); + if (e && e === event) { + // Only fire if this is still the current callback for this name + // (a newer call may have replaced it). + pendingEvents.delete(event.name); e.callback(); } - return queuedEvents.length !== 0; + return GLib.SOURCE_REMOVE; }); -} \ No newline at end of file +} diff --git a/src/wm/container.ts b/src/wm/container.ts index 5d97ee6..17c511a 100644 --- a/src/wm/container.ts +++ b/src/wm/container.ts @@ -9,6 +9,21 @@ enum Orientation { VERTICAL = 1, } +/** + * Build a split-ratio array of length `n` where every element equals 1/n, + * with the last slot absorbing any floating-point remainder so the array + * always sums to exactly 1.0. + */ +function equalRatios(n: number): number[] { + if (n <= 0) return []; + const base = 1 / n; + const ratios = Array(n).fill(base); + // Fix floating-point drift: make last slot exact + const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0); + ratios[n - 1] = 1 - sumExceptLast; + return ratios; +} + export default class WindowContainer { @@ -17,12 +32,40 @@ export default class WindowContainer { _orientation: Orientation = Orientation.HORIZONTAL; _workArea: Rect; - constructor(workspaceArea: Rect,) { + /** + * Per-child split ratios. Always satisfies: + * _splitRatios.length === _tiledItems.length + * _splitRatios.reduce((a,b) => a+b, 0) === 1.0 (within floating-point epsilon) + * every element >= MIN_RATIO + */ + _splitRatios: number[]; + + /** Minimum fraction any child may occupy (read from settings, default 0.10). */ + _minRatio: number; + + constructor(workspaceArea: Rect, minRatio: number = 0.10) { this._tiledItems = []; this._tiledWindowLookup = new Map(); this._workArea = workspaceArea; + this._splitRatios = []; + this._minRatio = minRatio; } + // ─── Helpers ──────────────────────────────────────────────────────────────── + + /** Rebuild _splitRatios as equal fractions after any structural change. */ + private _resetRatios(): void { + this._splitRatios = equalRatios(this._tiledItems.length); + } + + /** Total dimension for the active orientation (width for H, height for V). */ + private _totalDimension(): number { + return this._orientation === Orientation.HORIZONTAL + ? this._workArea.width + : this._workArea.height; + } + + // ─── Public API ───────────────────────────────────────────────────────────── move(rect: Rect): void { this._workArea = rect; @@ -40,13 +83,13 @@ export default class WindowContainer { addWindow(winWrap: WindowWrapper): void { this._tiledItems.push(winWrap); this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap); + this._resetRatios(); queueEvent({ name: "tiling-windows", callback: () => { this.tileWindows(); } - }, 100) - + }, 100); } getWindow(win_id: number): WindowWrapper | undefined { @@ -63,27 +106,27 @@ export default class WindowContainer { return item; } } - return undefined + return undefined; } - _getIndexOfWindow(win_id: number) { + _getIndexOfWindow(win_id: number): number { for (let i = 0; i < this._tiledItems.length; i++) { const item = this._tiledItems[i]; if (item instanceof WindowWrapper && item.getWindowId() === win_id) { return i; } } - return -1 + return -1; } removeWindow(win_id: number): void { if (this._tiledWindowLookup.has(win_id)) { - // Get index before deleting from lookup to avoid race condition const index = this._getIndexOfWindow(win_id); this._tiledWindowLookup.delete(win_id); if (index !== -1) { this._tiledItems.splice(index, 1); } + this._resetRatios(); } else { for (const item of this._tiledItems) { if (item instanceof WindowContainer) { @@ -91,33 +134,30 @@ export default class WindowContainer { } } } - this.tileWindows() + this.tileWindows(); } disconnectSignals(): void { this._tiledItems.forEach((item) => { - if (item instanceof WindowContainer) { - item.disconnectSignals() - } else { - item.disconnectWindowSignals(); - } + if (item instanceof WindowContainer) { + item.disconnectSignals(); + } else { + item.disconnectWindowSignals(); } - ) + }); } removeAllWindows(): void { - this._tiledItems = [] - this._tiledWindowLookup.clear() + this._tiledItems = []; + this._tiledWindowLookup.clear(); + this._splitRatios = []; } tileWindows() { - Logger.log("TILING WINDOWS IN CONTAINER") - + Logger.log("TILING WINDOWS IN CONTAINER"); Logger.log("WorkArea", this._workArea); - - this._tileItems() - - return true + this._tileItems(); + return true; } _tileItems() { @@ -125,16 +165,19 @@ export default class WindowContainer { return; } const bounds = this.getBounds(); + Logger.info(`_tileItems: ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}] bounds=[${bounds.map(b => `(${b.x},${b.y},${b.width},${b.height})`).join(', ')}]`); this._tiledItems.forEach((item, index) => { const rect = bounds[index]; if (item instanceof WindowContainer) { item.move(rect); } else { + Logger.info(`_tileItems: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`); item.safelyResizeWindow(rect); } - }) + }); } + // ─── Bounds Calculation ────────────────────────────────────────────────────── getBounds(): Rect[] { if (this._orientation === Orientation.HORIZONTAL) { @@ -144,33 +187,131 @@ export default class WindowContainer { } getVerticalBounds(): Rect[] { - const items = this._tiledItems - const containerHeight = Math.floor(this._workArea.height / items.length); + const items = this._tiledItems; + const totalHeight = this._workArea.height; + let usedHeight = 0; + return items.map((_, index) => { - const y = this._workArea.y + (index * containerHeight); + const y = this._workArea.y + usedHeight; + let height: number; + if (index === items.length - 1) { + // Last item gets the remainder to avoid pixel gaps from rounding + height = totalHeight - usedHeight; + } else { + height = Math.floor(this._splitRatios[index] * totalHeight); + } + usedHeight += height; return { x: this._workArea.x, y: y, width: this._workArea.width, - height: containerHeight + height: height, } as Rect; }); } getHorizontalBounds(): Rect[] { - const windowWidth = Math.floor(this._workArea.width / this._tiledItems.length); + const totalWidth = this._workArea.width; + let usedWidth = 0; return this._tiledItems.map((_, index) => { - const x = this._workArea.x + (index * windowWidth); + const x = this._workArea.x + usedWidth; + let width: number; + if (index === this._tiledItems.length - 1) { + // Last item gets the remainder to avoid pixel gaps from rounding + width = totalWidth - usedWidth; + } else { + width = Math.floor(this._splitRatios[index] * totalWidth); + } + usedWidth += width; return { x: x, y: this._workArea.y, - width: windowWidth, - height: this._workArea.height + width: width, + height: this._workArea.height, } as Rect; }); } + // ─── Boundary / Ratio Adjustment ───────────────────────────────────────────── + + /** + * Adjust the boundary between item[boundaryIndex] and item[boundaryIndex+1] + * by deltaPixels (positive = move right/down, negative = move left/up). + * + * Both affected ratios are clamped to [_minRatio, 1 - _minRatio] so no + * window can be squashed below the configured minimum. + * + * Returns true if the adjustment was applied, false if it was rejected + * (e.g. out of bounds index or clamping would violate minimum). + */ + adjustBoundary(boundaryIndex: number, deltaPixels: number): boolean { + if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) { + Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`); + return false; + } + + const totalDim = this._totalDimension(); + if (totalDim === 0) return false; + + const ratioDelta = deltaPixels / totalDim; + const minRatio = this._minRatio; + + const newLeft = this._splitRatios[boundaryIndex] + ratioDelta; + const newRight = this._splitRatios[boundaryIndex + 1] - ratioDelta; + + if (newLeft < minRatio || newRight < minRatio) { + Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}, min=${minRatio}`); + return false; + } + + this._splitRatios[boundaryIndex] = newLeft; + this._splitRatios[boundaryIndex + 1] = newRight; + + Logger.info(`adjustBoundary: boundary=${boundaryIndex} ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`); + return true; + } + + /** + * Adjust boundaries on BOTH axes simultaneously for corner resize ops. + * horizontalDelta applies to this container if HORIZONTAL, verticalDelta if VERTICAL. + * For nested containers the perpendicular delta is forwarded to the child container. + * + * boundaryIndex: the slot index whose right/bottom edge is being dragged. + */ + adjustBoundaryBothAxes( + boundaryIndex: number, + horizontalDelta: number, + verticalDelta: number, + ): void { + if (this._orientation === Orientation.HORIZONTAL) { + this.adjustBoundary(boundaryIndex, horizontalDelta); + } else { + this.adjustBoundary(boundaryIndex, verticalDelta); + } + } + + // ─── Container Lookup ──────────────────────────────────────────────────────── + + /** + * Returns the direct-parent WindowContainer that contains win_id as an + * immediate child (not recursed further). Returns null if not found. + */ + getContainerForWindow(win_id: number): WindowContainer | null { + for (const item of this._tiledItems) { + if (item instanceof WindowWrapper && item.getWindowId() === win_id) { + return this; + } + } + for (const item of this._tiledItems) { + if (item instanceof WindowContainer) { + const found = item.getContainerForWindow(win_id); + if (found !== null) return found; + } + } + return null; + } + getIndexOfItemNested(item: WindowWrapper): number { for (let i = 0; i < this._tiledItems.length; i++) { const container = this._tiledItems[i]; @@ -194,19 +335,30 @@ export default class WindowContainer { Logger.error("Item not found in container during drag op", item.getWindowId()); return; } - let new_index = this.getIndexOfItemNested(item); + let new_index = original_index; this.getBounds().forEach((rect, index) => { if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) { new_index = index; } - }) + }); if (original_index !== new_index) { - this._tiledItems.splice(original_index, 1); - this._tiledItems.splice(new_index, 0, item); - this.tileWindows() + // Swap only the items — ratios stay with their slots. + // e.g. slot 0 = 40%, slot 1 = 60%: when the window in slot 1 drags + // into slot 0, it takes slot 0's 40% size. The window it displaces + // moves to slot 1 and takes the 60% size. The slot ratios are unchanged. + [this._tiledItems[original_index], this._tiledItems[new_index]] = + [this._tiledItems[new_index], this._tiledItems[original_index]]; + Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`); + this.tileWindows(); } - } - -} \ No newline at end of file + /** + * Reset all split ratios in this container to equal fractions. + * Called when the user explicitly requests an equal-split reset (e.g. Ctrl+Z). + */ + resetRatios(): void { + this._resetRatios(); + this.tileWindows(); + } +} diff --git a/src/wm/monitor.ts b/src/wm/monitor.ts index 640f74d..48135e3 100644 --- a/src/wm/monitor.ts +++ b/src/wm/monitor.ts @@ -67,8 +67,8 @@ export default class Monitor { tileWindows(): void { this._workArea = global.workspace_manager.get_active_workspace().get_work_area_for_monitor(this._id); const activeWorkspace = global.workspace_manager.get_active_workspace(); + // move() already calls tileWindows() internally — don't call it again this._workspaces[activeWorkspace.index()].move(this._workArea); - this._workspaces[activeWorkspace.index()].tileWindows() } removeWorkspace(workspaceId: number): void { diff --git a/src/wm/window.ts b/src/wm/window.ts index 70bb350..bba005f 100644 --- a/src/wm/window.ts +++ b/src/wm/window.ts @@ -99,6 +99,9 @@ export class WindowWrapper { this._window.connect("position-changed", (_metaWindow) => { windowManager.handleWindowPositionChanged(this); }), + this._window.connect("size-changed", (_metaWindow) => { + windowManager.handleWindowPositionChanged(this); + }), ); } @@ -117,35 +120,41 @@ export class WindowWrapper { } } - safelyResizeWindow(rect: Rect, _retry: number = 2): void { - // Keep minimal logging + safelyResizeWindow(rect: Rect, _retry: number = 3): void { if (this._dragging) { - Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED") + Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED"); return; } - // Logger.log("SAFELY RESIZE", rect.x, rect.y, rect.width, rect.height); - const actor = this._window.get_compositor_private(); + const actor = this._window.get_compositor_private() as Clutter.Actor | null; if (!actor) { Logger.log("No actor available, can't resize safely yet"); return; } - let windowActor = this._window.get_compositor_private() as Clutter.Actor; - if (!windowActor) return; - windowActor.remove_all_transitions(); - // Logger.info("MOVING") - this._window.move_frame(true, rect.x, rect.y); - // Logger.info("RESIZING MOVING") + + actor.remove_all_transitions(); + + // Single call: move + resize atomically this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height); - let new_rect = this._window.get_frame_rect(); - if ( _retry > 0 && (new_rect.x != rect.x || rect.y != new_rect.y || rect.width < new_rect.width || rect.height < new_rect.height)) { - Logger.warn("RESIZING FAILED AS SMALLER", new_rect.x, new_rect.y, new_rect.width, new_rect.height, rect.x, rect.y, rect.width, rect.height); + + const new_rect = this._window.get_frame_rect(); + const TOLERANCE = 2; // pixels — allow compositor rounding + const mismatch = + Math.abs(new_rect.x - rect.x) > TOLERANCE || + Math.abs(new_rect.y - rect.y) > TOLERANCE || + Math.abs(new_rect.width - rect.width) > TOLERANCE || + Math.abs(new_rect.height - rect.height) > TOLERANCE; + + if (_retry > 0 && mismatch) { + Logger.warn("RESIZE MISMATCH, retrying", + `want(${rect.x},${rect.y},${rect.width},${rect.height})`, + `got(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`); queueEvent({ - name: "attempting_delayed_resize", + name: `delayed_resize_${this.getWindowId()}`, callback: () => { - this.safelyResizeWindow(rect, _retry-1); + this.safelyResizeWindow(rect, _retry - 1); } - }) + }, 50); } } diff --git a/src/wm/windowManager.ts b/src/wm/windowManager.ts index d9ce26d..cc03e4e 100644 --- a/src/wm/windowManager.ts +++ b/src/wm/windowManager.ts @@ -8,6 +8,7 @@ import * as Main from "resource:///org/gnome/shell/ui/main.js"; import {Logger} from "../utils/logger.js"; import Monitor from "./monitor.js"; import WindowContainer from "./container.js"; +import {Rect} from "../utils/rect.js"; export interface IWindowManager { @@ -49,6 +50,16 @@ export default class WindowManager implements IWindowManager { _showingOverview: boolean = false; + // ── Resize-drag tracking ────────────────────────────────────────────────── + _isResizeDrag: boolean = false; + _resizeDragWindowId: number = _UNUSED_WINDOW_ID; + _resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE; + /** Mouse position at the start of each incremental resize step. */ + _resizeDragLastMouseX: number = 0; + _resizeDragLastMouseY: number = 0; + /** Re-entrancy guard: true while tileWindows is propagating position-changed events. */ + _isTiling: boolean = false; + constructor() { @@ -208,25 +219,65 @@ export default class WindowManager implements IWindowManager { } - handleGrabOpBegin(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void { - if (op === Meta.GrabOp.MOVING_UNCONSTRAINED){ + /** + * Returns true if the grab op is a resize operation (any edge or corner). + */ + _isResizeOp(op: Meta.GrabOp): boolean { + return op === Meta.GrabOp.RESIZING_E || + op === Meta.GrabOp.RESIZING_W || + op === Meta.GrabOp.RESIZING_N || + op === Meta.GrabOp.RESIZING_S || + op === Meta.GrabOp.RESIZING_NE || + op === Meta.GrabOp.RESIZING_NW || + op === Meta.GrabOp.RESIZING_SE || + op === Meta.GrabOp.RESIZING_SW; + } - } + handleGrabOpBegin(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void { Logger.log("Grab Op Start", op); - Logger.log(display, window, op) - Logger.log(window.get_monitor()) - this._getWrappedWindow(window)?.startDragging(); - this._grabbedWindowMonitor = window.get_monitor(); - this._grabbedWindowId = window.get_id(); + + if (this._isResizeOp(op)) { + // ── Resize drag ────────────────────────────────────────────────── + Logger.log("Resize drag begin, op=", op); + this._isResizeDrag = true; + this._resizeDragWindowId = window.get_id(); + this._resizeDragOp = op; + const [startMouseX, startMouseY] = global.get_pointer(); + this._resizeDragLastMouseX = startMouseX; + this._resizeDragLastMouseY = startMouseY; + // Mark the window as dragging so safelyResizeWindow skips it while + // we tile the other windows in response to ratio changes. + this._getWrappedWindow(window)?.startDragging(); + } else { + // ── Move drag (existing behaviour) ─────────────────────────────── + this._getWrappedWindow(window)?.startDragging(); + this._grabbedWindowMonitor = window.get_monitor(); + this._grabbedWindowId = window.get_id(); + } } handleGrabOpEnd(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void { Logger.log("Grab Op End ", op); - Logger.log("primary display", display.get_primary_monitor()) - this._grabbedWindowId = _UNUSED_WINDOW_ID; - this._getWrappedWindow(window)?.stopDragging(); - this._tileMonitors(); - Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor()); + + if (this._isResizeDrag) { + // ── Resize drag end ────────────────────────────────────────────── + Logger.log("Resize drag end, op=", op); + this._isResizeDrag = false; + this._resizeDragWindowId = _UNUSED_WINDOW_ID; + this._resizeDragLastMouseX = 0; + this._resizeDragLastMouseY = 0; + this._resizeDragOp = Meta.GrabOp.NONE; + // Stop suppressing the window, then snap everything to computed ratios + this._getWrappedWindow(window)?.stopDragging(); + this._tileMonitors(); + } else { + // ── Move drag end (existing behaviour) ─────────────────────────── + Logger.log("primary display", display.get_primary_monitor()) + this._grabbedWindowId = _UNUSED_WINDOW_ID; + this._getWrappedWindow(window)?.stopDragging(); + this._tileMonitors(); + Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor()); + } } _getWrappedWindow(window: Meta.Window): WindowWrapper | undefined { @@ -265,9 +316,21 @@ export default class WindowManager implements IWindowManager { } public handleWindowPositionChanged(winWrap: WindowWrapper): void { + // Ignore position changes that we triggered ourselves via tileWindows + if (this._isTiling) { + return; + } if (this._changingGrabbedMonitor) { return; } + + // ── Live resize-drag handling ───────────────────────────────────────── + if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) { + this._handleResizeDragUpdate(winWrap); + return; + } + + // ── Move-drag handling (existing behaviour) ─────────────────────────── if (winWrap.getWindowId() === this._grabbedWindowId) { const [mouseX, mouseY, _] = global.get_pointer(); @@ -281,18 +344,108 @@ export default class WindowManager implements IWindowManager { } } if (monitorIndex === -1) { - return + return; } if (monitorIndex !== this._grabbedWindowMonitor) { this._changingGrabbedMonitor = true; this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex); - this._changingGrabbedMonitor = false + this._changingGrabbedMonitor = false; + } + // Guard _isTiling so that tileWindows() calls triggered by itemDragged + // (which repositions the displaced window) don't re-enter this handler. + this._isTiling = true; + try { + this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY); + } finally { + this._isTiling = false; } - this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY); } } + /** + * Called on every position-changed event while a resize drag is in progress. + * Computes the pixel delta from the drag-start rect, maps it to the correct + * container boundary, and calls adjustBoundary() for live feedback. + */ + private _handleResizeDragUpdate(winWrap: WindowWrapper): void { + const op = this._resizeDragOp; + const winId = winWrap.getWindowId(); + + // Read the current mouse position — this is unclamped by the compositor + // and always reflects the true user intent, unlike the window's frame rect + // which gets clamped when adjacent windows block expansion. + const [mouseX, mouseY] = global.get_pointer(); + const dx = mouseX - this._resizeDragLastMouseX; + const dy = mouseY - this._resizeDragLastMouseY; + + if (dx === 0 && dy === 0) return; + + // Update last position first so even if we return early the baseline advances + this._resizeDragLastMouseX = mouseX; + this._resizeDragLastMouseY = mouseY; + + // Find the container that directly holds this window + const container = this._findContainerForWindowAcrossMonitors(winId); + if (!container) { + Logger.warn("_handleResizeDragUpdate: no container found for window", winId); + return; + } + + const itemIndex = container._getIndexOfWindow(winId); + if (itemIndex === -1) return; + + const isHorizontal = container._orientation === 0; // Orientation.HORIZONTAL + + // Map the mouse delta to the correct boundary. + // + // East/South edge → boundary AFTER the item (boundaryIndex = itemIndex) + // positive dx/dy grows this item, shrinks the next one. + // West/North edge → boundary BEFORE the item (boundaryIndex = itemIndex - 1) + // positive dx/dy moves the left edge right, growing the left neighbour + // and shrinking this item — so we negate the delta. + + let adjusted = false; + if (isHorizontal) { + if (op === Meta.GrabOp.RESIZING_E || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_SE) { + adjusted = container.adjustBoundary(itemIndex, dx); + } else if (op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_NW || op === Meta.GrabOp.RESIZING_SW) { + adjusted = container.adjustBoundary(itemIndex - 1, dx); + } + } else { + if (op === Meta.GrabOp.RESIZING_S || op === Meta.GrabOp.RESIZING_SE || op === Meta.GrabOp.RESIZING_SW) { + adjusted = container.adjustBoundary(itemIndex, dy); + } else if (op === Meta.GrabOp.RESIZING_N || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_NW) { + adjusted = container.adjustBoundary(itemIndex - 1, dy); + } + } + + // Tile all windows with the updated ratios, guarded so the resulting + // position-changed events don't re-enter this handler. + if (adjusted) { + this._isTiling = true; + try { + container.tileWindows(); + } finally { + this._isTiling = false; + } + } + } + + /** + * Searches all monitors for the WindowContainer that directly holds win_id. + */ + private _findContainerForWindowAcrossMonitors(winId: number): WindowContainer | null { + const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index(); + for (const monitor of this._monitors.values()) { + if (activeWorkspaceIndex >= monitor._workspaces.length) continue; + const workspace = monitor._workspaces[activeWorkspaceIndex]; + const container = workspace.getContainerForWindow(winId); + if (container !== null) return container; + } + return null; + } + public handleWindowMinimized(winWrap: WindowWrapper): void { const monitor_id = winWrap.getWindow().get_monitor() @@ -372,9 +525,13 @@ export default class WindowManager implements IWindowManager { } _tileMonitors(): void { - - for (const monitor of this._monitors.values()) { - monitor.tileWindows() + this._isTiling = true; + try { + for (const monitor of this._monitors.values()) { + monitor.tileWindows(); + } + } finally { + this._isTiling = false; } } @@ -455,6 +612,25 @@ export default class WindowManager implements IWindowManager { } } + /** + * Resets all split ratios in the active window's container to equal fractions. + * Bound to Ctrl+Z by default. + */ + public resetActiveContainerRatios(): void { + if (this._activeWindowId === null) { + Logger.warn("No active window, cannot reset container ratios"); + return; + } + + const activeContainer = this._findActiveContainer(); + if (activeContainer) { + Logger.info("Resetting container ratios to equal splits"); + activeContainer.resetRatios(); + } else { + Logger.warn("Could not find container for active window"); + } + } + /** * Finds the container that directly contains the active window * @returns The container holding the active window, or null if not found