From b2743e4feced4242fc20e1978487205a87f984bd Mon Sep 17 00:00:00 2001 From: Domenico Ferraro Date: Sun, 16 Jun 2024 18:38:19 +0200 Subject: [PATCH] v8.0: custom activation keys, always show indicator during layout editing, share layouts --- README.md | 6 + esbuild.mjs | 7 +- layouts.example.json | 137 +------ package.json | 2 +- resources/metadata.json | 3 +- ...e.shell.extensions.tilingshell.gschema.xml | 14 +- src/components/snapassist/snapAssist.ts | 3 +- .../tilepreview/selectionTilePreview.ts | 2 + src/components/tilingsystem/resizeManager.ts | 269 +++++--------- src/components/tilingsystem/tilingManager.ts | 50 ++- src/extension.ts | 4 +- src/indicator/indicator.ts | 4 +- src/prefs.ts | 340 ++++++++++++++++-- src/settings.ts | 54 ++- 14 files changed, 515 insertions(+), 380 deletions(-) diff --git a/README.md b/README.md index 82e61fa..7df6173 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,12 @@ To enable via the command line you can run /usr/bin/gnome-extensions enable tilingshell@ferrarodomenico.com ``` +To read the logs you can run + +```bash +journalctl --follow /usr/bin/gnome-shell +``` + ### Uninstall Tiling Shell To uninstall, first disable the extension and then remove it. To disable via the command line you can run diff --git a/esbuild.mjs b/esbuild.mjs index aedb698..c208628 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -35,6 +35,10 @@ function init(meta) { const prefsBanner = `// For GNOME Shell version before 45 class ExtensionPreferences { + constructor(metadata) { + this.metadata = metadata; + } + getSettings() { return imports.misc.extensionUtils.getSettings(); } @@ -47,7 +51,8 @@ function init() { } function fillPreferencesWindow(window) { - const prefs = new TilingShellExtensionPreferences(); + const metadata = imports.misc.extensionUtils.getCurrentExtension().metadata; + const prefs = new TilingShellExtensionPreferences(metadata); prefs.fillPreferencesWindow(window); } `; diff --git a/layouts.example.json b/layouts.example.json index dc7fba0..66bbe25 100644 --- a/layouts.example.json +++ b/layouts.example.json @@ -1,136 +1 @@ -[ - { - "id": "Layout 1", - "tiles": [ - { - "x": 0, - "y": 0, - "width": 0.22, - "height": 0.5, - "groups": [ - 1, - 2 - ] - }, - { - "x": 0, - "y": 0.5, - "width": 0.22, - "height": 0.5, - "groups": [ - 1, - 2 - ] - }, - { - "x": 0.22, - "y": 0, - "width": 0.56, - "height": 1, - "groups": [ - 2, - 3 - ] - }, - { - "x": 0.78, - "y": 0, - "width": 0.22, - "height": 0.5, - "groups": [ - 3, - 4 - ] - }, - { - "x": 0.78, - "y": 0.5, - "width": 0.22, - "height": 0.5, - "groups": [ - 3, - 4 - ] - } - ] - }, - { - "id": "Layout 2", - "tiles": [ - { - "x": 0, - "y": 0, - "width": 0.22, - "height": 1, - "groups": [ - 1 - ] - }, - { - "x": 0.22, - "y": 0, - "width": 0.56, - "height": 1, - "groups": [ - 1, - 2 - ] - }, - { - "x": 0.78, - "y": 0, - "width": 0.22, - "height": 1, - "groups": [ - 2 - ] - } - ] - }, - { - "id": "Layout 3", - "tiles": [ - { - "x": 0, - "y": 0, - "width": 0.33, - "height": 1, - "groups": [ - 1 - ] - }, - { - "x": 0.33, - "y": 0, - "width": 0.67, - "height": 1, - "groups": [ - 1 - ] - } - ] - }, - { - "id": "Layout 4", - "tiles": [ - { - "x": 0, - "y": 0, - "width": 0.67, - "height": 1, - "groups": [ - 1 - ] - }, - { - "x": 0.67, - "y": 0, - "width": 0.33, - "height": 1, - "groups": [ - 1 - ] - } - ] - } -] \ No newline at end of file +[{"id":"Layout 1","tiles":[{"x":0,"y":0,"width":0.22,"height":0.5,"groups":[1,2]},{"x":0,"y":0.5,"width":0.22,"height":0.5,"groups":[1,2]},{"x":0.22,"y":0,"width":0.56,"height":1,"groups":[2,3]},{"x":0.78,"y":0,"width":0.22,"height":0.5,"groups":[3,4]},{"x":0.78,"y":0.5,"width":0.22,"height":0.5,"groups":[3,4]}]},{"id":"Layout 2","tiles":[{"x":0,"y":0,"width":0.22,"height":1,"groups":[1]},{"x":0.22,"y":0,"width":0.56,"height":1,"groups":[1,2]},{"x":0.78,"y":0,"width":0.22,"height":1,"groups":[2]}]},{"id":"Layout 3","tiles":[{"x":0,"y":0,"width":0.33,"height":1,"groups":[1]},{"x":0.33,"y":0,"width":0.67,"height":1,"groups":[1]}]},{"id":"Layout 4","tiles":[{"x":0,"y":0,"width":0.67,"height":1,"groups":[1]},{"x":0.67,"y":0,"width":0.33,"height":1,"groups":[1]}]}] \ No newline at end of file diff --git a/package.json b/package.json index f1ba848..6f40fc6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tilingshell", - "version": "7.0.0", + "version": "8.0", "author": "Domenico Ferraro ", "private": true, "license": "GPL v2.0", diff --git a/resources/metadata.json b/resources/metadata.json index 6e52123..5a3ee5a 100644 --- a/resources/metadata.json +++ b/resources/metadata.json @@ -9,7 +9,8 @@ "45", "46" ], - "version": 7, + "version": 8, + "version-name": "8.0", "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 e62fdf7..efbf3e1 100644 --- a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml +++ b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml @@ -2,8 +2,8 @@ - - 0 + + "0" Last version installed Last version installed of this extension. @@ -12,6 +12,11 @@ Enable tiling system Hold CTRL while moving a window to tile it. + + [] + Tiling system activation key + Which key to hold while moving a window to activate the tiling system. + true Enable snap assist @@ -37,6 +42,11 @@ Span multiple tiles Hold ALT to span multiple tiles. + + [] + Key to span multiple tiles + Which key to hold to span multiple tiles. + '[]' Layouts diff --git a/src/components/snapassist/snapAssist.ts b/src/components/snapassist/snapAssist.ts index ac0209e..4b16190 100644 --- a/src/components/snapassist/snapAssist.ts +++ b/src/components/snapassist/snapAssist.ts @@ -165,7 +165,8 @@ class SnapAssistContent extends St.BoxLayout { } } - const size = this._isEnlarged ? this.height:(this.height/2); + const distanceWhenOpen = 8; + const size = this._isEnlarged ? (this.height + distanceWhenOpen):(this.height/2); const isNear = this.isBetween(this._container.x + this.x - this._activationAreaOffset, currPointerPos.x, this._container.x + this.x + this.width + this._activationAreaOffset) && this.isBetween(this._container.y - this._activationAreaOffset, currPointerPos.y, this._container.y + this._enlargedVerticalDistance + size + this._activationAreaOffset); diff --git a/src/components/tilepreview/selectionTilePreview.ts b/src/components/tilepreview/selectionTilePreview.ts index ea38589..b1699ab 100644 --- a/src/components/tilepreview/selectionTilePreview.ts +++ b/src/components/tilepreview/selectionTilePreview.ts @@ -19,6 +19,8 @@ export default class SelectionTilePreview extends TilePreview { this._recolor(); }); this.connect("destroy", () => St.ThemeContext.get_for_stage(global.get_stage()).disconnect(styleChangedSignalID)); + this._rect.width = this.gaps.left + this.gaps.right; + this._rect.height = this.gaps.top + this.gaps.bottom; } _init() { diff --git a/src/components/tilingsystem/resizeManager.ts b/src/components/tilingsystem/resizeManager.ts index fd4f53a..2a80ac8 100644 --- a/src/components/tilingsystem/resizeManager.ts +++ b/src/components/tilingsystem/resizeManager.ts @@ -1,154 +1,22 @@ import Meta from "gi://Meta"; import Mtk from "gi://Mtk"; import St from "gi://St"; -import Shell from "gi://Shell"; -import Clutter from "gi://Clutter"; -import * as AltTab from 'resource:///org/gnome/shell/ui/altTab.js'; import { logger } from "@/utils/shell"; import SignalHandling from "@signalHandling"; import Settings from "@settings"; import ExtendedWindow from "./extendedWindow"; -import { registerGObjectClass } from "@utils/gjs"; -import { buildRectangle } from "@utils/ui"; const debug = logger(`ResizingManager`); -const WINDOW_CLONE_RESIZE_ANIMATION_TIME = 150; -const APP_ICON_SIZE = 96; - -@registerGObjectClass -class WindowClone extends St.Widget { - private _clone: Clutter.Actor; - //private _blurWidget: St.Widget; - - constructor(window: Meta.Window) { - super({ layoutManager: new Clutter.BinLayout(), styleClass: "custom-tile-preview" }); - global.windowGroup.add_child(this); - - this._clone = this._createWindowClone(window); - this.add_child(this._clone); - const sigma = 36; - this._clone.add_effect_with_name('blur', new Shell.BlurEffect({ - //@ts-ignore - sigma: sigma, - //radius: sigma * 2, - brightness: 1, - mode: Shell.BlurMode.ACTOR, // blur the widget - })); - /*const windowContainer = new Clutter.Actor({ - //@ts-ignore - pivotPoint: new Graphene.Point({ x: 0.5, y: 0.5 }), - }); - windowContainer.layoutManager = new Shell.WindowPreviewLayout(); - this.add_child(windowContainer); - //@ts-ignore - this._clone = windowContainer.layoutManager.add_window(window); - const sigma = 36; - windowContainer.add_effect( - new Shell.BlurEffect({ - //@ts-ignore - sigma: sigma, - //radius: sigma * 2, - brightness: 1, - mode: Shell.BlurMode.ACTOR, // blur the widget - }), - );*/ - - /*this._clone.add_effect( - new Shell.BlurEffect({ - //@ts-ignore - sigma: sigma, - //radius: sigma * 2, - brightness: 1, - mode: Shell.BlurMode.ACTOR, // blur the widget - }), - );*/ - - /*this._blurWidget = new St.Widget({ width: this._clone.width, height: this._clone.height }); - this.add_child(this._blurWidget); - this._blurWidget.add_effect_with_name('blur', new Shell.BlurEffect({ - //@ts-ignore - sigma: sigma, - //radius: sigma * 2, - brightness: 1, - mode: Shell.BlurMode.BACKGROUND, // blur the widget - })); - this._blurWidget.add_style_class_name("custom-tile-preview");*/ - /*this._blurWidget.set_style("border: 2px solid white");*/ - - const box = new St.BoxLayout({ - xAlign: Clutter.ActorAlign.CENTER, - yAlign: Clutter.ActorAlign.CENTER, - xExpand: true, - yExpand: true, - vertical: true, - style: "spacing: 16px;" - }); - box.add_child(this._createAppIcon(window, APP_ICON_SIZE)); - box.add_child(new St.Label({ - xAlign: Clutter.ActorAlign.CENTER, - yAlign: Clutter.ActorAlign.CENTER, - text: window.get_title(), - style: "color: white;" - })); - this.add_child(box); - - const windowRect = window.get_frame_rect(); - this.set_position(windowRect.x, windowRect.y); - this.set_size(windowRect.width, windowRect.height); - - //this.updateEffect(); - } - - private _createWindowClone(window: Meta.Window) { - /*//@ts-ignore - const actor: Clutter.Actor = window.get_compositor_private(); - return new Clutter.Clone({ - source: actor - });*/ - //@ts-ignore - const actor: Clutter.Actor = window.get_compositor_private(); - - //@ts-ignore - let actorContent = actor.paint_to_content(window.get_frame_rect()); - let actorClone = new St.Widget({ - content: actorContent, - width: window.get_frame_rect().width, - height: window.get_frame_rect().height, - xExpand: true, - yExpand: true - }); - actorClone.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); - return actorClone; - } - - private _createAppIcon(window: Meta.Window, size: number) { - let tracker = Shell.WindowTracker.get_default(); - const app = tracker.get_window_app(window); - let appIcon = app - ? app.create_icon_texture(size) - : new St.Icon({ iconName: 'application-x-executable', iconSize: size }); - appIcon.xExpand = appIcon.yExpand = true; - appIcon.xAlign = appIcon.yAlign = Clutter.ActorAlign.CENTER; - - return appIcon; - } -} - export class ResizingManager { private readonly _signals: SignalHandling; - //private _windowToClone: Map; - constructor() { this._signals = new SignalHandling(); - //this._windowToClone = new Map(); } public destroy() { this._signals.disconnect(); - /*this._windowToClone.forEach((windowClone) => windowClone.destroy()); - this._windowToClone.clear();*/ } /** From Gnome Shell: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/altTab.js#L53 @@ -244,15 +112,6 @@ export class ResizingManager { ); } - /*private _createWindowClone(window: Meta.Window) { - if (this._windowToClone.has(window)) return; - - const windowClone = new WindowClone(window); - this._windowToClone.set(window, windowClone); - windowClone.set_opacity(0); - windowClone.hide(); - }*/ - private _oppositeSide(side: St.Side): St.Side { switch(side) { case St.Side.TOP: @@ -272,7 +131,7 @@ export class ResizingManager { const windowRect = window.get_frame_rect(); const borderRect = windowRect.copy(); const innerGaps = Settings.get_inner_gaps(); - const errorFactor = 4; + const errorFactor = innerGaps.right * 4; switch(side) { case St.Side.TOP: borderRect.height = innerGaps.top + errorFactor; @@ -337,30 +196,6 @@ export class ResizingManager { public onWindowResizingEnd(window: Meta.Window) { this._signals.disconnect(window); - /*//@ts-ignore - window.get_compositor_private().get_first_child().set_opacity(255); - - this._windowToClone.forEach((windowClone, otherWindow) => { - //@ts-ignore - otherWindow.get_compositor_private().show(); - otherWindow.move_resize_frame( - false, - windowClone.x, - windowClone.y, - windowClone.width, - windowClone.height - ); - - //@ts-ignore - windowClone.ease({ - opacity: 0, - duration: WINDOW_CLONE_RESIZE_ANIMATION_TIME, - onComplete: () => { - windowClone.destroy(); - } - }); - }); - this._windowToClone.clear();*/ } private _onResizingWindow( @@ -371,13 +206,6 @@ export class ResizingManager { windowsToResize: [Meta.Window, Mtk.Rectangle, number, number][] ) { const currentRect = window.get_frame_rect(); - /*if (this._windowToClone.has(window)) { - this._windowToClone.get(window)?.show(); - - this._windowToClone.get(window)?.set_opacity(255); - this._windowToClone.get(window)?.set_position(currentRect.x, currentRect.y); - this._windowToClone.get(window)?.set_size(currentRect.width, currentRect.height); - }*/ const resizedRect = { x: (currentRect.x - startingRect.x), @@ -409,15 +237,6 @@ export class ResizingManager { rect[3] = otherWindowRect.height + (isSameVerticalSide ? resizedRect.height:resizedRect.y); } - /*if (this._windowToClone.has(otherWindow)) { - this._windowToClone.get(otherWindow)?.show(); - - this._windowToClone.get(otherWindow)?.set_opacity(255); - this._windowToClone.get(otherWindow)?.set_position(Math.max(0, rect[0]), Math.max(0, rect[1])); - this._windowToClone.get(otherWindow)?.set_size(Math.max(1, rect[2]), Math.max(1, rect[3])); - } - //@ts-ignore - otherWindow.get_compositor_private().hide();*/ otherWindow.move_resize_frame( false, Math.max(0, rect[0]), @@ -427,4 +246,88 @@ export class ResizingManager { ); }); } -} \ No newline at end of file +} + +/* +const WINDOW_CLONE_RESIZE_ANIMATION_TIME = 150; +const APP_ICON_SIZE = 96; + +@registerGObjectClass +class WindowClone extends St.Widget { + private _clone: Clutter.Actor; + //private _blurWidget: St.Widget; + + constructor(window: Meta.Window) { + super({ layoutManager: new Clutter.BinLayout(), styleClass: "custom-tile-preview" }); + global.windowGroup.add_child(this); + + this._clone = this._createWindowClone(window); + this.add_child(this._clone); + const sigma = 36; + this._clone.add_effect_with_name('blur', new Shell.BlurEffect({ + //@ts-ignore + sigma: sigma, + //radius: sigma * 2, + brightness: 1, + mode: Shell.BlurMode.ACTOR, // blur the widget + })); + + const box = new St.BoxLayout({ + xAlign: Clutter.ActorAlign.CENTER, + yAlign: Clutter.ActorAlign.CENTER, + xExpand: true, + yExpand: true, + vertical: true, + style: "spacing: 16px;" + }); + box.add_child(this._createAppIcon(window, APP_ICON_SIZE)); + box.add_child(new St.Label({ + xAlign: Clutter.ActorAlign.CENTER, + yAlign: Clutter.ActorAlign.CENTER, + text: window.get_title(), + style: "color: white;" + })); + this.add_child(box); + + const windowRect = window.get_frame_rect(); + this.set_position(windowRect.x, windowRect.y); + this.set_size(windowRect.width, windowRect.height); + + //this.updateEffect(); + } + + private _createWindowClone(window: Meta.Window) { + //@ts-ignore + //const actor: Clutter.Actor = window.get_compositor_private(); + //return new Clutter.Clone({ + // source: actor + //}); + //@ts-ignore + const actor: Clutter.Actor = window.get_compositor_private(); + + //@ts-ignore + let actorContent = actor.paint_to_content(window.get_frame_rect()); + let actorClone = new St.Widget({ + content: actorContent, + width: window.get_frame_rect().width, + height: window.get_frame_rect().height, + xExpand: true, + yExpand: true + }); + actorClone.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); + return actorClone; + } + + private _createAppIcon(window: Meta.Window, size: number) { + let tracker = Shell.WindowTracker.get_default(); + const app = tracker.get_window_app(window); + let appIcon = app + ? app.create_icon_texture(size) + : new St.Icon({ iconName: 'application-x-executable', iconSize: size }); + appIcon.xExpand = appIcon.yExpand = true; + appIcon.xAlign = appIcon.yAlign = Clutter.ActorAlign.CENTER; + + return appIcon; + } +} +*/ \ No newline at end of file diff --git a/src/components/tilingsystem/tilingManager.ts b/src/components/tilingsystem/tilingManager.ts index d732fa4..0604a9a 100644 --- a/src/components/tilingsystem/tilingManager.ts +++ b/src/components/tilingsystem/tilingManager.ts @@ -8,7 +8,7 @@ import Clutter from "gi://Clutter"; import GLib from "gi://GLib"; import SnapAssist from '../snapassist/snapAssist'; import SelectionTilePreview from '../tilepreview/selectionTilePreview'; -import Settings from '@/settings'; +import Settings, { ActivationKey } from '@/settings'; import SignalHandling from '@/signalHandling'; import Layout from '../layout/Layout'; import Tile from '../layout/Tile'; @@ -34,8 +34,8 @@ export class TilingManager { private _isGrabbingWindow: boolean; private _movingWindowTimerDuration: number = 15; private _lastCursorPos: {x: number, y: number} | null = null; - private _wasAltPressed: boolean; - private _wasCtrlPressed: boolean; + private _wasSpanMultipleTilesActivated: boolean; + private _wasTilingSystemActivated: boolean; private _isSnapAssisting: boolean; private _movingWindowTimerId: number | null = null; @@ -49,8 +49,8 @@ export class TilingManager { */ constructor(monitor: Monitor, enableScaling: boolean) { this._isGrabbingWindow = false; - this._wasAltPressed = false; - this._wasCtrlPressed = false; + this._wasSpanMultipleTilesActivated = false; + this._wasTilingSystemActivated = false; this._isSnapAssisting = false; this._enableScaling = enableScaling; this._monitor = monitor; @@ -169,6 +169,17 @@ export class TilingManager { this._onMovingWindow(window); } + private _activationKeyToNumber(key: ActivationKey) { + switch (key) { + case ActivationKey.CTRL: + return 2//Clutter.ModifierType.CONTROL_MASK + case ActivationKey.ALT: + return 3//Clutter.ModifierType.MOD1_MASK + case ActivationKey.SUPER: + return 6//Clutter.ModifierType.SUPER_MASK + } + } + private _onMovingWindow(window: Meta.Window) { // if the window is no longer grabbed, disable handler if (!this._isGrabbingWindow) { @@ -206,24 +217,25 @@ export class TilingManager { const [x, y, modifier] = global.get_pointer(); const currPointerPos = { x, y }; - const isAltPressed = (modifier & Clutter.ModifierType.MOD1_MASK) != 0; - const isCtrlPressed = (modifier & Clutter.ModifierType.CONTROL_MASK) != 0; - const allowSpanMultipleTiles = Settings.get_span_multiple_tiles() && isAltPressed; - const showTilingSystem = Settings.get_tiling_system_enabled() && isCtrlPressed; + + const isSpanMultiTilesActivated = (modifier & 1 << this._activationKeyToNumber(Settings.get_span_multiple_tiles_activation_key())) != 0; + const isTilingSystemActivated = (modifier & 1 << this._activationKeyToNumber(Settings.get_tiling_system_activation_key())) != 0; + const allowSpanMultipleTiles = Settings.get_span_multiple_tiles() && isSpanMultiTilesActivated; + const showTilingSystem = Settings.get_tiling_system_enabled() && isTilingSystemActivated; // ensure we handle window movement only when needed - // if the ALT key status is not changed and the mouse is on the same position as before - // and the CTRL key status is not changed, we have nothing to do - const changedSpanMultipleTiles = Settings.get_span_multiple_tiles() && isAltPressed !== this._wasAltPressed; - const changedShowTilingSystem = Settings.get_tiling_system_enabled() && isCtrlPressed !== this._wasCtrlPressed; + // if the snap assistant activation key status is not changed and the mouse is on the same position as before + // and the tiling system activation key status is not changed, we have nothing to do + const changedSpanMultipleTiles = Settings.get_span_multiple_tiles() && isSpanMultiTilesActivated !== this._wasSpanMultipleTilesActivated; + const changedShowTilingSystem = Settings.get_tiling_system_enabled() && isTilingSystemActivated !== this._wasTilingSystemActivated; if (!changedSpanMultipleTiles && !changedShowTilingSystem && currPointerPos.x === this._lastCursorPos?.x && currPointerPos.y === this._lastCursorPos?.y) { return GLib.SOURCE_CONTINUE; } this._lastCursorPos = currPointerPos; - this._wasCtrlPressed = isCtrlPressed; - this._wasAltPressed = isAltPressed; + this._wasTilingSystemActivated = isTilingSystemActivated; + this._wasSpanMultipleTilesActivated = isSpanMultiTilesActivated; - // layout must not be shown if it was disabled or if it is enabled but CTRL key is not pressed + // layout must not be shown if it was disabled or if it is enabled but tiling system activation key is not pressed // then close it and open snap assist (if enabled) if (!showTilingSystem) { if (this._tilingLayout.showing) { @@ -258,7 +270,7 @@ export class TilingManager { if (!selectionRect) return GLib.SOURCE_CONTINUE; selectionRect = selectionRect.copy(); - if (allowSpanMultipleTiles) { + if (allowSpanMultipleTiles && this._selectedTilesPreview.showing) { selectionRect = selectionRect.union(this._selectedTilesPreview.rect); } this._tilingLayout.hoverTilesInRect(selectionRect, !allowSpanMultipleTiles); @@ -292,8 +304,8 @@ export class TilingManager { this._snapAssist.close(true); this._lastCursorPos = null; - const isCtrlPressed = (global.get_pointer()[2] & Clutter.ModifierType.CONTROL_MASK); - if (!isCtrlPressed && !this._isSnapAssisting) return; + const isTilingSystemActivated = (global.get_pointer()[2] & 1 << this._activationKeyToNumber(Settings.get_tiling_system_activation_key())) != 0; + if (!isTilingSystemActivated && !this._isSnapAssisting) return; // disable snap assistance this._isSnapAssisting = false; diff --git a/src/extension.ts b/src/extension.ts index aace2d4..2c073cb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -42,8 +42,8 @@ export default class TilingShellExtension extends Extension { private _validateSettings() { // Setting used for compatibility changes if necessary // Settings.get_last_version_installed() - if (this.metadata.version) { - Settings.set_last_version_installed(Number(this.metadata.version)); + if (this.metadata['version-name']) { + Settings.set_last_version_installed(this.metadata['version-name'] || "0"); } const selectedLayouts = Settings.get_selected_layouts(); diff --git a/src/indicator/indicator.ts b/src/indicator/indicator.ts index e17861a..1e33f4c 100644 --- a/src/indicator/indicator.ts +++ b/src/indicator/indicator.ts @@ -41,7 +41,7 @@ export default class Indicator extends PanelMenu.Button { // Bind the "show-indicator" setting to the "visible" property. //@ts-ignore - Settings.bind(Settings.SETTING_SHOW_INDICATOR, this, 'visible'); + Settings.bind(Settings.SETTING_SHOW_INDICATOR, this, 'visible', Gio.SettingsBindFlags.GET); const icon = new St.Icon({ gicon: Gio.icon_new_for_string(`${path}/icons/indicator-symbolic.svg`), @@ -205,10 +205,12 @@ export default class Indicator extends PanelMenu.Button { switch(newState) { case IndicatorState.DEFAULT: this._currentMenu = new DefaultMenu(this); + if (!Settings.get_show_indicator()) this.hide(); break; case IndicatorState.CREATE_NEW: case IndicatorState.EDITING_LAYOUT: this._currentMenu = new EditingMenu(this); + this.show(); break; } } diff --git a/src/prefs.ts b/src/prefs.ts index 899febf..e6d264a 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -1,11 +1,13 @@ import Gtk from "gi://Gtk"; // Starting from GNOME 40, the preferences dialog uses GTK4 import Adw from "gi://Adw"; import Gio from "gi://Gio"; -import Settings from "./settings"; +import GLib from "gi://GLib"; +import Settings, { ActivationKey, activationKeys } from "./settings"; import { logger } from "./utils/shell"; import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; +import Layout from "@components/layout/Layout"; + /*import Layout from "@/components/layout/Layout"; -import GObject from "gi://GObject"; import Cairo from "@gi-types/cairo1";*/ const debug = logger("prefs"); @@ -30,7 +32,7 @@ function buildPrefsWidget(): Gtk.Widget { export default class TilingShellExtensionPreferences extends ExtensionPreferences { private readonly NAME = "Tiling Shell"; - + /** * This function is called when the preferences window is first created to fill * the `Adw.PreferencesWindow`. @@ -82,27 +84,35 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference }); prefsPage.add(behaviourGroup); - const restoreToOriginalSizeRow = this._buildSwitchRow( - Settings.SETTING_RESTORE_WINDOW_ORIGINAL_SIZE, - "Restore window size", - "Whether to restore the windows to their original size when untiled" + const snapAssistRow = this._buildSwitchRow( + Settings.SETTING_SNAP_ASSIST, + "Enable Snap Assistant", + "Move the window on top of the screen to snap assist it" ); - behaviourGroup.add(restoreToOriginalSizeRow); + behaviourGroup.add(snapAssistRow); - const pressCtrlRow = this._buildSwitchRow( + const enableTilingSystemRow = this._buildSwitchRow( Settings.SETTING_TILING_SYSTEM, - "Enable tiling system", - "Hold CTRL while moving a window to tile it" + "Enable Tiling System", + "Hold the activation key while moving a window to tile it", + this._buildShortcutButton( + Settings.get_tiling_system_activation_key(), + (newVal: ActivationKey) => Settings.set_tiling_system_activation_key(newVal) + ) ); - behaviourGroup.add(pressCtrlRow); + behaviourGroup.add(enableTilingSystemRow); - const pressAltRow = this._buildSwitchRow( + const spanMultipleTilesRow = this._buildSwitchRow( Settings.SETTING_SPAN_MULTIPLE_TILES, "Span multiple tiles", - "Hold ALT to span multiple tiles" + "Hold the activation key to span multiple tiles", + this._buildShortcutButton( + Settings.get_span_multiple_tiles_activation_key(), + (newVal: ActivationKey) => Settings.set_span_multiple_tiles_activation_key(newVal) + ) ); - behaviourGroup.add(pressAltRow); - + behaviourGroup.add(spanMultipleTilesRow); + const resizeComplementingRow = this._buildSwitchRow( Settings.SETTING_RESIZE_COMPLEMENTING_WINDOWS, "Enable auto-resize of the complementing tiled windows", @@ -110,12 +120,12 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference ); behaviourGroup.add(resizeComplementingRow); - const snapAssistRow = this._buildSwitchRow( - Settings.SETTING_SNAP_ASSIST, - "Enable snap assist", - "Move the window on top of the screen to snap assist it" + const restoreToOriginalSizeRow = this._buildSwitchRow( + Settings.SETTING_RESTORE_WINDOW_ORIGINAL_SIZE, + "Restore window size", + "Whether to restore the windows to their original size when untiled" ); - behaviourGroup.add(snapAssistRow); + behaviourGroup.add(restoreToOriginalSizeRow); // Layouts section const layoutsGroup = new Adw.PreferencesGroup({ @@ -124,7 +134,7 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference }); prefsPage.add(layoutsGroup); - const editLayoutsBtn =this._buildButtonRow( + const editLayoutsBtn = this._buildButtonRow( "Edit layouts", "Edit layouts", "Open the layouts editor", @@ -132,7 +142,107 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference ); layoutsGroup.add(editLayoutsBtn); - const resetBtn =this._buildButtonRow( + const exportLayoutsBtn = this._buildButtonRow( + "Export layouts", + "Export layouts", + "Export layouts to a file", + () => { + const fc = new Gtk.FileChooserDialog({ + title: "Export layouts", + select_multiple: false, + action: Gtk.FileChooserAction.SAVE, + transient_for: window, + filter: new Gtk.FileFilter({ suffixes: ["json"], name: "JSON" }), + }); + fc.set_current_folder(Gio.File.new_for_path(GLib.get_home_dir())); + fc.add_button("Cancel", Gtk.ResponseType.CANCEL); + fc.add_button("Save", Gtk.ResponseType.OK); + fc.connect("response", (_source: Gtk.FileChooserDialog, response_id: number) => { + try { + if (response_id === Gtk.ResponseType.OK) { + const file = _source.get_file(); + if (!file) throw "no file selected"; + + debug(`Create file with path ${file.get_path()}`); + const content = JSON.stringify(Settings.get_layouts_json()); + file.replace_contents_bytes_async( + new TextEncoder().encode(content), + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null, + (file, res) => { + try { + file?.replace_contents_finish(res); + } catch (e) { + debug(e); + } + } + ); + } + } catch (error: any) { + debug(error); + } + + _source.destroy(); + }); + + fc.present(); + } + ); + layoutsGroup.add(exportLayoutsBtn); + + const importLayoutsBtn = this._buildButtonRow( + "Import layouts", + "Import layouts", + "Import layouts from a file. The current layouts will be replaced and this operation cannot be reverted", + () => { + const fc = new Gtk.FileChooserDialog({ + title: "Select layouts file", + select_multiple: false, + action: Gtk.FileChooserAction.OPEN, + transient_for: window, + filter: new Gtk.FileFilter({ suffixes: ["json"], name: "JSON" }), + }); + fc.set_current_folder(Gio.File.new_for_path(GLib.get_home_dir())); + fc.add_button("Cancel", Gtk.ResponseType.CANCEL); + fc.add_button("Open", Gtk.ResponseType.OK); + fc.connect("response", (_source: Gtk.FileChooserDialog, response_id: number) => { + try { + if (response_id === Gtk.ResponseType.OK) { + const file = _source.get_file(); + if (!file) { + _source.destroy(); + return; + } + debug(`Selected path ${file.get_path()}`); + const [success, content, etags] = file.load_contents(null); + if (success) { + let layouts = JSON.parse(new TextDecoder("utf-8").decode(content)) as Layout[]; + if (layouts.length === 0) throw "At least one layout is required"; + + layouts = layouts.filter(layout => layout.tiles.length > 0); + Settings.save_layouts_json(layouts); + const selected = Settings.get_selected_layouts().map(val => layouts[0].id); + Settings.save_selected_layouts_json(selected); + } else { + debug("Error while opening file"); + } + } + } catch (error: any) { + debug(error); + } + + _source.destroy(); + }); + + fc.present(); + }, + "destructive-action" + ); + layoutsGroup.add(importLayoutsBtn); + + const resetBtn = this._buildButtonRow( "Reset layouts", "Reset layouts", "Bring back the default layouts", @@ -145,20 +255,41 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference "destructive-action" ); layoutsGroup.add(resetBtn); + + const footerGroup = new Adw.PreferencesGroup(); + prefsPage.add(footerGroup); + + // footer + footerGroup.add(new Gtk.Label({ + label: "Have issues, you want to suggest a new feature or contribute?", + margin_bottom: 4 + })); + footerGroup.add(new Gtk.Label({ + label: "Open a new issue on GitHub!", + useMarkup: true, + margin_bottom: 16 + })); window.searchEnabled = true; window.connect('close-request', () => { Settings.destroy(); }); + + if (this.metadata["version-name"]) { + footerGroup.add(new Gtk.Label({ + label: `· Tiling Shell v${this.metadata["version-name"]} ·`, + })); + } } - _buildSwitchRow(settingsKey: string, title: string, subtitle: string): Adw.ActionRow { + _buildSwitchRow(settingsKey: string, title: string, subtitle: string, suffix?: Gtk.Widget): Adw.ActionRow { const gtkSwitch = new Gtk.Switch({ vexpand: false, valign: Gtk.Align.CENTER }); const adwRow = new Adw.ActionRow({ title: title, subtitle: subtitle, activatableWidget: gtkSwitch }); + if (suffix) adwRow.add_suffix(suffix); adwRow.add_suffix(gtkSwitch); //@ts-ignore Settings.bind(settingsKey, gtkSwitch, 'active'); @@ -219,9 +350,168 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference console.error(e); } } + + _buildShortcutButton(value: ActivationKey, onSelected: (v: ActivationKey) => void, styleClass?: string) { + const options = new Gtk.StringList(); + activationKeys.forEach(k => options.append(ActivationKey[k])); + const dropdown = new Gtk.DropDown({ + model: options, + selected: value + }); + dropdown.connect("notify::selected-item", (_: Gtk.DropDown) => { + const selected = activationKeys[dropdown.get_selected()]; + onSelected(selected); + }); + if (styleClass) dropdown.add_css_class(styleClass); + dropdown.set_vexpand(false); + dropdown.set_valign(Gtk.Align.CENTER); + return dropdown; + } } -//export default { init, fillPreferencesWindow }; +//@ts-ignore +/*const genParam = (type: string, name: string, ...dflt: any[]) => GObject.ParamSpec[type](name, name, name, GObject.ParamFlags.READWRITE, ...dflt); + +const ShortcutSettingButton = class extends Gtk.Button { + + static { + GObject.registerClass({ + Properties: { + shortcut: genParam('string', 'shortcut', ''), + }, + Signals: { + changed: { param_types: [GObject.TYPE_STRING] }, + }, + }, this); + } + + private _editor: Adw.Window | null; + private _label: Gtk.ShortcutLabel; + + constructor() { + super({ + halign: Gtk.Align.CENTER, + hexpand: false, + vexpand: false, + has_frame: false + }); + + this._editor = null; + this._label = new Gtk.ShortcutLabel({ + disabled_text: 'New accelerator…', + valign: Gtk.Align.CENTER, + hexpand: false, + vexpand: false + }); + + this.set_child(this._label); + + // Bind signals + this.connect('clicked', this._onActivated.bind(this)); + this.bind_property('shortcut', this._label, 'accelerator', GObject.BindingFlags.DEFAULT); + this.shortcut = ''; + } + + isAcceleratorSet() { + if(this._label.get_accelerator()) { + return true; + } else { + return false; + } + } + + _onActivated(widget: Gtk.Widget) { + let ctl = new Gtk.EventControllerKey(); + + let content = new Adw.StatusPage({ + title: 'New accelerator…', + //description: this._description, + icon_name: 'preferences-desktop-keyboard-shortcuts-symbolic', + }); + + this._editor = new Adw.Window({ + modal: true, + hide_on_close: true, + //@ts-ignore + transient_for: widget.get_root(), + width_request: 480, + height_request: 320, + content, + }); + + this._editor.add_controller(ctl); + ctl.connect('key-pressed', this._onKeyPressed.bind(this)); + this._editor.present(); + } + + _onKeyPressed(_widget: Gtk.Widget, keyval: number, keycode: number, state: number) { + let mask = state & Gtk.accelerator_get_default_mod_mask(); + mask &= ~Gdk.ModifierType.LOCK_MASK; + + if (!mask && keyval === Gdk.KEY_Escape) { + this._editor?.close(); + return Gdk.EVENT_STOP; + } + + if (!this.isValidBinding(mask, keycode, keyval) || !this.isValidAccel(mask, keyval)) + return Gdk.EVENT_STOP; + + if (!keyval && !keycode) { + this._editor?.destroy(); + return Gdk.EVENT_STOP; + } else { + this.shortcut = Gtk.accelerator_name_with_keycode(null, keyval, keycode, mask); + } + + this.emit('changed', this.shortcut); + this._editor?.destroy(); + return Gdk.EVENT_STOP; + } + + // Functions from https://gitlab.gnome.org/GNOME/gnome-control-center/-/blob/main/panels/keyboard/keyboard-shortcuts.c + + keyvalIsForbidden(keyval: number) { + return [ + // Navigation keys + Gdk.KEY_Home, + Gdk.KEY_Left, + Gdk.KEY_Up, + Gdk.KEY_Right, + Gdk.KEY_Down, + Gdk.KEY_Page_Up, + Gdk.KEY_Page_Down, + Gdk.KEY_End, + Gdk.KEY_Tab, + + // Return + Gdk.KEY_KP_Enter, + Gdk.KEY_Return, + + Gdk.KEY_Mode_switch, + ].includes(keyval); + } + + isValidBinding(mask: number, keycode: number, keyval: number) { + //@ts-ignore + return !(mask === 0 || mask === Gdk.SHIFT_MASK && keycode !== 0 && + ((keyval >= Gdk.KEY_a && keyval <= Gdk.KEY_z) || + (keyval >= Gdk.KEY_A && keyval <= Gdk.KEY_Z) || + (keyval >= Gdk.KEY_0 && keyval <= Gdk.KEY_9) || + (keyval >= Gdk.KEY_kana_fullstop && keyval <= Gdk.KEY_semivoicedsound) || + (keyval >= Gdk.KEY_Arabic_comma && keyval <= Gdk.KEY_Arabic_sukun) || + (keyval >= Gdk.KEY_Serbian_dje && keyval <= Gdk.KEY_Cyrillic_HARDSIGN) || + (keyval >= Gdk.KEY_Greek_ALPHAaccent && keyval <= Gdk.KEY_Greek_omega) || + (keyval >= Gdk.KEY_hebrew_doublelowline && keyval <= Gdk.KEY_hebrew_taf) || + (keyval >= Gdk.KEY_Thai_kokai && keyval <= Gdk.KEY_Thai_lekkao) || + (keyval >= Gdk.KEY_Hangul_Kiyeog && keyval <= Gdk.KEY_Hangul_J_YeorinHieuh) || + (keyval === Gdk.KEY_space && mask === 0) || this.keyvalIsForbidden(keyval)) + ); + } + + isValidAccel(mask: number, keyval: number) { + return Gtk.accelerator_valid(keyval, mask) || (keyval === Gdk.KEY_Tab && mask !== 0); + } +};*/ /*class LayoutWidget extends Gtk.DrawingArea { private _layout: Layout; diff --git a/src/settings.ts b/src/settings.ts index d71835f..ad2a1e8 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -7,13 +7,15 @@ export default class Settings { static _settings: Gio.Settings | null; static _is_initialized: boolean = false; - static SETTING_LAST_VERSION_INSTALLED = 'last-version-installed'; + static SETTING_LAST_VERSION_NAME_INSTALLED = 'last-version-name-installed'; static SETTING_TILING_SYSTEM = 'enable-tiling-system'; + static SETTING_TILING_SYSTEM_ACTIVATION_KEY = 'tiling-system-activation-key'; static SETTING_SNAP_ASSIST = 'enable-snap-assist'; static SETTING_SHOW_INDICATOR = 'show-indicator'; static SETTING_INNER_GAPS = 'inner-gaps'; static SETTING_OUTER_GAPS = 'outer-gaps'; static SETTING_SPAN_MULTIPLE_TILES = 'enable-span-multiple-tiles'; + static SETTING_SPAN_MULTIPLE_TILES_ACTIVATION_KEY = 'span-multiple-tiles-activation-key'; static SETTING_LAYOUTS_JSON = 'layouts-json'; static SETTING_SELECTED_LAYOUTS = 'selected-layouts'; static SETTING_RESTORE_WINDOW_ORIGINAL_SIZE = 'restore-window-original-size'; @@ -33,13 +35,13 @@ export default class Settings { } } - static bind(key: string, object: GObject.Object, property: string): void { + static bind(key: string, object: GObject.Object, property: string, flags: Gio.SettingsBindFlags = Gio.SettingsBindFlags.DEFAULT): void { //@ts-ignore - this._settings?.bind(key, object, property, Gio.SettingsBindFlags.DEFAULT); + this._settings?.bind(key, object, property, flags); } - static get_last_version_installed() : number { - return this._settings?.get_uint(this.SETTING_LAST_VERSION_INSTALLED) || -1; + static get_last_version_installed() : string { + return this._settings?.get_string(this.SETTING_LAST_VERSION_NAME_INSTALLED) || "0"; } static get_tiling_system_enabled() : boolean { @@ -51,7 +53,8 @@ export default class Settings { } static get_show_indicator() : boolean { - return this._settings?.get_boolean(this.SETTING_SHOW_INDICATOR) || true; + if (!this._settings) return true; + return this._settings.get_boolean(this.SETTING_SHOW_INDICATOR); } static get_inner_gaps(scaleFactor: number = 1) : { top: number, bottom: number, left: number, right: number } { @@ -103,8 +106,32 @@ export default class Settings { return this._settings?.get_boolean(Settings.SETTING_RESIZE_COMPLEMENTING_WINDOWS) || false; } - static set_last_version_installed(version: number) { - this._settings?.set_uint(this.SETTING_LAST_VERSION_INSTALLED, version); + static get_tiling_system_activation_key() : ActivationKey { + const val = this._settings?.get_strv(this.SETTING_TILING_SYSTEM_ACTIVATION_KEY); + if (!val || val.length === 0) return ActivationKey.CTRL; + return Number(val[0]); + } + + static get_span_multiple_tiles_activation_key() : ActivationKey { + const val = this._settings?.get_strv(this.SETTING_SPAN_MULTIPLE_TILES_ACTIVATION_KEY); + if (!val || val.length === 0) return ActivationKey.ALT; + return Number(val[0]); + } + + static set_last_version_installed(version: string) { + this._settings?.set_string(this.SETTING_LAST_VERSION_NAME_INSTALLED, version); + } + + static set_tiling_system_activation_key(key: ActivationKey) { + this._settings?.set_strv(this.SETTING_TILING_SYSTEM_ACTIVATION_KEY, [String(key)]); + } + + static set_span_multiple_tiles_activation_key(key: ActivationKey) { + this._settings?.set_strv(this.SETTING_SPAN_MULTIPLE_TILES_ACTIVATION_KEY, [String(key)]); + } + + static set_show_indicator(value: boolean) { + this._settings?.set_boolean(this.SETTING_SHOW_INDICATOR, value); } static reset_layouts_json() { @@ -149,3 +176,14 @@ export default class Settings { } } +export enum ActivationKey { + CTRL = 0, + ALT, + SUPER +} + +export const activationKeys = [ + ActivationKey.CTRL, + ActivationKey.ALT, + ActivationKey.SUPER +]; \ No newline at end of file