From 477b4e3d455a368495fbd3e5388ae7090a61631e Mon Sep 17 00:00:00 2001 From: ochi12 Date: Wed, 13 May 2026 22:07:36 +0800 Subject: [PATCH 01/30] Initial commit: Add symbolic icons --- .../media/tile-bottom-symbolic.svg | 23 +++++++++++++++ .../media/tile-horizontal-symbolic.svg | 20 +++++++++++++ .../media/tile-left-symbolic.svg | 23 +++++++++++++++ .../media/tile-q1-symbolic.svg | 29 +++++++++++++++++++ .../media/tile-q2-symbolic.svg | 29 +++++++++++++++++++ .../media/tile-q3-symbolic.svg | 29 +++++++++++++++++++ .../media/tile-q4-symbolic.svg | 29 +++++++++++++++++++ .../media/tile-quarter-symbolic.svg | 26 +++++++++++++++++ .../media/tile-right-symbolic.svg | 23 +++++++++++++++ .../media/tile-top-symbolic.svg | 23 +++++++++++++++ .../media/tile-vertical-symbolic.svg | 20 +++++++++++++ 11 files changed, 274 insertions(+) create mode 100644 tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg diff --git a/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg new file mode 100644 index 00000000..73590765 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg @@ -0,0 +1,23 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg new file mode 100644 index 00000000..9ee212e7 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg @@ -0,0 +1,20 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg new file mode 100644 index 00000000..01b8df7b --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg @@ -0,0 +1,23 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg new file mode 100644 index 00000000..a487bb80 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg @@ -0,0 +1,29 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg new file mode 100644 index 00000000..692704c1 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg @@ -0,0 +1,29 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg new file mode 100644 index 00000000..f507b11d --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg @@ -0,0 +1,29 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg new file mode 100644 index 00000000..2e8ef256 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg @@ -0,0 +1,29 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg new file mode 100644 index 00000000..d4fd1a10 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg @@ -0,0 +1,26 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg new file mode 100644 index 00000000..bea76035 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg @@ -0,0 +1,23 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg new file mode 100644 index 00000000..bd08659d --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg @@ -0,0 +1,23 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg new file mode 100644 index 00000000..5d80e794 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg @@ -0,0 +1,20 @@ + + + + From ac711b481052646ea583e51ba96161a4af738cf8 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Wed, 13 May 2026 22:15:27 +0800 Subject: [PATCH 02/30] Add css entires for theming the tile picker --- tiling-assistant@leleat-on-github/stylesheet.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tiling-assistant@leleat-on-github/stylesheet.css b/tiling-assistant@leleat-on-github/stylesheet.css index 27878117..4901b227 100644 --- a/tiling-assistant@leleat-on-github/stylesheet.css +++ b/tiling-assistant@leleat-on-github/stylesheet.css @@ -6,3 +6,18 @@ .layout-shortcut .boxed-list { box-shadow: none; } + +.tiling-menu-container { + padding: 0; + padding-top: 6px; +} + +.tiling-menu-container > StBoxLayout { + spacing: 15px; + padding: 15px; + border-radius: 20px; +} + +.tiling-menu-container > StBoxLayout > StIcon { + icon-size: 64px; +} From fb2f36ff5f26efe583db5f7c310ab746e9c27d5d Mon Sep 17 00:00:00 2001 From: ochi12 Date: Wed, 13 May 2026 22:16:20 +0800 Subject: [PATCH 03/30] Add File: layoutPicker.js this file exports LayoutPicker. A widget where user can hover-select a tile layout. Its current state is purely visual and is not yet integrated with moveHandler.js. --- .../src/extension/layoutPicker.js | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 tiling-assistant@leleat-on-github/src/extension/layoutPicker.js diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js new file mode 100644 index 00000000..f911ea5c --- /dev/null +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -0,0 +1,292 @@ +import { Clutter, GObject, St, Gio } from '../dependencies/gi.js'; +import { Main, Extension } from '../dependencies/shell.js'; + +const LayoutPickerVisibility = { + HIDDEN: 0, + PEAK: 1, + SHOWN: 2 +}; + +function iconPath(name) { + const path = Extension.lookupByURL(import.meta.url) + .dir + .get_child(`media/${name}-symbolic.svg`) + .get_path(); + + return path; +} + +const LayoutPickerTileType = Object.freeze({ + NONE: 0, + LEFT: 1, + RIGHT: 2, + TOP: 3, + BOTTOM: 4, + Q1: 5, + Q2: 6, + Q3: 7, + Q4: 8 +}); + +export const LayoutPicker = GObject.registerClass( +class LayoutPicker extends St.Bin { + _init() { + super._init({ + style_class: 'tiling-menu-container' + }); + + this._container = new St.BoxLayout({ + x_expand: true, + orientation: Clutter.Orientation.HORIZONTAL, + style_class: 'popup-menu-content' + }); + this._visibility = LayoutPickerVisibility.HIDDEN; + + this.set_child(this._container); + + this._addChrome(); + + this.reactive = true; + this.visible = false; + + this._vertIcon = this._createIcon(iconPath('tile-vertical')); + this._horIcon = this._createIcon(iconPath('tile-horizontal')); + this._quartIcon = this._createIcon(iconPath('tile-quarter')); + + this._container.add_child(this._vertIcon); + this._container.add_child(this._horIcon); + this._container.add_child(this._quartIcon); + + this._tileType = LayoutPickerTileType.NONE; + + + // Defer allocation until after the actor has been laid out + this._allocationId = this.connect('notify::allocation', () => { + this.disconnect(this._allocationId); + this._allocationId = null; + this._updateAllocation(); + }); + } + + get tileType() { + return this._tileType; + } + + _setVisibility(visibility) { + this._visibility = visibility; + this._updateAllocation(); + } + + onMoving(curX, curY) { + let [mx, my] = this.get_transformed_position(); + let [w, h] = this.get_size(); + + const monitorIndex = global.display.get_current_monitor(); + const monitorArea = Main.layoutManager.monitors[monitorIndex]; + const monitorY = monitorArea ? monitorArea.y : my; // fallback to my just in case monitorArea is null; + + // 1.75 acts as an early trigger when transitioning from PEAK to SHOWN. + // This ensures the picker becomes visible before the cursor reaches it. + // The multiplier is only applied for PEAK → SHOWN, not SHOWN → PEAK. + const triggerHeight = this._visibility === LayoutPickerVisibility.PEAK ? h * 1.75 : h; + + // using monitorY instead (my) as upper bound to compensate with chromes such us the top bar height + // placing cursor above my could cause glitch. (my) and monitorY will be same for other monitor anyways. + if ( + curY >= monitorY && + curY <= my + triggerHeight && + curX >= mx && + curX <= mx + w + ) + this._setVisibility(LayoutPickerVisibility.SHOWN); + else + this._setVisibility(LayoutPickerVisibility.PEAK); + + + this._updateLayoutPickerTileType(curX, curY); + } + + onMoveStarted() { + this._setVisibility(LayoutPickerVisibility.PEAK); + } + + onMoveFinished() { + this._setVisibility(LayoutPickerVisibility.HIDDEN); + } + + _setLayoutPickerIcon(hoveredType) { + this._clearIcons(); + switch (hoveredType) { + case LayoutPickerTileType.NONE: { + break; + } + case LayoutPickerTileType.LEFT: { + this._horIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-left')) + ); + break; + } + case LayoutPickerTileType.RIGHT: { + this._horIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-right')) + ); + break; + } + + case LayoutPickerTileType.TOP: { + this._vertIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-top')) + ); + break; + } + + case LayoutPickerTileType.BOTTOM: { + this._vertIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-bottom')) + ); + break; + } + + case LayoutPickerTileType.Q1: { + this._quartIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-q1')) + ); + break; + } + + case LayoutPickerTileType.Q2: { + this._quartIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-q2')) + ); + break; + } + + case LayoutPickerTileType.Q3: { + this._quartIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-q3')) + ); + break; + } + + case LayoutPickerTileType.Q4: { + this._quartIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-q4')) + ); + break; + } + } + } + + _createIcon(path) { + let gicon = Gio.FileIcon.new( + Gio.File.new_for_path(path) + ); + + return new St.Icon({ + gicon + }); + } + + _clearIcons() { + this._vertIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-vertical')) + ); + this._horIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-horizontal')) + ); + this._quartIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-quarter')) + ); + } + + _updateLayoutPickerTileType(curX, curY) { + let rect = icon => { + let [mx, my] = icon.get_transformed_position(); + let [mw, mh] = icon.get_size(); + return [mx, my, mw, mh]; + }; + let contains = (icon, x, y) => { + let [mx, my, mw, mh] = rect(icon); + return x >= mx && x <= mx + mw && + y >= my && y <= my + mh; + }; + + if (contains(this._horIcon, curX, curY)) { + let [mx, my_, mw, mh_] = rect(this._horIcon); + let mid = mx + (mw * 0.5); + + if (curX <= mid) + this._tileType = LayoutPickerTileType.LEFT; + else + this._tileType = LayoutPickerTileType.RIGHT; + } else if (contains(this._vertIcon, curX, curY)) { + let [mx_, my, mw_, mh] = rect(this._vertIcon); + let mid = my + (mh * 0.5); + + if (curY <= mid) + this._tileType = LayoutPickerTileType.TOP; + else + this._tileType = LayoutPickerTileType.BOTTOM; + } else if (contains(this._quartIcon, curX, curY)) { + let [mx, my, mw, mh] = rect(this._quartIcon); + let midX = mx + (mw * 0.5); + let midY = my + (mh * 0.5); + + if (curX >= midX && curY <= midY) + this._tileType = LayoutPickerTileType.Q1; + else if (curX < midX && curY <= midY) + this._tileType = LayoutPickerTileType.Q2; + else if (curX < midX && curY > midY) + this._tileType = LayoutPickerTileType.Q3; + else + this._tileType = LayoutPickerTileType.Q4; + } + else { + this._tileType = LayoutPickerTileType.NONE; + } + + this._setLayoutPickerIcon(this._tileType); + } + + _updateAllocation() { + const activeWs = global.workspace_manager.get_active_workspace(); + const monitorIndex = global.display.get_current_monitor(); + const workArea = activeWs.get_work_area_for_monitor(monitorIndex); + + if (!workArea) + return; + + this.x = (workArea.x + (workArea.width * 0.5)) - this.width * 0.5; + + this.visible = true; + + if (this._visibility === LayoutPickerVisibility.HIDDEN) { + this.y = workArea.y - this.height; + this.visible = false; + } else if (this._visibility === LayoutPickerVisibility.PEAK) { + this.y = workArea.y - this.height + this._container.get_theme_node().get_padding(St.Side.Bottom); + } else if (this._visibility === LayoutPickerVisibility.SHOWN) { + this.y = workArea.y; + } + + this.set_clip(0, Math.abs(this.y - workArea.y), workArea.width, workArea.height); + } + + _addChrome() { + Main.layoutManager.addChrome(this); + } + + _untrackChrome() { + Main.layoutManager.untrackChrome(this); + } + + destroy() { + this._untrackChrome(); + + this._container?.destroy(); + this._container = null; + + super.destroy(); + } +}); + From a7b6e805e95900c2a0d039f266dacf9331ba4060 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Wed, 13 May 2026 22:19:33 +0800 Subject: [PATCH 04/30] Add initial integration with moveHandler.js. layout picker is instantiated in TilingMoveHandler class. This commit does not fully integrate layout picker yet but window drag movement is already integrated in this commit. --- .../src/extension/moveHandler.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index d224221f..99dda335 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -5,6 +5,7 @@ import { WINDOW_ANIMATION_TIME } from '../dependencies/unexported/windowManager. import { MoveModes, Orientation, Settings } from '../common.js'; import { Rect, Util } from './utility.js'; import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import { LayoutPicker } from './layoutPicker.js'; const [MajorShellVersion] = Util.getShellVersion(); @@ -108,6 +109,8 @@ export default class TilingMoveHandler { this ); handleWindowActionKeyConflict(); + + this._layoutPicker = new LayoutPicker(); } destroy() { @@ -119,6 +122,8 @@ export default class TilingMoveHandler { this._tilePreview.destroy(); + this._layoutPicker.destroy(); + if (this._latestMonitorLockTimerId) { GLib.Source.remove(this._latestMonitorLockTimerId); this._latestMonitorLockTimerId = null; @@ -158,6 +163,8 @@ export default class TilingMoveHandler { if (window.is_skip_taskbar()) return; + this._layoutPicker.onMoveStarted(); + // Also work with a window, which was maximized by GNOME natively // because it may have been tiled with this extension before being // maximized so we need to restore its size to pre-tiling. @@ -256,6 +263,8 @@ export default class TilingMoveHandler { } _onMoveFinished(window) { + this._layoutPicker.onMoveFinished(); + try { // Ignore the expected error when the window was destroyed during the grab. try { @@ -334,6 +343,8 @@ export default class TilingMoveHandler { const [x, y] = this.getDragCoords(); const currPointerPos = { x, y }; + this._layoutPicker.onMoving(x, y); + if (lowPerfMode) { if (!this._isGrabOp) { this._movingTimerId = null; From bcace044084c06a9f465f028516192b323e5244f Mon Sep 17 00:00:00 2001 From: ochi12 Date: Thu, 14 May 2026 01:43:27 +0800 Subject: [PATCH 05/30] Integrate layout picker with tiling preview In this commit window snap now works from layout picker. This commit also includes blocking some snapping behavior when currently picking from the layout picker. --- .../src/extension/layoutPicker.js | 23 ++++++----- .../src/extension/moveHandler.js | 39 +++++++++++++------ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index f911ea5c..2a1242a1 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -16,7 +16,7 @@ function iconPath(name) { return path; } -const LayoutPickerTileType = Object.freeze({ +export const LayoutPickerTileType = Object.freeze({ NONE: 0, LEFT: 1, RIGHT: 2, @@ -66,18 +66,27 @@ class LayoutPicker extends St.Bin { this._allocationId = null; this._updateAllocation(); }); + + this._dragging = false; } get tileType() { return this._tileType; } + get picking() { + return this._tileType !== LayoutPickerTileType.NONE; + } + _setVisibility(visibility) { this._visibility = visibility; this._updateAllocation(); } onMoving(curX, curY) { + if (this._dragging === false) + return; + let [mx, my] = this.get_transformed_position(); let [w, h] = this.get_size(); @@ -92,12 +101,7 @@ class LayoutPicker extends St.Bin { // using monitorY instead (my) as upper bound to compensate with chromes such us the top bar height // placing cursor above my could cause glitch. (my) and monitorY will be same for other monitor anyways. - if ( - curY >= monitorY && - curY <= my + triggerHeight && - curX >= mx && - curX <= mx + w - ) + if (curY >= monitorY && curY <= my + triggerHeight && curX >= mx && curX <= mx + w) this._setVisibility(LayoutPickerVisibility.SHOWN); else this._setVisibility(LayoutPickerVisibility.PEAK); @@ -107,10 +111,12 @@ class LayoutPicker extends St.Bin { } onMoveStarted() { + this._dragging = true; this._setVisibility(LayoutPickerVisibility.PEAK); } onMoveFinished() { + this._dragging = false; this._setVisibility(LayoutPickerVisibility.HIDDEN); } @@ -207,8 +213,7 @@ class LayoutPicker extends St.Bin { }; let contains = (icon, x, y) => { let [mx, my, mw, mh] = rect(icon); - return x >= mx && x <= mx + mw && - y >= my && y <= my + mh; + return x >= mx && x <= mx + mw && y >= my && y <= my + mh; }; if (contains(this._horIcon, curX, curY)) { diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 99dda335..242eb8ca 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -5,7 +5,7 @@ import { WINDOW_ANIMATION_TIME } from '../dependencies/unexported/windowManager. import { MoveModes, Orientation, Settings } from '../common.js'; import { Rect, Util } from './utility.js'; import { TilingWindowManager as Twm } from './tilingWindowManager.js'; -import { LayoutPicker } from './layoutPicker.js'; +import { LayoutPicker, LayoutPickerTileType } from './layoutPicker.js'; const [MajorShellVersion] = Util.getShellVersion(); @@ -538,23 +538,38 @@ export default class TilingMoveHandler { const wRect = window.get_frame_rect(); const workArea = new Rect(window.get_work_area_for_monitor(this._monitorNr)); + const layoutPickerTileType = this._layoutPicker.tileType; + const vDetectionSize = Settings.getInt('vertical-preview-area'); - const pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize; - const pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize; + let pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize || + layoutPickerTileType === LayoutPickerTileType.TOP; + let pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize || + layoutPickerTileType === LayoutPickerTileType.BOTTOM; const hDetectionSize = Settings.getInt('horizontal-preview-area'); - const pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize; - const pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize; + let pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize || + layoutPickerTileType === LayoutPickerTileType.LEFT; + let pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize || + layoutPickerTileType === LayoutPickerTileType.RIGHT; // Also use window's pos for top and bottom area detection for quarters // because global.get_pointer's y isn't accurate (no idea why...) when // grabbing the titlebar & slowly going from the left/right sides to // the top/bottom corners. const titleBarGrabbed = this._lastPointerPos.y - wRect.y < 50; - const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y; - const windowAtBottomEdge = wRect.y >= workArea.y2 - 75; - const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge); - const tileTopRightQuarter = pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge); - const tileBottomLeftQuarter = pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge); - const tileBottomRightQuarter = pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge); + const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y && !this._layoutPicker.picking; + const windowAtBottomEdge = wRect.y >= workArea.y2 - 75 && !this._layoutPicker.picking; + const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge) || + layoutPickerTileType === LayoutPickerTileType.Q2; + const tileTopRightQuarter = pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge) || + layoutPickerTileType === LayoutPickerTileType.Q1; + const tileBottomLeftQuarter = pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge) || + layoutPickerTileType === LayoutPickerTileType.Q3; + const tileBottomRightQuarter = pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge) || + layoutPickerTileType === LayoutPickerTileType.Q4; + + // we cannot just for example do this: + // const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge) || layoutPickerTileType == LayoutPickerTileType.Q2; + // this can be buggy when both are true like triggering top right preview even when in LayoutPickerTileType.RIGHT + // so reassigning the value is a must if (tileTopLeftQuarter) { this._tileRect = Twm.getTileFor('tile-topleft-quarter', workArea, this._monitorNr); @@ -575,7 +590,7 @@ export default class TilingMoveHandler { const shouldMaximize = isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-landscape') || !isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-portrait'); - const tileRect = shouldMaximize + const tileRect = shouldMaximize && !this._layoutPicker.picking ? workArea : Twm.getTileFor('tile-top-half', workArea, this._monitorNr); const holdTileRect = shouldMaximize From 5bdffcff25ce7c351a84f2074d47271474c505dc Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sat, 16 May 2026 11:06:30 +0800 Subject: [PATCH 06/30] Add reveal animation this commit also includes problems fixed which are not shown when animation is not yet implemented, such as layoutPicker shown in random place initially and incorrect visibility trigger bounds. animation focused on translation_y instead of moving the object itself to a new position. --- .../src/extension/layoutPicker.js | 104 +++++++++++------- .../src/extension/moveHandler.js | 1 + 2 files changed, 66 insertions(+), 39 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 2a1242a1..5939e8fd 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -40,15 +40,14 @@ class LayoutPicker extends St.Bin { orientation: Clutter.Orientation.HORIZONTAL, style_class: 'popup-menu-content' }); + this._visibility = LayoutPickerVisibility.HIDDEN; + this._dragging = false; this.set_child(this._container); this._addChrome(); - this.reactive = true; - this.visible = false; - this._vertIcon = this._createIcon(iconPath('tile-vertical')); this._horIcon = this._createIcon(iconPath('tile-horizontal')); this._quartIcon = this._createIcon(iconPath('tile-quarter')); @@ -59,15 +58,17 @@ class LayoutPicker extends St.Bin { this._tileType = LayoutPickerTileType.NONE; - - // Defer allocation until after the actor has been laid out - this._allocationId = this.connect('notify::allocation', () => { - this.disconnect(this._allocationId); - this._allocationId = null; - this._updateAllocation(); - }); - - this._dragging = false; + // just in case extension is enabled and disable + this._updateAllocation(global.display.get_current_monitor()); + + this.connectObject('notify::translation-y', () => { + this.set_clip( + 0, + this.height - this.translation_y, + this.width, + this.height + ); + }, this); } get tileType() { @@ -79,34 +80,57 @@ class LayoutPicker extends St.Bin { } _setVisibility(visibility) { + if (this._visibility === visibility) + return; + this._visibility = visibility; - this._updateAllocation(); + + this.opacity = 255; + this.reactive = true; + + let positions = [ + 0, + this._container.get_theme_node().get_padding(St.Side.Bottom), + this.height + ]; + + this.remove_all_transitions(); + + this.ease({ + translation_y: positions[visibility], + duration: 250, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this.opacity = this._visibility !== LayoutPickerVisibility.HIDDEN ? 255 : 0; + this.reactive = this._visibility !== LayoutPickerVisibility.HIDDEN; + } + + }); + } + + onMonitorEntered(monitorIndex) { + this._updateAllocation(monitorIndex); } onMoving(curX, curY) { if (this._dragging === false) return; - let [mx, my] = this.get_transformed_position(); let [w, h] = this.get_size(); + let [mx, my_] = this.get_transformed_position(); const monitorIndex = global.display.get_current_monitor(); const monitorArea = Main.layoutManager.monitors[monitorIndex]; - const monitorY = monitorArea ? monitorArea.y : my; // fallback to my just in case monitorArea is null; - - // 1.75 acts as an early trigger when transitioning from PEAK to SHOWN. - // This ensures the picker becomes visible before the cursor reaches it. - // The multiplier is only applied for PEAK → SHOWN, not SHOWN → PEAK. - const triggerHeight = this._visibility === LayoutPickerVisibility.PEAK ? h * 1.75 : h; + const activeWs = global.workspace_manager.get_active_workspace(); + const workArea = activeWs.get_work_area_for_monitor(monitorIndex); - // using monitorY instead (my) as upper bound to compensate with chromes such us the top bar height - // placing cursor above my could cause glitch. (my) and monitorY will be same for other monitor anyways. - if (curY >= monitorY && curY <= my + triggerHeight && curX >= mx && curX <= mx + w) + // using monitorArea.y instead of workArea.y as upper bound to compensate with chromes such us the top bar height + // placing cursor above workArea.y causes visibility glitch. workArea.y and monitorArea.y will be same for other monitor anyways. + if (curY >= monitorArea.y && curY <= workArea.y + h && curX >= mx && curX <= mx + w) this._setVisibility(LayoutPickerVisibility.SHOWN); else this._setVisibility(LayoutPickerVisibility.PEAK); - this._updateLayoutPickerTileType(curX, curY); } @@ -253,32 +277,34 @@ class LayoutPicker extends St.Bin { this._setLayoutPickerIcon(this._tileType); } - _updateAllocation() { + _updateAllocation(monitorIndex) { const activeWs = global.workspace_manager.get_active_workspace(); - const monitorIndex = global.display.get_current_monitor(); const workArea = activeWs.get_work_area_for_monitor(monitorIndex); - if (!workArea) + if (workArea === null) return; - this.x = (workArea.x + (workArea.width * 0.5)) - this.width * 0.5; + const [, natWidth] = this.get_preferred_width(-1); + const [, natHeight] = this.get_preferred_height(-1); - this.visible = true; + this.set_position( + Math.round(workArea.x + (workArea.width - natWidth) / 2), + Math.round(workArea.y - natHeight) + ); - if (this._visibility === LayoutPickerVisibility.HIDDEN) { - this.y = workArea.y - this.height; - this.visible = false; - } else if (this._visibility === LayoutPickerVisibility.PEAK) { - this.y = workArea.y - this.height + this._container.get_theme_node().get_padding(St.Side.Bottom); - } else if (this._visibility === LayoutPickerVisibility.SHOWN) { - this.y = workArea.y; - } + this.remove_all_transitions(); - this.set_clip(0, Math.abs(this.y - workArea.y), workArea.width, workArea.height); + this._visibility = LayoutPickerVisibility.HIDDEN; + this.translation_y = 0; + this.opacity = 0; + this.reactive = false; } _addChrome() { - Main.layoutManager.addChrome(this); + Main.layoutManager.addChrome(this, { + affectsStruts: false, + trackFullscreen: false + }); } _untrackChrome() { diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 242eb8ca..30a3cbe3 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -150,6 +150,7 @@ export default class TilingMoveHandler { // Reset preview mode: // Currently only needed to grab the favorite layout for the new monitor. this._preparePreviewModeChange(this._currPreviewMode, window); + this._layoutPicker.onMonitorEntered(monitorNr); } getDragCoords() { From b5001aa8503551a126b33bd56c2f29cc7511f085 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sun, 17 May 2026 00:10:33 +0800 Subject: [PATCH 07/30] Update tile type after picker is fully shown --- .../src/extension/layoutPicker.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 5939e8fd..6e0495aa 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -103,6 +103,14 @@ class LayoutPicker extends St.Bin { onComplete: () => { this.opacity = this._visibility !== LayoutPickerVisibility.HIDDEN ? 255 : 0; this.reactive = this._visibility !== LayoutPickerVisibility.HIDDEN; + + // once picker is fully shown onMoving might not be able to update tile type + // for situations like no cursor update after picker is fully shown. + // this could causes incorrect tile type shown. + if (this._visibility === LayoutPickerVisibility.SHOWN) { + let [curX, curY] = global.get_pointer(); + this._updateLayoutPickerTileType(curX, curY); + } } }); From b6753088b030df7e8d9f9b2caab7fffd93f7f2e3 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sun, 17 May 2026 10:36:49 +0800 Subject: [PATCH 08/30] Increase top padding from 6px to 20px --- tiling-assistant@leleat-on-github/stylesheet.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiling-assistant@leleat-on-github/stylesheet.css b/tiling-assistant@leleat-on-github/stylesheet.css index 4901b227..aa567649 100644 --- a/tiling-assistant@leleat-on-github/stylesheet.css +++ b/tiling-assistant@leleat-on-github/stylesheet.css @@ -9,7 +9,7 @@ .tiling-menu-container { padding: 0; - padding-top: 6px; + padding-top: 20px; } .tiling-menu-container > StBoxLayout { From 52c96405caf0209b84d12ee1638bc63d1fe3a849 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sun, 17 May 2026 11:12:35 +0800 Subject: [PATCH 09/30] Add maximize picker tile --- .../media/tile-base-symbolic.svg | 16 ++++++++++++++++ .../media/tile-maximize-symbolic.svg | 19 +++++++++++++++++++ .../src/extension/layoutPicker.js | 17 ++++++++++++++++- .../src/extension/moveHandler.js | 7 ++++--- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg create mode 100644 tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg diff --git a/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg new file mode 100644 index 00000000..c91ecfba --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg @@ -0,0 +1,16 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg new file mode 100644 index 00000000..8f0226b2 --- /dev/null +++ b/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg @@ -0,0 +1,19 @@ + + + + diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 6e0495aa..53f8d055 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -25,7 +25,8 @@ export const LayoutPickerTileType = Object.freeze({ Q1: 5, Q2: 6, Q3: 7, - Q4: 8 + Q4: 8, + MAXIMIZE: 9 }); export const LayoutPicker = GObject.registerClass( @@ -51,10 +52,12 @@ class LayoutPicker extends St.Bin { this._vertIcon = this._createIcon(iconPath('tile-vertical')); this._horIcon = this._createIcon(iconPath('tile-horizontal')); this._quartIcon = this._createIcon(iconPath('tile-quarter')); + this._maxIcon = this._createIcon(iconPath('tile-base')); this._container.add_child(this._vertIcon); this._container.add_child(this._horIcon); this._container.add_child(this._quartIcon); + this._container.add_child(this._maxIcon); this._tileType = LayoutPickerTileType.NONE; @@ -212,6 +215,13 @@ class LayoutPicker extends St.Bin { ); break; } + + case LayoutPickerTileType.MAXIMIZE: { + this._maxIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-maximize')) + ); + break; + } } } @@ -235,6 +245,9 @@ class LayoutPicker extends St.Bin { this._quartIcon.gicon = Gio.FileIcon.new( Gio.File.new_for_path(iconPath('tile-quarter')) ); + this._maxIcon.gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath('tile-base')) + ); } _updateLayoutPickerTileType(curX, curY) { @@ -277,6 +290,8 @@ class LayoutPicker extends St.Bin { this._tileType = LayoutPickerTileType.Q3; else this._tileType = LayoutPickerTileType.Q4; + } else if (contains(this._maxIcon, curX, curY)) { + this._tileType = LayoutPickerTileType.MAXIMIZE; } else { this._tileType = LayoutPickerTileType.NONE; diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 30a3cbe3..b270f54f 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -543,7 +543,8 @@ export default class TilingMoveHandler { const vDetectionSize = Settings.getInt('vertical-preview-area'); let pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize || - layoutPickerTileType === LayoutPickerTileType.TOP; + layoutPickerTileType === LayoutPickerTileType.TOP || + layoutPickerTileType === LayoutPickerTileType.MAXIMIZE; let pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize || layoutPickerTileType === LayoutPickerTileType.BOTTOM; const hDetectionSize = Settings.getInt('horizontal-preview-area'); @@ -591,10 +592,10 @@ export default class TilingMoveHandler { const shouldMaximize = isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-landscape') || !isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-portrait'); - const tileRect = shouldMaximize && !this._layoutPicker.picking + const tileRect = shouldMaximize && !this._layoutPicker.picking || layoutPickerTileType === LayoutPickerTileType.MAXIMIZE ? workArea : Twm.getTileFor('tile-top-half', workArea, this._monitorNr); - const holdTileRect = shouldMaximize + const holdTileRect = shouldMaximize && !this._layoutPicker.picking || layoutPickerTileType === LayoutPickerTileType.TOP ? Twm.getTileFor('tile-top-half', workArea, this._monitorNr) : workArea; // Dont open preview / start new timer if preview was already one for the top From 8ce65d779263258d7376e4c3121ccfd55a3c2808 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sun, 17 May 2026 13:24:16 +0800 Subject: [PATCH 10/30] Fix shadow clipping --- .../src/extension/layoutPicker.js | 7 +++++-- tiling-assistant@leleat-on-github/stylesheet.css | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 53f8d055..1589dd87 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -91,9 +91,11 @@ class LayoutPicker extends St.Bin { this.opacity = 255; this.reactive = true; + let positions = [ 0, - this._container.get_theme_node().get_padding(St.Side.Bottom), + this._container.get_theme_node().get_padding(St.Side.Bottom) + + this.get_theme_node().get_padding(St.Side.BOTTOM), this.height ]; @@ -128,6 +130,7 @@ class LayoutPicker extends St.Bin { return; let [w, h] = this.get_size(); + let bottomPadding = this.get_theme_node().get_padding(St.Side.BOTTOM); let [mx, my_] = this.get_transformed_position(); const monitorIndex = global.display.get_current_monitor(); @@ -137,7 +140,7 @@ class LayoutPicker extends St.Bin { // using monitorArea.y instead of workArea.y as upper bound to compensate with chromes such us the top bar height // placing cursor above workArea.y causes visibility glitch. workArea.y and monitorArea.y will be same for other monitor anyways. - if (curY >= monitorArea.y && curY <= workArea.y + h && curX >= mx && curX <= mx + w) + if (curY >= monitorArea.y && curY <= workArea.y + h - bottomPadding && curX >= mx && curX <= mx + w) this._setVisibility(LayoutPickerVisibility.SHOWN); else this._setVisibility(LayoutPickerVisibility.PEAK); diff --git a/tiling-assistant@leleat-on-github/stylesheet.css b/tiling-assistant@leleat-on-github/stylesheet.css index aa567649..d500907f 100644 --- a/tiling-assistant@leleat-on-github/stylesheet.css +++ b/tiling-assistant@leleat-on-github/stylesheet.css @@ -8,8 +8,7 @@ } .tiling-menu-container { - padding: 0; - padding-top: 20px; + padding: 20px; } .tiling-menu-container > StBoxLayout { From d3da95694b4f650bff01fd5e4b10cdab8ecbe2f0 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sun, 17 May 2026 13:57:35 +0800 Subject: [PATCH 11/30] Ensure moveFinised function for layout picker is triggered --- tiling-assistant@leleat-on-github/src/extension/moveHandler.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index b270f54f..fc86a19a 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -180,6 +180,8 @@ export default class TilingMoveHandler { // Try to restore the window size if (window.tiledRect || this._wasMaximizedOnStart) { + this._layoutPicker.onMoveFinished(); + let counter = 0; this._restoreSizeTimerId && GLib.Source.remove(this._restoreSizeTimerId); this._restoreSizeTimerId = GLib.timeout_add(GLib.PRIORITY_HIGH_IDLE, 10, () => { From 288af0dcb82d8d5ad2de9f98a1a3a0b1af4d30be Mon Sep 17 00:00:00 2001 From: ochi12 Date: Sun, 17 May 2026 19:29:25 +0800 Subject: [PATCH 12/30] Redesign icons to fit vanilla gnome aesthetics --- .../media/tile-base-symbolic.svg | 6 +- .../media/tile-bottom-symbolic.svg | 21 +++-- .../media/tile-horizontal-symbolic.svg | 15 ++-- .../media/tile-left-symbolic.svg | 21 +++-- .../media/tile-maximize-symbolic.svg | 10 +-- .../media/tile-q1-symbolic.svg | 33 ++++--- .../media/tile-q2-symbolic.svg | 33 ++++--- .../media/tile-q3-symbolic.svg | 33 ++++--- .../media/tile-q4-symbolic.svg | 33 ++++--- .../media/tile-quarter-symbolic.svg | 27 +++--- .../media/tile-right-symbolic.svg | 21 +++-- .../media/tile-top-symbolic.svg | 21 +++-- .../media/tile-vertical-symbolic.svg | 15 ++-- .../src/extension/layoutPicker.js | 87 +++++++++---------- 14 files changed, 179 insertions(+), 197 deletions(-) diff --git a/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg index c91ecfba..126bc597 100644 --- a/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg @@ -11,6 +11,6 @@ xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + d="M 8,0 C 3.568,0 0,3.568 0,8 v 48 c 0,4.432 3.568,8 8,8 h 48 c 4.432,0 8,-3.568 8,-8 V 8 C 64,3.568 60.432,0 56,0 Z m 0,3 h 48 c 2.77,0 5,2.23 5,5 v 48 c 0,2.77 -2.23,5 -5,5 H 8 C 5.23,61 3,58.77 3,56 V 8 C 3,5.23 5.23,3 8,3 Z" + style="fill:#2e3436;fill-opacity:0.35;fill-rule:evenodd" + id="path5" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg index 73590765..67c176a7 100644 --- a/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg @@ -10,14 +10,13 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg index 9ee212e7..a5bead91 100644 --- a/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg @@ -10,11 +10,10 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg index 01b8df7b..b05a1efa 100644 --- a/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg @@ -10,14 +10,13 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg index 8f0226b2..0fdeecbf 100644 --- a/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg @@ -11,9 +11,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + d="M 8,5 C 6.3034055,5 5,6.3034055 5,8 v 48 c 0,1.696594 1.3034055,3 3,3 h 48 c 1.696594,0 3,-1.303406 3,-3 V 8 C 59,6.3034055 57.696594,5 56,5 Z" + style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#2e3436;fill-opacity:1;fill-rule:evenodd;enable-background:accumulate;stop-color:#000000;stop-opacity:1" + id="path9" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg index a487bb80..62bb30f8 100644 --- a/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg @@ -10,20 +10,19 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg index 692704c1..9e951c38 100644 --- a/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg @@ -10,20 +10,19 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg index f507b11d..492a17a3 100644 --- a/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg @@ -10,20 +10,19 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg index 2e8ef256..c38b541b 100644 --- a/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg @@ -10,20 +10,19 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg index d4fd1a10..c2e1e192 100644 --- a/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg @@ -10,17 +10,16 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg index bea76035..a333b058 100644 --- a/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg @@ -10,14 +10,13 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg index bd08659d..f6346e72 100644 --- a/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg @@ -10,14 +10,13 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg index 5d80e794..3ee05ea9 100644 --- a/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg +++ b/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg @@ -10,11 +10,10 @@ xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 1589dd87..c6fdcd96 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -49,10 +49,10 @@ class LayoutPicker extends St.Bin { this._addChrome(); - this._vertIcon = this._createIcon(iconPath('tile-vertical')); - this._horIcon = this._createIcon(iconPath('tile-horizontal')); - this._quartIcon = this._createIcon(iconPath('tile-quarter')); - this._maxIcon = this._createIcon(iconPath('tile-base')); + this._vertIcon = this._createIcon('tile-vertical'); + this._horIcon = this._createIcon('tile-horizontal'); + this._quartIcon = this._createIcon('tile-quarter'); + this._maxIcon = this._createIcon('tile-base'); this._container.add_child(this._vertIcon); this._container.add_child(this._horIcon); @@ -165,92 +165,85 @@ class LayoutPicker extends St.Bin { break; } case LayoutPickerTileType.LEFT: { - this._horIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-left')) - ); + this._updateIcon(this._horIcon, 'tile-left'); break; } case LayoutPickerTileType.RIGHT: { - this._horIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-right')) - ); + this._updateIcon(this._horIcon, 'tile-right'); break; } case LayoutPickerTileType.TOP: { - this._vertIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-top')) - ); + this._updateIcon(this._vertIcon, 'tile-top'); break; } case LayoutPickerTileType.BOTTOM: { - this._vertIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-bottom')) - ); + this._updateIcon(this._vertIcon, 'tile-bottom'); break; } case LayoutPickerTileType.Q1: { - this._quartIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-q1')) - ); + this._updateIcon(this._quartIcon, 'tile-q1'); break; } case LayoutPickerTileType.Q2: { - this._quartIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-q2')) - ); + this._updateIcon(this._quartIcon, 'tile-q2'); break; } case LayoutPickerTileType.Q3: { - this._quartIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-q3')) - ); + this._updateIcon(this._quartIcon, 'tile-q3'); break; } case LayoutPickerTileType.Q4: { - this._quartIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-q4')) - ); + this._updateIcon(this._quartIcon, 'tile-q4'); break; } case LayoutPickerTileType.MAXIMIZE: { - this._maxIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-maximize')) - ); + this._updateIcon(this._maxIcon, 'tile-maximize'); break; } } } - _createIcon(path) { - let gicon = Gio.FileIcon.new( - Gio.File.new_for_path(path) + _createIcon(name) { + let fallback_gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath(name)) ); + let gicon = new Gio.ThemedIcon({ + name: `${name}-symbolic` + }); + return new St.Icon({ - gicon + gicon, + fallback_gicon }); } - _clearIcons() { - this._vertIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-vertical')) - ); - this._horIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-horizontal')) - ); - this._quartIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-quarter')) - ); - this._maxIcon.gicon = Gio.FileIcon.new( - Gio.File.new_for_path(iconPath('tile-base')) + _updateIcon(icon, name) { + let fallback_gicon = Gio.FileIcon.new( + Gio.File.new_for_path(iconPath(name)) ); + + let gicon = new Gio.ThemedIcon({ + name: `${name}-symbolic` + }); + icon.set({ + gicon, + fallback_gicon + }); + } + + _clearIcons() { + this._updateIcon(this._vertIcon, 'tile-vertical'); + this._updateIcon(this._horIcon, 'tile-horizontal'); + this._updateIcon(this._quartIcon, 'tile-quarter'); + this._updateIcon(this._maxIcon, 'tile-base'); } _updateLayoutPickerTileType(curX, curY) { From 6f896364833a5495123b4d380f80bc302f162cd6 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Mon, 18 May 2026 00:27:48 +0800 Subject: [PATCH 13/30] Add additional signals that ensures layout picker correct position --- .../src/extension/layoutPicker.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index c6fdcd96..6cd99f50 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -61,6 +61,15 @@ class LayoutPicker extends St.Bin { this._tileType = LayoutPickerTileType.NONE; + // e.g ubuntu dock is enabled and or disabled + global.display.connectObject('workareas-changed', () => { + this._updateAllocation(global.display.get_current_monitor()); + }, this); + + Main.layoutManager.connectObject('monitors-changed', () => { + this._updateAllocation(global.display.get_current_monitor()); + }, this); + // just in case extension is enabled and disable this._updateAllocation(global.display.get_current_monitor()); @@ -333,6 +342,9 @@ class LayoutPicker extends St.Bin { destroy() { this._untrackChrome(); + global.display.disconnectObject(this); + Main.layoutManager.disconnectObject(this); + this._container?.destroy(); this._container = null; From 2ae689cb03d91e62866c38f8b5cc06aeca7cda65 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Mon, 18 May 2026 14:57:23 +0800 Subject: [PATCH 14/30] Compensate for left and right padding for triggering visibility --- .../src/extension/layoutPicker.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 6cd99f50..4c18930d 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -61,7 +61,7 @@ class LayoutPicker extends St.Bin { this._tileType = LayoutPickerTileType.NONE; - // e.g ubuntu dock is enabled and or disabled + // e.g a dock is enabled and or disabled global.display.connectObject('workareas-changed', () => { this._updateAllocation(global.display.get_current_monitor()); }, this); @@ -139,9 +139,13 @@ class LayoutPicker extends St.Bin { return; let [w, h] = this.get_size(); - let bottomPadding = this.get_theme_node().get_padding(St.Side.BOTTOM); let [mx, my_] = this.get_transformed_position(); + const themeNode = this.get_theme_node(); + let paddingLeft = themeNode.get_padding(St.Side.LEFT); + let paddingRight = themeNode.get_padding(St.Side.RIGHT); + let paddingBottom = this.get_theme_node().get_padding(St.Side.BOTTOM); + const monitorIndex = global.display.get_current_monitor(); const monitorArea = Main.layoutManager.monitors[monitorIndex]; const activeWs = global.workspace_manager.get_active_workspace(); @@ -149,7 +153,12 @@ class LayoutPicker extends St.Bin { // using monitorArea.y instead of workArea.y as upper bound to compensate with chromes such us the top bar height // placing cursor above workArea.y causes visibility glitch. workArea.y and monitorArea.y will be same for other monitor anyways. - if (curY >= monitorArea.y && curY <= workArea.y + h - bottomPadding && curX >= mx && curX <= mx + w) + if ( + curY >= monitorArea.y && + curY <= workArea.y + h - paddingBottom && + curX >= mx + paddingLeft && + curX <= mx + w - paddingRight + ) this._setVisibility(LayoutPickerVisibility.SHOWN); else this._setVisibility(LayoutPickerVisibility.PEAK); From 5f79c283f43fd9a67a879c7f479b3673363ec967 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Mon, 18 May 2026 17:16:03 +0800 Subject: [PATCH 15/30] Fix unreliable signal triggering update allocation Initially, the main signal triggering update allocation was window-entered-monitor. It was initially thought to be reliable, but this was overlooked. When windows are opened on the same monitor and one is dragged into another monitor, it actually updates the picker position to the other monitor. When I dragged the window left on the first monitor, the picker was still actually placed in the second monitor area. --- .../src/extension/layoutPicker.js | 17 +++++++++-------- .../src/extension/moveHandler.js | 1 - 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 4c18930d..4782e6b5 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -130,14 +130,12 @@ class LayoutPicker extends St.Bin { }); } - onMonitorEntered(monitorIndex) { - this._updateAllocation(monitorIndex); - } - onMoving(curX, curY) { if (this._dragging === false) return; + this._updateAllocation(global.display.get_current_monitor()); + let [w, h] = this.get_size(); let [mx, my_] = this.get_transformed_position(); @@ -324,10 +322,13 @@ class LayoutPicker extends St.Bin { const [, natWidth] = this.get_preferred_width(-1); const [, natHeight] = this.get_preferred_height(-1); - this.set_position( - Math.round(workArea.x + (workArea.width - natWidth) / 2), - Math.round(workArea.y - natHeight) - ); + const targetX = Math.round(workArea.x + (workArea.width - natWidth) / 2); + const targetY = Math.round(workArea.y - natHeight); + + if (targetX === this.x && targetY === this.y) + return; + + this.set_position(targetX, targetY); this.remove_all_transitions(); diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index fc86a19a..fdf38561 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -150,7 +150,6 @@ export default class TilingMoveHandler { // Reset preview mode: // Currently only needed to grab the favorite layout for the new monitor. this._preparePreviewModeChange(this._currPreviewMode, window); - this._layoutPicker.onMonitorEntered(monitorNr); } getDragCoords() { From 082349afe54a20d8f13fb321a1ddcd0736ec74ad Mon Sep 17 00:00:00 2001 From: ochi12 Date: Mon, 18 May 2026 23:06:01 +0800 Subject: [PATCH 16/30] Refactor: centralize layout picker icon configuration --- .../src/extension/layoutPicker.js | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 4782e6b5..f2c4690b 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -29,6 +29,46 @@ export const LayoutPickerTileType = Object.freeze({ MAXIMIZE: 9 }); +// omit '-symbolic.svg' as it is managed by iconPath function already +const ICONS = { + vertical: { + none: 'tile-vertical', + + states: { + [LayoutPickerTileType.TOP]: 'tile-top', + [LayoutPickerTileType.BOTTOM]: 'tile-bottom' + } + }, + + horizontal: { + none: 'tile-horizontal', + + states: { + [LayoutPickerTileType.LEFT]: 'tile-left', + [LayoutPickerTileType.RIGHT]: 'tile-right' + } + }, + + quarter: { + none: 'tile-quarter', + + states: { + [LayoutPickerTileType.Q1]: 'tile-q1', + [LayoutPickerTileType.Q2]: 'tile-q2', + [LayoutPickerTileType.Q3]: 'tile-q3', + [LayoutPickerTileType.Q4]: 'tile-q4' + } + }, + + maximize: { + none: 'tile-base', + + states: { + [LayoutPickerTileType.MAXIMIZE]: 'tile-maximize' + } + } +}; + export const LayoutPicker = GObject.registerClass( class LayoutPicker extends St.Bin { _init() { @@ -49,15 +89,17 @@ class LayoutPicker extends St.Bin { this._addChrome(); - this._vertIcon = this._createIcon('tile-vertical'); - this._horIcon = this._createIcon('tile-horizontal'); - this._quartIcon = this._createIcon('tile-quarter'); - this._maxIcon = this._createIcon('tile-base'); + this._icons = {}; + + for (const [group, config] of Object.entries(ICONS)) { + // start with icons that appear to be not hovered + const icon = this._createIcon(config.none); + + config.icon = icon; + this._icons[group] = icon; - this._container.add_child(this._vertIcon); - this._container.add_child(this._horIcon); - this._container.add_child(this._quartIcon); - this._container.add_child(this._maxIcon); + this._container.add_child(icon); + } this._tileType = LayoutPickerTileType.NONE; @@ -174,55 +216,17 @@ class LayoutPicker extends St.Bin { this._setVisibility(LayoutPickerVisibility.HIDDEN); } - _setLayoutPickerIcon(hoveredType) { + _setLayoutPickerIcon(tileType) { this._clearIcons(); - switch (hoveredType) { - case LayoutPickerTileType.NONE: { - break; - } - case LayoutPickerTileType.LEFT: { - this._updateIcon(this._horIcon, 'tile-left'); - break; - } - case LayoutPickerTileType.RIGHT: { - this._updateIcon(this._horIcon, 'tile-right'); - break; - } - case LayoutPickerTileType.TOP: { - this._updateIcon(this._vertIcon, 'tile-top'); - break; - } - - case LayoutPickerTileType.BOTTOM: { - this._updateIcon(this._vertIcon, 'tile-bottom'); - break; - } + for (const config of Object.values(ICONS)) { + const iconName = config.states[tileType]; - case LayoutPickerTileType.Q1: { - this._updateIcon(this._quartIcon, 'tile-q1'); - break; - } + if (!iconName) + continue; - case LayoutPickerTileType.Q2: { - this._updateIcon(this._quartIcon, 'tile-q2'); - break; - } - - case LayoutPickerTileType.Q3: { - this._updateIcon(this._quartIcon, 'tile-q3'); - break; - } - - case LayoutPickerTileType.Q4: { - this._updateIcon(this._quartIcon, 'tile-q4'); - break; - } - - case LayoutPickerTileType.MAXIMIZE: { - this._updateIcon(this._maxIcon, 'tile-maximize'); - break; - } + this._updateIcon(config.icon, iconName); + break; } } @@ -256,10 +260,8 @@ class LayoutPicker extends St.Bin { } _clearIcons() { - this._updateIcon(this._vertIcon, 'tile-vertical'); - this._updateIcon(this._horIcon, 'tile-horizontal'); - this._updateIcon(this._quartIcon, 'tile-quarter'); - this._updateIcon(this._maxIcon, 'tile-base'); + for (const config of Object.values(ICONS)) + this._updateIcon(config.icon, config.none); } _updateLayoutPickerTileType(curX, curY) { @@ -273,24 +275,24 @@ class LayoutPicker extends St.Bin { return x >= mx && x <= mx + mw && y >= my && y <= my + mh; }; - if (contains(this._horIcon, curX, curY)) { - let [mx, my_, mw, mh_] = rect(this._horIcon); + if (contains(this._icons.horizontal, curX, curY)) { + let [mx, my_, mw, mh_] = rect(this._icons.horizontal); let mid = mx + (mw * 0.5); if (curX <= mid) this._tileType = LayoutPickerTileType.LEFT; else this._tileType = LayoutPickerTileType.RIGHT; - } else if (contains(this._vertIcon, curX, curY)) { - let [mx_, my, mw_, mh] = rect(this._vertIcon); + } else if (contains(this._icons.vertical, curX, curY)) { + let [mx_, my, mw_, mh] = rect(this._icons.vertical); let mid = my + (mh * 0.5); if (curY <= mid) this._tileType = LayoutPickerTileType.TOP; else this._tileType = LayoutPickerTileType.BOTTOM; - } else if (contains(this._quartIcon, curX, curY)) { - let [mx, my, mw, mh] = rect(this._quartIcon); + } else if (contains(this._icons.quarter, curX, curY)) { + let [mx, my, mw, mh] = rect(this._icons.quarter); let midX = mx + (mw * 0.5); let midY = my + (mh * 0.5); @@ -302,7 +304,7 @@ class LayoutPicker extends St.Bin { this._tileType = LayoutPickerTileType.Q3; else this._tileType = LayoutPickerTileType.Q4; - } else if (contains(this._maxIcon, curX, curY)) { + } else if (contains(this._icons.maximize, curX, curY)) { this._tileType = LayoutPickerTileType.MAXIMIZE; } else { From 56aa71f52a5ab8c448097121bb6b8c3af57bf1e5 Mon Sep 17 00:00:00 2001 From: ochi12 Date: Mon, 18 May 2026 23:46:08 +0800 Subject: [PATCH 17/30] Refactor: simplify layout picker tile update logic --- .../src/extension/layoutPicker.js | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index f2c4690b..809ee1a4 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -266,51 +266,65 @@ class LayoutPicker extends St.Bin { _updateLayoutPickerTileType(curX, curY) { let rect = icon => { - let [mx, my] = icon.get_transformed_position(); - let [mw, mh] = icon.get_size(); - return [mx, my, mw, mh]; - }; - let contains = (icon, x, y) => { - let [mx, my, mw, mh] = rect(icon); - return x >= mx && x <= mx + mw && y >= my && y <= my + mh; + let [x, y] = icon.get_transformed_position(); + let [w, h] = icon.get_size(); + return { x, y, w, h }; }; - if (contains(this._icons.horizontal, curX, curY)) { - let [mx, my_, mw, mh_] = rect(this._icons.horizontal); - let mid = mx + (mw * 0.5); + let contains = ({ x, y, w, h }) => + curX >= x && + curX <= x + w && + curY >= y && + curY <= y + h; + const horizontal = rect(this._icons.horizontal); - if (curX <= mid) - this._tileType = LayoutPickerTileType.LEFT; - else - this._tileType = LayoutPickerTileType.RIGHT; - } else if (contains(this._icons.vertical, curX, curY)) { - let [mx_, my, mw_, mh] = rect(this._icons.vertical); - let mid = my + (mh * 0.5); + if (contains(horizontal)) { + const leftPortion = curX < horizontal.x + horizontal.w / 2; - if (curY <= mid) - this._tileType = LayoutPickerTileType.TOP; - else - this._tileType = LayoutPickerTileType.BOTTOM; - } else if (contains(this._icons.quarter, curX, curY)) { - let [mx, my, mw, mh] = rect(this._icons.quarter); - let midX = mx + (mw * 0.5); - let midY = my + (mh * 0.5); - - if (curX >= midX && curY <= midY) - this._tileType = LayoutPickerTileType.Q1; - else if (curX < midX && curY <= midY) - this._tileType = LayoutPickerTileType.Q2; - else if (curX < midX && curY > midY) - this._tileType = LayoutPickerTileType.Q3; - else - this._tileType = LayoutPickerTileType.Q4; - } else if (contains(this._icons.maximize, curX, curY)) { - this._tileType = LayoutPickerTileType.MAXIMIZE; + this._tileType = leftPortion + ? LayoutPickerTileType.LEFT + : LayoutPickerTileType.RIGHT; + + this._setLayoutPickerIcon(this._tileType); + return; } - else { - this._tileType = LayoutPickerTileType.NONE; + + const vertical = rect(this._icons.vertical); + + if (contains(vertical)) { + const topPortion = curY <= horizontal.y + horizontal.h / 2; + + this._tileType = topPortion + ? LayoutPickerTileType.TOP + : LayoutPickerTileType.BOTTOM; + + this._setLayoutPickerIcon(this._tileType); + return; } + const quarter = rect(this._icons.quarter); + + if (contains(quarter)) { + const leftPortion = curX < quarter.x + quarter.w / 2; + const topPortion = curY <= quarter.y + quarter.h / 2; + + if (topPortion) + { this._tileType = leftPortion + ? LayoutPickerTileType.Q2 + : LayoutPickerTileType.Q1; } + else + { this._tileType = leftPortion + ? LayoutPickerTileType.Q3 + : LayoutPickerTileType.Q4; } + + this._setLayoutPickerIcon(this._tileType); + return; + } + + this._tileType = contains(rect(this._icons.maximize)) + ? LayoutPickerTileType.MAXIMIZE + : LayoutPickerTileType.NONE; + this._setLayoutPickerIcon(this._tileType); } From 4504e7f2b04b00482ac9aba4bf15a32ea8b9a4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:06:44 +0000 Subject: [PATCH 18/30] layoutPicker: Reset the tile type when a move starts or ends The tile type is only recomputed while the picker classifies the pointer position, but it was never cleared once a move ended. So after picking a tile, the stale type was still reported as the current one (and its icon kept being shown) until the next move happened to reclassify it. Clear the type and restore the default icons both when a move starts and when it ends so the picker never exposes a stale selection. --- .../src/extension/layoutPicker.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 809ee1a4..c4015548 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -208,14 +208,21 @@ class LayoutPicker extends St.Bin { onMoveStarted() { this._dragging = true; + this._resetTileType(); this._setVisibility(LayoutPickerVisibility.PEAK); } onMoveFinished() { this._dragging = false; + this._resetTileType(); this._setVisibility(LayoutPickerVisibility.HIDDEN); } + _resetTileType() { + this._tileType = LayoutPickerTileType.NONE; + this._clearIcons(); + } + _setLayoutPickerIcon(tileType) { this._clearIcons(); From 4cd85bb138001ac1ef7f774d8913337b57e27dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:07:04 +0000 Subject: [PATCH 19/30] layoutPicker: Use the vertical icon geometry for its hit test The top/bottom classification of the vertical icon was based on the horizontal icon's position and size instead of the vertical icon's own. The two icons currently share the same row, so the outcome is the same, but that is accidental and would silently break as soon as the layout changes. Use the vertical icon's own geometry. --- tiling-assistant@leleat-on-github/src/extension/layoutPicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index c4015548..f183853a 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -299,7 +299,7 @@ class LayoutPicker extends St.Bin { const vertical = rect(this._icons.vertical); if (contains(vertical)) { - const topPortion = curY <= horizontal.y + horizontal.h / 2; + const topPortion = curY <= vertical.y + vertical.h / 2; this._tileType = topPortion ? LayoutPickerTileType.TOP From 863f632e483aff6d4a3721beb522f4c5daf5ea3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:07:30 +0000 Subject: [PATCH 20/30] moveHandler: Let an explicit picker selection win over edge detection The quarter detection was OR-ing the picker's tile type with the pointer/window edge detection, so a physically detected edge could override the type the user explicitly selected in the picker. For example hovering the picker's right-half icon near the top of the screen could still preview the top-right quarter. Give the picker's selection precedence and only fall back to the edge detection when nothing is being picked. While at it drop the comment that described this as an unresolved issue and declare the pointer edge variables as constants, since they are never reassigned. --- .../src/extension/moveHandler.js | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index fdf38561..c086f4c6 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -541,38 +541,37 @@ export default class TilingMoveHandler { const workArea = new Rect(window.get_work_area_for_monitor(this._monitorNr)); const layoutPickerTileType = this._layoutPicker.tileType; + const isPicking = layoutPickerTileType !== LayoutPickerTileType.NONE; const vDetectionSize = Settings.getInt('vertical-preview-area'); - let pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize || + const pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize || layoutPickerTileType === LayoutPickerTileType.TOP || layoutPickerTileType === LayoutPickerTileType.MAXIMIZE; - let pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize || + const pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize || layoutPickerTileType === LayoutPickerTileType.BOTTOM; const hDetectionSize = Settings.getInt('horizontal-preview-area'); - let pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize || + const pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize || layoutPickerTileType === LayoutPickerTileType.LEFT; - let pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize || + const pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize || layoutPickerTileType === LayoutPickerTileType.RIGHT; // Also use window's pos for top and bottom area detection for quarters // because global.get_pointer's y isn't accurate (no idea why...) when // grabbing the titlebar & slowly going from the left/right sides to // the top/bottom corners. const titleBarGrabbed = this._lastPointerPos.y - wRect.y < 50; - const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y && !this._layoutPicker.picking; - const windowAtBottomEdge = wRect.y >= workArea.y2 - 75 && !this._layoutPicker.picking; - const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge) || - layoutPickerTileType === LayoutPickerTileType.Q2; - const tileTopRightQuarter = pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge) || - layoutPickerTileType === LayoutPickerTileType.Q1; - const tileBottomLeftQuarter = pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge) || - layoutPickerTileType === LayoutPickerTileType.Q3; - const tileBottomRightQuarter = pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge) || - layoutPickerTileType === LayoutPickerTileType.Q4; - - // we cannot just for example do this: - // const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge) || layoutPickerTileType == LayoutPickerTileType.Q2; - // this can be buggy when both are true like triggering top right preview even when in LayoutPickerTileType.RIGHT - // so reassigning the value is a must + const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y; + const windowAtBottomEdge = wRect.y >= workArea.y2 - 75; + // An explicit picker selection always takes precedence over the edge + // detection, otherwise a physically detected edge could override it + // (e.g. show a quarter while the picker selected a half). + const tileTopLeftQuarter = layoutPickerTileType === LayoutPickerTileType.Q2 || + !isPicking && pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge); + const tileTopRightQuarter = layoutPickerTileType === LayoutPickerTileType.Q1 || + !isPicking && pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge); + const tileBottomLeftQuarter = layoutPickerTileType === LayoutPickerTileType.Q3 || + !isPicking && pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge); + const tileBottomRightQuarter = layoutPickerTileType === LayoutPickerTileType.Q4 || + !isPicking && pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge); if (tileTopLeftQuarter) { this._tileRect = Twm.getTileFor('tile-topleft-quarter', workArea, this._monitorNr); @@ -593,10 +592,10 @@ export default class TilingMoveHandler { const shouldMaximize = isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-landscape') || !isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-portrait'); - const tileRect = shouldMaximize && !this._layoutPicker.picking || layoutPickerTileType === LayoutPickerTileType.MAXIMIZE + const tileRect = shouldMaximize && !isPicking || layoutPickerTileType === LayoutPickerTileType.MAXIMIZE ? workArea : Twm.getTileFor('tile-top-half', workArea, this._monitorNr); - const holdTileRect = shouldMaximize && !this._layoutPicker.picking || layoutPickerTileType === LayoutPickerTileType.TOP + const holdTileRect = shouldMaximize && !isPicking || layoutPickerTileType === LayoutPickerTileType.TOP ? Twm.getTileFor('tile-top-half', workArea, this._monitorNr) : workArea; // Dont open preview / start new timer if preview was already one for the top From a8c737c996bfc74f866acd33d4fc082e7c1b1ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:07:52 +0000 Subject: [PATCH 21/30] layoutPicker: Follow the dragged monitor, not the pointer one The picker positioned itself on the monitor under the pointer, while the move handler can deliberately keep tiling on the previous monitor for a short grace period when the pointer crosses a monitor edge. On multi-monitor setups this placed the picker on a different monitor than the one the tile preview was being computed for. Let the caller pass the monitor the move is being handled for, falling back to the pointer monitor when it is not given. --- .../src/extension/layoutPicker.js | 5 ++--- .../src/extension/moveHandler.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index f183853a..17de6d5e 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -172,11 +172,11 @@ class LayoutPicker extends St.Bin { }); } - onMoving(curX, curY) { + onMoving(curX, curY, monitorIndex = global.display.get_current_monitor()) { if (this._dragging === false) return; - this._updateAllocation(global.display.get_current_monitor()); + this._updateAllocation(monitorIndex); let [w, h] = this.get_size(); let [mx, my_] = this.get_transformed_position(); @@ -186,7 +186,6 @@ class LayoutPicker extends St.Bin { let paddingRight = themeNode.get_padding(St.Side.RIGHT); let paddingBottom = this.get_theme_node().get_padding(St.Side.BOTTOM); - const monitorIndex = global.display.get_current_monitor(); const monitorArea = Main.layoutManager.monitors[monitorIndex]; const activeWs = global.workspace_manager.get_active_workspace(); const workArea = activeWs.get_work_area_for_monitor(monitorIndex); diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index c086f4c6..484daf56 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -345,7 +345,7 @@ export default class TilingMoveHandler { const [x, y] = this.getDragCoords(); const currPointerPos = { x, y }; - this._layoutPicker.onMoving(x, y); + this._layoutPicker.onMoving(x, y, this._monitorNr); if (lowPerfMode) { if (!this._isGrabOp) { From 1a372de9ec03895f047695590b981586a303847a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:08:04 +0000 Subject: [PATCH 22/30] moveHandler: Don't show the picker on keyboard-driven moves The picker is meant to be driven by the pointer, but it was also shown for Meta.GrabOp.KEYBOARD_MOVING, where the pointer is unrelated to the window being moved. Skip it for those grabs. --- .../src/extension/moveHandler.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 484daf56..0e92a47a 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -163,7 +163,10 @@ export default class TilingMoveHandler { if (window.is_skip_taskbar()) return; - this._layoutPicker.onMoveStarted(); + // The picker is driven by the pointer, so it is useless for a + // keyboard-driven move. + if (grabOp !== Meta.GrabOp.KEYBOARD_MOVING) + this._layoutPicker.onMoveStarted(); // Also work with a window, which was maximized by GNOME natively // because it may have been tiled with this extension before being From ad94cf0b7a2dad2ff27447e5d9727be42b3b9ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:08:17 +0000 Subject: [PATCH 23/30] layoutPicker: Only classify the tile type while the picker is shown The pointer position was classified on every move, even while the picker was merely peeking or hidden. During the reveal animation the icons can already overlap the pointer, so a tile type could be reported before the picker was actually usable. Only classify once the picker is fully shown and clear the selection otherwise. --- .../src/extension/layoutPicker.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 17de6d5e..a694ff86 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -202,7 +202,10 @@ class LayoutPicker extends St.Bin { else this._setVisibility(LayoutPickerVisibility.PEAK); - this._updateLayoutPickerTileType(curX, curY); + if (this._visibility === LayoutPickerVisibility.SHOWN) + this._updateLayoutPickerTileType(curX, curY); + else + this._resetTileType(); } onMoveStarted() { From 4d10d3fead38e0f82df6cc6483647be127463fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:08:32 +0000 Subject: [PATCH 24/30] layoutPicker: Let the actor destroy its own children Destroying the container manually before calling super.destroy() is redundant, since St.Bin already destroys its child as part of the actor destruction. Drop it and follow what the rest of the extension does. --- .../src/extension/layoutPicker.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index a694ff86..d1f981c0 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -380,9 +380,6 @@ class LayoutPicker extends St.Bin { global.display.disconnectObject(this); Main.layoutManager.disconnectObject(this); - this._container?.destroy(); - this._container = null; - super.destroy(); } }); From cea8d70a76ab7e55b807b8cee7a3d61904673ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:11:28 +0000 Subject: [PATCH 25/30] layoutPicker: Add a setting to enable or disable the picker The picker changes the behavior of every window drag, so give users a way to turn it off. Default it to enabled to preserve the current behavior and expose it as a switch in the 'Other' group of the General preferences. --- tiling-assistant@leleat-on-github/prefs.js | 1 + .../org.gnome.shell.extensions.tiling-assistant.gschema.xml | 3 +++ .../src/extension/moveHandler.js | 3 ++- tiling-assistant@leleat-on-github/src/ui/prefs.ui | 6 ++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tiling-assistant@leleat-on-github/prefs.js b/tiling-assistant@leleat-on-github/prefs.js index 2769cccc..5b63d72d 100644 --- a/tiling-assistant@leleat-on-github/prefs.js +++ b/tiling-assistant@leleat-on-github/prefs.js @@ -74,6 +74,7 @@ export default class Prefs extends ExtensionPreferences { 'enable-advanced-experimental-features', 'disable-tile-groups', 'low-performance-move-mode', + 'enable-layout-picker', 'monitor-switch-grace-period', 'adapt-edge-tiling-to-favorite-layout', 'enable-tile-animations', diff --git a/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml b/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml index 88c07c63..38f477ff 100644 --- a/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml +++ b/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml @@ -239,6 +239,9 @@ false + + true + false diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 0e92a47a..3b083c82 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -165,7 +165,8 @@ export default class TilingMoveHandler { // The picker is driven by the pointer, so it is useless for a // keyboard-driven move. - if (grabOp !== Meta.GrabOp.KEYBOARD_MOVING) + if (grabOp !== Meta.GrabOp.KEYBOARD_MOVING && + Settings.getBoolean('enable-layout-picker')) this._layoutPicker.onMoveStarted(); // Also work with a window, which was maximized by GNOME natively diff --git a/tiling-assistant@leleat-on-github/src/ui/prefs.ui b/tiling-assistant@leleat-on-github/src/ui/prefs.ui index a9b5a6cd..b23d92b2 100644 --- a/tiling-assistant@leleat-on-github/src/ui/prefs.ui +++ b/tiling-assistant@leleat-on-github/src/ui/prefs.ui @@ -500,6 +500,12 @@ bind-flags="sync-create"/> + + + Show Layout Picker + When moving a window, reveal a menu at the top of the screen to pick a tile (halves, quarters or maximize) with the pointer. + + Adapt 'Edge Tiling' to your Favorite Layout From 7a578ae90fa791b43a0ea008acae3a245c482395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 01:13:55 +0000 Subject: [PATCH 26/30] layoutPicker: Let the picker manage its own hiding The move handler had to remember to call onMoveFinished() from every code path that can end a move. Have the picker hide itself when the grab ends instead, and turn the tile type into a regular GObject property that the move handler keeps reading directly. This removes the duplicated lifecycle bookkeeping and the custom getter. The restore path still hides the picker explicitly, since the grab is not released while the tiled window's size is being restored. --- .../src/extension/layoutPicker.js | 47 ++++++++++--------- .../src/extension/moveHandler.js | 2 - 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index d1f981c0..00afe74a 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -69,8 +69,17 @@ const ICONS = { } }; -export const LayoutPicker = GObject.registerClass( -class LayoutPicker extends St.Bin { +export const LayoutPicker = GObject.registerClass({ + Properties: { + 'tile-type': GObject.ParamSpec.int( + 'tile-type', 'tile-type', 'tile-type', + GObject.ParamFlags.READWRITE, + LayoutPickerTileType.NONE, + LayoutPickerTileType.MAXIMIZE, + LayoutPickerTileType.NONE + ) + } +}, class LayoutPicker extends St.Bin { _init() { super._init({ style_class: 'tiling-menu-container' @@ -101,8 +110,6 @@ class LayoutPicker extends St.Bin { this._container.add_child(icon); } - this._tileType = LayoutPickerTileType.NONE; - // e.g a dock is enabled and or disabled global.display.connectObject('workareas-changed', () => { this._updateAllocation(global.display.get_current_monitor()); @@ -112,6 +119,10 @@ class LayoutPicker extends St.Bin { this._updateAllocation(global.display.get_current_monitor()); }, this); + // The picker only needs to know when the move it reacts to is over, + // so it can hide itself without the move handler having to track it. + global.display.connectObject('grab-op-end', () => this.onMoveFinished(), this); + // just in case extension is enabled and disable this._updateAllocation(global.display.get_current_monitor()); @@ -125,14 +136,6 @@ class LayoutPicker extends St.Bin { }, this); } - get tileType() { - return this._tileType; - } - - get picking() { - return this._tileType !== LayoutPickerTileType.NONE; - } - _setVisibility(visibility) { if (this._visibility === visibility) return; @@ -221,7 +224,7 @@ class LayoutPicker extends St.Bin { } _resetTileType() { - this._tileType = LayoutPickerTileType.NONE; + this.tileType = LayoutPickerTileType.NONE; this._clearIcons(); } @@ -290,11 +293,11 @@ class LayoutPicker extends St.Bin { if (contains(horizontal)) { const leftPortion = curX < horizontal.x + horizontal.w / 2; - this._tileType = leftPortion + this.tileType = leftPortion ? LayoutPickerTileType.LEFT : LayoutPickerTileType.RIGHT; - this._setLayoutPickerIcon(this._tileType); + this._setLayoutPickerIcon(this.tileType); return; } @@ -303,11 +306,11 @@ class LayoutPicker extends St.Bin { if (contains(vertical)) { const topPortion = curY <= vertical.y + vertical.h / 2; - this._tileType = topPortion + this.tileType = topPortion ? LayoutPickerTileType.TOP : LayoutPickerTileType.BOTTOM; - this._setLayoutPickerIcon(this._tileType); + this._setLayoutPickerIcon(this.tileType); return; } @@ -318,23 +321,23 @@ class LayoutPicker extends St.Bin { const topPortion = curY <= quarter.y + quarter.h / 2; if (topPortion) - { this._tileType = leftPortion + { this.tileType = leftPortion ? LayoutPickerTileType.Q2 : LayoutPickerTileType.Q1; } else - { this._tileType = leftPortion + { this.tileType = leftPortion ? LayoutPickerTileType.Q3 : LayoutPickerTileType.Q4; } - this._setLayoutPickerIcon(this._tileType); + this._setLayoutPickerIcon(this.tileType); return; } - this._tileType = contains(rect(this._icons.maximize)) + this.tileType = contains(rect(this._icons.maximize)) ? LayoutPickerTileType.MAXIMIZE : LayoutPickerTileType.NONE; - this._setLayoutPickerIcon(this._tileType); + this._setLayoutPickerIcon(this.tileType); } _updateAllocation(monitorIndex) { diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 3b083c82..92a9fb8e 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -269,8 +269,6 @@ export default class TilingMoveHandler { } _onMoveFinished(window) { - this._layoutPicker.onMoveFinished(); - try { // Ignore the expected error when the window was destroyed during the grab. try { From bccec71fc2c14952e41cf18de8e70ded36d50de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 02:14:15 +0000 Subject: [PATCH 27/30] layoutPicker: Move the enable setting to its own preferences group The picker is going to grow more options, so give it a dedicated group instead of leaving the switch in the 'Other' section, and place it first in the General preferences. --- .../src/ui/prefs.ui | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/ui/prefs.ui b/tiling-assistant@leleat-on-github/src/ui/prefs.ui index b23d92b2..627afa2b 100644 --- a/tiling-assistant@leleat-on-github/src/ui/prefs.ui +++ b/tiling-assistant@leleat-on-github/src/ui/prefs.ui @@ -40,6 +40,20 @@ General view-app-grid-symbolic + + + + + + Layout Picker + + + Show Layout Picker + When moving a window, reveal a menu at the top of the screen to pick a tile (halves, quarters or maximize) with the pointer. + + + + @@ -500,12 +514,6 @@ bind-flags="sync-create"/> - - - Show Layout Picker - When moving a window, reveal a menu at the top of the screen to pick a tile (halves, quarters or maximize) with the pointer. - - Adapt 'Edge Tiling' to your Favorite Layout From 4c6ab2d9d2286dfbe4e993344433e8f0aa810591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 03:38:51 +0000 Subject: [PATCH 28/30] moveHandler: Don't toggle the top edge tile when the picker is enabled Keeping the pointer at the top edge made the preview toggle between maximize and the top-half tile after a short timeout. Now that the layout picker can select the top half explicitly, just propose maximize when the picker is enabled and let the user pick the top half from the picker. The toggle and the hold-maximize-inverse settings are kept when the picker is disabled. --- .../src/extension/moveHandler.js | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index 92a9fb8e..56cd4b8a 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -131,7 +131,7 @@ export default class TilingMoveHandler { if (this._latestPreviewTimerId) { GLib.Source.remove(this._latestPreviewTimerId); - this._latestPreviewTimerId = null; + this._latestPreviewTimerId = 0; } if (this._restoreSizeTimerId) { @@ -594,10 +594,15 @@ export default class TilingMoveHandler { const shouldMaximize = isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-landscape') || !isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-portrait'); - const tileRect = shouldMaximize && !isPicking || layoutPickerTileType === LayoutPickerTileType.MAXIMIZE + const onlyMaximize = !isPicking && + Settings.getBoolean('enable-layout-picker'); + const tileRect = onlyMaximize || shouldMaximize && !isPicking || + layoutPickerTileType === LayoutPickerTileType.MAXIMIZE ? workArea : Twm.getTileFor('tile-top-half', workArea, this._monitorNr); - const holdTileRect = shouldMaximize && !isPicking || layoutPickerTileType === LayoutPickerTileType.TOP + const holdTileRect = !onlyMaximize && + (shouldMaximize && !isPicking || + layoutPickerTileType === LayoutPickerTileType.TOP) ? Twm.getTileFor('tile-top-half', workArea, this._monitorNr) : workArea; // Dont open preview / start new timer if preview was already one for the top @@ -609,6 +614,14 @@ export default class TilingMoveHandler { this._tileRect = tileRect; this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + if (tileRect.equal(holdTileRect)) { + if (this._latestPreviewTimerId) { + GLib.Source.remove(this._latestPreviewTimerId); + this._latestPreviewTimerId = 0; + } + return; + } + let timerId = 0; this._latestPreviewTimerId && GLib.Source.remove(this._latestPreviewTimerId); this._latestPreviewTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, @@ -622,7 +635,7 @@ export default class TilingMoveHandler { this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); } - this._latestPreviewTimerId = null; + this._latestPreviewTimerId = 0; return GLib.SOURCE_REMOVE; }); timerId = this._latestPreviewTimerId; From 322952082ccd8d5314b3f9145c295568ad8407da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 03:57:43 +0000 Subject: [PATCH 29/30] layoutPicker: Stop handling monitor changes when the shell shuts down GNOME Shell doesn't disable extensions when shutting down, so the monitors-changed and workareas-changed handlers kept running while the monitors are being torn down, eventually reading a monitor that is already gone. Disconnect them as soon as the shell starts shutting down. --- .../src/extension/layoutPicker.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 00afe74a..456cefd7 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -119,6 +119,13 @@ export const LayoutPicker = GObject.registerClass({ this._updateAllocation(global.display.get_current_monitor()); }, this); + // GNOME Shell doesn't disable extensions when shutting down, so stop + // reacting to monitor and work-area changes as soon as it shuts down. + global.connectObject('shutdown', () => { + global.display.disconnectObject(this); + Main.layoutManager.disconnectObject(this); + }, this); + // The picker only needs to know when the move it reacts to is over, // so it can hide itself without the move handler having to track it. global.display.connectObject('grab-op-end', () => this.onMoveFinished(), this); From 607fded4c7d2a611f05ec7e4e033ec1b86e16e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Sun, 20 Sep 2026 03:58:46 +0000 Subject: [PATCH 30/30] layoutPicker: Don't use a monitor that is already gone monitors-changed can be emitted after the last monitor was removed while get_current_monitor() still reports it, e.g. when shutting down. Passing that number to get_work_area_for_monitor() trips a Mutter assertion, so bail out early when the monitor no longer exists. --- .../src/extension/layoutPicker.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js index 456cefd7..1ec4a072 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js @@ -348,6 +348,12 @@ export const LayoutPicker = GObject.registerClass({ } _updateAllocation(monitorIndex) { + // The current monitor can already be gone (e.g. when the last monitor + // is removed during shutdown), so make sure it still exists before + // asking Mutter for its work area. + if (!Main.layoutManager.monitors[monitorIndex]) + return; + const activeWs = global.workspace_manager.get_active_workspace(); const workArea = activeWs.get_work_area_for_monitor(monitorIndex);