From cce8cfc3809347f59c5101ad56413d11a683a506 Mon Sep 17 00:00:00 2001 From: Domenico Ferraro Date: Fri, 5 Jul 2024 22:55:39 +0200 Subject: [PATCH] version 11: support per-monitor layout, enable/disable edge-tiling --- resources/icons/add-symbolic.svg | 6 +- resources/icons/cancel-symbolic.svg | 6 +- resources/icons/delete-symbolic.svg | 9 + resources/icons/done-symbolic.svg | 3 - resources/icons/edit-symbolic.svg | 3 + resources/icons/info-symbolic.svg | 6 +- resources/icons/menu-symbolic.svg | 10 + resources/icons/save-symbolic.svg | 5 + resources/metadata.json | 4 +- ...e.shell.extensions.tilingshell.gschema.xml | 10 + src/components/editor/editorDialog.ts | 13 +- src/components/tilepreview/tilePreview.ts | 7 + .../tilingsystem/edgeTilingManager.ts | 191 +++++++++++ src/components/tilingsystem/resizeManager.ts | 35 +- src/components/tilingsystem/tilingLayout.ts | 51 ++- src/components/tilingsystem/tilingManager.ts | 311 +++++++----------- src/extension.ts | 42 ++- src/indicator/defaultMenu.ts | 224 ++++++++++--- src/indicator/editingMenu.ts | 10 +- src/indicator/indicator.ts | 32 +- src/indicator/utils.ts | 3 +- src/prefs.ts | 28 +- src/settings.ts | 10 + src/styles/indicator.scss | 10 + 24 files changed, 709 insertions(+), 320 deletions(-) create mode 100644 resources/icons/delete-symbolic.svg delete mode 100644 resources/icons/done-symbolic.svg create mode 100644 resources/icons/edit-symbolic.svg create mode 100644 resources/icons/menu-symbolic.svg create mode 100644 resources/icons/save-symbolic.svg create mode 100644 src/components/tilingsystem/edgeTilingManager.ts diff --git a/resources/icons/add-symbolic.svg b/resources/icons/add-symbolic.svg index 5cc1d48..ce0889a 100644 --- a/resources/icons/add-symbolic.svg +++ b/resources/icons/add-symbolic.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/resources/icons/cancel-symbolic.svg b/resources/icons/cancel-symbolic.svg index 8c6f953..d7e0061 100644 --- a/resources/icons/cancel-symbolic.svg +++ b/resources/icons/cancel-symbolic.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/resources/icons/delete-symbolic.svg b/resources/icons/delete-symbolic.svg new file mode 100644 index 0000000..c7a40c6 --- /dev/null +++ b/resources/icons/delete-symbolic.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/icons/done-symbolic.svg b/resources/icons/done-symbolic.svg deleted file mode 100644 index 0f0e97c..0000000 --- a/resources/icons/done-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/edit-symbolic.svg b/resources/icons/edit-symbolic.svg new file mode 100644 index 0000000..89c6373 --- /dev/null +++ b/resources/icons/edit-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/info-symbolic.svg b/resources/icons/info-symbolic.svg index c246fae..155cdf4 100644 --- a/resources/icons/info-symbolic.svg +++ b/resources/icons/info-symbolic.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/resources/icons/menu-symbolic.svg b/resources/icons/menu-symbolic.svg new file mode 100644 index 0000000..ceddc55 --- /dev/null +++ b/resources/icons/menu-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/icons/save-symbolic.svg b/resources/icons/save-symbolic.svg new file mode 100644 index 0000000..b408978 --- /dev/null +++ b/resources/icons/save-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/metadata.json b/resources/metadata.json index 9b761e0..9709a53 100644 --- a/resources/metadata.json +++ b/resources/metadata.json @@ -9,8 +9,8 @@ "45", "46" ], - "version": 10, - "version-name": "10.0", + "version": 99, + "version-name": "11", "url": "https://github.com/domferr/tilingshell", "settings-schema": "org.gnome.shell.extensions.tilingshell", "donations": { diff --git a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml index b7186b1..75cecc5 100644 --- a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml +++ b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml @@ -87,6 +87,16 @@ Overridden settings The settings that are overridden and their old value. + + true + Active Screen Edges + Drag windows against the top, left and right screen edges to resize them. + + + false + Drag agains top edge to maximize window + Drag windows against the top edge to maximize them + ['<Super>Right'] diff --git a/src/components/editor/editorDialog.ts b/src/components/editor/editorDialog.ts index 005bcb0..f0c6847 100644 --- a/src/components/editor/editorDialog.ts +++ b/src/components/editor/editorDialog.ts @@ -145,14 +145,7 @@ export default class EditorDialog extends ModalDialog.ModalDialog { })); const suggestion4 = new St.BoxLayout({ vertical: false, xExpand: true, margin_top: 16 }); - // RIGHT-CLICK to delete a tile - /*suggestion4.add_child(new St.Label({ - text: "Use the indicator ", - xAlign: Clutter.ActorAlign.CENTER, - yAlign: Clutter.ActorAlign.CENTER, - styleClass: '', - xExpand: false - }));*/ + // use indicator to save or cancel suggestion4.add_child(new St.Icon({ iconSize: 16, yAlign: Clutter.ActorAlign.CENTER, @@ -217,7 +210,7 @@ export default class EditorDialog extends ModalDialog.ModalDialog { xAlign: Clutter.ActorAlign.CENTER, styleClass: "message-list-clear-button icon-button button delete-layout-button" }); - deleteBtn.child = new St.Icon({ iconName: "edit-delete-symbolic", iconSize: 16 }); + deleteBtn.child = new St.Icon({ gicon: Gio.icon_new_for_string(`${params.path}/icons/delete-symbolic.svg`), iconSize: 16 }); deleteBtn.connect('clicked', (self) => { params.onDeleteLayout(btnInd, lay); this._drawLayouts({ ...params, layouts: GlobalState.get().layouts }); @@ -238,7 +231,7 @@ export default class EditorDialog extends ModalDialog.ModalDialog { }); this._layoutsBoxLayout.add_child(box); const newLayoutBtn = new LayoutButton(box, new Layout([new Tile({x: 0, y: 0, width: 1, height: 1, groups: []})], "New Layout"), gaps, this._layoutHeight, this._layoutWidth); - const icon = new St.Icon({ iconName: "list-add-symbolic", iconSize: 32 }); + const icon = new St.Icon({ gicon: Gio.icon_new_for_string(`${params.path}/icons/add-symbolic.svg`), iconSize: 32 }); icon.set_size(newLayoutBtn.child.width, newLayoutBtn.child.height); newLayoutBtn.child.add_child(icon); newLayoutBtn.connect('clicked', (self) => { diff --git a/src/components/tilepreview/tilePreview.ts b/src/components/tilepreview/tilePreview.ts index 5a52fdc..7d34837 100644 --- a/src/components/tilepreview/tilePreview.ts +++ b/src/components/tilepreview/tilePreview.ts @@ -42,6 +42,13 @@ export default class TilePreview extends St.Widget { this._gaps.right = gaps.right * scalingFactor; this._gaps.bottom = gaps.bottom * scalingFactor; this._gaps.left = gaps.left * scalingFactor; + + if (this._gaps.top === 0 && this._gaps.bottom === 0 + && this._gaps.right === 0 && this._gaps.left === 0) { + this.remove_style_class_name("custom-tile-preview"); + } else { + this.add_style_class_name("custom-tile-preview"); + } } public get gaps(): Clutter.Margin { diff --git a/src/components/tilingsystem/edgeTilingManager.ts b/src/components/tilingsystem/edgeTilingManager.ts new file mode 100644 index 0000000..1248907 --- /dev/null +++ b/src/components/tilingsystem/edgeTilingManager.ts @@ -0,0 +1,191 @@ +import { buildRectangle, isPointInsideRect } from "@utils/ui"; +import Mtk from "gi://Mtk"; +import St from "gi://St"; +import Settings from "@settings"; + +const EDGE_TILING_OFFSET = 16; +const TOP_EDGE_TILING_OFFSET = 8; +const QUARTER_PERCENTAGE = 0.5; +const ACTIVATION_PERCENTAGE = 0.4; + +export default class EdgeTilingManager { + private _workArea: Mtk.Rectangle; + + // activation zones + private _topLeft: Mtk.Rectangle; + private _topRight: Mtk.Rectangle; + private _bottomLeft: Mtk.Rectangle; + private _bottomRight: Mtk.Rectangle; + private _topCenter: Mtk.Rectangle; + private _leftCenter: Mtk.Rectangle; + private _rightCenter: Mtk.Rectangle; + + // current active zone + private _activeEdgeTile: Mtk.Rectangle | null; + + constructor(initialWorkArea: Mtk.Rectangle) { + this._workArea = buildRectangle(); + this._topLeft = buildRectangle(); + this._topRight = buildRectangle(); + this._bottomLeft = buildRectangle(); + this._bottomRight = buildRectangle(); + this._topCenter = buildRectangle(); + this._leftCenter = buildRectangle(); + this._rightCenter = buildRectangle(); + this._activeEdgeTile = null; + this.workarea = initialWorkArea; + } + + public set workarea(newWorkArea: Mtk.Rectangle) { + this._workArea.x = newWorkArea.x; + this._workArea.y = newWorkArea.y; + this._workArea.width = newWorkArea.width; + this._workArea.height = newWorkArea.height; + + const width = this._workArea.width * ACTIVATION_PERCENTAGE; + const height = this._workArea.height * ACTIVATION_PERCENTAGE; + + this._topLeft.x = this._workArea.x; + this._topLeft.y = this._workArea.y; + this._topLeft.width = width; + this._topLeft.height = height; + + this._topRight.x = this._workArea.x + this._workArea.width - this._topLeft.width; + this._topRight.y = this._topLeft.y; + this._topRight.width = width; + this._topRight.height = height; + + this._bottomLeft.x = this._workArea.x; + this._bottomLeft.y = this._workArea.y + this._workArea.height - height; + this._bottomLeft.width = width; + this._bottomLeft.height = height; + + this._bottomRight.x = this._topRight.x; + this._bottomRight.y = this._bottomLeft.y; + this._bottomRight.width = width; + this._bottomRight.height = height; + + this._topCenter.x = this._topLeft.x + this._topLeft.width; + this._topCenter.y = this._topRight.y; + this._topCenter.height = this._topRight.height; + this._topCenter.width = this._topRight.x - this._topCenter.x; + + this._leftCenter.x = this._topLeft.x; + this._leftCenter.y = this._topLeft.y + this._topLeft.height; + this._leftCenter.height = this._bottomLeft.y - this._leftCenter.y; + this._leftCenter.width = this._topLeft.width; + + this._rightCenter.x = this._topRight.x; + this._rightCenter.y = this._topRight.y + this._topRight.height; + this._rightCenter.height = this._bottomRight.y - this._rightCenter.y; + this._rightCenter.width = this._topRight.width; + } + + public canActivateEdgeTiling(x: number, y: number) { + return x <= this._workArea.x + EDGE_TILING_OFFSET + || y <= this._workArea.y + TOP_EDGE_TILING_OFFSET + || x >= this._workArea.x + this._workArea.width - EDGE_TILING_OFFSET + || y >= this._workArea.y + this._workArea.height - EDGE_TILING_OFFSET; + } + + public isPerformingEdgeTiling(): boolean { + return this._activeEdgeTile !== null; + } + + public startEdgeTiling(x: number, y: number): { changed: boolean, rect: Mtk.Rectangle } { + const previewRect = buildRectangle(); + + if (this._activeEdgeTile && isPointInsideRect({x, y}, this._activeEdgeTile)) { + return { + changed: false, + rect: previewRect + }; + } + + if (!this._activeEdgeTile) this._activeEdgeTile = buildRectangle(); + + previewRect.width = this._workArea.width * QUARTER_PERCENTAGE; + previewRect.height = this._workArea.height * QUARTER_PERCENTAGE; + previewRect.y = this._workArea.y; + previewRect.x = this._workArea.x; + if (isPointInsideRect({x, y}, this._topCenter)) { + previewRect.width = this._workArea.width; + previewRect.height = this._workArea.height; + + this._activeEdgeTile = this._topCenter; + // center-left (full edge tile) + } else if (isPointInsideRect({x, y}, this._leftCenter)) { + previewRect.width = this._workArea.width * QUARTER_PERCENTAGE; + previewRect.height = this._workArea.height; + + this._activeEdgeTile = this._leftCenter; + // center-right (full edge tile) + } else if (isPointInsideRect({x, y}, this._rightCenter)) { + previewRect.x = this._workArea.x + this._workArea.width - previewRect.width; + previewRect.width = this._workArea.width * QUARTER_PERCENTAGE; + previewRect.height = this._workArea.height; + + this._activeEdgeTile = this._rightCenter; + // left side + } else if (x <= this._workArea.x + (this._workArea.width / 2)) { + // top-left corner + if (isPointInsideRect({x, y}, this._topLeft)) { + this._activeEdgeTile = this._topLeft; + // bottom-left corner + } else if (isPointInsideRect({x, y}, this._bottomLeft)) { + previewRect.y = this._workArea.y + this._workArea.height - previewRect.height; + this._activeEdgeTile = this._bottomLeft; + // bottom-center + } else { + return { + changed: false, + rect: previewRect + }; + } + // right side + } else { + previewRect.x = this._workArea.x + this._workArea.width - previewRect.width; + // top-right corner + if (isPointInsideRect({x, y}, this._topRight)) { + this._activeEdgeTile = this._topRight; + // bottom-right corner + } else if (isPointInsideRect({x, y}, this._bottomRight)) { + previewRect.y = this._workArea.y + this._workArea.height - previewRect.height; + this._activeEdgeTile = this._bottomRight; + // bottom-center + } else { + return { + changed: false, + rect: previewRect + }; + } + } + + // uncomment to show active tile debugging + /*global.windowGroup.get_children().filter(c => c.get_name() === "debug")[0]?.destroy(); + const debug = new St.Widget({ + x: this._activeEdgeTile.x, + y: this._activeEdgeTile.y, + height: this._activeEdgeTile.height, + width: this._activeEdgeTile.width, + style: "border: 2px solid red", + name: "debug" + }); + global.windowGroup.add_child(debug);*/ + + return { + changed: true, + rect: previewRect + }; + } + + public needMaximize(): boolean { + return this._activeEdgeTile !== null + && Settings.get_top_edge_maximize() + && this._activeEdgeTile === this._topCenter; + } + + public abortEdgeTiling() { + this._activeEdgeTile = null; + } +} \ No newline at end of file diff --git a/src/components/tilingsystem/resizeManager.ts b/src/components/tilingsystem/resizeManager.ts index 43f1a13..4b187ac 100644 --- a/src/components/tilingsystem/resizeManager.ts +++ b/src/components/tilingsystem/resizeManager.ts @@ -9,18 +9,36 @@ import ExtendedWindow from "./extendedWindow"; const debug = logger(`ResizingManager`); export class ResizingManager { - private readonly _signals: SignalHandling; + private _signals: SignalHandling | null; constructor() { + this._signals = null; + } + + public enable() { + if (this._signals) this._signals.disconnect(); this._signals = new SignalHandling(); + + this._signals.connect(global.display, 'grab-op-begin', (_display: Meta.Display, window: Meta.Window, grabOp: Meta.GrabOp) => { + const moving = (grabOp & ~1024) === 1; + if (moving || !Settings.get_resize_complementing_windows()) return; + + this._onWindowResizingBegin(window, grabOp); + }); + + this._signals.connect(global.display, 'grab-op-end', (_display: Meta.Display, window: Meta.Window, grabOp: Meta.GrabOp) => { + const moving = (grabOp & ~1024) === 1; + if (moving) return; + + this._onWindowResizingEnd(window); + }); } public destroy() { - this._signals.disconnect(); + if (this._signals) this._signals.disconnect(); } - /** From Gnome Shell: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/altTab.js#L53 - */ + /** From Gnome Shell: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/altTab.js#L53 */ private _getWindows(): Meta.Window[] { const workspace = global.workspaceManager.get_active_workspace(); // We ignore skip-taskbar windows in switchers, but if they are attached @@ -35,9 +53,8 @@ export class ResizingManager { }).filter((w, i, a) => w !== null && !w.skipTaskbar && a.indexOf(w) === i); } - public onWindowResizingBegin(window: Meta.Window, grabOp: Meta.GrabOp) { - if (!Settings.get_resize_complementing_windows()) return; - if (!window || !(window as ExtendedWindow).isTiled) return; + private _onWindowResizingBegin(window: Meta.Window, grabOp: Meta.GrabOp) { + if (!window || !(window as ExtendedWindow).isTiled || !this._signals) return; const verticalSide: [boolean, St.Side] = [false, 0]; const horizontalSide: [boolean, St.Side] = [false, 0]; @@ -198,8 +215,8 @@ export class ResizingManager { return result; } - public onWindowResizingEnd(window: Meta.Window) { - this._signals.disconnect(window); + private _onWindowResizingEnd(window: Meta.Window) { + if (this._signals) this._signals.disconnect(window); } private _onResizingWindow( diff --git a/src/components/tilingsystem/tilingLayout.ts b/src/components/tilingsystem/tilingLayout.ts index 12f92b4..7ed95ac 100644 --- a/src/components/tilingsystem/tilingLayout.ts +++ b/src/components/tilingsystem/tilingLayout.ts @@ -46,7 +46,7 @@ class DynamicTilePreview extends TilePreview { * hovered tile. */ @registerGObjectClass -export default class TilingLayout extends LayoutWidget { +export default class TilingLayout extends LayoutWidget { private _showing: boolean; constructor(layout: Layout, innerGaps: Clutter.Margin, outerGaps: Clutter.Margin, workarea: Mtk.Rectangle, scalingFactor?: number) { @@ -287,11 +287,9 @@ export default class TilingLayout extends LayoutWidget { public getNearestTile(source: Mtk.Rectangle, direction: Meta.Direction): Mtk.Rectangle | undefined { let previewFound: DynamicTilePreview | undefined = undefined; let bestDistance = -1; - + for (let i = 0; i < this._previews.length; i++) { const preview = this._previews[i]; - const euclideanDistance = ((preview.x - source.x) * (preview.x - source.x)) - + ((preview.y - source.y) * (preview.y - source.y)); switch (direction) { case Meta.Direction.RIGHT: @@ -301,7 +299,7 @@ export default class TilingLayout extends LayoutWidget { if (preview.x >= source.x) continue; break; case Meta.Direction.BOTTOM: - if (preview.y <= source.y + source.height) continue; + if (preview.y <= source.y) continue; break; case Meta.Direction.TOP: if (preview.y >= source.y) continue; @@ -310,6 +308,9 @@ export default class TilingLayout extends LayoutWidget { continue; } + const euclideanDistance = ((preview.x - source.x) * (preview.x - source.x)) + + ((preview.y - source.y) * (preview.y - source.y)); + if (!previewFound || euclideanDistance < bestDistance) { previewFound = preview; bestDistance = euclideanDistance; @@ -325,4 +326,44 @@ export default class TilingLayout extends LayoutWidget { height: previewFound.innerHeight }); } + + public getRightmostTile(): Mtk.Rectangle { + let previewFound: DynamicTilePreview = this._previews[0]; + + for (let i = 1; i < this._previews.length; i++) { + const preview = this._previews[i]; + if (preview.x + preview.width < previewFound.x + previewFound.width) continue; + + if (preview.x + preview.width > previewFound.x + previewFound.width) previewFound = preview; + else if (preview.y < previewFound.y) previewFound = preview; + + } + + return buildRectangle({ + x: previewFound.innerX, + y: previewFound.innerY, + width: previewFound.innerWidth, + height: previewFound.innerHeight + }); + } + + public getLeftmostTile(): Mtk.Rectangle { + let previewFound: DynamicTilePreview = this._previews[0]; + + for (let i = 1; i < this._previews.length; i++) { + const preview = this._previews[i]; + if (preview.x > previewFound.x) continue; + + if (preview.x < previewFound.x) previewFound = preview; + else if (preview.y < previewFound.y) previewFound = preview; + + } + + return buildRectangle({ + x: previewFound.innerX, + y: previewFound.innerY, + width: previewFound.innerWidth, + height: previewFound.innerHeight + }); + } } \ No newline at end of file diff --git a/src/components/tilingsystem/tilingManager.ts b/src/components/tilingsystem/tilingManager.ts index 34d2e7b..e718267 100644 --- a/src/components/tilingsystem/tilingManager.ts +++ b/src/components/tilingsystem/tilingManager.ts @@ -6,7 +6,6 @@ import { buildMargin, buildRectangle, buildTileGaps, getScalingFactor, getScalin import TilingLayout from "@/components/tilingsystem/tilingLayout"; import Clutter from "gi://Clutter"; import GLib from "gi://GLib"; -import Gio from "gi://Gio"; import SnapAssist from '../snapassist/snapAssist'; import SelectionTilePreview from '../tilepreview/selectionTilePreview'; import Settings, { ActivationKey } from '@/settings'; @@ -17,10 +16,7 @@ import TileUtils from '../layout/TileUtils'; import GlobalState from '@/globalState'; import { Monitor } from 'resource:///org/gnome/shell/ui/layout.js'; import ExtendedWindow from "./extendedWindow"; -import { ResizingManager } from "./resizeManager"; -import SettingsOverride from "@settingsOverride"; - -const EDGE_TILING_OFFSET = 15; +import EdgeTilingManager from "./edgeTilingManager"; export class TilingManager { private readonly _monitor: Monitor; @@ -28,7 +24,7 @@ export class TilingManager { private _selectedTilesPreview: SelectionTilePreview; private _snapAssist: SnapAssist; private _tilingLayout: TilingLayout; - private _resizingManager: ResizingManager; + private _edgeTilingManager: EdgeTilingManager; private _workArea: Mtk.Rectangle; private _innerGaps: Clutter.Margin; @@ -41,7 +37,6 @@ export class TilingManager { private _wasSpanMultipleTilesActivated: boolean; private _wasTilingSystemActivated: boolean; private _isSnapAssisting: boolean; - private _activeEdgeTile: Mtk.Rectangle | null; private _movingWindowTimerId: number | null = null; @@ -57,7 +52,6 @@ export class TilingManager { this._wasSpanMultipleTilesActivated = false; this._wasTilingSystemActivated = false; this._isSnapAssisting = false; - this._activeEdgeTile = null; this._enableScaling = enableScaling; this._monitor = monitor; this._signals = new SignalHandling(); @@ -72,6 +66,7 @@ export class TilingManager { // get the monitor's workarea this._workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index); this._debug(`Work area for monitor ${this._monitor.index}: ${this._workArea.x} ${this._workArea.y} ${this._workArea.width}x${this._workArea.height}`); + this._edgeTilingManager = new EdgeTilingManager(this._workArea); const monitorScalingFactor = this._enableScaling ? getScalingFactor(monitor.index):undefined; // build the tiling layout @@ -82,8 +77,6 @@ export class TilingManager { // build the snap assistant this._snapAssist = new SnapAssist(Main.uiGroup, this._workArea, monitorScalingFactor); - - this._resizingManager = new ResizingManager(); } /** @@ -113,19 +106,14 @@ export class TilingManager { this._signals.connect(global.display, 'grab-op-begin', (_display: Meta.Display, window: Meta.Window, grabOp: Meta.GrabOp) => { const moving = (grabOp & ~1024) === 1; - if (!moving) { - this._resizingManager.onWindowResizingBegin(window, grabOp); - return; - } + if (!moving) return; - this._onWindowGrabBegin(window); + this._onWindowGrabBegin(window, grabOp); }); this._signals.connect(global.display, 'grab-op-end', (_display: Meta.Display, window: Meta.Window, grabOp: Meta.GrabOp) => { - if (!this._isGrabbingWindow) { - this._resizingManager.onWindowResizingEnd(window); - return; - } + if (!this._isGrabbingWindow) return; + this._onWindowGrabEnd(window); }); @@ -133,9 +121,28 @@ export class TilingManager { } public onKeyboardMoveWindow(window: Meta.Window, direction: Meta.Direction) { - const windowRect = window.get_frame_rect().copy(); - - const destinationRect = this._tilingLayout.getNearestTile(windowRect, direction); + let destinationRect: Mtk.Rectangle | undefined = undefined; + if (window.get_maximized()) { + switch (direction) { + case Meta.Direction.DOWN: + window.unmaximize(Meta.MaximizeFlags.BOTH); + return; + case Meta.Direction.UP: + return; + case Meta.Direction.LEFT: + destinationRect = this._tilingLayout.getLeftmostTile(); + break; + case Meta.Direction.RIGHT: + destinationRect = this._tilingLayout.getRightmostTile(); + break; + } + } + + const windowRect = window.get_frame_rect().copy(); + if (!destinationRect) { + destinationRect = this._tilingLayout.getNearestTile(windowRect, direction); + } + if (!destinationRect) { // handle maximize of window if (direction === Meta.Direction.UP && window.can_maximize()) { @@ -144,7 +151,7 @@ export class TilingManager { return; } - if (!(window as ExtendedWindow).isTiled) { + if (!(window as ExtendedWindow).isTiled && !window.get_maximized()) { (window as ExtendedWindow).originalSize = windowRect; } (window as ExtendedWindow).isTiled = true; @@ -165,11 +172,10 @@ export class TilingManager { this._signals.disconnect(); this._isGrabbingWindow = false; this._isSnapAssisting = false; - this._activeEdgeTile = null; + this._edgeTilingManager.abortEdgeTiling(); this._tilingLayout.destroy(); this._snapAssist.destroy(); this._selectedTilesPreview.destroy(); - this._resizingManager.destroy(); } public set workArea(newWorkArea: Mtk.Rectangle) { @@ -177,14 +183,15 @@ export class TilingManager { this._workArea = newWorkArea; this._debug(`new work area for monitor ${this._monitor.index}: ${newWorkArea.x} ${newWorkArea.y} ${newWorkArea.width}x${newWorkArea.height}`); - + // notify the tiling layout that the workarea changed and trigger a new relayout // so we will have the layout already computed to be shown quickly when needed this._tilingLayout.relayout({ containerRect: this._workArea }); this._snapAssist.workArea = this._workArea; + this._edgeTilingManager.workarea = this._workArea; } - private _onWindowGrabBegin(window: Meta.Window) { + private _onWindowGrabBegin(window: Meta.Window, grabOp: number) { if (this._isGrabbingWindow) return; // workaround for gnome-shell bug https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857 @@ -204,10 +211,10 @@ export class TilingManager { this._movingWindowTimerId = GLib.timeout_add( GLib.PRIORITY_DEFAULT_IDLE, this._movingWindowTimerDuration, - this._onMovingWindow.bind(this, window) + this._onMovingWindow.bind(this, window, grabOp) ); - this._onMovingWindow(window); + this._onMovingWindow(window, grabOp); } private _activationKeyStatus(modifier: number, key: ActivationKey): boolean { @@ -228,7 +235,7 @@ export class TilingManager { return (modifier & 1 << val) != 0; } - private _onMovingWindow(window: Meta.Window) { + private _onMovingWindow(window: Meta.Window, grabOp: number) { // if the window is no longer grabbed, disable handler if (!this._isGrabbingWindow) { this._movingWindowTimerId = null; @@ -241,30 +248,62 @@ export class TilingManager { this._selectedTilesPreview.close(true); this._snapAssist.close(true); this._isSnapAssisting = false; - this._activeEdgeTile = null; + this._edgeTilingManager.abortEdgeTiling(); return GLib.SOURCE_CONTINUE; } + const [x, y, modifier] = global.get_pointer(); const extWin = window as ExtendedWindow; extWin.isTiled = false; // if there is "originalSize" attached, it means the window were tiled and // it is the first time the window is moved. If that's the case, change // window's size to the size it had before it were tiled (the originalSize) if (extWin.originalSize) { - const newSize = buildRectangle({ - x: window.get_frame_rect().x, - y: window.get_frame_rect().y, - width: extWin.originalSize.width, - height: extWin.originalSize.height - }); if (Settings.get_restore_window_original_size()) { - this._easeWindowRect(window, newSize); + const windowRect = window.get_frame_rect(); + const offsetX = (x - windowRect.x) / windowRect.width; + const offsetY = (y - windowRect.y) / windowRect.height; + + const newSize = buildRectangle({ + x: x - (extWin.originalSize.width * offsetX), + y: y - (extWin.originalSize.height * offsetY), + width: extWin.originalSize.width, + height: extWin.originalSize.height + }); + + // restart grab for GNOME 42 + //@ts-ignore + const restartGrab = global.display.end_grab_op && global.display.begin_grab_op; + if (restartGrab) { + //@ts-ignore + global.display.end_grab_op(global.get_current_time()); + } + // if we restarted the grab, we need to force window movement and to + // perform user operation + this._easeWindowRect(window, newSize, restartGrab, restartGrab); + + if (restartGrab) { + // must be done now, before begin_grab_op, because begin_grab_op will trigger + // _onMovingWindow again, so we will go into infinite loop on restoring the window size + extWin.originalSize = undefined; + //@ts-ignore + global.display.begin_grab_op( + window, + grabOp, + true, // pointer already grabbed + true, // frame action + -1, // Button + modifier, + global.get_current_time(), + x, + y, + ); + } } extWin.originalSize = undefined; } - const [x, y, modifier] = global.get_pointer(); const currPointerPos = { x, y }; const isSpanMultiTilesActivated = this._activationKeyStatus(modifier, Settings.get_span_multiple_tiles_activation_key()); @@ -291,14 +330,16 @@ export class TilingManager { this._tilingLayout.close(); this._selectedTilesPreview.close(true); } - - if (!this._isSnapAssisting && this._isEdgeTiling(global.get_pointer()[0], global.get_pointer()[1])) { - this._onEdgeTiling(window); + + if (Settings.get_active_screen_edges() && !this._isSnapAssisting + && this._edgeTilingManager.canActivateEdgeTiling(global.get_pointer()[0], global.get_pointer()[1])) { + const { changed, rect } = this._edgeTilingManager.startEdgeTiling(x, y); + if (changed) this._showEdgeTiling(window, rect, x, y); this._snapAssist.close(true); } else { - if (this._activeEdgeTile) { + if (this._edgeTilingManager.isPerformingEdgeTiling()) { this._selectedTilesPreview.close(true); - this._activeEdgeTile = null; + this._edgeTilingManager.abortEdgeTiling(); } if (Settings.get_snap_assist_enabled()) { @@ -315,9 +356,9 @@ export class TilingManager { this._tilingLayout.openAbove(window); this._snapAssist.close(true); // close selection tile if we were performing edge-tiling - if (this._activeEdgeTile) { + if (this._edgeTilingManager.isPerformingEdgeTiling()) { this._selectedTilesPreview.close(true); - this._activeEdgeTile = null; + this._edgeTilingManager.abortEdgeTiling(); } } // if it was snap assisting then close the selection tile preview. We may reopen it if that's the case @@ -375,14 +416,19 @@ export class TilingManager { global.get_pointer()[2], Settings.get_tiling_system_activation_key() ); - if (!isTilingSystemActivated && !this._isSnapAssisting && !this._activeEdgeTile) { - return; + if (!isTilingSystemActivated && !this._isSnapAssisting + && !this._edgeTilingManager.isPerformingEdgeTiling()) { + return; } - // disable snap assistance this._isSnapAssisting = false; + + if (this._edgeTilingManager.isPerformingEdgeTiling() + && this._edgeTilingManager.needMaximize() && window.can_maximize()) { + window.maximize(Meta.MaximizeFlags.BOTH); + } // disable edge-tiling - this._activeEdgeTile = null; + this._edgeTilingManager.abortEdgeTiling(); // abort if the pointer is moving on another monitor: the user moved // the window to another monitor not handled by this tiling manager @@ -395,10 +441,10 @@ export class TilingManager { (window as ExtendedWindow).originalSize = window.get_frame_rect().copy(); (window as ExtendedWindow).isTiled = true; - this._easeWindowRect(window, selectionRect); + if (!window.get_maximized()) this._easeWindowRect(window, selectionRect); } - private _easeWindowRect(window: Meta.Window, destRect: Mtk.Rectangle) { + private _easeWindowRect(window: Meta.Window, destRect: Mtk.Rectangle, user_op: boolean = false, force: boolean = false) { // apply animations when tiling the window const windowActor = window.get_compositor_private(); // @ts-ignore @@ -413,8 +459,9 @@ export class TilingManager { // move and resize the window to the current selection window.move_to_monitor(this._monitor.index); + if (force) window.move_frame(user_op, destRect.x, destRect.y); window.move_resize_frame( - false, + user_op, destRect.x, destRect.y, destRect.width, @@ -463,140 +510,9 @@ export class TilingManager { && y >= this._monitor.y && y <= this._monitor.y + this._monitor.height; } - private _isEdgeTiling(x: number, y: number) { - return x <= this._workArea.x + EDGE_TILING_OFFSET - || y <= this._workArea.y + EDGE_TILING_OFFSET - || x >= this._workArea.x + this._workArea.width - EDGE_TILING_OFFSET - || y >= this._workArea.y + this._workArea.height - EDGE_TILING_OFFSET; - } - - private _onEdgeTiling(window: Meta.Window) { - const [x, y] = global.get_pointer(); - - const previewRect = buildRectangle(); - const quarterPercentage = 0.5; - const activationPercentage = 0.4; - - if (this._activeEdgeTile && isPointInsideRect({x, y}, this._activeEdgeTile)) { - return; - } - - if (!this._activeEdgeTile) this._activeEdgeTile = buildRectangle(); - - const width = this._workArea.width * activationPercentage; - const height = this._workArea.height * activationPercentage; - - const topLeft = buildRectangle({ - x: this._workArea.x, - y: this._workArea.y, - width, - height - }); - const topRight = buildRectangle({ - x: this._workArea.x + this._workArea.width - topLeft.width, - y: topLeft.y, - width, - height - }); - const bottomLeft = buildRectangle({ - x: this._workArea.x, - y: this._workArea.y + this._workArea.height - height, - width, - height - }); - const bottomRight = buildRectangle({ - x: topRight.x, - y: bottomLeft.y, - width, - height - }); - - let isRightSide = false; - let isCenterSide = false; - - previewRect.width = this._workArea.width * quarterPercentage; - previewRect.height = this._workArea.height * quarterPercentage; - previewRect.y = this._workArea.y; - // left side - if (x <= this._workArea.x + (this._workArea.width / 2)) { - previewRect.x = this._workArea.x; - // top-left corner - if (isPointInsideRect({x, y}, topLeft)) { - this._activeEdgeTile = topLeft; - // bottom-left corner - } else if (isPointInsideRect({x, y}, bottomLeft)) { - previewRect.y = this._workArea.y + this._workArea.height - previewRect.height; - this._activeEdgeTile = bottomLeft; - // top-center (full size) - } else if (y <= topRight.y + topRight.height) { - isCenterSide = true; - // center-left (full edge tile) - } else if (y <= bottomLeft.y) { - previewRect.width = this._workArea.width * quarterPercentage; - previewRect.height = this._workArea.height; - - this._activeEdgeTile.x = topLeft.x; - this._activeEdgeTile.y = topLeft.y + topLeft.height; - this._activeEdgeTile.height = bottomLeft.y - this._activeEdgeTile.y; - this._activeEdgeTile.width = topLeft.width; - // bottom-center - } else { - return; - } - // right side - } else { - isRightSide = true; - previewRect.x = this._workArea.x + this._workArea.width - previewRect.width; - // top-right corner - if (isPointInsideRect({x, y}, topRight)) { - this._activeEdgeTile = topRight; - // bottom-right corner - } else if (isPointInsideRect({x, y}, bottomRight)) { - previewRect.y = this._workArea.y + this._workArea.height - previewRect.height; - this._activeEdgeTile = bottomRight; - // top-center (full size) - } else if (y <= topRight.y + topRight.height) { - isCenterSide = true; - // center-right (full edge tile) - } else if (y <= bottomRight.y) { - previewRect.width = this._workArea.width * quarterPercentage; - previewRect.height = this._workArea.height; - - this._activeEdgeTile.x = topRight.x; - this._activeEdgeTile.y = topRight.y + topRight.height; - this._activeEdgeTile.height = bottomRight.y - this._activeEdgeTile.y; - this._activeEdgeTile.width = topRight.width; - // bottom-center - } else { - return; - } - } - - if (isCenterSide) { - previewRect.width = this._workArea.width; - previewRect.height = this._workArea.height; - previewRect.x = this._workArea.x; - - this._activeEdgeTile.x = topLeft.x + topLeft.width; - this._activeEdgeTile.y = topRight.y; - this._activeEdgeTile.height = topRight.height; - this._activeEdgeTile.width = topRight.x - this._activeEdgeTile.x; - } - - /*// uncomment to show active tile debugging - global.windowGroup.get_children().filter(c => c.get_name() === "debug")[0]?.destroy(); - const debug = new St.Widget({ - x: this._activeEdgeTile.x, - y: this._activeEdgeTile.y, - height: this._activeEdgeTile.height, - width: this._activeEdgeTile.width, - style: "border: 2px solid red", - name: "debug" - }); - global.windowGroup.add_child(debug);*/ - + private _showEdgeTiling(window: Meta.Window, edgeTile: Mtk.Rectangle, pointerX: number, pointerY: number) { this._selectedTilesPreview.gaps = buildTileGaps( - previewRect, + edgeTile, this._tilingLayout.innerGaps, this._tilingLayout.outerGaps, this._workArea, @@ -604,23 +520,22 @@ export class TilingManager { ); if (!this._selectedTilesPreview.showing) { - const startingWidth = previewRect.width * 0.2; - const startingHeight = previewRect.height * 0.2; - this._selectedTilesPreview.open( - false, - buildRectangle({ - x: isRightSide ? (previewRect.x + previewRect.width - startingWidth):previewRect.x, - y: y - (startingHeight / 2), - width: startingWidth, - height: startingHeight - }) - ); + const { left, right, top, bottom } = this._selectedTilesPreview.gaps; + const initialRect = buildRectangle({ + x: pointerX, + y: pointerY, + width: left + right + 8, // width without gaps will be 8 + height: top + bottom + 8 // height without gaps will be 8 + }); + initialRect.x -= initialRect.width / 2; + initialRect.y -= initialRect.height / 2; + this._selectedTilesPreview.open(false, initialRect); } this._selectedTilesPreview.openAbove( window, true, - previewRect, + edgeTile, ); } } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index e2dfccf..480ec0f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { Extension, ExtensionMetadata } from 'resource:///org/gnome/shell/extens import DBus from './dbus'; import KeyBindings from './keybindings'; import SettingsOverride from '@settingsOverride'; +import { ResizingManager } from '@components/tilingsystem/resizeManager'; const debug = logger('extension'); @@ -25,6 +26,7 @@ export default class TilingShellExtension extends Extension { private _dbus: DBus | null; private _signals: SignalHandling | null; private _keybindings: KeyBindings | null; + private _resizingManager: ResizingManager | null; constructor(metadata: ExtensionMetadata) { super(metadata); @@ -34,6 +36,7 @@ export default class TilingShellExtension extends Extension { this._indicator = null; this._dbus = null; this._keybindings = null; + this._resizingManager = null; } createIndicator() { @@ -85,6 +88,15 @@ export default class TilingShellExtension extends Extension { if (this._keybindings) this._keybindings.destroy(); this._keybindings = new KeyBindings(this.getSettings()); + // disable native edge tiling + if (Settings.get_active_screen_edges()) { + SettingsOverride.get().override( + new Gio.Settings({ schema_id: 'org.gnome.mutter' }), + 'edge-tiling', + new GLib.Variant('b', false) + ); + } + //@ts-ignore if (Main.layoutManager._startingUp) { this._signals.connect(Main.layoutManager, 'startup-complete', () => { @@ -95,19 +107,15 @@ export default class TilingShellExtension extends Extension { this._createTilingManagers(); this._setupSignals(); } + + this._resizingManager = new ResizingManager(); + this._resizingManager.enable(); this.createIndicator(); if (this._dbus) this._dbus.disable(); this._dbus = new DBus(); this._dbus.enable(this); - - // disable native edge tiling - SettingsOverride.get().override( - new Gio.Settings({ schema_id: 'org.gnome.mutter' }), - 'edge-tiling', - new GLib.Variant('b', false) - ); debug('extension is enabled'); } @@ -162,6 +170,17 @@ export default class TilingShellExtension extends Extension { if (this._keybindings) { this._signals.connect(this._keybindings, 'move-window', this._onKeyboardMoveWin.bind(this)); } + + this._signals.connect(Settings, Settings.SETTING_ACTIVE_SCREEN_EDGES, () => { + // disable native edge tiling + const nativeIsActive = !Settings.get_active_screen_edges(); + + SettingsOverride.get().override( + new Gio.Settings({ schema_id: 'org.gnome.mutter' }), + 'edge-tiling', + new GLib.Variant('b', nativeIsActive) + ); + }); } private _onKeyboardMoveWin(kb: KeyBindings, display: Meta.Display, direction: Meta.Direction) { @@ -190,7 +209,7 @@ export default class TilingShellExtension extends Extension { disable(): void { // bring back overridden keybindings - if (this._keybindings) this._keybindings.destroy(); + this._keybindings?.destroy(); this._keybindings = null; // destroy indicator @@ -202,11 +221,14 @@ export default class TilingShellExtension extends Extension { this._tilingManagers = []; // disconnect signals - if (this._signals) this._signals.disconnect(); + this._signals?.disconnect(); this._signals = null; + this._resizingManager?.destroy(); + this._resizingManager = null; + // disable dbus - if (this._dbus) this._dbus.disable(); + this._dbus?.disable(); this._dbus = null; // destroy state and settings diff --git a/src/indicator/defaultMenu.ts b/src/indicator/defaultMenu.ts index 30e226a..d9141a6 100644 --- a/src/indicator/defaultMenu.ts +++ b/src/indicator/defaultMenu.ts @@ -1,9 +1,11 @@ import St from 'gi://St'; import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; import SignalHandling from "@/signalHandling"; import Indicator from "./indicator"; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; -import { getScalingFactorOf } from '@/utils/ui'; +import { enableScalingFactorSupport, getMonitors, getScalingFactor, getScalingFactorOf } from '@/utils/ui'; import Settings from '@/settings'; import * as IndicatorUtils from './utils'; import GlobalState from '@/globalState'; @@ -11,37 +13,117 @@ import CurrentMenu from './currentMenu'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; import LayoutButton from './layoutButton'; import { logger } from '@utils/shell'; +import { registerGObjectClass } from '@utils/gjs'; +import { Monitor } from 'resource:///org/gnome/shell/ui/layout.js'; +import Layout from '@components/layout/Layout'; const debug = logger("DefaultMenu"); +@registerGObjectClass +class LayoutsRow extends St.BoxLayout { + static metaInfo: GObject.MetaInfo = { + GTypeName: "LayoutsRow", + Signals: { + "selected-layout": { + param_types: [ GObject.TYPE_STRING ] + }, + } + } + + private _layoutsBox: St.BoxLayout; + private _layoutsButtons: LayoutButton[]; + private _label: St.Label; + private _monitor: Monitor; + + constructor(parent: Clutter.Actor, layouts: Layout[], selectedId: string, showMonitorName: boolean, monitor: Monitor) { + super({ + xAlign: Clutter.ActorAlign.CENTER, + yAlign: Clutter.ActorAlign.CENTER, + xExpand: true, + yExpand: true, + vertical: true, + style: "spacing: 8px" + }); + this._layoutsBox = new St.BoxLayout({ + xAlign: Clutter.ActorAlign.CENTER, + yAlign: Clutter.ActorAlign.CENTER, + xExpand: true, + yExpand: true, + vertical: false, // horizontal box layout + styleClass: "layouts-box-layout" + }); + this._monitor = monitor; + this._label = new St.Label({ text: `Monitor ${this._monitor.index+1}`, styleClass: "monitor-layouts-title" }); + this.add_child(this._label); + if (!showMonitorName) this._label.hide(); + this.add_child(this._layoutsBox); + + parent.add_child(this); + + const selectedIndex = layouts.findIndex(lay => lay.id === selectedId); + const hasGaps = Settings.get_inner_gaps(1).top > 0; + + const layoutHeight: number = 36; + const layoutWidth: number = 64; // 16:9 ratio. -> (16*layoutHeight) / 9 and then rounded to int + + this._layoutsButtons = layouts.map((lay, ind) => { + const btn = new LayoutButton(this._layoutsBox, lay, hasGaps ? 2:0, layoutHeight, layoutWidth); + btn.connect('clicked', (self) => !btn.checked && this.emit("selected-layout", lay.id)); + if (ind === selectedIndex) btn.set_checked(true); + return btn; + }); + } + + public selectLayout(selectedId: string) { + const selectedIndex = GlobalState.get().layouts.findIndex(lay => lay.id === selectedId); + this._layoutsButtons.forEach((btn, ind) => btn.set_checked(ind === selectedIndex)); + } + + public updateMonitorName(showMonitorName: boolean, monitorsDetails: { name: string, x: number, y: number, height: number, width: number }[]) { + if (!showMonitorName) this._label.hide(); + else this._label.show(); + + const details = monitorsDetails.find(m => m.x === this._monitor.x && m.y === this._monitor.y); + if (!details) return; + + this._label.set_text(details.name); + } +} + export default class DefaultMenu implements CurrentMenu { private readonly _signals: SignalHandling; private readonly _indicator: Indicator; - private _layoutsBoxLayout: St.BoxLayout; - private _layoutsButtons: LayoutButton[]; + private _layoutsRows: LayoutsRow[]; + private _container: St.BoxLayout; private _scalingFactor: number; + private _children: St.Widget[]; - constructor(indicator: Indicator) { - this._layoutsButtons = []; + constructor(indicator: Indicator, enableScalingFactor: boolean) { this._indicator = indicator; this._signals = new SignalHandling(); - - this._layoutsBoxLayout = new St.BoxLayout({ + this._children = []; + const layoutsPopupMenu = new PopupMenu.PopupBaseMenuItem({ style_class: 'indicator-menu-item' }); + this._children.push(layoutsPopupMenu); + this._container = new St.BoxLayout({ xAlign: Clutter.ActorAlign.CENTER, yAlign: Clutter.ActorAlign.CENTER, xExpand: true, yExpand: true, - vertical: false, // horizontal box layout - styleClass: "layouts-box-layout" + vertical: true, + styleClass: "default-menu-container" }); - - const layoutsPopupMenu = new PopupMenu.PopupBaseMenuItem({ style_class: 'indicator-menu-item' }); - layoutsPopupMenu.add_child(this._layoutsBoxLayout); - + layoutsPopupMenu.add_child(this._container); (this._indicator.menu as PopupMenu.PopupMenu).addMenuItem(layoutsPopupMenu); - this._scalingFactor = getScalingFactorOf(this._layoutsBoxLayout)[1]; + if (enableScalingFactor) { + const monitor = Main.layoutManager.findMonitorForActor(this._container); + const scalingFactor = getScalingFactor(monitor?.index || Main.layoutManager.primaryIndex); + enableScalingFactorSupport(this._container, scalingFactor); + } + this._scalingFactor = getScalingFactorOf(this._container)[1]; + + this._layoutsRows = []; this._drawLayouts(); // update the layouts shown by the indicator when they are modified this._signals.connect(Settings, Settings.SETTING_LAYOUTS_JSON, () => { @@ -51,25 +133,84 @@ export default class DefaultMenu implements CurrentMenu { this._drawLayouts(); }); - const buttonsPopupMenu = this._buildEditingButtonsRow(); - (this._indicator.menu as PopupMenu.PopupMenu).addMenuItem(buttonsPopupMenu); - // if the selected layout was changed externaly, update the selected button this._signals.connect(Settings, Settings.SETTING_SELECTED_LAYOUTS, () => { - const selectedId = Settings.get_selected_layouts()[Main.layoutManager.primaryIndex]; - const selectedIndex = GlobalState.get().layouts.findIndex(lay => lay.id === selectedId); - if (this._layoutsButtons[selectedIndex].checked) return; - this._layoutsButtons.forEach((btn, layInd) => btn.set_checked(layInd === selectedIndex)); + this._updateScaling(); + if (this._layoutsRows.length !== getMonitors().length) { + this._drawLayouts(); + } + Settings.get_selected_layouts().forEach((selectedId, index) => { + this._layoutsRows[index].selectLayout(selectedId); + }); }); - + this._signals.connect(Main.layoutManager, 'monitors-changed', () => { - debug("monitors-changed") + if (!enableScalingFactor) return; + + const monitor = Main.layoutManager.findMonitorForActor(this._container); + const scalingFactor = getScalingFactor(monitor?.index || Main.layoutManager.primaryIndex); + enableScalingFactorSupport(this._container, scalingFactor); + this._updateScaling(); + if (this._layoutsRows.length !== getMonitors().length) { + this._drawLayouts(); + } + + // compute monitors details and update labels asynchronously (if we have successful results...) + this._computeMonitorsDetails(); }); + + // compute monitors details and update labels asynchronously (if we have successful results...) + this._computeMonitorsDetails(); + + const buttonsPopupMenu = this._buildEditingButtonsRow(); + (this._indicator.menu as PopupMenu.PopupMenu).addMenuItem(buttonsPopupMenu); + this._children.push(buttonsPopupMenu); + } + + // compute monitors details and update labels asynchronously (if we have successful results...) + private _computeMonitorsDetails() { + if (getMonitors().length === 1) { + this._layoutsRows.forEach(lr => lr.updateMonitorName(false, [])); + return; + } + + try { + // Since Gdk.Monitor has monitor's name but we can't import Gdk into gnome-shell, we run a gjs code in a subprocess. + // This code will just get all the monitors, printing into JSON format to stdout each monitor's name and geometry. + // If we are successfull, we parse the stdout of the subprocess and update monitor's name + const importsCode = imports && imports.gi ? `imports.gi.versions.Gtk = "4.0"; const { Gtk, Gdk } = imports.gi;`:`import Gtk from 'gi://Gtk?version=4.0'; import Gdk from 'gi://Gdk?version=4.0;';` + const code = `${importsCode} + Gtk.init(); + const monitors = Gdk.Display.get_default().get_monitors(); + const details = []; + for (const m of monitors) { + const { x, y, width, height } = m.get_geometry(); + details.push({ name: m.get_description(), x, y, width, height }); + } + + print(JSON.stringify(details));`; + const proc = Gio.Subprocess.new(["gjs", "-c", code], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + + proc.communicate_utf8_async(null, null, (pr: Gio.Subprocess | null, res: Gio.AsyncResult) => { + if (!pr) return; + + const [_, stdout, stderr] = pr.communicate_utf8_finish(res); + if (pr.get_successful()) { + debug(stdout); + const monitorsDetails = JSON.parse(stdout); + this._layoutsRows.forEach(lr => lr.updateMonitorName(true, monitorsDetails)); + } else { + debug("error:", stderr); + } + }); + } catch (e) { + debug(e); + } } private _updateScaling() { - const newScalingFactor = getScalingFactorOf(this._layoutsBoxLayout)[1]; + const newScalingFactor = getScalingFactorOf(this._container)[1]; if (this._scalingFactor === newScalingFactor) return; this._scalingFactor = newScalingFactor; @@ -86,7 +227,7 @@ export default class DefaultMenu implements CurrentMenu { styleClass: "buttons-box-layout" }); - const editLayoutsBtn = IndicatorUtils.createButton("document-edit-symbolic", "Edit Layouts..."); + const editLayoutsBtn = IndicatorUtils.createButton("edit-symbolic", "Edit Layouts...", this._indicator.path); editLayoutsBtn.connect('clicked', (self) => this._indicator.openLayoutEditor() ); buttonsBoxLayout.add_child(editLayoutsBtn); const newLayoutBtn = IndicatorUtils.createButton("add-symbolic", "New Layout...", this._indicator.path); @@ -101,30 +242,25 @@ export default class DefaultMenu implements CurrentMenu { private _drawLayouts() { const layouts = GlobalState.get().layouts; - this._layoutsButtons.forEach(btn => btn.destroy()); - this._layoutsButtons = []; - this._layoutsBoxLayout.remove_all_children(); - - const hasGaps = Settings.get_inner_gaps(1).top > 0; + this._container.destroy_all_children(); + this._layoutsRows = []; - const layoutHeight: number = 36; - const layoutWidth: number = 64; // 16:9 ratio. -> (16*layoutHeight) / 9 and then rounded to int - this._layoutsButtons = layouts.map((lay, btnInd) => { - const btn = new LayoutButton(this._layoutsBoxLayout, lay, hasGaps ? 2:0, layoutHeight, layoutWidth); - btn.connect('clicked', (self) => !btn.checked && this._indicator.selectLayoutOnClick(lay)); - return btn; + const selectedIdPerMonitor = Settings.get_selected_layouts(); + const monitors = getMonitors(); + this._layoutsRows = monitors.map((monitor, index) => { + const selectedId = selectedIdPerMonitor[index]; + const row = new LayoutsRow(this._container, layouts, selectedId, monitors.length > 1, monitor); + row.connect("selected-layout", (r: LayoutsRow, layoutId: string) => { + this._indicator.selectLayoutOnClick(index, layoutId) + }); + return row; }); - - const selectedId = Settings.get_selected_layouts()[Main.layoutManager.primaryIndex]; - const selectedIndex = GlobalState.get().layouts.findIndex(lay => lay.id === selectedId); - this._layoutsButtons[selectedIndex]?.set_checked(true); } public destroy() { this._signals.disconnect(); - this._layoutsButtons.forEach(btn => btn.destroy()); - this._layoutsButtons = []; - this._layoutsBoxLayout.destroy(); - (this._indicator.menu as PopupMenu.PopupMenu).removeAll(); + this._children.forEach(c => c.destroy()); + this._children = []; + this._layoutsRows = []; } } \ No newline at end of file diff --git a/src/indicator/editingMenu.ts b/src/indicator/editingMenu.ts index bc9428f..3128a58 100644 --- a/src/indicator/editingMenu.ts +++ b/src/indicator/editingMenu.ts @@ -17,7 +17,7 @@ export default class EditingMenu implements CurrentMenu { style: "spacing: 8px" }); - const openMenuBtn = IndicatorUtils.createButton("video-display-symbolic", "Menu "); + const openMenuBtn = IndicatorUtils.createButton("menu-symbolic", "Menu ", this._indicator.path); openMenuBtn.connect('clicked', (self) => this._indicator.openMenu(false) ); boxLayout.add_child(openMenuBtn); @@ -25,7 +25,7 @@ export default class EditingMenu implements CurrentMenu { infoMenuBtn.connect('clicked', (self) => this._indicator.openMenu(true) ); boxLayout.add_child(infoMenuBtn); - const saveBtn = IndicatorUtils.createButton("done-symbolic", "Save ", this._indicator.path); + const saveBtn = IndicatorUtils.createButton("save-symbolic", "Save ", this._indicator.path); saveBtn.connect('clicked', (self) => { this._indicator.menu.toggle(); this._indicator.saveLayoutOnClick(); @@ -42,12 +42,10 @@ export default class EditingMenu implements CurrentMenu { const menuItem = new PopupMenu.PopupBaseMenuItem({ style_class: 'indicator-menu-item' }); menuItem.add_child(boxLayout); - //@ts-ignore todo - this._indicator.menu.addMenuItem(menuItem); + (this._indicator.menu as PopupMenu.PopupMenu).addMenuItem(menuItem); } destroy(): void { - //@ts-ignore todo - this._indicator.menu.removeAll(); + (this._indicator.menu as PopupMenu.PopupMenu).removeAll(); } } \ No newline at end of file diff --git a/src/indicator/indicator.ts b/src/indicator/indicator.ts index 078d9b5..3db0c2c 100644 --- a/src/indicator/indicator.ts +++ b/src/indicator/indicator.ts @@ -4,7 +4,6 @@ import Shell from 'gi://Shell'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; import { logger } from '@/utils/shell'; -import { enableScalingFactorSupport, getMonitors, getScalingFactor } from '@/utils/ui'; import Settings from '@/settings'; import Layout from '@/components/layout/Layout'; import Tile from '@/components/layout/Tile'; @@ -29,8 +28,7 @@ enum IndicatorState { export default class Indicator extends PanelMenu.Button { private _layoutEditor: LayoutEditor | null; private _editorDialog: EditorDialog | null; - //@ts-ignore todo - private _currentMenu: CurrentMenu; + private _currentMenu: CurrentMenu | null; private _state: IndicatorState; private _enableScaling: boolean; private _path: string; @@ -39,8 +37,7 @@ export default class Indicator extends PanelMenu.Button { super(0.5, 'Tiling Shell Indicator', false); Main.panel.addToStatusArea(uuid, this, 1, 'right'); - // Bind the "show-indicator" setting to the "visible" property. - //@ts-ignore + // Bind the "show-indicator" setting to the "visible" property Settings.bind(Settings.SETTING_SHOW_INDICATOR, this, 'visible', Gio.SettingsBindFlags.GET); const icon = new St.Icon({ @@ -51,6 +48,7 @@ export default class Indicator extends PanelMenu.Button { this.add_child(icon); this._layoutEditor = null; this._editorDialog = null; + this._currentMenu = null; this._state = IndicatorState.DEFAULT; this._enableScaling = false; this._path = path; @@ -65,22 +63,16 @@ export default class Indicator extends PanelMenu.Button { public set enableScaling(value: boolean) { if (this._enableScaling === value) return; this._enableScaling = value; - - if (value) { - const monitor = Main.layoutManager.findMonitorForActor(this); - const scalingFactor = getScalingFactor(monitor?.index || Main.layoutManager.primaryIndex); - enableScalingFactorSupport((this.menu as PopupMenu.PopupMenu).box, scalingFactor); - } if (this._currentMenu && this._state === IndicatorState.DEFAULT) { this._currentMenu.destroy(); - this._currentMenu = new DefaultMenu(this); + this._currentMenu = new DefaultMenu(this, this._enableScaling); } } public enable() { (this.menu as PopupMenu.PopupMenu).removeAll(); - this._currentMenu = new DefaultMenu(this); + this._currentMenu = new DefaultMenu(this, this._enableScaling); // todo /*Main.panel.statusArea.appMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); @@ -113,9 +105,10 @@ export default class Indicator extends PanelMenu.Button { });*/ } - public selectLayoutOnClick(layoutToSelect: Layout) { - // change the layout of all the monitors - Settings.save_selected_layouts_json(getMonitors().map((monitor) => layoutToSelect.id)); + public selectLayoutOnClick(monitorIndex: number, layoutToSelectId: string) { + const selected = Settings.get_selected_layouts(); + selected[monitorIndex] = layoutToSelectId; + Settings.save_selected_layouts_json(selected); this.menu.toggle(); } @@ -202,10 +195,10 @@ export default class Indicator extends PanelMenu.Button { private _setState(newState: IndicatorState) { if (this._state === newState) return; this._state = newState; - this._currentMenu.destroy(); + this._currentMenu?.destroy(); switch(newState) { case IndicatorState.DEFAULT: - this._currentMenu = new DefaultMenu(this); + this._currentMenu = new DefaultMenu(this, this._enableScaling); if (!Settings.get_show_indicator()) this.hide(); break; case IndicatorState.CREATE_NEW: @@ -218,8 +211,9 @@ export default class Indicator extends PanelMenu.Button { private _onDestroy() { this._editorDialog?.destroy(); + this._editorDialog = null; this._layoutEditor?.destroy(); this._layoutEditor = null; - this._currentMenu.destroy(); + (this.menu as PopupMenu.PopupMenu).removeAll(); } } \ No newline at end of file diff --git a/src/indicator/utils.ts b/src/indicator/utils.ts index c78591d..ac1c7cc 100644 --- a/src/indicator/utils.ts +++ b/src/indicator/utils.ts @@ -31,7 +31,8 @@ export const createIconButton = (iconName: string, path?: string) : St.Button => const icon = new St.Icon({ iconSize: 16, - yAlign: Clutter.ActorAlign.CENTER + yAlign: Clutter.ActorAlign.CENTER, + style: "padding: 6px" }); if (path) { icon.gicon = Gio.icon_new_for_string(`${path}/icons/${iconName}.svg`); diff --git a/src/prefs.ts b/src/prefs.ts index 1ef3c32..2725afe 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -146,6 +146,24 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference ); behaviourGroup.add(restoreToOriginalSizeRow); + // Screen Edges section + const activeScreenEdgesGroup = new Adw.PreferencesGroup({ + title: 'Screen Edges', + description: 'Drag windows against the top, left and right screen edges to resize them', + headerSuffix: new Gtk.Switch({ vexpand: false, valign: Gtk.Align.CENTER }) + }); + Settings.bind(Settings.SETTING_ACTIVE_SCREEN_EDGES, activeScreenEdgesGroup.headerSuffix, 'active'); + + const topEdgeMaximize = this._buildSwitchRow( + Settings.SETTING_TOP_EDGE_MAXIMIZE, + "Drag against top edge to maximize window", + "Drag windows against the top edge to maximize them" + ); + Settings.bind(Settings.SETTING_ACTIVE_SCREEN_EDGES, topEdgeMaximize, 'sensitive'); + activeScreenEdgesGroup.add(topEdgeMaximize); + + prefsPage.add(activeScreenEdgesGroup); + // Layouts section const layoutsGroup = new Adw.PreferencesGroup({ title: 'Layouts', @@ -280,7 +298,6 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference description: `Use hotkeys to move the focused window through the tiles of the active layout`, headerSuffix: new Gtk.Switch({ vexpand: false, valign: Gtk.Align.CENTER }) }); - //@ts-ignore Settings.bind(Settings.SETTING_ENABLE_MOVE_KEYBINDINGS, keybindingsGroup.headerSuffix, 'active'); prefsPage.add(keybindingsGroup); @@ -290,6 +307,7 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference "Move the focused window to the tile on its right", (_: any, value: string) => Settings.set_kb_move_window_right(value) ); + Settings.bind(Settings.SETTING_ENABLE_MOVE_KEYBINDINGS, moveRightKB, 'sensitive'); keybindingsGroup.add(moveRightKB); const moveLeftKB = this._buildShortcutButtonRow( @@ -298,6 +316,7 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference "Move the focused window to the tile on its left", (_: any, value: string) => Settings.set_kb_move_window_left(value) ); + Settings.bind(Settings.SETTING_ENABLE_MOVE_KEYBINDINGS, moveLeftKB, 'sensitive'); keybindingsGroup.add(moveLeftKB); const moveUpKB = this._buildShortcutButtonRow( @@ -306,6 +325,7 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference "Move the focused window to the tile above", (_: any, value: string) => Settings.set_kb_move_window_up(value) ); + Settings.bind(Settings.SETTING_ENABLE_MOVE_KEYBINDINGS, moveUpKB, 'sensitive'); keybindingsGroup.add(moveUpKB); const moveDownKB = this._buildShortcutButtonRow( @@ -314,6 +334,7 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference "Move the focused window to the tile below", (_: any, value: string) => Settings.set_kb_move_window_down(value) ); + Settings.bind(Settings.SETTING_ENABLE_MOVE_KEYBINDINGS, moveDownKB, 'sensitive'); keybindingsGroup.add(moveDownKB); // footer @@ -357,7 +378,6 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference }); if (suffix) adwRow.add_suffix(suffix); adwRow.add_suffix(gtkSwitch); - //@ts-ignore Settings.bind(settingsKey, gtkSwitch, 'active'); return adwRow; @@ -373,7 +393,6 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference activatableWidget: spinBtn }); adwRow.add_suffix(spinBtn); - //@ts-ignore Settings.bind(settingsKey, spinBtn, 'value'); return adwRow; @@ -409,8 +428,7 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference null ); } catch (e) { - //@ts-ignore - if (e instanceof Gio.DBusError) //@ts-ignore + if (e instanceof Gio.DBusError) Gio.DBusError.strip_remote_error(e); console.error(e); diff --git a/src/settings.ts b/src/settings.ts index efb4e76..178c1a7 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -24,6 +24,8 @@ export default class Settings { static SETTING_ENABLE_BLUR_SNAP_ASSISTANT = "enable-blur-snap-assistant"; static SETTING_ENABLE_BLUR_SELECTED_TILEPREVIEW = "enable-blur-selected-tilepreview"; static SETTING_ENABLE_MOVE_KEYBINDINGS = 'enable-move-keybindings'; + static SETTING_ACTIVE_SCREEN_EDGES = 'active-screen-edges'; + static SETTING_TOP_EDGE_MAXIMIZE = 'top-edge-maximize'; static SETTING_MOVE_WINDOW_RIGHT = 'move-window-right'; static SETTING_MOVE_WINDOW_LEFT = 'move-window-left'; @@ -142,6 +144,14 @@ export default class Settings { return this._settings?.get_string(this.SETTING_OVERRIDDEN_SETTINGS) ?? '{}'; } + static get_active_screen_edges(): boolean { + return this._settings?.get_boolean(this.SETTING_ACTIVE_SCREEN_EDGES) ?? false; + } + + static get_top_edge_maximize(): boolean { + return this._settings?.get_boolean(this.SETTING_TOP_EDGE_MAXIMIZE) ?? false; + } + static set_last_version_installed(version: string) { this._settings?.set_string(this.SETTING_LAST_VERSION_NAME_INSTALLED, version); } diff --git a/src/styles/indicator.scss b/src/styles/indicator.scss index 9fdb810..4e570bf 100644 --- a/src/styles/indicator.scss +++ b/src/styles/indicator.scss @@ -19,4 +19,14 @@ .layout-button { border-width: calc($snap-assist-tile-border-width / 2); } +} + +.default-menu-container { + spacing: 16px; +} + +.monitor-layouts-title { + //font-family: monospace; + @include fontsize(9pt); + text-align: center; } \ No newline at end of file