From dd565da626428f2399880085883eb2635526722c Mon Sep 17 00:00:00 2001 From: Lucas Oskorep Date: Tue, 28 Oct 2025 12:59:11 -0400 Subject: [PATCH] feat: add resizing --- README.md | 2 +- extension.ts | 16 + justfile | 4 +- prettyborders.zip | Bin 4666 -> 0 bytes ...ome.shell.extensions.aerospike.gschema.xml | 6 + src/wm/container.ts | 508 ++++++++++++++++-- src/wm/divider.ts | 45 ++ src/wm/monitor.ts | 13 + src/wm/window.ts | 18 +- src/wm/windowManager.ts | 70 ++- 10 files changed, 635 insertions(+), 47 deletions(-) delete mode 100644 prettyborders.zip create mode 100644 src/wm/divider.ts diff --git a/README.md b/README.md index 2e0ff42..accd172 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Aerospike Gnome (Tiling Window Manager) \ No newline at end of file +# Aerospike Gnome (Tiling Window Manager) diff --git a/extension.ts b/extension.ts index a503b40..bbb1c85 100644 --- a/extension.ts +++ b/extension.ts @@ -53,6 +53,11 @@ export default class aerospike extends Extension { this.refreshKeybinding('join-with-right'); }); + this.settings.connect('changed::remove-all-dividers', () => { + log(`Keybinding remove-all-dividers changed to: ${this.settings.get_strv('remove-all-dividers')}`); + this.refreshKeybinding('remove-all-dividers'); + }); + this.settings.connect('changed::dropdown-option', () => { log(`Dropdown option changed to: ${this.settings.get_string('dropdown-option')}`); }); @@ -88,6 +93,12 @@ export default class aerospike extends Extension { Logger.info('Keybinding 4 was pressed!'); }); break; + case 'remove-all-dividers': + this.bindKeybinding('remove-all-dividers', () => { + Logger.info('Remove all dividers keybinding pressed!'); + this.windowManager.removeAllDividersFromActiveContainer(); + }); + break; } } @@ -114,6 +125,11 @@ export default class aerospike extends Extension { this.bindKeybinding('join-with-right', () => { Logger.info('Keybinding 4 was pressed!'); }); + + this.bindKeybinding('remove-all-dividers', () => { + Logger.info('Remove all dividers keybinding pressed!'); + this.windowManager.removeAllDividersFromActiveContainer(); + }); } private bindKeybinding(settingName: string, callback: () => void) { diff --git a/justfile b/justfile index 86e9f39..1c9b80b 100644 --- a/justfile +++ b/justfile @@ -28,7 +28,7 @@ install: build cp -r dist/* ~/.local/share/gnome-shell/extensions/{{NAME}}@{{DOMAIN}}/ run: - env MUTTER_DEBUG_DUMMY_MODE_SPECS=1280x720 dbus-run-session -- gnome-shell --devkit --wayland + env MUTTER_DEBUG_DUMMY_MODE_SPECS=1280x720 dbus-run-session -- gnome-shell --nested --wayland install-and-run: install run @@ -42,4 +42,4 @@ live-debug: # --schema ../schemas/org.gnome.shell.extensions.aerospike.gschema.xml # #install-pack: pack -# gnome-extensions install ./{{FULL_NAME}}.shell-extension.zip --force \ No newline at end of file +# gnome-extensions install ./{{FULL_NAME}}.shell-extension.zip --force diff --git a/prettyborders.zip b/prettyborders.zip deleted file mode 100644 index 8ad6a1bc5a262e32aa5f4f0ab7ab865a87333e70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4666 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 diff --git a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml index cdea8da..172e380 100644 --- a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml +++ b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml @@ -37,5 +37,11 @@ Keyboard shortcut for triggering action 4 + + z']]]> + Remove all dividers from active container + Keyboard shortcut for removing all dividers from the container with the active window + + \ No newline at end of file diff --git a/src/wm/container.ts b/src/wm/container.ts index 159d477..c36e033 100644 --- a/src/wm/container.ts +++ b/src/wm/container.ts @@ -3,16 +3,18 @@ import {Logger} from "../utils/logger.js"; import Meta from "gi://Meta"; import queueEvent from "../utils/events.js"; import {Rect} from "../utils/rect.js"; +import {Divider} from "./divider.js"; enum Orientation { HORIZONTAL = 0, VERTICAL = 1, } +type ContainerItem = WindowWrapper | WindowContainer | Divider; export default class WindowContainer { - _tiledItems: (WindowWrapper | WindowContainer)[]; + _tiledItems: ContainerItem[]; _tiledWindowLookup: Map; _orientation: Orientation = Orientation.HORIZONTAL; _workArea: Rect; @@ -49,12 +51,14 @@ export default class WindowContainer { return this._tiledWindowLookup.get(win_id); } for (const item of this._tiledItems) { - if (item instanceof WindowContainer) { + if (Divider.isDivider(item)) { + continue; // Skip dividers + } else if (item instanceof WindowContainer) { const win = item.getWindow(win_id); if (win) { return win; } - } else if (item.getWindowId() === win_id) { + } else if (item instanceof WindowWrapper && item.getWindowId() === win_id) { return item; } } @@ -76,6 +80,7 @@ export default class WindowContainer { this._tiledWindowLookup.delete(win_id); const index = this._getIndexOfWindow(win_id) this._tiledItems.splice(index, 1); + this._cleanupInvalidDividers(); } else { for (const item of this._tiledItems) { if (item instanceof WindowContainer) { @@ -86,11 +91,44 @@ export default class WindowContainer { this.tileWindows() } + /** + * Removes invalid dividers from the items list. + * Invalid dividers are: + * - Dividers at the start or end of the list (no window on one side) + * - Consecutive dividers (two dividers in a row) + */ + _cleanupInvalidDividers(): void { + let i = 0; + while (i < this._tiledItems.length) { + const item = this._tiledItems[i]; + + if (Divider.isDivider(item)) { + // Check if divider is at start or end + const isAtStart = i === 0; + const isAtEnd = i === this._tiledItems.length - 1; + + // Check if next item is also a divider + const nextIsDivider = i < this._tiledItems.length - 1 && + Divider.isDivider(this._tiledItems[i + 1]); + + if (isAtStart || isAtEnd || nextIsDivider) { + Logger.log(`Removing invalid divider at index ${i}`); + this._tiledItems.splice(i, 1); + continue; // Don't increment i, check the same position again + } + } + i++; + } + } + disconnectSignals(): void { this._tiledItems.forEach((item) => { - if (item instanceof WindowContainer) { + if (Divider.isDivider(item)) { + // Skip dividers - they don't have signals + return; + } else if (item instanceof WindowContainer) { item.disconnectSignals() - } else { + } else if (item instanceof WindowWrapper) { item.disconnectWindowSignals(); } } @@ -118,13 +156,21 @@ export default class WindowContainer { return; } const bounds = this.getBounds(); - this._tiledItems.forEach((item, index) => { - const rect = bounds[index]; + + // Apply bounds to non-divider items + let boundsIndex = 0; + this._tiledItems.forEach((item) => { + if (Divider.isDivider(item)) { + return; // Skip dividers + } + + const rect = bounds[boundsIndex]; if (item instanceof WindowContainer) { item.move(rect); - } else { + } else if (item instanceof WindowWrapper) { item.safelyResizeWindow(rect); } + boundsIndex++; }) } @@ -137,42 +183,177 @@ export default class WindowContainer { } getVerticalBounds(): Rect[] { - const items = this._tiledItems - const containerHeight = Math.floor(this._workArea.height / items.length); - return items.map((_, index) => { - const y = this._workArea.y + (index * containerHeight); - return { - x: this._workArea.x, - y: y, - width: this._workArea.width, - height: containerHeight - } as Rect; - }); + // Filter out dividers to get only windows/containers + const nonDividerItems = this._tiledItems.filter(item => !Divider.isDivider(item)); + + if (nonDividerItems.length === 0) { + return []; + } + + // If no dividers, use equal distribution + const hasDividers = this._tiledItems.some(item => Divider.isDivider(item)); + if (!hasDividers) { + const containerHeight = Math.floor(this._workArea.height / nonDividerItems.length); + return nonDividerItems.map((_, index) => { + const y = this._workArea.y + (index * containerHeight); + return { + x: this._workArea.x, + y: y, + width: this._workArea.width, + height: containerHeight + } as Rect; + }); + } + + // Calculate bounds based on divider positions + const bounds: Rect[] = []; + let currentY = this._workArea.y; + let itemIndex = 0; + + for (let i = 0; i < this._tiledItems.length; i++) { + const item = this._tiledItems[i]; + + if (Divider.isDivider(item)) { + // Next segment starts at divider position + currentY = this._workArea.y + Math.floor(item.getPosition() * this._workArea.height); + } else { + // Find the end position for this item + let endY: number = this._workArea.y + this._workArea.height; + + // Look ahead to find next divider or end of container + for (let j = i + 1; j < this._tiledItems.length; j++) { + if (Divider.isDivider(this._tiledItems[j])) { + const divider = this._tiledItems[j] as Divider; + endY = this._workArea.y + Math.floor(divider.getPosition() * this._workArea.height); + break; + } + } + + // Count non-divider items until next divider + let itemCount = 0; + let itemsInSegment: number[] = []; + for (let j = i; j < this._tiledItems.length; j++) { + if (Divider.isDivider(this._tiledItems[j])) { + break; + } + itemsInSegment.push(j); + itemCount++; + } + + // Divide space equally among items in this segment + const segmentHeight = endY - currentY; + const itemHeight = Math.floor(segmentHeight / itemCount); + + for (let k = 0; k < itemsInSegment.length; k++) { + const itemY = currentY + (k * itemHeight); + bounds.push({ + x: this._workArea.x, + y: itemY, + width: this._workArea.width, + height: itemHeight + } as Rect); + } + + // Skip the items we just processed + i += itemCount - 1; + currentY = endY; + } + } + + return bounds; } getHorizontalBounds(): Rect[] { - const windowWidth = Math.floor(this._workArea.width / this._tiledItems.length); + // Filter out dividers to get only windows/containers + const nonDividerItems = this._tiledItems.filter(item => !Divider.isDivider(item)); - return this._tiledItems.map((_, index) => { - const x = this._workArea.x + (index * windowWidth); - return { - x: x, - y: this._workArea.y, - width: windowWidth, - height: this._workArea.height - } as Rect; - }); + if (nonDividerItems.length === 0) { + return []; + } + + // If no dividers, use equal distribution + const hasDividers = this._tiledItems.some(item => Divider.isDivider(item)); + if (!hasDividers) { + const windowWidth = Math.floor(this._workArea.width / nonDividerItems.length); + return nonDividerItems.map((_, index) => { + const x = this._workArea.x + (index * windowWidth); + return { + x: x, + y: this._workArea.y, + width: windowWidth, + height: this._workArea.height + } as Rect; + }); + } + + // Calculate bounds based on divider positions + const bounds: Rect[] = []; + let currentX = this._workArea.x; + + for (let i = 0; i < this._tiledItems.length; i++) { + const item = this._tiledItems[i]; + + if (Divider.isDivider(item)) { + // Next segment starts at divider position + currentX = this._workArea.x + Math.floor(item.getPosition() * this._workArea.width); + } else { + // Find the end position for this item + let endX: number = this._workArea.x + this._workArea.width; + + // Look ahead to find next divider or end of container + for (let j = i + 1; j < this._tiledItems.length; j++) { + if (Divider.isDivider(this._tiledItems[j])) { + const divider = this._tiledItems[j] as Divider; + endX = this._workArea.x + Math.floor(divider.getPosition() * this._workArea.width); + break; + } + } + + // Count non-divider items until next divider + let itemCount = 0; + let itemsInSegment: number[] = []; + for (let j = i; j < this._tiledItems.length; j++) { + if (Divider.isDivider(this._tiledItems[j])) { + break; + } + itemsInSegment.push(j); + itemCount++; + } + + // Divide space equally among items in this segment + const segmentWidth = endX - currentX; + const itemWidth = Math.floor(segmentWidth / itemCount); + + for (let k = 0; k < itemsInSegment.length; k++) { + const itemX = currentX + (k * itemWidth); + bounds.push({ + x: itemX, + y: this._workArea.y, + width: itemWidth, + height: this._workArea.height + } as Rect); + } + + // Skip the items we just processed + i += itemCount - 1; + currentX = endX; + } + } + + return bounds; } getIndexOfItemNested(item: WindowWrapper): number { for (let i = 0; i < this._tiledItems.length; i++) { const container = this._tiledItems[i]; - if (container instanceof WindowContainer) { + if (Divider.isDivider(container)) { + continue; // Skip dividers + } else if (container instanceof WindowContainer) { const index = container.getIndexOfItemNested(item); if (index !== -1) { return i; } - } else if (container.getWindowId() === item.getWindowId()) { + } else if (container instanceof WindowWrapper && container.getWindowId() === item.getWindowId()) { return i; } } @@ -181,24 +362,271 @@ export default class WindowContainer { // TODO: update this to work with nested containers - all other logic should already be working itemDragged(item: WindowWrapper, x: number, y: number): void { - let original_index = this.getIndexOfItemNested(item); + // Find the actual index in _tiledItems (including dividers) + const original_actual_index = this._getIndexOfWindow(item.getWindowId()); - if (original_index === -1) { + if (original_actual_index === -1) { Logger.error("Item not found in container during drag op", item.getWindowId()); return; } - let new_index = this.getIndexOfItemNested(item); - this.getBounds().forEach((rect, index) => { + + // Find which visual slot (non-divider index) we're moving to + let new_visual_index = this.getIndexOfItemNested(item); + const bounds = this.getBounds(); + bounds.forEach((rect, index) => { if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) { - new_index = index; + new_visual_index = index; } }) - if (original_index !== new_index) { - this._tiledItems.splice(original_index, 1); - this._tiledItems.splice(new_index, 0, item); - this.tileWindows() + + // Get current visual index (counting only non-dividers before this item) + let original_visual_index = 0; + for (let i = 0; i < original_actual_index; i++) { + if (!Divider.isDivider(this._tiledItems[i])) { + original_visual_index++; + } } + if (original_visual_index === new_visual_index) { + return; // No movement needed + } + + Logger.log(`Swapping window from visual index ${original_visual_index} to ${new_visual_index}`); + + // Find the target window at the new visual index + let target_actual_index = -1; + let visual_count = 0; + for (let i = 0; i < this._tiledItems.length; i++) { + if (!Divider.isDivider(this._tiledItems[i])) { + if (visual_count === new_visual_index) { + target_actual_index = i; + break; + } + visual_count++; + } + } + + if (target_actual_index === -1) { + Logger.warn("Could not find target position for drag"); + return; + } + + // Simply swap the two windows in place, leaving dividers where they are + const temp = this._tiledItems[original_actual_index]; + this._tiledItems[original_actual_index] = this._tiledItems[target_actual_index]; + this._tiledItems[target_actual_index] = temp; + + this.tileWindows(); + } + + /** + * Handles window resize operations. Creates or updates dividers based on resize direction. + * @param item - The window being resized + * @param resizeEdge - The edge being resized (N, S, E, W, etc.) + * @param newRect - The new rectangle after resize + */ + handleWindowResize(item: WindowWrapper, resizeEdge: Meta.GrabOp, newRect: Rect): void { + const itemIndex = this._getIndexOfWindow(item.getWindowId()); + if (itemIndex === -1) { + Logger.warn("Window not found in container during resize", item.getWindowId()); + return; + } + + // Determine if this is a valid resize for this container orientation + const isHorizontalResize = this._isHorizontalResizeOp(resizeEdge); + const isVerticalResize = this._isVerticalResizeOp(resizeEdge); + + // Only allow horizontal resizes in horizontal containers + // Only allow vertical resizes in vertical containers + if (this._orientation === Orientation.HORIZONTAL && !isHorizontalResize) { + Logger.log("Ignoring vertical resize in horizontal container"); + return; + } + if (this._orientation === Orientation.VERTICAL && !isVerticalResize) { + Logger.log("Ignoring horizontal resize in vertical container"); + return; + } + + // Determine which edge is being resized and find adjacent window + let adjacentIndex = -1; + let dividerPosition = 0; + + if (this._orientation === Orientation.HORIZONTAL) { + // East/West resize + if (this._isEastResizeOp(resizeEdge)) { + // Resizing east edge - divider goes after this window + adjacentIndex = itemIndex + 1; + // Calculate divider position as ratio of container width + const rightEdge = newRect.x + newRect.width; + dividerPosition = (rightEdge - this._workArea.x) / this._workArea.width; + } else if (this._isWestResizeOp(resizeEdge)) { + // Resizing west edge - divider goes before this window + adjacentIndex = itemIndex - 1; + dividerPosition = (newRect.x - this._workArea.x) / this._workArea.width; + } + } else { + // Vertical orientation - North/South resize + if (this._isSouthResizeOp(resizeEdge)) { + // Resizing south edge - divider goes after this window + adjacentIndex = itemIndex + 1; + const bottomEdge = newRect.y + newRect.height; + dividerPosition = (bottomEdge - this._workArea.y) / this._workArea.height; + } else if (this._isNorthResizeOp(resizeEdge)) { + // Resizing north edge - divider goes before this window + adjacentIndex = itemIndex - 1; + dividerPosition = (newRect.y - this._workArea.y) / this._workArea.height; + } + } + + // Make sure there's an adjacent item + if (adjacentIndex < 0 || adjacentIndex >= this._tiledItems.length) { + Logger.log("No adjacent window for resize operation"); + return; + } + + // Skip if adjacent item is already a divider + if (Divider.isDivider(this._tiledItems[adjacentIndex])) { + // Update existing divider + const divider = this._tiledItems[adjacentIndex] as Divider; + divider.setPosition(dividerPosition); + Logger.log(`Updated divider at index ${adjacentIndex} to position ${dividerPosition}`); + } else { + // Insert new divider between items + const dividerIndex = Math.max(itemIndex, adjacentIndex); + const newDivider = new Divider(dividerPosition, this._orientation); + this._tiledItems.splice(dividerIndex, 0, newDivider); + Logger.log(`Inserted new divider at index ${dividerIndex} with position ${dividerPosition}`); + } + + this.tileWindows(); + } + + private _isHorizontalResizeOp(op: Meta.GrabOp): boolean { + return op === Meta.GrabOp.RESIZING_E || + op === Meta.GrabOp.RESIZING_W || + op === Meta.GrabOp.RESIZING_NE || + op === Meta.GrabOp.RESIZING_NW || + op === Meta.GrabOp.RESIZING_SE || + op === Meta.GrabOp.RESIZING_SW; + } + + private _isVerticalResizeOp(op: Meta.GrabOp): boolean { + return 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; + } + + private _isEastResizeOp(op: Meta.GrabOp): boolean { + return op === Meta.GrabOp.RESIZING_E || + op === Meta.GrabOp.RESIZING_NE || + op === Meta.GrabOp.RESIZING_SE; + } + + private _isWestResizeOp(op: Meta.GrabOp): boolean { + return op === Meta.GrabOp.RESIZING_W || + op === Meta.GrabOp.RESIZING_NW || + op === Meta.GrabOp.RESIZING_SW; + } + + private _isSouthResizeOp(op: Meta.GrabOp): boolean { + return op === Meta.GrabOp.RESIZING_S || + op === Meta.GrabOp.RESIZING_SE || + op === Meta.GrabOp.RESIZING_SW; + } + + private _isNorthResizeOp(op: Meta.GrabOp): boolean { + return op === Meta.GrabOp.RESIZING_N || + op === Meta.GrabOp.RESIZING_NE || + op === Meta.GrabOp.RESIZING_NW; + } + + /** + * Removes all dividers from this container, reverting to equal space distribution + */ + removeAllDividers(): void { + Logger.log("Removing all dividers from container"); + this._tiledItems = this._tiledItems.filter(item => !Divider.isDivider(item)); + this.tileWindows(); + } + + /** + * Updates divider position during a live resize operation (or creates if doesn't exist) + * This is called repeatedly during resize for live feedback + */ + updateDividerDuringResize(item: WindowWrapper, resizeEdge: Meta.GrabOp, newRect: Rect): void { + const itemIndex = this._getIndexOfWindow(item.getWindowId()); + if (itemIndex === -1) { + return; + } + + // Determine if this is a valid resize for this container orientation + const isHorizontalResize = this._isHorizontalResizeOp(resizeEdge); + const isVerticalResize = this._isVerticalResizeOp(resizeEdge); + + if (this._orientation === Orientation.HORIZONTAL && !isHorizontalResize) { + return; + } + if (this._orientation === Orientation.VERTICAL && !isVerticalResize) { + return; + } + + // Determine which edge is being resized and find adjacent window + let adjacentIndex = -1; + let dividerPosition = 0; + + if (this._orientation === Orientation.HORIZONTAL) { + if (this._isEastResizeOp(resizeEdge)) { + adjacentIndex = itemIndex + 1; + const rightEdge = newRect.x + newRect.width; + dividerPosition = (rightEdge - this._workArea.x) / this._workArea.width; + } else if (this._isWestResizeOp(resizeEdge)) { + adjacentIndex = itemIndex - 1; + dividerPosition = (newRect.x - this._workArea.x) / this._workArea.width; + } + } else { + if (this._isSouthResizeOp(resizeEdge)) { + adjacentIndex = itemIndex + 1; + const bottomEdge = newRect.y + newRect.height; + dividerPosition = (bottomEdge - this._workArea.y) / this._workArea.height; + } else if (this._isNorthResizeOp(resizeEdge)) { + adjacentIndex = itemIndex - 1; + dividerPosition = (newRect.y - this._workArea.y) / this._workArea.height; + } + } + + // Make sure there's an adjacent item (window or container, not out of bounds) + if (adjacentIndex < 0 || adjacentIndex >= this._tiledItems.length) { + Logger.log(`No adjacent item at index ${adjacentIndex}`); + return; + } + + // Determine where divider should be inserted/updated + // For East/South resizes: divider between current (itemIndex) and next (itemIndex+1) + // For West/North resizes: divider between previous (itemIndex-1) and current (itemIndex) + let dividerIndex: number; + + if (this._orientation === Orientation.HORIZONTAL) { + dividerIndex = this._isEastResizeOp(resizeEdge) ? itemIndex + 1 : itemIndex; + } else { + dividerIndex = this._isSouthResizeOp(resizeEdge) ? itemIndex + 1 : itemIndex; + } + + // Check if there's already a divider at this position + if (dividerIndex < this._tiledItems.length && Divider.isDivider(this._tiledItems[dividerIndex])) { + // Update existing divider + const divider = this._tiledItems[dividerIndex] as Divider; + divider.setPosition(dividerPosition); + } else { + // Insert new divider + const newDivider = new Divider(dividerPosition, this._orientation); + this._tiledItems.splice(dividerIndex, 0, newDivider); + } + + // Retile to show live updates + this.tileWindows(); } diff --git a/src/wm/divider.ts b/src/wm/divider.ts new file mode 100644 index 0000000..c64b380 --- /dev/null +++ b/src/wm/divider.ts @@ -0,0 +1,45 @@ +import {Logger} from "../utils/logger.js"; + +enum Orientation { + HORIZONTAL = 0, + VERTICAL = 1, +} + +/** + * Represents a divider between windows in a container. + * Dividers track the split position as a ratio (0-1) of the container's size. + */ +export class Divider { + private _position: number; // Position as ratio 0-1 + private _orientation: Orientation; + + /** + * Creates a new divider + * @param position - Position as ratio between 0 and 1 + * @param orientation - Orientation of the divider (HORIZONTAL or VERTICAL) + */ + constructor(position: number, orientation: Orientation) { + this._position = Math.max(0, Math.min(1, position)); // Clamp between 0 and 1 + this._orientation = orientation; + } + + getPosition(): number { + return this._position; + } + + setPosition(position: number): void { + this._position = Math.max(0, Math.min(1, position)); // Clamp between 0 and 1 + Logger.log(`Divider position updated to ${this._position}`); + } + + getOrientation(): Orientation { + return this._orientation; + } + + /** + * Check if this is a divider instance + */ + static isDivider(item: any): item is Divider { + return item instanceof Divider; + } +} diff --git a/src/wm/monitor.ts b/src/wm/monitor.ts index 640f74d..d5f652a 100644 --- a/src/wm/monitor.ts +++ b/src/wm/monitor.ts @@ -83,4 +83,17 @@ export default class Monitor { this._workspaces[item.getWorkspace()].itemDragged(item, x, y); } + handleWindowResize(item: WindowWrapper, resizeEdge: Meta.GrabOp, newRect: Rect): void { + this._workspaces[item.getWorkspace()].handleWindowResize(item, resizeEdge, newRect); + } + + updateDividerDuringResize(item: WindowWrapper, resizeEdge: Meta.GrabOp, newRect: Rect): void { + this._workspaces[item.getWorkspace()].updateDividerDuringResize(item, resizeEdge, newRect); + } + + removeAllDividersFromActiveContainer(): void { + const activeWorkspace = global.workspace_manager.get_active_workspace(); + this._workspaces[activeWorkspace.index()].removeAllDividers(); + } + } \ No newline at end of file diff --git a/src/wm/window.ts b/src/wm/window.ts index 5fab360..5ab9f08 100644 --- a/src/wm/window.ts +++ b/src/wm/window.ts @@ -16,6 +16,7 @@ export class WindowWrapper { readonly _signals: number[] = []; _parent: WindowContainer | null = null; _dragging: boolean = false; + _resizing: boolean = false; constructor( window: Meta.Window, @@ -53,6 +54,13 @@ export class WindowWrapper { this._dragging = false; } + startResizing(): void { + this._resizing = true; + } + stopResizing(): void { + this._resizing = false; + } + // setParent(parent: WindowContainer): void { // this._parent = parent; // } @@ -124,8 +132,8 @@ export class WindowWrapper { safelyResizeWindow(rect: Rect, _retry: number = 2): void { // Keep minimal logging - if (this._dragging) { - Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED") + if (this._dragging && !this._resizing) { + // During drag operations (not resize), skip this entirely return; } // Logger.log("SAFELY RESIZE", rect.x, rect.y, rect.width, rect.height); @@ -142,6 +150,12 @@ export class WindowWrapper { this._window.move_frame(true, rect.x, rect.y); // Logger.info("RESIZING MOVING") this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height); + + // Don't retry during live resize operations - it causes spam and isn't needed + if (this._resizing) { + return; + } + 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); diff --git a/src/wm/windowManager.ts b/src/wm/windowManager.ts index d095593..2ea7fe7 100644 --- a/src/wm/windowManager.ts +++ b/src/wm/windowManager.ts @@ -44,6 +44,7 @@ export default class WindowManager implements IWindowManager { _grabbedWindowMonitor: number = _UNUSED_MONITOR_ID; _grabbedWindowId: number = _UNUSED_WINDOW_ID; + _grabbedOp: Meta.GrabOp | null = null; _changingGrabbedMonitor: boolean = false; _showingOverview: boolean = false; @@ -205,20 +206,54 @@ export default class WindowManager implements IWindowManager { Logger.log("Grab Op Start", op); Logger.log(display, window, op) Logger.log(window.get_monitor()) - this._getWrappedWindow(window)?.startDragging(); + + const winWrap = this._getWrappedWindow(window); + if (this._isResizeOp(op)) { + winWrap?.startResizing(); + } else { + winWrap?.startDragging(); + } + this._grabbedWindowMonitor = window.get_monitor(); this._grabbedWindowId = window.get_id(); + this._grabbedOp = op; } 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()) + + // Handle resize operations + if (this._isResizeOp(op)) { + const winWrap = this._getWrappedWindow(window); + if (winWrap && this._grabbedOp) { + const newRect = window.get_frame_rect(); + const monitorId = window.get_monitor(); + Logger.log(`Handling resize operation: ${op}, new rect:`, newRect); + this._monitors.get(monitorId)?.handleWindowResize(winWrap, this._grabbedOp, newRect); + winWrap.stopResizing(); + } + } else { + this._getWrappedWindow(window)?.stopDragging(); + } + this._grabbedWindowId = _UNUSED_WINDOW_ID; - this._getWrappedWindow(window)?.stopDragging(); + this._grabbedOp = null; this._tileMonitors(); Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor()); } + private _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; + } + _getWrappedWindow(window: Meta.Window): WindowWrapper | undefined { let wrapped = undefined; for (const monitor of this._monitors.values()) { @@ -258,6 +293,16 @@ export default class WindowManager implements IWindowManager { if (this._changingGrabbedMonitor) { return; } + + // Handle resize operations - update dividers in real-time + if (this._grabbedOp && this._isResizeOp(this._grabbedOp)) { + const window = winWrap.getWindow(); + const newRect = window.get_frame_rect(); + const monitorId = window.get_monitor(); + this._monitors.get(monitorId)?.updateDividerDuringResize(winWrap, this._grabbedOp, newRect); + return; + } + if (winWrap.getWindowId() === this._grabbedWindowId) { const [mouseX, mouseY, _] = global.get_pointer(); @@ -419,5 +464,26 @@ export default class WindowManager implements IWindowManager { return null; } + /** + * Removes all dividers from the container with the currently active window + */ + public removeAllDividersFromActiveContainer(): void { + const activeWindow = global.display.focus_window; + if (!activeWindow) { + Logger.log("No active window, cannot remove dividers"); + return; + } + + const monitorId = activeWindow.get_monitor(); + const monitor = this._monitors.get(monitorId); + + if (monitor) { + Logger.log(`Removing all dividers from monitor ${monitorId}`); + monitor.removeAllDividersFromActiveContainer(); + } else { + Logger.warn(`Monitor ${monitorId} not found`); + } + } + }