From 6216d740e799005b0fb4e9dea082e3d89291a97f Mon Sep 17 00:00:00 2001 From: thameem-abbas Date: Tue, 21 Apr 2026 15:28:18 -0400 Subject: [PATCH] feat: filter layouts by minimum monitor resolution Auto-hide layouts on monitors too small to render their tiles usably. Each tile's pixel width is checked against a configurable threshold (default 300px). Layouts can also set explicit minimumMonitorResolution. - Add MinimumResolution interface and optional field to Layout class - Add minimum-tile-width-threshold GSettings key and prefs spin button - Add centralized filtering utility (layoutFiltering.ts) - Add GlobalState.getLayoutsForMonitor() for per-monitor filtered lists - Update indicator menu, layout switcher, snap assistant to use filtered layouts - Validate selected layouts against monitor suitability on monitor change - Fix npm install: add overrides for unpublished @girs/mtk-17@rc.4 Co-Authored-By: Claude Opus 4.6 --- package.json | 7 +++ ...e.shell.extensions.tilingshell.gschema.xml | 5 ++ src/components/layout/Layout.ts | 13 ++++- .../layoutSwitcher/layoutSwitcher.ts | 15 ++++-- src/components/snapassist/snapAssist.ts | 10 +++- src/indicator/defaultMenu.ts | 21 ++++++-- src/indicator/indicator.ts | 3 ++ src/prefs.ts | 11 +++++ src/settings/settings.ts | 9 ++++ src/utils/globalState.ts | 48 ++++++++++++++++--- src/utils/layoutFiltering.ts | 43 +++++++++++++++++ 11 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 src/utils/layoutFiltering.ts diff --git a/package.json b/package.json index 9e6cbc5f..b5ce8025 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,12 @@ }, "dependencies": { "@girs/gnome-shell": "^49.1.0" + }, + "overrides": { + "@girs/mtk-17": "17.0.0-4.0.0-rc.3", + "@girs/st-17": "17.0.0-4.0.0-rc.3", + "@girs/meta-17": "17.0.0-4.0.0-rc.3", + "@girs/clutter-17": "17.0.0-4.0.0-rc.3", + "@girs/shell-17": "17.0.0-4.0.0-rc.3" } } diff --git a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml index 67fe8399..1928423a 100644 --- a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml +++ b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml @@ -207,6 +207,11 @@ Edge tiling behavior Controls how windows snap to screen edges:\n'default' (corners snap to quarters, edges to halves),\n'adaptive' (snap to layout tiles by column),\n'granular' (snap to exact tile under cursor). + + 300 + Minimum tile width threshold + Minimum width in pixels each tile must have for a layout to be usable on a monitor. + diff --git a/src/components/layout/Layout.ts b/src/components/layout/Layout.ts index 29b90d63..dfda7448 100644 --- a/src/components/layout/Layout.ts +++ b/src/components/layout/Layout.ts @@ -1,11 +1,22 @@ import Tile from './Tile'; +export interface MinimumResolution { + width: number; + height: number; +} + export default class Layout { id: string; tiles: Tile[]; + minimumMonitorResolution?: MinimumResolution; - constructor(tiles: Tile[], id: string) { + constructor( + tiles: Tile[], + id: string, + minimumMonitorResolution?: MinimumResolution, + ) { this.tiles = tiles; this.id = id; + this.minimumMonitorResolution = minimumMonitorResolution; } } diff --git a/src/components/layoutSwitcher/layoutSwitcher.ts b/src/components/layoutSwitcher/layoutSwitcher.ts index ed53a1d9..f33bae19 100644 --- a/src/components/layoutSwitcher/layoutSwitcher.ts +++ b/src/components/layoutSwitcher/layoutSwitcher.ts @@ -94,14 +94,15 @@ export class LayoutSwitcherPopup extends SwitcherPopup.SwitcherPopup { private _backwardAction: number; constructor(action: number, backwardAction: number, enableScaling: boolean) { + const monitorIndex = + LayoutSwitcherPopup._getMonitorIndex(); // @ts-expect-error "Parent can take a list" - super(GlobalState.get().layouts); + super(GlobalState.get().getLayoutsForMonitor(monitorIndex)); this._action = action; this._backwardAction = backwardAction; - // handle scale factor of the monitor const monitorScalingFactor = enableScaling - ? getMonitorScalingFactor(this._getCurrentMonitorIndex()) + ? getMonitorScalingFactor(monitorIndex) : undefined; this._switcherList = new LayoutSwitcherList( this._items, @@ -115,7 +116,7 @@ export class LayoutSwitcherPopup extends SwitcherPopup.SwitcherPopup { this._getCurrentMonitorIndex(), global.workspaceManager.get_active_workspace_index(), ); - this._selectedIndex = GlobalState.get().layouts.findIndex( + this._selectedIndex = this._items.findIndex( (lay) => lay.id === selectedLay.id, ); // backward is the one passed to show function @@ -147,10 +148,14 @@ export class LayoutSwitcherPopup extends SwitcherPopup.SwitcherPopup { ); } - private _getCurrentMonitorIndex(): number { + private static _getMonitorIndex(): number { const focusWindow = global.display.focus_window; if (focusWindow) return focusWindow.get_monitor(); return Main.layoutManager.primaryIndex; } + + private _getCurrentMonitorIndex(): number { + return LayoutSwitcherPopup._getMonitorIndex(); + } } diff --git a/src/components/snapassist/snapAssist.ts b/src/components/snapassist/snapAssist.ts index dd9fa52d..a47c2cc4 100644 --- a/src/components/snapassist/snapAssist.ts +++ b/src/components/snapassist/snapAssist.ts @@ -117,12 +117,18 @@ class SnapAssistContent extends St.BoxLayout { }, ); - this._setLayouts(GlobalState.get().layouts); + this._setLayouts( + GlobalState.get().getLayoutsForMonitor(this._monitorIndex), + ); this._signals.connect( GlobalState.get(), GlobalState.SIGNAL_LAYOUTS_CHANGED, () => { - this._setLayouts(GlobalState.get().layouts); + this._setLayouts( + GlobalState.get().getLayoutsForMonitor( + this._monitorIndex, + ), + ); }, ); diff --git a/src/indicator/defaultMenu.ts b/src/indicator/defaultMenu.ts index 26c793d2..8489eeaf 100644 --- a/src/indicator/defaultMenu.ts +++ b/src/indicator/defaultMenu.ts @@ -36,6 +36,7 @@ class LayoutsRow extends St.BoxLayout { private _layoutsBox: St.BoxLayout; private _layoutsButtons: LayoutButton[]; + private _layouts: Layout[]; private _label: St.Label; private _monitor: Monitor; @@ -61,6 +62,7 @@ class LayoutsRow extends St.BoxLayout { yExpand: true, styleClass: 'layouts-box-layout', }); + this._layouts = layouts; this._monitor = monitor; this._label = new St.Label({ text: `Monitor ${this._monitor.index + 1}`, @@ -96,8 +98,11 @@ class LayoutsRow extends St.BoxLayout { } public selectLayout(selectedId: string) { - const selectedIndex = GlobalState.get().layouts.findIndex( - (lay) => lay.id === selectedId, + const selectedIndex = this._layoutsButtons.findIndex( + (_, ind) => { + const lay = this._layouts[ind]; + return lay && lay.id === selectedId; + }, ); this._layoutsButtons.forEach((btn, ind) => btn.set_checked(ind === selectedIndex), @@ -200,6 +205,13 @@ export default class DefaultMenu implements CurrentMenu { this._signals.connect(Settings, Settings.KEY_INNER_GAPS, () => { this._drawLayouts(); }); + this._signals.connect( + Settings, + Settings.KEY_MINIMUM_TILE_WIDTH_THRESHOLD, + () => { + this._drawLayouts(); + }, + ); // if the selected layout was changed externaly, update the selected button this._signals.connect( @@ -415,7 +427,6 @@ export default class DefaultMenu implements CurrentMenu { } private _drawLayouts() { - const layouts = GlobalState.get().layouts; this._container.destroy_all_children(); this._layoutsRows = []; @@ -423,6 +434,8 @@ export default class DefaultMenu implements CurrentMenu { const ws_index = global.workspaceManager.get_active_workspace_index(); const monitors = getMonitors(); this._layoutsRows = monitors.map((monitor) => { + const layouts = + GlobalState.get().getLayoutsForMonitor(monitor.index); const ws_selected_layouts = ws_index < selected_layouts.length ? selected_layouts[ws_index] @@ -430,7 +443,7 @@ export default class DefaultMenu implements CurrentMenu { const selectedId = monitor.index < ws_selected_layouts.length ? ws_selected_layouts[monitor.index] - : GlobalState.get().layouts[0].id; + : layouts[0].id; const row = new LayoutsRow( this._container, layouts, diff --git a/src/indicator/indicator.ts b/src/indicator/indicator.ts index 213e83b0..42afd6a4 100644 --- a/src/indicator/indicator.ts +++ b/src/indicator/indicator.ts @@ -142,6 +142,9 @@ export default class Indicator extends PanelMenu.Button { }), ), lay.id, + lay.minimumMonitorResolution + ? { ...lay.minimumMonitorResolution } + : undefined, ); if (this._layoutEditor) { diff --git a/src/prefs.ts b/src/prefs.ts index 16abe499..51e5fdf8 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -581,6 +581,17 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference ); layoutsGroup.add(resetBtn); + const minTileWidthRow = this._buildSpinButtonRow( + Settings.KEY_MINIMUM_TILE_WIDTH_THRESHOLD, + _('Minimum tile width'), + _( + 'Minimum tile pixel width for a layout to be shown on a monitor (0 to disable filtering)', + ), + 0, + 800, + ); + layoutsGroup.add(minTileWidthRow); + // Keybindings section const keybindingsGroup = new Adw.PreferencesGroup({ title: _('Keybindings'), diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 59ff551c..4d75c1a4 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -123,6 +123,7 @@ export default class Settings { static KEY_ENABLE_SNAP_ASSISTANT_WINDOWS_SUGGESTIONS = 'enable-snap-assistant-windows-suggestions'; static KEY_ENABLE_SCREEN_EDGES_WINDOWS_SUGGESTIONS = 'enable-screen-edges-windows-suggestions'; static KEY_EDGE_TILING_MODE = 'edge-tiling-mode'; + static KEY_MINIMUM_TILE_WIDTH_THRESHOLD = 'minimum-tile-width-threshold'; static SETTING_MOVE_WINDOW_RIGHT = 'move-window-right'; static SETTING_MOVE_WINDOW_LEFT = 'move-window-left'; @@ -506,6 +507,14 @@ export default class Settings { set_string(Settings.KEY_EDGE_TILING_MODE, val); } + static get MINIMUM_TILE_WIDTH_THRESHOLD(): number { + return get_unsigned_number(Settings.KEY_MINIMUM_TILE_WIDTH_THRESHOLD); + } + + static set MINIMUM_TILE_WIDTH_THRESHOLD(val: number) { + set_unsigned_number(Settings.KEY_MINIMUM_TILE_WIDTH_THRESHOLD, val); + } + static get_inner_gaps(scaleFactor: number = 1): { top: number; bottom: number; diff --git a/src/utils/globalState.ts b/src/utils/globalState.ts index 73996f38..3cfefa2c 100644 --- a/src/utils/globalState.ts +++ b/src/utils/globalState.ts @@ -7,6 +7,7 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { logger } from './logger'; import { getWindows } from './ui'; import ExtendedWindow from '../components/tilingsystem/extendedWindow'; +import { getLayoutsForMonitor } from './layoutFiltering'; const debug = logger('GlobalState'); @@ -80,6 +81,15 @@ export default class GlobalState extends GObject.Object { }, ); + this._signals.connect( + Settings, + Settings.KEY_MINIMUM_TILE_WIDTH_THRESHOLD, + () => { + this.validate_selected_layouts(); + this.emit(GlobalState.SIGNAL_LAYOUTS_CHANGED); + }, + ); + this._signals.connect( Settings, Settings.KEY_SETTING_SELECTED_LAYOUTS, @@ -210,13 +220,24 @@ export default class GlobalState extends GObject.Object { monitors_layouts.push(this._layouts[0].id); while (monitors_layouts.length > n_monitors) monitors_layouts.pop(); - monitors_layouts.forEach((_, ind) => { + monitors_layouts.forEach((_, monitorIndex) => { if ( this._layouts.findIndex( - (lay) => lay.id === monitors_layouts[ind], + (lay) => lay.id === monitors_layouts[monitorIndex], ) === -1 ) - monitors_layouts[ind] = monitors_layouts[0]; + monitors_layouts[monitorIndex] = monitors_layouts[0]; + + const suitableLayouts = + this.getLayoutsForMonitor(monitorIndex); + if ( + !suitableLayouts.find( + (lay) => + lay.id === monitors_layouts[monitorIndex], + ) + ) { + monitors_layouts[monitorIndex] = suitableLayouts[0].id; + } }); this._selected_layouts.set(ws, monitors_layouts); @@ -243,6 +264,16 @@ export default class GlobalState extends GObject.Object { return this._layouts; } + public getLayoutsForMonitor(monitorIndex: number): Layout[] { + const workArea = + Main.layoutManager.getWorkAreaForMonitor(monitorIndex); + return getLayoutsForMonitor( + this._layouts, + workArea.width, + workArea.height, + ); + } + public addLayout(newLay: Layout) { this._layouts.push(newLay); // easy way to trigger save and signal emission @@ -312,11 +343,16 @@ export default class GlobalState extends GObject.Object { if (monitorIndex < 0 || monitorIndex >= monitors_selected.length) monitorIndex = 0; - return ( + const selectedLayout = this._layouts.find( (lay) => lay.id === monitors_selected[monitorIndex], - ) || this._layouts[0] - ); + ) || this._layouts[0]; + + const suitableLayouts = this.getLayoutsForMonitor(monitorIndex); + if (suitableLayouts.find((lay) => lay.id === selectedLayout.id)) { + return selectedLayout; + } + return suitableLayouts[0]; } public get tilePreviewAnimationTime(): number { diff --git a/src/utils/layoutFiltering.ts b/src/utils/layoutFiltering.ts new file mode 100644 index 00000000..f3ab8cd2 --- /dev/null +++ b/src/utils/layoutFiltering.ts @@ -0,0 +1,43 @@ +import Layout from '../components/layout/Layout'; +import Settings from '../settings/settings'; + +export function isLayoutSuitableForMonitor( + layout: Layout, + monitorWidth: number, + monitorHeight: number, +): boolean { + if (layout.minimumMonitorResolution) { + return ( + monitorWidth >= layout.minimumMonitorResolution.width && + monitorHeight >= layout.minimumMonitorResolution.height + ); + } + + const minTileWidthThreshold = Settings.MINIMUM_TILE_WIDTH_THRESHOLD; + if (layout.tiles.length === 0 || minTileWidthThreshold === 0) return true; + const minTileHeightThreshold = Math.round((minTileWidthThreshold * 2) / 3); + + const smallestTileWidth = Math.min(...layout.tiles.map((t) => t.width)); + const smallestTileHeight = Math.min(...layout.tiles.map((t) => t.height)); + + return ( + smallestTileWidth * monitorWidth >= minTileWidthThreshold && + smallestTileHeight * monitorHeight >= minTileHeightThreshold + ); +} + +export function getLayoutsForMonitor( + layouts: Layout[], + monitorWidth: number, + monitorHeight: number, +): Layout[] { + const suitable = layouts.filter((lay) => + isLayoutSuitableForMonitor(lay, monitorWidth, monitorHeight), + ); + + if (suitable.length === 0 && layouts.length > 0) { + return [layouts[0]]; + } + + return suitable.length > 0 ? suitable : layouts; +}