Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55dd6ca691 | |||
| 573ddc2702 | |||
| a4ce7f2613 | |||
| f2971f2c1c | |||
| 72fee16254 | |||
| d127480261 | |||
| 24f5dba546 | |||
| c3c47dc025 | |||
| c83e987550 | |||
| f5c5890fe4 | |||
| dd6b1d067e | |||
| 18e5f4594c | |||
| a101ad0988 | |||
| 8feccbde10 | |||
| 7c9e3e8122 | |||
| b3e00c8f94 | |||
| b50c2780d3 | |||
| a09c934f1b | |||
| 9a9e724654 | |||
| bb28c6b936 | |||
| 6bf3b17054 | |||
| 0e14ecbd18 | |||
| 20d9e94417 | |||
| 58ca81740b | |||
| 405871c0f3 | |||
| 053f5fe90a | |||
| 6165bc6b62 | |||
| 1289af64ce | |||
| 7ac6d58665 | |||
| 6e03f07486 | |||
| f07c66101b | |||
| 992438d480 | |||
| a5b888b58f | |||
| 8a14b04958 | |||
| c34cc0f48e | |||
| 825e35ff05 | |||
| cef62f2ad1 | |||
| 7422622b4d | |||
| f9cd5255c3 | |||
| 374f857152 | |||
| 81ea1db46f | |||
| 1b2025cb81 |
@@ -1,6 +0,0 @@
|
|||||||
# GNOME Shell Extension specific
|
|
||||||
schemas/gschemas.compiled
|
|
||||||
|
|
||||||
# Common temporary files
|
|
||||||
*~
|
|
||||||
*.swp
|
|
||||||
@@ -1,87 +1,119 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Simple‑Tiling – Makefile
|
# Simple-Tiling – Makefile
|
||||||
#
|
#
|
||||||
# make build → baut beide ZIP‑Pakete
|
# make build → Erzeugt alle drei Versionen als Archivdatei
|
||||||
# make build-legacy → nur Legacy‑ZIP (Shell 3.38‑44)
|
# make build-legacy → Erzeugt Legacy-ZIP (Shell 3.38)
|
||||||
# make build-modern → nur Modern‑ZIP (Shell 45‑48)
|
# make build-interim → Erzeugt Interim-ZIP (Shell 40-44)
|
||||||
# make clean → räumt auf
|
# make build-modern → Erzeugt Modern-ZIP (Shell 45+)
|
||||||
|
# make install-legacy → Installiert Legacy Extension
|
||||||
|
# make install-interim → Installiert Interim Extension
|
||||||
|
# make install-modern → Installiert Modern Extension
|
||||||
|
# make clean → Bereinigt das Ausgangsverzeichnis
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
UUID := simple-tiling@domoel
|
UUID := simple-tiling@domoel
|
||||||
VERSION := 6
|
VERSION := 7.1
|
||||||
|
EXTDIR := $(HOME)/.local/share/gnome-shell/extensions
|
||||||
|
|
||||||
# Dateien/Ordner, die in *beide* Pakete gehören
|
COMMON_FILES := prefs.js schemas exceptions.txt locale *.css README.md LICENSE
|
||||||
COMMON_FILES := schemas exceptions.txt locale *.css README.md LICENSE
|
LEGACY_PREFS := prefs_legacy.js
|
||||||
|
INTERIM_PREFS := prefs_interim.js
|
||||||
# Pref‑Dateien (zwei Varianten)
|
MODERN_PREFS := prefs_modern.js
|
||||||
LEGACY_PREFS := prefs_legacy.js
|
|
||||||
MODERN_PREFS := prefs_modern.js
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Helfer: copies <file list> <dest>
|
# Helper: copies <file list> <dest>
|
||||||
###############################################################################
|
###############################################################################
|
||||||
define copies
|
define copies
|
||||||
@for f in $(1) ; do \
|
@for f in $(1) ; do \
|
||||||
if [ -e $$f ] ; then \
|
if [ -e $$f ] ; then \
|
||||||
cp -r $$f $(2)/ ; \
|
cp -r $$f $(2)/ ; \
|
||||||
fi ; \
|
fi ; \
|
||||||
done
|
done
|
||||||
endef
|
endef
|
||||||
|
|
||||||
.PHONY: build build-legacy build-modern clean
|
.PHONY: build build-legacy build-interim build-modern \
|
||||||
build: build-legacy build-modern
|
install-legacy install-interim install-modern clean
|
||||||
|
|
||||||
|
build: build-legacy build-interim build-modern
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Legacy‑Build
|
# Erzeugt Legacy-ZIP (Shell 3.38)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
build-legacy:
|
build-legacy:
|
||||||
@echo "==> Building LEGACY package (3.38‑44)…"
|
@echo "==> Building LEGACY zip (for GNOME 3.38)..."
|
||||||
@rm -rf build && mkdir -p build/$(UUID)
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
$(call copies,$(COMMON_FILES),build/$(UUID))
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
|
||||||
# Schema kompilieren
|
|
||||||
@glib-compile-schemas build/$(UUID)/schemas
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
|
|
||||||
# Haupt‑ und Pref‑Skript
|
|
||||||
@cp legacy.js build/$(UUID)/extension.js
|
@cp legacy.js build/$(UUID)/extension.js
|
||||||
@cp $(LEGACY_PREFS) build/$(UUID)/prefs.js
|
@cp $(LEGACY_PREFS) build/$(UUID)/prefs.js
|
||||||
|
|
||||||
# metadata.json anpassen
|
|
||||||
@sed -e "s/__UUID__/$(UUID)/g" \
|
@sed -e "s/__UUID__/$(UUID)/g" \
|
||||||
-e "s/__VERSION__/$(VERSION)/g" \
|
-e "s/__VERSION__/$(VERSION)/g" \
|
||||||
metadata_legacy.json.in > build/$(UUID)/metadata.json
|
metadata_legacy.json.in > build/$(UUID)/metadata.json
|
||||||
|
|
||||||
# Zip‑Paket
|
|
||||||
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
|
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
|
||||||
@rm -rf build
|
@rm -rf build
|
||||||
@echo "✓ created $(UUID)-legacy-v$(VERSION).zip"
|
@echo "✓ $(UUID)-legacy-v$(VERSION).zip created"
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Modern‑Build
|
# Erzeugt Interim-ZIP (Shell 40-44)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
build-modern:
|
build-interim:
|
||||||
@echo "==> Building MODERN package (45‑48)…"
|
@echo "==> Building INTERIM zip (for GNOME 40-44)..."
|
||||||
@rm -rf build && mkdir -p build/$(UUID)
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
$(call copies,$(COMMON_FILES),build/$(UUID))
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
|
||||||
# Schema kompilieren
|
|
||||||
@glib-compile-schemas build/$(UUID)/schemas
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
|
@cp interim.js build/$(UUID)/extension.js
|
||||||
|
@cp $(INTERIM_PREFS) build/$(UUID)/prefs.js
|
||||||
|
@sed -e "s/__UUID__/$(UUID)/g" \
|
||||||
|
-e "s/__VERSION__/$(VERSION)/g" \
|
||||||
|
metadata_interim.json.in > build/$(UUID)/metadata.json
|
||||||
|
@cd build && zip -qr ../$(UUID)-interim-v$(VERSION).zip .
|
||||||
|
@rm -rf build
|
||||||
|
@echo "✓ $(UUID)-interim-v$(VERSION).zip created"
|
||||||
|
|
||||||
# Haupt‑ und Pref‑Skript
|
###############################################################################
|
||||||
|
# Erzeugt Modern-ZIP (Shell 45+)
|
||||||
|
###############################################################################
|
||||||
|
build-modern:
|
||||||
|
@echo "==> Building MODERN zip (for GNOME 45+)..."
|
||||||
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
@cp modern.js build/$(UUID)/extension.js
|
@cp modern.js build/$(UUID)/extension.js
|
||||||
@cp $(MODERN_PREFS) build/$(UUID)/prefs.js
|
@cp $(MODERN_PREFS) build/$(UUID)/prefs.js
|
||||||
|
|
||||||
# metadata.json anpassen
|
|
||||||
@sed -e "s/__UUID__/$(UUID)/g" \
|
@sed -e "s/__UUID__/$(UUID)/g" \
|
||||||
-e "s/__VERSION__/$(VERSION)/g" \
|
-e "s/__VERSION__/$(VERSION)/g" \
|
||||||
metadata_modern.json.in > build/$(UUID)/metadata.json
|
metadata_modern.json.in > build/$(UUID)/metadata.json
|
||||||
|
|
||||||
# Zip‑Paket
|
|
||||||
@cd build && zip -qr ../$(UUID)-modern-v$(VERSION).zip .
|
@cd build && zip -qr ../$(UUID)-modern-v$(VERSION).zip .
|
||||||
@rm -rf build
|
@rm -rf build
|
||||||
@echo "✓ created $(UUID)-modern-v$(VERSION).zip"
|
@echo "✓ $(UUID)-modern-v$(VERSION).zip created"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Installiert die verschiedenen Versionen
|
||||||
|
###############################################################################
|
||||||
|
install-legacy: build-legacy
|
||||||
|
@echo "==> Installing LEGACY Extension..."
|
||||||
|
@rm -rf $(EXTDIR)/$(UUID)
|
||||||
|
@unzip -q $(UUID)-legacy-v$(VERSION).zip -d $(EXTDIR)
|
||||||
|
@rm -f $(UUID)-legacy-v$(VERSION).zip
|
||||||
|
@echo "✓ Legacy Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
|
||||||
|
|
||||||
|
install-interim: build-interim
|
||||||
|
@echo "==> Installing INTERIM Extension..."
|
||||||
|
@rm -rf $(EXTDIR)/$(UUID)
|
||||||
|
@unzip -q $(UUID)-interim-v$(VERSION).zip -d $(EXTDIR)
|
||||||
|
@rm -f $(UUID)-interim-v$(VERSION).zip
|
||||||
|
@echo "✓ Interim Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
|
||||||
|
|
||||||
|
install-modern: build-modern
|
||||||
|
@echo "==> Installing MODERN Extension..."
|
||||||
|
@rm -rf $(EXTDIR)/$(UUID)
|
||||||
|
@unzip -q $(UUID)-modern-v$(VERSION).zip -d $(EXTDIR)
|
||||||
|
@rm -f $(UUID)-modern-v$(VERSION).zip
|
||||||
|
@echo "✓ Modern Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Bereinigt das Ausgangsverzeichnis
|
||||||
###############################################################################
|
###############################################################################
|
||||||
clean:
|
clean:
|
||||||
@rm -rf build $(UUID)-legacy-v$(VERSION).zip $(UUID)-modern-v$(VERSION).zip
|
@rm -f $(UUID)-*.zip
|
||||||
@echo "Build‑Ordner und ZIPs entfernt."
|
@echo "Build directory and ZIPs removed."
|
||||||
|
|||||||
@@ -49,42 +49,32 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
|
|||||||
|
|
||||||
#### Manual Installation
|
#### Manual Installation
|
||||||
|
|
||||||
The repository includes a Makefile that produces ready‑to‑install ZIP packages for the two supported GNOME‑Shell lines (a legacy build (Gnome-Shell 3.38 - 44) and a modern build for Gnome-Shell 45+).
|
The repository includes a Makefile that produces ready‑to‑install ZIP packages for the three supported Gnome‑Shell lines (a legacy build for Gnome-Shell 3.38, an interim build for Gnome-Shell 40 - 44 and a modern build for Gnome-Shell 45+).
|
||||||
|
|
||||||
1. **Clone the Source**
|
1. **Clone the Source**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YourUser/Simple-Tiling.git
|
git clone https://github.com/Domoel/Simple-Tiling.git
|
||||||
cd Simple-Tiling
|
cd Simple-Tiling
|
||||||
```
|
```
|
||||||
|
|
||||||
|
2. **Install the package that matches your GNOME-Shell version**
|
||||||
|
|
||||||
2 · **Create the package that matches your GNOME-Shell version**
|
|
||||||
Open the Terminal within the Simple-Tiling directory and run
|
Open the Terminal within the Simple-Tiling directory and run
|
||||||
```bash
|
```bash
|
||||||
make build
|
make install-legacy # Installs Legacy Extension (Gnome-Shell 3.38)
|
||||||
|
make install-interim # Installs Interim Extension (Gnome-Shell 40 - 44)
|
||||||
|
make install-modern # Installs Modern Extension (Gnome-Shell 45+)
|
||||||
```
|
```
|
||||||
**Note:** This will create a ready to go .zip archive of both, the modern and the legacy version of the extension ready to be used. Alternativley you can also run "make build-legacy" or "make build-modern" to only compile one of both versions.
|
**Note:** This command will directly install the extension in the choosen variant (legacy, interim or modern). If you want to manually create and upload the extension to your gnome extensions directory `(~/.local/share/gnome-shell/extensions)` you can just run `make build` to create all versions as .zip or `make build-legacy`, `make build-interim` or `make build-modern` to create them seperately as .zip. To enable them you need to unzip these archives and put them into your extensions directory.
|
||||||
|
|
||||||
3 · **Locate the output**
|
4. **Reload the shell**
|
||||||
```bash
|
```bash
|
||||||
ls -1 ../simple-tiling@domoel-*-v*.zip
|
Press Alt + F2, type r , hit ↩ (works for X11 and Wayland)
|
||||||
```
|
```
|
||||||
4 · **Install & enable**
|
5. **Clean up (optional)**
|
||||||
```bash
|
```bash
|
||||||
gnome-extensions install ../simple-tiling@domoel-legacy-v6.zip
|
make clean # perform this command in the downloaded folder to remove builds and generated ZIPs
|
||||||
gnome-extensions enable simple-tiling@domoel
|
```
|
||||||
```
|
|
||||||
**Note:** You can also unzip the file and put the folder right into your extensions directory (~/.local/share/gnome-shell/extensions/)
|
|
||||||
|
|
||||||
5 · **Reload the shell**
|
|
||||||
```bash
|
|
||||||
Press Alt + F2, type r , hit ↩ (works for X11 and Wayland)
|
|
||||||
```
|
|
||||||
6 · **Clean up (optional)**
|
|
||||||
```bash
|
|
||||||
make clean # removes build/ folder and generated ZIPs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** You have to use "simple-tiling@domoel" as your extension folder / directory. Put all necessary files into this directory. Otherwise the extension will not show up in extension manager.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -98,13 +88,13 @@ All keyboard shortcuts can be configured through the Settings panel of Simple Ti
|
|||||||
|
|
||||||
#### Ignoring Applications (`exceptions.txt`)
|
#### Ignoring Applications (`exceptions.txt`)
|
||||||
|
|
||||||
To prevent an application from being tiled, you can add its `WM_CLASS` to the `exceptions.txt` file in the extension's directory.
|
To prevent an application from being tiled, you can add its `WM_CLASS` (x11) or `App ID` (Wayland) to the `exceptions.txt` file in the extension's directory.
|
||||||
|
|
||||||
* Each application's `WM_CLASS` should be on a new line.
|
* Each application's `WM_CLASS` or `App ID` should be on a new line.
|
||||||
* Lines starting with `#` are treated as comments and are ignored.
|
* Lines starting with `#` are treated as comments and are ignored.
|
||||||
* The check is case-insensitive.
|
* The check is case-insensitive.
|
||||||
|
|
||||||
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude.
|
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude. To find the `App ID`, Press Alt + F2, type 'lg', and press Enter. In the Looking Glass window, click the "Windows" tab. Click on the desired window to see its details. Find the value for "app id" and add it to a new line below.
|
||||||
|
|
||||||
An Example of an exceptions.txt can be found in the repo.
|
An Example of an exceptions.txt can be found in the repo.
|
||||||
|
|
||||||
|
|||||||
+605
@@ -0,0 +1,605 @@
|
|||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// Simple‑Tiling – MODERN (GNOME Shell 40 - 44) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
|
import { Extension } from "resource:///org/gnome/shell/extensions/js/extensions/extension.js";
|
||||||
|
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
||||||
|
import Meta from "gi://Meta";
|
||||||
|
import Shell from "gi://Shell";
|
||||||
|
import Gio from "gi://Gio";
|
||||||
|
import GLib from "gi://GLib";
|
||||||
|
|
||||||
|
// ── CONST ────────────────────────────────────────────
|
||||||
|
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
|
||||||
|
|
||||||
|
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
||||||
|
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
||||||
|
|
||||||
|
const KEYBINDINGS = {
|
||||||
|
'swap-master-window': (self) => self._swapWithMaster(),
|
||||||
|
'swap-left-window': (self) => self._swapInDirection('left'),
|
||||||
|
'swap-right-window': (self) => self._swapInDirection('right'),
|
||||||
|
'swap-up-window': (self) => self._swapInDirection('up'),
|
||||||
|
'swap-down-window': (self) => self._swapInDirection('down'),
|
||||||
|
'focus-left': (self) => self._focusInDirection('left'),
|
||||||
|
'focus-right': (self) => self._focusInDirection('right'),
|
||||||
|
'focus-up': (self) => self._focusInDirection('up'),
|
||||||
|
'focus-down': (self) => self._focusInDirection('down'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
||||||
|
function getPointerXY() {
|
||||||
|
if (global.get_pointer) {
|
||||||
|
const [x, y] = global.get_pointer();
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = Clutter.get_current_event();
|
||||||
|
if (ev) {
|
||||||
|
const coords = ev.get_coords();
|
||||||
|
if (Array.isArray(coords))
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = Clutter.get_default_backend()
|
||||||
|
.get_default_seat()
|
||||||
|
.get_pointer();
|
||||||
|
return device ? device.get_position() : [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INTERACTIONHANDLER ───────────────────────────────────
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = this.tiler.settings;
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
||||||
|
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId =
|
||||||
|
this._settings.connect('changed', () => this._onSettingsChanged());
|
||||||
|
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-begin',
|
||||||
|
(_, __, win) => { if (this.tiler.windows.includes(win))
|
||||||
|
this.tiler.grabbedWindow = win; })
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-end', () => this._onGrabEnd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
||||||
|
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
||||||
|
this._grabOpIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, handler) {
|
||||||
|
global.display.add_keybinding(
|
||||||
|
key,
|
||||||
|
this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
(..._args) => handler(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
|
||||||
|
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) global.display.remove_keybinding(k); }
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
|
||||||
|
const add = key => { if (schema.has_key(key)) keys.push(key); };
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-tiled-left'))
|
||||||
|
keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
||||||
|
else {
|
||||||
|
add('tile-left'); add('tile-right');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-maximized'))
|
||||||
|
keys.push('toggle-maximized');
|
||||||
|
else {
|
||||||
|
add('maximize'); add('unmaximize');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(k => this._savedWmShortcuts[k] =
|
||||||
|
this._wmSettings.get_value(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
const tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (tgt) tgt.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const w = this.tiler.windows;
|
||||||
|
if (w.length < 2) return;
|
||||||
|
const foc = global.display.get_focus_window();
|
||||||
|
if (!foc || !w.includes(foc)) return;
|
||||||
|
const idx = w.indexOf(foc);
|
||||||
|
if (idx > 0) [w[0], w[idx]] = [w[idx], w[0]];
|
||||||
|
else [w[0], w[1]] = [w[1], w[0]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
w[0]?.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
let tgt = null;
|
||||||
|
const idx = this.tiler.windows.indexOf(src);
|
||||||
|
if (idx === 0 && direction==='right' && this.tiler.windows.length>1)
|
||||||
|
tgt = this.tiler.windows[1];
|
||||||
|
else
|
||||||
|
tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (!tgt) return;
|
||||||
|
const tidx = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[idx], this.tiler.windows[tidx]] =
|
||||||
|
[this.tiler.windows[tidx], this.tiler.windows[idx]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
src.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(src, dir) {
|
||||||
|
const sRect = src.get_frame_rect(), cand=[];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win===src) continue;
|
||||||
|
const r=win.get_frame_rect();
|
||||||
|
if (dir==='left' && r.x<sRect.x) cand.push(win);
|
||||||
|
if (dir==='right'&& r.x>sRect.x) cand.push(win);
|
||||||
|
if (dir==='up' && r.y<sRect.y) cand.push(win);
|
||||||
|
if (dir==='down' && r.y>sRect.y) cand.push(win);
|
||||||
|
}
|
||||||
|
if (!cand.length) return null;
|
||||||
|
let best=null, min=Infinity;
|
||||||
|
for (const w of cand) {
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const dev = (dir==='left'||dir==='right')
|
||||||
|
? Math.abs(sRect.y - r.y)
|
||||||
|
: Math.abs(sRect.x - r.x);
|
||||||
|
if (dev<min){min=dev; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbed = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbed) return;
|
||||||
|
const tgt = this._findTargetUnderPointer(grabbed);
|
||||||
|
if (tgt) {
|
||||||
|
const a = this.tiler.windows.indexOf(grabbed);
|
||||||
|
const b = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[a], this.tiler.windows[b]] =
|
||||||
|
[this.tiler.windows[b], this.tiler.windows[a]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(exclude) {
|
||||||
|
const [x,y] = getPointerXY();
|
||||||
|
const wins = global.get_window_actors()
|
||||||
|
.map(a=>a.meta_window)
|
||||||
|
.filter(w=>w && w!==exclude &&
|
||||||
|
this.tiler.windows.includes(w) && (()=>{const f=w.get_frame_rect();
|
||||||
|
return x>=f.x && x<f.x+f.width &&
|
||||||
|
y>=f.y && y<f.y+f.height;})());
|
||||||
|
if (wins.length) return wins[wins.length-1];
|
||||||
|
|
||||||
|
let best=null, max=0, sRect=exclude.get_frame_rect();
|
||||||
|
for (const w of this.tiler.windows) {
|
||||||
|
if (w===exclude) continue;
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const ovX=Math.max(0, Math.min(sRect.x+sRect.width, r.x+r.width)-Math.max(sRect.x,r.x));
|
||||||
|
const ovY=Math.max(0, Math.min(sRect.y+sRect.height,r.y+r.height)-Math.max(sRect.y,r.y));
|
||||||
|
const area=ovX*ovY;
|
||||||
|
if (area>max){max=area; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TILER ────────────────────────────────────────────────
|
||||||
|
class Tiler {
|
||||||
|
constructor(extension) {
|
||||||
|
this._extension = extension;
|
||||||
|
this.settings = this._extension.getSettings();
|
||||||
|
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical= this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal = this.settings.get_int('outer-gap-horizontal');
|
||||||
|
|
||||||
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
this._workspaceManager = global.workspace_manager;
|
||||||
|
|
||||||
|
this._signalIds.set('workspace-changed', {
|
||||||
|
object: this._workspaceManager,
|
||||||
|
id: this._workspaceManager.connect('active-workspace-changed',
|
||||||
|
()=>this._onActiveWorkspaceChanged())
|
||||||
|
});
|
||||||
|
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
|
||||||
|
this._signalIds.set('settings-changed', {
|
||||||
|
object: this.settings,
|
||||||
|
id: this.settings.connect('changed', ()=>this._onSettingsChanged())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._tileTimeoutId) {
|
||||||
|
GLib.source_remove(this._tileTimeoutId);
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
}
|
||||||
|
this._centerTimeoutIds.forEach(id=>GLib.source_remove(id));
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
|
||||||
|
for (const [,sig] of this._signalIds) {
|
||||||
|
try { sig.object.disconnect(sig.id); } catch {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical = this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal= this.settings.get_int('outer-gap-horizontal');
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.File.new_for_path(this._extension.path + '/exceptions.txt');
|
||||||
|
if (!file.query_exists(null)) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const [ok,data] = file.load_contents(null);
|
||||||
|
if (!ok) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const txt = new TextDecoder('utf-8').decode(data);
|
||||||
|
this._exceptions = txt.split('\n')
|
||||||
|
.map(l=>l.trim())
|
||||||
|
.filter(l=>l && !l.startsWith('#'))
|
||||||
|
.map(l=>l.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
if (!win) return false;
|
||||||
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return (
|
||||||
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
const timeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._centeringDelay,
|
||||||
|
() => {
|
||||||
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index > -1) this._centerTimeoutIds.splice(index, 1);
|
||||||
|
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (win.get_maximized())
|
||||||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
|
||||||
|
const monitorIndex = win.get_monitor();
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(
|
||||||
|
monitorIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = win.get_frame_rect();
|
||||||
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y +
|
||||||
|
Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
|
if (win.get_display()) {
|
||||||
|
if (typeof win.set_keep_above === "function")
|
||||||
|
win.set_keep_above(true);
|
||||||
|
else if (typeof win.make_above === "function")
|
||||||
|
win.make_above();
|
||||||
|
}
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowAdded(workspace, win) {
|
||||||
|
if (this.windows.includes(win)) return;
|
||||||
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._isTileable(win)) {
|
||||||
|
if (this.settings.get_string("new-window-behavior") === "master") {
|
||||||
|
this.windows.unshift(win);
|
||||||
|
} else {
|
||||||
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
const id = win.get_id();
|
||||||
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("unmanaged", () =>
|
||||||
|
this._onWindowRemoved(null, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("notify::minimized", () =>
|
||||||
|
this._onWindowMinimizedStateChanged()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowRemoved(workspace, win) {
|
||||||
|
const index = this.windows.indexOf(win);
|
||||||
|
if (index > -1) this.windows.splice(index, 1);
|
||||||
|
|
||||||
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActiveWorkspaceChanged() {
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
this._connectToWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToWorkspace() {
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
workspace
|
||||||
|
.list_windows()
|
||||||
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
|
this._signalIds.set("window-added", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", (ws, win) =>
|
||||||
|
this._onWindowAdded(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", (ws, win) =>
|
||||||
|
this._onWindowRemoved(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTile() {
|
||||||
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
|
this._tileInProgress = true;
|
||||||
|
this._tileTimeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._tilingDelay,
|
||||||
|
() => {
|
||||||
|
this._tileWindows();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_splitLayout(windows, area) {
|
||||||
|
if (windows.length === 0) return;
|
||||||
|
if (windows.length === 1) {
|
||||||
|
windows[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const primaryWindows = [windows[0]];
|
||||||
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
if (area.width > area.height) {
|
||||||
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: primaryWidth,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tileWindows() {
|
||||||
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
|
if (windowsToTile.length === 0) return;
|
||||||
|
|
||||||
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(monitor.index);
|
||||||
|
|
||||||
|
const innerArea = {
|
||||||
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
windowsToTile.forEach((win) => {
|
||||||
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
if (windowsToTile.length === 1) {
|
||||||
|
windowsToTile[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
|
const master = windowsToTile[0];
|
||||||
|
master.move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
masterWidth,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
||||||
|
export default class InterimExtension extends Extension {
|
||||||
|
enable() {
|
||||||
|
this.tiler = new Tiler(this);
|
||||||
|
this.tiler.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,24 @@
|
|||||||
///////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
// Simple‑Tiling – LEGACY (GNOME Shell 3.38 ‑ 44) //
|
// Simple-Tiling – LEGACY (for GNOME Shell 3.38) //
|
||||||
// © 2025 domoel – MIT //
|
// © 2025 domoel – MIT //
|
||||||
/////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
const Main = imports.ui.main;
|
||||||
const Main = imports.ui.main;
|
const Meta = imports.gi.Meta;
|
||||||
const Meta = imports.gi.Meta;
|
const Shell = imports.gi.Shell;
|
||||||
const Shell = imports.gi.Shell;
|
const Gio = imports.gi.Gio;
|
||||||
const Gio = imports.gi.Gio;
|
const GLib = imports.gi.GLib;
|
||||||
const GLib = imports.gi.GLib;
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
const ExtensionUtils= imports.misc.extensionUtils;
|
const ByteArray = imports.byteArray;
|
||||||
const ByteArray = imports.byteArray;
|
|
||||||
|
|
||||||
const Me = ExtensionUtils.getCurrentExtension();
|
const Me = ExtensionUtils.getCurrentExtension();
|
||||||
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
|
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
||||||
|
|
||||||
// ── CONST ────────────────────────────────────────────
|
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
||||||
const SCHEMA_NAME = 'org.gnome.shell.extensions.simple-tiling.domoel';
|
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
||||||
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
|
|
||||||
|
|
||||||
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
|
||||||
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
|
||||||
|
|
||||||
const KEYBINDINGS = {
|
const KEYBINDINGS = {
|
||||||
'swap-master-window': (self) => self._swapWithMaster(),
|
'swap-master-window': (self) => self._swapWithMaster(),
|
||||||
@@ -35,285 +32,319 @@ const KEYBINDINGS = {
|
|||||||
'focus-down': (self) => self._focusInDirection('down'),
|
'focus-down': (self) => self._focusInDirection('down'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
// --- INTERACTIONHANDLER ---
|
||||||
function addKeybinding(name, settings, flags, mode, handler) {
|
|
||||||
if (Main.wm?.addKeybinding)
|
|
||||||
Main.wm.addKeybinding(name, settings, flags, mode, handler);
|
|
||||||
else
|
|
||||||
global.display.add_keybinding(name, settings, flags, mode, handler);
|
|
||||||
}
|
|
||||||
function removeKeybinding(name) {
|
|
||||||
if (Main.wm?.removeKeybinding)
|
|
||||||
Main.wm.removeKeybinding(name);
|
|
||||||
else
|
|
||||||
global.display.remove_keybinding(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkAreaForMonitor(monitorIndex) {
|
|
||||||
if (Main.layoutManager?.getWorkAreaForMonitor)
|
|
||||||
return Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
|
|
||||||
|
|
||||||
return global.workspace_manager
|
|
||||||
.get_active_workspace()
|
|
||||||
.get_work_area_for_monitor(monitorIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeUtf8(bytes) {
|
|
||||||
if (typeof ByteArray !== 'undefined')
|
|
||||||
return ByteArray.toString(bytes);
|
|
||||||
return new TextDecoder('utf-8').decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointer() {
|
|
||||||
return global.get_pointer ? global.get_pointer()
|
|
||||||
: global.display.get_pointer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── INTERACTIONHANDLER ───────────────────────────────────
|
|
||||||
class InteractionHandler {
|
class InteractionHandler {
|
||||||
constructor(tiler) {
|
constructor(tiler) {
|
||||||
this.tiler = tiler;
|
this.tiler = tiler;
|
||||||
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
this._wmKeysToDisable = [];
|
this._wmKeysToDisable = [];
|
||||||
this._savedWmShortcuts= {};
|
this._savedWmShortcuts = {};
|
||||||
this._grabOpIds = [];
|
this._grabOpIds = [];
|
||||||
this._settingsChangedId = null;
|
this._settingsChangedId = null;
|
||||||
|
|
||||||
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
|
|
||||||
this._prepareWmShortcuts();
|
this._prepareWmShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
if (this._wmKeysToDisable.length)
|
if (this._wmKeysToDisable.length) {
|
||||||
this._wmKeysToDisable.forEach(k =>
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
this._wmSettings.set_value(key, new GLib.Variant("as", []))
|
||||||
|
);
|
||||||
|
}
|
||||||
this._bindAllShortcuts();
|
this._bindAllShortcuts();
|
||||||
this._settingsChangedId =
|
this._settingsChangedId = this._settings.connect(
|
||||||
this._settings.connect('changed', this._onSettingsChanged);
|
"changed",
|
||||||
|
this._onSettingsChanged
|
||||||
|
);
|
||||||
this._grabOpIds.push(
|
this._grabOpIds.push(
|
||||||
global.display.connect('grab-op-begin',
|
global.display.connect(
|
||||||
(display, screen, win) => {
|
"grab-op-begin",
|
||||||
if (this.tiler.windows.includes(win))
|
(display, screen, window) => {
|
||||||
this.tiler.grabbedWindow = win;
|
if (this.tiler.windows.includes(window)) {
|
||||||
}));
|
this.tiler.grabbedWindow = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
this._grabOpIds.push(
|
this._grabOpIds.push(
|
||||||
global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
|
global.display.connect("grab-op-end", this._onGrabEnd.bind(this))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
if (this._wmKeysToDisable.length)
|
if (this._wmKeysToDisable.length) {
|
||||||
this._wmKeysToDisable.forEach(k =>
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
this._wmSettings.set_value(key, this._savedWmShortcuts[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
this._unbindAllShortcuts();
|
this._unbindAllShortcuts();
|
||||||
|
|
||||||
if (this._settingsChangedId) {
|
if (this._settingsChangedId) {
|
||||||
this._settings.disconnect(this._settingsChangedId);
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
this._settingsChangedId = null;
|
this._settingsChangedId = null;
|
||||||
}
|
}
|
||||||
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
this._grabOpIds.forEach((id) => global.display.disconnect(id));
|
||||||
this._grabOpIds = [];
|
this._grabOpIds = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_bind(key, callback) {
|
_bind(key, callback) {
|
||||||
addKeybinding(key, this._settings,
|
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL,
|
||||||
Meta.KeyBindingFlags.NONE,
|
() => callback(this));
|
||||||
Shell.ActionMode.NORMAL,
|
}
|
||||||
() => callback(this));
|
|
||||||
|
_bindAllShortcuts() {
|
||||||
|
for (const [key, handler] of Object.entries(KEYBINDINGS)) {
|
||||||
|
this._bind(key, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_unbindAllShortcuts() {
|
||||||
|
for (const key in KEYBINDINGS) {
|
||||||
|
Main.wm.removeKeybinding(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k,h); }
|
|
||||||
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) removeKeybinding(k); }
|
|
||||||
|
|
||||||
_onSettingsChanged() {
|
_onSettingsChanged() {
|
||||||
this._unbindAllShortcuts();
|
this._unbindAllShortcuts();
|
||||||
this._bindAllShortcuts();
|
this._bindAllShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
_prepareWmShortcuts() {
|
_prepareWmShortcuts() {
|
||||||
const schema = this._wmSettings.settings_schema;
|
const schema = this._wmSettings.settings_schema;
|
||||||
const keys = [];
|
const keys = [];
|
||||||
|
if (schema.has_key("toggle-tiled-left"))
|
||||||
if (schema.has_key('toggle-tiled-left'))
|
keys.push("toggle-tiled-left", "toggle-tiled-right");
|
||||||
keys.push('toggle-tiled-left','toggle-tiled-right');
|
else if (schema.has_key("tile-left"))
|
||||||
else if (schema.has_key('tile-left'))
|
keys.push("tile-left", "tile-right");
|
||||||
keys.push('tile-left','tile-right');
|
if (schema.has_key("toggle-maximized")) keys.push("toggle-maximized");
|
||||||
|
|
||||||
if (schema.has_key('toggle-maximized'))
|
|
||||||
keys.push('toggle-maximized');
|
|
||||||
else {
|
else {
|
||||||
if (schema.has_key('maximize')) keys.push('maximize');
|
if (schema.has_key("maximize")) keys.push("maximize");
|
||||||
if (schema.has_key('unmaximize')) keys.push('unmaximize');
|
if (schema.has_key("unmaximize")) keys.push("unmaximize");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.length) {
|
if (keys.length) {
|
||||||
this._wmKeysToDisable = keys;
|
this._wmKeysToDisable = keys;
|
||||||
keys.forEach(k => this._savedWmShortcuts[k] =
|
keys.forEach(
|
||||||
this._wmSettings.get_value(k));
|
(key) => (this._savedWmShortcuts[key] = this._wmSettings.get_value(key))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_focusInDirection(direction) {
|
_focusInDirection(direction) {
|
||||||
const src = global.display.get_focus_window();
|
const sourceWindow = global.display.get_focus_window();
|
||||||
if (!src || !this.tiler.windows.includes(src)) return;
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
const tgt = this._findTargetInDirection(src, direction);
|
|
||||||
if (tgt) tgt.activate(global.get_current_time());
|
const targetWindow = this._findTargetInDirection(
|
||||||
|
sourceWindow,
|
||||||
|
direction
|
||||||
|
);
|
||||||
|
if (targetWindow) {
|
||||||
|
targetWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_swapWithMaster() {
|
_swapWithMaster() {
|
||||||
const w = this.tiler.windows;
|
const windows = this.tiler.windows;
|
||||||
if (w.length < 2) return;
|
if (windows.length < 2) return;
|
||||||
const foc = global.display.get_focus_window();
|
const focusedWindow = global.display.get_focus_window();
|
||||||
if (!foc || !w.includes(foc)) return;
|
if (!focusedWindow || !windows.includes(focusedWindow)) return;
|
||||||
const idx = w.indexOf(foc);
|
const focusedIndex = windows.indexOf(focusedWindow);
|
||||||
if (idx > 0)
|
if (focusedIndex > 0) {
|
||||||
[w[0], w[idx]] = [w[idx], w[0]];
|
[windows[0], windows[focusedIndex]] = [
|
||||||
else
|
windows[focusedIndex],
|
||||||
[w[0], w[1]] = [w[1], w[0]];
|
windows[0],
|
||||||
|
];
|
||||||
|
} else if (focusedIndex === 0) {
|
||||||
|
[windows[0], windows[1]] = [windows[1], windows[0]];
|
||||||
|
}
|
||||||
this.tiler.tileNow();
|
this.tiler.tileNow();
|
||||||
w[0]?.activate(global.get_current_time());
|
if (windows.length > 0) windows[0].activate(global.get_current_time());
|
||||||
}
|
}
|
||||||
|
|
||||||
_swapInDirection(direction) {
|
_swapInDirection(direction) {
|
||||||
const src = global.display.get_focus_window();
|
const sourceWindow = global.display.get_focus_window();
|
||||||
if (!src || !this.tiler.windows.includes(src)) return;
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
let targetWindow = null;
|
||||||
let tgt = null;
|
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
|
||||||
const srcIdx = this.tiler.windows.indexOf(src);
|
if (
|
||||||
if (srcIdx === 0 && direction === 'right' && this.tiler.windows.length>1)
|
sourceIndex === 0 &&
|
||||||
tgt = this.tiler.windows[1];
|
direction === "right" &&
|
||||||
else
|
this.tiler.windows.length > 1
|
||||||
tgt = this._findTargetInDirection(src, direction);
|
) {
|
||||||
|
targetWindow = this.tiler.windows[1];
|
||||||
if (!tgt) return;
|
} else {
|
||||||
const tgtIdx = this.tiler.windows.indexOf(tgt);
|
targetWindow = this._findTargetInDirection(sourceWindow, direction);
|
||||||
[this.tiler.windows[srcIdx], this.tiler.windows[tgtIdx]] =
|
}
|
||||||
[this.tiler.windows[tgtIdx], this.tiler.windows[srcIdx]];
|
if (!targetWindow) return;
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
this.tiler.tileNow();
|
this.tiler.tileNow();
|
||||||
src.activate(global.get_current_time());
|
sourceWindow.activate(global.get_current_time());
|
||||||
}
|
}
|
||||||
|
|
||||||
_findTargetInDirection(src, direction) {
|
_findTargetInDirection(source, direction) {
|
||||||
const sRect = src.get_frame_rect();
|
const sourceRect = source.get_frame_rect();
|
||||||
const cands = [];
|
let candidates = [];
|
||||||
|
|
||||||
for (const win of this.tiler.windows) {
|
for (const win of this.tiler.windows) {
|
||||||
if (win === src) continue;
|
if (win === source) continue;
|
||||||
const tRect = win.get_frame_rect();
|
const targetRect = win.get_frame_rect();
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case 'left': if (tRect.x < sRect.x) cands.push(win); break;
|
case "left":
|
||||||
case 'right': if (tRect.x > sRect.x) cands.push(win); break;
|
if (targetRect.x < sourceRect.x) candidates.push(win);
|
||||||
case 'up': if (tRect.y < sRect.y) cands.push(win); break;
|
break;
|
||||||
case 'down': if (tRect.y > sRect.y) cands.push(win); break;
|
case "right":
|
||||||
|
if (targetRect.x > sourceRect.x) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "up":
|
||||||
|
if (targetRect.y < sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
if (targetRect.y > sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!cands.length) return null;
|
if (candidates.length === 0) return null;
|
||||||
|
let bestTarget = null;
|
||||||
let best=null, min=Infinity;
|
let minDeviation = Infinity;
|
||||||
for (const win of cands) {
|
for (const win of candidates) {
|
||||||
const tRect = win.get_frame_rect();
|
const targetRect = win.get_frame_rect();
|
||||||
const dev = (direction==='left'||direction==='right')
|
let deviation;
|
||||||
? Math.abs(sRect.y - tRect.y)
|
if (direction === "left" || direction === "right") {
|
||||||
: Math.abs(sRect.x - tRect.x);
|
deviation = Math.abs(sourceRect.y - targetRect.y);
|
||||||
if (dev < min) { min=dev; best=win; }
|
} else {
|
||||||
|
deviation = Math.abs(sourceRect.x - targetRect.x);
|
||||||
|
}
|
||||||
|
if (deviation < minDeviation) {
|
||||||
|
minDeviation = deviation;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return best;
|
return bestTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onGrabEnd() {
|
_onGrabEnd() {
|
||||||
const grabbed = this.tiler.grabbedWindow;
|
const grabbedWindow = this.tiler.grabbedWindow;
|
||||||
if (!grabbed) return;
|
if (!grabbedWindow) return;
|
||||||
|
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
|
||||||
const tgt = this._findTargetUnderPointer(grabbed);
|
if (targetWindow) {
|
||||||
if (tgt) {
|
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
|
||||||
const a = this.tiler.windows.indexOf(grabbed);
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
const b = this.tiler.windows.indexOf(tgt);
|
[
|
||||||
[this.tiler.windows[a], this.tiler.windows[b]] =
|
this.tiler.windows[sourceIndex],
|
||||||
[this.tiler.windows[b], this.tiler.windows[a]];
|
this.tiler.windows[targetIndex],
|
||||||
|
] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
this.tiler.queueTile();
|
this.tiler.queueTile();
|
||||||
this.tiler.grabbedWindow = null;
|
this.tiler.grabbedWindow = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_findTargetUnderPointer(exclude) {
|
_findTargetUnderPointer(excludeWindow) {
|
||||||
const [x,y] = getPointer();
|
let [pointerX, pointerY] = global.get_pointer();
|
||||||
const wins = global.get_window_actors()
|
let windows = global
|
||||||
.map(a => a.meta_window)
|
.get_window_actors()
|
||||||
.filter(w => w && w!==exclude &&
|
.map((actor) => actor.meta_window)
|
||||||
this.tiler.windows.includes(w) &&
|
.filter((win) => {
|
||||||
((()=>{ const f=w.get_frame_rect();
|
if (
|
||||||
return x>=f.x && x<f.x+f.width &&
|
!win ||
|
||||||
y>=f.y && y<f.y+f.height;})()));
|
win === excludeWindow ||
|
||||||
if (wins.length) return wins[wins.length-1];
|
!this.tiler.windows.includes(win)
|
||||||
|
)
|
||||||
let best=null, max=0, sRect=exclude.get_frame_rect();
|
return false;
|
||||||
for (const w of this.tiler.windows) {
|
let frame = win.get_frame_rect();
|
||||||
if (w===exclude) continue;
|
return (
|
||||||
const tRect=w.get_frame_rect();
|
pointerX >= frame.x &&
|
||||||
const ovX = Math.max(0, Math.min(sRect.x+sRect.width,
|
pointerX < frame.x + frame.width &&
|
||||||
tRect.x+tRect.width) -
|
pointerY >= frame.y &&
|
||||||
Math.max(sRect.x, tRect.x));
|
pointerY < frame.y + frame.height
|
||||||
const ovY = Math.max(0, Math.min(sRect.y+sRect.height,
|
);
|
||||||
tRect.y+tRect.height) -
|
});
|
||||||
Math.max(sRect.y, tRect.y));
|
if (windows.length > 0) {
|
||||||
const area = ovX*ovY;
|
return windows[windows.length - 1];
|
||||||
if (area>max){ max=area; best=w; }
|
|
||||||
}
|
}
|
||||||
return best;
|
|
||||||
|
let bestTarget = null;
|
||||||
|
let maxOverlap = 0;
|
||||||
|
const sourceFrame = excludeWindow.get_frame_rect();
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === excludeWindow) continue;
|
||||||
|
const targetFrame = win.get_frame_rect();
|
||||||
|
const overlapX = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.x + sourceFrame.width,
|
||||||
|
targetFrame.x + targetFrame.width
|
||||||
|
) - Math.max(sourceFrame.x, targetFrame.x)
|
||||||
|
);
|
||||||
|
const overlapY = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.y + sourceFrame.height,
|
||||||
|
targetFrame.y + targetFrame.height
|
||||||
|
) - Math.max(sourceFrame.y, targetFrame.y)
|
||||||
|
);
|
||||||
|
const overlapArea = overlapX * overlapY;
|
||||||
|
if (overlapArea > maxOverlap) {
|
||||||
|
maxOverlap = overlapArea;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TILER ────────────────────────────────────────────────
|
// --- TILER ---
|
||||||
class Tiler {
|
class Tiler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
this.grabbedWindow = null;
|
this.grabbedWindow = null;
|
||||||
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
this._signalIds = new Map();
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
this._tileTimeoutId = null;
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
this._centerTimeoutIds= [];
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
this._tileInProgress = false;
|
|
||||||
|
|
||||||
this._innerGap = this._settings.get_int('inner-gap');
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
this._outerGapVertical= this._settings.get_int('outer-gap-vertical');
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
this._outerGapHorizontal = this._settings.get_int('outer-gap-horizontal');
|
|
||||||
|
|
||||||
this._tilingDelay = TILING_DELAY_MS;
|
this._exceptions = [];
|
||||||
this._centeringDelay= CENTERING_DELAY_MS;
|
|
||||||
|
|
||||||
this._exceptions = [];
|
|
||||||
this._interactionHandler = new InteractionHandler(this);
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
this._onWindowAdded = this._onWindowAdded.bind(this);
|
this._tileTimeoutId = null;
|
||||||
this._onWindowRemoved= this._onWindowRemoved.bind(this);
|
this._centerTimeoutIds = [];
|
||||||
this._onActiveWorkspaceChanged =
|
|
||||||
this._onActiveWorkspaceChanged.bind(this);
|
this._onWindowAdded = this._onWindowAdded.bind(this);
|
||||||
this._onWindowMinimizedStateChanged =
|
this._onWindowRemoved = this._onWindowRemoved.bind(this);
|
||||||
this._onWindowMinimizedStateChanged.bind(this);
|
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
this._loadExceptions();
|
this._loadExceptions();
|
||||||
|
const workspaceManager = global.workspace_manager;
|
||||||
const wm = global.workspace_manager;
|
this._signalIds.set("workspace-changed", {
|
||||||
this._signalIds.set('workspace-changed', {
|
object: workspaceManager,
|
||||||
object: wm,
|
id: workspaceManager.connect(
|
||||||
id: wm.connect('active-workspace-changed',
|
"active-workspace-changed",
|
||||||
this._onActiveWorkspaceChanged),
|
this._onActiveWorkspaceChanged
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._connectToWorkspace();
|
this._connectToWorkspace();
|
||||||
|
|
||||||
this._interactionHandler.enable();
|
this._interactionHandler.enable();
|
||||||
|
this._signalIds.set("settings-changed", {
|
||||||
this._signalIds.set('settings-changed', {
|
|
||||||
object: this._settings,
|
object: this._settings,
|
||||||
id: this._settings.connect('changed', this._onSettingsChanged),
|
id: this._settings.connect("changed", this._onSettingsChanged),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,112 +358,147 @@ class Tiler {
|
|||||||
|
|
||||||
this._interactionHandler.disable();
|
this._interactionHandler.disable();
|
||||||
this._disconnectFromWorkspace();
|
this._disconnectFromWorkspace();
|
||||||
|
for (const [, signal] of this._signalIds) {
|
||||||
for (const [,sig] of this._signalIds) {
|
try {
|
||||||
try { sig.object.disconnect(sig.id); } catch {}
|
signal.object.disconnect(signal.id);
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
this._signalIds.clear();
|
this._signalIds.clear();
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSettingsChanged() {
|
_onSettingsChanged() {
|
||||||
this._innerGap = this._settings.get_int('inner-gap');
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
this._outerGapVertical = this._settings.get_int('outer-gap-vertical');
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
this._outerGapHorizontal= this._settings.get_int('outer-gap-horizontal');
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadExceptions() {
|
_loadExceptions() {
|
||||||
const file = Gio.File.new_for_path(Me.path + '/exceptions.txt');
|
const file = Gio.file_new_for_path(Me.path + "/exceptions.txt");
|
||||||
if (!file.query_exists(null)) { this._exceptions=[]; return; }
|
if (!file.query_exists(null)) {
|
||||||
|
this._exceptions = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
const [ok, data] = file.load_contents(null);
|
const [ok, data] = file.load_contents(null);
|
||||||
this._exceptions = ok ? decodeUtf8(data)
|
this._exceptions = ok
|
||||||
.split('\n')
|
? ByteArray.toString(data)
|
||||||
.map(l => l.trim())
|
.split("\n")
|
||||||
.filter(l => l && !l.startsWith('#'))
|
.map((l) => l.trim())
|
||||||
.map(l => l.toLowerCase())
|
.filter((l) => l && !l.startsWith("#"))
|
||||||
: [];
|
.map((l) => l.toLowerCase())
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_isException(win) {
|
_isException(win) {
|
||||||
if (!win) return false;
|
if (!win) return false;
|
||||||
const wmClass = (win.get_wm_class() || '').toLowerCase();
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
const appId = (win.get_gtk_application_id() || '').toLowerCase();
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isTileable(win) {
|
_isTileable(win) {
|
||||||
return win && !win.minimized && !this._isException(win) &&
|
return (
|
||||||
win.get_window_type() === Meta.WindowType.NORMAL;
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_centerWindow(win) {
|
_centerWindow(win) {
|
||||||
const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
|
const timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._centeringDelay, () => {
|
||||||
this._centeringDelay, () => {
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
const idx = this._centerTimeoutIds.indexOf(id);
|
if (index > -1) {
|
||||||
if (idx>-1) this._centerTimeoutIds.splice(idx,1);
|
this._centerTimeoutIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
if (win.get_maximized())
|
if (win.get_maximized()) {
|
||||||
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
}
|
||||||
const monitorIndex = win.get_monitor();
|
const monitorIndex = win.get_monitor();
|
||||||
const workArea = getWorkAreaForMonitor(monitorIndex);
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(
|
||||||
const frame = win.get_frame_rect();
|
monitorIndex
|
||||||
win.move_frame(true,
|
);
|
||||||
workArea.x + Math.floor((workArea.width - frame.width )/2),
|
const frame = win.get_frame_rect();
|
||||||
workArea.y + Math.floor((workArea.height - frame.height)/2));
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y + Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
if (win.get_display()) {
|
if (win.get_display()) {
|
||||||
if (typeof win.set_keep_above === 'function')
|
if (typeof win.set_keep_above === "function")
|
||||||
win.set_keep_above(true);
|
win.set_keep_above(true);
|
||||||
else if (typeof win.make_above === 'function')
|
else if (typeof win.make_above === "function")
|
||||||
win.make_above();
|
win.make_above();
|
||||||
}
|
}
|
||||||
return GLib.SOURCE_REMOVE;
|
return GLib.SOURCE_REMOVE;
|
||||||
});
|
});
|
||||||
return GLib.SOURCE_REMOVE;
|
return GLib.SOURCE_REMOVE;
|
||||||
});
|
});
|
||||||
this._centerTimeoutIds.push(id);
|
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWindowMinimizedStateChanged(){ this.queueTile(); }
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
_onWindowAdded(workspace, win) {
|
_onWindowAdded(workspace, win) {
|
||||||
if (this.windows.includes(win)) return;
|
if (this.windows.includes(win)) return;
|
||||||
|
|
||||||
if (this._isException(win)) { this._centerWindow(win); return; }
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._isTileable(win)) {
|
if (this._isTileable(win)) {
|
||||||
if (this._settings.get_string('new-window-behavior') === 'master')
|
if (this._settings.get_string("new-window-behavior") === "master") {
|
||||||
this.windows.unshift(win);
|
this.windows.unshift(win);
|
||||||
else
|
} else {
|
||||||
this.windows.push(win);
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
|
||||||
const id = win.get_id();
|
const id = win.get_id();
|
||||||
this._signalIds.set(`unmanaged-${id}`, {
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
object: win, id: win.connect('unmanaged',
|
object: win,
|
||||||
()=>this._onWindowRemoved(null, win))});
|
id: win.connect("unmanaged", () =>
|
||||||
this._signalIds.set(`size-${id}`, {
|
this._onWindowRemoved(null, win)
|
||||||
object: win, id: win.connect('size-changed',
|
),
|
||||||
()=>{ if (!this.grabbedWindow) this.queueTile(); })});
|
});
|
||||||
this._signalIds.set(`min-${id}`, {
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
object: win, id: win.connect('notify::minimized',
|
object: win,
|
||||||
this._onWindowMinimizedStateChanged)});
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect(
|
||||||
|
"notify::minimized",
|
||||||
|
this._onWindowMinimizedStateChanged
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWindowRemoved(workspace, win) {
|
_onWindowRemoved(workspace, win) {
|
||||||
const idx = this.windows.indexOf(win);
|
const index = this.windows.indexOf(win);
|
||||||
if (idx>-1) this.windows.splice(idx,1);
|
if (index > -1) {
|
||||||
|
this.windows.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
['unmanaged','size','min'].forEach(pref=>{
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
const key = `${pref}-${win.get_id()}`;
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
if (this._signalIds.has(key)) {
|
if (this._signalIds.has(key)) {
|
||||||
const {object,id} = this._signalIds.get(key);
|
const { object, id } = this._signalIds.get(key);
|
||||||
try{ object.disconnect(id);}catch{}
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
this._signalIds.delete(key);
|
this._signalIds.delete(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -445,21 +511,31 @@ class Tiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_connectToWorkspace() {
|
_connectToWorkspace() {
|
||||||
const ws = global.workspace_manager.get_active_workspace();
|
const workspace = global.workspace_manager.get_active_workspace();
|
||||||
ws.list_windows().forEach(w=>this._onWindowAdded(ws,w));
|
workspace
|
||||||
this._signalIds.set('win-add', {
|
.list_windows()
|
||||||
object: ws, id: ws.connect('window-added', this._onWindowAdded)});
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
this._signalIds.set('win-rem', {
|
this._signalIds.set("window-added", {
|
||||||
object: ws, id: ws.connect('window-removed', this._onWindowRemoved)});
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", this._onWindowAdded),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", this._onWindowRemoved),
|
||||||
|
});
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnectFromWorkspace() {
|
_disconnectFromWorkspace() {
|
||||||
this.windows.slice().forEach(w=>this._onWindowRemoved(null,w));
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
['win-add','win-rem'].forEach(k=>{
|
|
||||||
if (this._signalIds.has(k)) {
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
const {object,id}=this._signalIds.get(k);
|
if (this._signalIds.has(key)) {
|
||||||
try{ object.disconnect(id);}catch{}
|
const { object, id } = this._signalIds.get(key);
|
||||||
this._signalIds.delete(k);
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -467,88 +543,139 @@ class Tiler {
|
|||||||
queueTile() {
|
queueTile() {
|
||||||
if (this._tileInProgress || this._tileTimeoutId) return;
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
this._tileInProgress = true;
|
this._tileInProgress = true;
|
||||||
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
|
|
||||||
this._tilingDelay, () => {
|
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._tilingDelay, () => {
|
||||||
this._tileWindows();
|
this._tileWindows();
|
||||||
this._tileInProgress = false;
|
this._tileInProgress = false;
|
||||||
this._tileTimeoutId = null;
|
this._tileTimeoutId = null;
|
||||||
return GLib.SOURCE_REMOVE;
|
return GLib.SOURCE_REMOVE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
tileNow() { if (!this._tileInProgress) this._tileWindows(); }
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_splitLayout(windows, area) {
|
_splitLayout(windows, area) {
|
||||||
if (!windows.length) return;
|
if (windows.length === 0) return;
|
||||||
if (windows.length === 1) {
|
if (windows.length === 1) {
|
||||||
windows[0].move_resize_frame(true,
|
windows[0].move_resize_frame(
|
||||||
area.x, area.y, area.width, area.height);
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gap = Math.floor(this._innerGap/2);
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
const prim = [windows[0]];
|
const primaryWindows = [windows[0]];
|
||||||
const sec = windows.slice(1);
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
|
||||||
let primArea, secArea;
|
|
||||||
if (area.width > area.height) {
|
if (area.width > area.height) {
|
||||||
const pW = Math.floor(area.width/2) - gap;
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
primArea = {x: area.x, y: area.y,
|
primaryArea = {
|
||||||
width: pW, height: area.height};
|
x: area.x,
|
||||||
secArea = {x: area.x+pW+this._innerGap, y: area.y,
|
y: area.y,
|
||||||
width: area.width-pW-this._innerGap,
|
width: primaryWidth,
|
||||||
height: area.height};
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const pH = Math.floor(area.height/2) - gap;
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
primArea = {x: area.x, y: area.y,
|
primaryArea = {
|
||||||
width: area.width, height: pH};
|
x: area.x,
|
||||||
secArea = {x: area.x, y: area.y+pH+this._innerGap,
|
y: area.y,
|
||||||
width: area.width,
|
width: area.width,
|
||||||
height: area.height-pH-this._innerGap};
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
this._splitLayout(prim, primArea);
|
|
||||||
this._splitLayout(sec, secArea);
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
_tileWindows() {
|
_tileWindows() {
|
||||||
const wins = this.windows.filter(w=>!w.minimized);
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
if (!wins.length) return;
|
if (windowsToTile.length === 0) return;
|
||||||
|
|
||||||
const monitor = Main.layoutManager.primaryMonitor;
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
const work = getWorkAreaForMonitor(monitor.index);
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(
|
||||||
const inner = { x: work.x + this._outerGapHorizontal,
|
monitor.index
|
||||||
y: work.y + this._outerGapVertical,
|
);
|
||||||
width: work.width - 2*this._outerGapHorizontal,
|
const innerArea = {
|
||||||
height: work.height - 2*this._outerGapVertical };
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
|
||||||
wins.forEach(w=>{ if (w.get_maximized())
|
windowsToTile.forEach((win) => {
|
||||||
w.unmaximize(Meta.MaximizeFlags.BOTH); });
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
|
||||||
if (wins.length===1) {
|
if (windowsToTile.length === 1) {
|
||||||
wins[0].move_resize_frame(true,
|
windowsToTile[0].move_resize_frame(
|
||||||
inner.x, inner.y, inner.width, inner.height);
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gap = Math.floor(this._innerGap/2);
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
const masterW = Math.floor(inner.width/2) - gap;
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
const master = wins[0];
|
const master = windowsToTile[0];
|
||||||
master.move_resize_frame(true,
|
master.move_resize_frame(
|
||||||
inner.x, inner.y, masterW, inner.height);
|
true,
|
||||||
|
innerArea.x,
|
||||||
const stack = { x: inner.x + masterW + this._innerGap,
|
innerArea.y,
|
||||||
y: inner.y,
|
masterWidth,
|
||||||
width: inner.width - masterW - this._innerGap,
|
innerArea.height
|
||||||
height: inner.height };
|
);
|
||||||
this._splitLayout(wins.slice(1), stack);
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
// --- EXTENSION-WRAPPER (for legacy loader) ---
|
||||||
class SimpleTilingExtension {
|
var LegacyExtension = class {
|
||||||
enable() { this.tiler = new Tiler(); this.tiler.enable(); }
|
constructor(metadata) {
|
||||||
disable() { this.tiler?.disable(); this.tiler = null; }
|
this.tiler = null;
|
||||||
}
|
}
|
||||||
|
enable() {
|
||||||
|
this.tiler = new Tiler();
|
||||||
|
this.tiler.enable();
|
||||||
|
}
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function init() {
|
function init(metadata) {
|
||||||
return new SimpleTilingExtension();
|
return new LegacyExtension(metadata);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"uuid": "__UUID__",
|
||||||
|
"name": "Simple Tiling",
|
||||||
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
|
"version": __VERSION__,
|
||||||
|
"shell-version": [
|
||||||
|
"40",
|
||||||
|
"41",
|
||||||
|
"42",
|
||||||
|
"43",
|
||||||
|
"44"
|
||||||
|
],
|
||||||
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
|
"preferences_ui": "prefs.js",
|
||||||
|
"url": "https://github.com/Domoel/Simple-Tiling",
|
||||||
|
"gettext-domain": "__UUID__"
|
||||||
|
}
|
||||||
@@ -4,12 +4,7 @@
|
|||||||
"description": "A Simple Tiling Extension for Gnome Shell.",
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
"version": __VERSION__,
|
"version": __VERSION__,
|
||||||
"shell-version": [
|
"shell-version": [
|
||||||
"3.38",
|
"3.38"
|
||||||
"40",
|
|
||||||
"41",
|
|
||||||
"42",
|
|
||||||
"43",
|
|
||||||
"44"
|
|
||||||
],
|
],
|
||||||
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
"preferences_ui": "prefs.js",
|
"preferences_ui": "prefs.js",
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ class InteractionHandler {
|
|||||||
key,
|
key,
|
||||||
this._settings,
|
this._settings,
|
||||||
Meta.KeyBindingFlags.NONE,
|
Meta.KeyBindingFlags.NONE,
|
||||||
Shell.ActionMode.NORMAL,
|
|
||||||
(..._args) => handler(this)
|
(..._args) => handler(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// Simple-Tiling – MODERN MENU (GNOME Shell 40-44) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import Adw from 'gi://Adw';
|
||||||
|
import Gio from 'gi://Gio';
|
||||||
|
import Gtk from 'gi://Gtk';
|
||||||
|
import GLib from 'gi://GLib';
|
||||||
|
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/shell/extensions/js/extensions/prefs.js';
|
||||||
|
|
||||||
|
export default class SimpleTilingPrefs extends ExtensionPreferences {
|
||||||
|
fillPreferencesWindow(window) {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
const page = new Adw.PreferencesPage();
|
||||||
|
window.add(page);
|
||||||
|
|
||||||
|
// --- Group for Window Gaps ---
|
||||||
|
const groupGaps = new Adw.PreferencesGroup({ title: 'Window Gaps' });
|
||||||
|
page.add(groupGaps);
|
||||||
|
|
||||||
|
const rowInnerGap = new Adw.SpinRow({
|
||||||
|
title: 'Inner Gap',
|
||||||
|
subtitle: 'The gap between windows in pixels.',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowInnerGap);
|
||||||
|
settings.bind('inner-gap', rowInnerGap, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
const rowOuterH = new Adw.SpinRow({
|
||||||
|
title: 'Outer Gap (horizontal)',
|
||||||
|
subtitle: 'Left / right screen edges (pixels)',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowOuterH);
|
||||||
|
settings.bind('outer-gap-horizontal', rowOuterH, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
const rowOuterV = new Adw.SpinRow({
|
||||||
|
title: 'Outer Gap (vertical)',
|
||||||
|
subtitle: 'Top / bottom screen edges (pixels)',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowOuterV);
|
||||||
|
settings.bind('outer-gap-vertical', rowOuterV, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
// --- Group for Window Behavior ---
|
||||||
|
const groupBehavior = new Adw.PreferencesGroup({ title: 'Window Behavior' });
|
||||||
|
page.add(groupBehavior);
|
||||||
|
|
||||||
|
const rowNewWindow = new Adw.ComboRow({
|
||||||
|
title: 'Open new windows as',
|
||||||
|
subtitle: 'Whether a new window starts as Master or Stack',
|
||||||
|
model: new Gtk.StringList({
|
||||||
|
strings: ['Stack Window (Default)', 'Master Window'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
groupBehavior.add(rowNewWindow);
|
||||||
|
|
||||||
|
const currentBehavior = settings.get_string('new-window-behavior');
|
||||||
|
rowNewWindow.selected = currentBehavior === 'master' ? 1 : 0;
|
||||||
|
|
||||||
|
rowNewWindow.connect('notify::selected', () => {
|
||||||
|
const newVal = rowNewWindow.selected === 1 ? 'master' : 'stack';
|
||||||
|
settings.set_string('new-window-behavior', newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Group for Keybindings ---
|
||||||
|
const groupKeys = new Adw.PreferencesGroup({ title: 'Keybindings' });
|
||||||
|
page.add(groupKeys);
|
||||||
|
|
||||||
|
const rowKeys = new Adw.ActionRow({
|
||||||
|
title: 'Configure Shortcuts',
|
||||||
|
subtitle: 'Adjust all shortcuts in GNOME Keyboard settings.',
|
||||||
|
});
|
||||||
|
groupKeys.add(rowKeys);
|
||||||
|
|
||||||
|
const btnOpenKeyboard = new Gtk.Button({ label: 'Open Keyboard Settings' });
|
||||||
|
btnOpenKeyboard.connect('clicked', () => {
|
||||||
|
const appInfo = Gio.AppInfo.create_from_commandline(
|
||||||
|
'gnome-control-center keyboard', null, Gio.AppInfoCreateFlags.NONE
|
||||||
|
);
|
||||||
|
appInfo.launch([], null);
|
||||||
|
});
|
||||||
|
rowKeys.add_suffix(btnOpenKeyboard);
|
||||||
|
rowKeys.set_activatable_widget(btnOpenKeyboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-16
@@ -1,13 +1,14 @@
|
|||||||
///////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////
|
||||||
// Simple‑Tiling – MODERN MENU (GNOME Shell 45+) //
|
// Simple-Tiling – MODERN MENU (GNOME Shell 45+) //
|
||||||
// © 2025 domoel – MIT //
|
// © 2025 domoel – MIT //
|
||||||
/////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
import { ExtensionPreferences } from 'resource:///org/gnome/shell/extensions/extension.js';
|
import Adw from 'gi://Adw';
|
||||||
import Adw from 'gi://Adw';
|
import Gio from 'gi://Gio';
|
||||||
import Gio from 'gi://Gio';
|
import Gtk from 'gi://Gtk';
|
||||||
import Gtk from 'gi://Gtk';
|
import GLib from 'gi://GLib';
|
||||||
|
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/shell/extensions/prefs.js';
|
||||||
|
|
||||||
export default class SimpleTilingPrefs extends ExtensionPreferences {
|
export default class SimpleTilingPrefs extends ExtensionPreferences {
|
||||||
fillPreferencesWindow(window) {
|
fillPreferencesWindow(window) {
|
||||||
@@ -17,13 +18,13 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
|
|||||||
|
|
||||||
// ── WINDOW GAPS ────────────────────────────────────────────
|
// ── WINDOW GAPS ────────────────────────────────────────────
|
||||||
const groupGaps = new Adw.PreferencesGroup({
|
const groupGaps = new Adw.PreferencesGroup({
|
||||||
title: 'Window Gaps',
|
title: 'Window Gaps',
|
||||||
description: 'Adjust spacing between windows and screen edges.'
|
description: 'Adjust spacing between windows and screen edges.'
|
||||||
});
|
});
|
||||||
page.add(groupGaps);
|
page.add(groupGaps);
|
||||||
|
|
||||||
const rowInnerGap = new Adw.SpinRow({
|
const rowInnerGap = new Adw.SpinRow({
|
||||||
title: 'Inner Gap',
|
title: 'Inner Gap',
|
||||||
subtitle: 'Space between tiled windows (pixels)',
|
subtitle: 'Space between tiled windows (pixels)',
|
||||||
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
});
|
});
|
||||||
@@ -31,7 +32,7 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
|
|||||||
settings.bind('inner-gap', rowInnerGap, 'value', Gio.SettingsBindFlags.DEFAULT);
|
settings.bind('inner-gap', rowInnerGap, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
const rowOuterH = new Adw.SpinRow({
|
const rowOuterH = new Adw.SpinRow({
|
||||||
title: 'Outer Gap (horizontal)',
|
title: 'Outer Gap (horizontal)',
|
||||||
subtitle: 'Left / right screen edges (pixels)',
|
subtitle: 'Left / right screen edges (pixels)',
|
||||||
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
});
|
});
|
||||||
@@ -39,7 +40,7 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
|
|||||||
settings.bind('outer-gap-horizontal', rowOuterH, 'value', Gio.SettingsBindFlags.DEFAULT);
|
settings.bind('outer-gap-horizontal', rowOuterH, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
const rowOuterV = new Adw.SpinRow({
|
const rowOuterV = new Adw.SpinRow({
|
||||||
title: 'Outer Gap (vertical)',
|
title: 'Outer Gap (vertical)',
|
||||||
subtitle: 'Top / bottom screen edges (pixels)',
|
subtitle: 'Top / bottom screen edges (pixels)',
|
||||||
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
});
|
});
|
||||||
@@ -47,19 +48,20 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
|
|||||||
settings.bind('outer-gap-vertical', rowOuterV, 'value', Gio.SettingsBindFlags.DEFAULT);
|
settings.bind('outer-gap-vertical', rowOuterV, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
// ── WINDOW BEHAVIOR ────────────────────────────────────────────
|
// ── WINDOW BEHAVIOR ────────────────────────────────────────────
|
||||||
const groupBehavior = new Adw.PreferencesGroup({ title: 'Window Behavior' });
|
const groupBehavior = new Adw.PreferencesGroup({ title: 'Window Behavior' });
|
||||||
page.add(groupBehavior);
|
page.add(groupBehavior);
|
||||||
|
|
||||||
const rowNewWindow = new Adw.ComboRow({
|
const rowNewWindow = new Adw.ComboRow({
|
||||||
title: 'Open new windows as',
|
title: 'Open new windows as',
|
||||||
subtitle: 'Whether a new window starts as Master or Stack',
|
subtitle: 'Whether a new window starts as Master or Stack',
|
||||||
model: new Gtk.StringList({
|
model: new Gtk.StringList({
|
||||||
strings: ['Stack Window (Default)', 'Master Window'],
|
strings: ['Stack Window (Default)', 'Master Window'],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
groupBehavior.add(rowNewWindow);
|
groupBehavior.add(rowNewWindow);
|
||||||
|
|
||||||
rowNewWindow.selected = settings.get_string('new-window-behavior') === 'master' ? 1 : 0;
|
const currentBehavior = settings.get_string('new-window-behavior');
|
||||||
|
rowNewWindow.selected = currentBehavior === 'master' ? 1 : 0;
|
||||||
|
|
||||||
rowNewWindow.connect('notify::selected', () => {
|
rowNewWindow.connect('notify::selected', () => {
|
||||||
const newVal = rowNewWindow.selected === 1 ? 'master' : 'stack';
|
const newVal = rowNewWindow.selected === 1 ? 'master' : 'stack';
|
||||||
@@ -72,11 +74,11 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
|
|||||||
|
|
||||||
const rowKeys = new Adw.ActionRow({
|
const rowKeys = new Adw.ActionRow({
|
||||||
title: 'Configure Shortcuts',
|
title: 'Configure Shortcuts',
|
||||||
subtitle: 'Adjust all shortcuts in GNOME Keyboard settings.',
|
subtitle: 'Adjust all shortcuts in GNOME Keyboard settings.',
|
||||||
});
|
});
|
||||||
groupKeys.add(rowKeys);
|
groupKeys.add(rowKeys);
|
||||||
|
|
||||||
const btnOpenKeyboard = new Gtk.Button({ label: 'Open Keyboard Settings' });
|
const btnOpenKeyboard = new Gtk.Button({ label: 'Open Keyboard Settings' });
|
||||||
btnOpenKeyboard.connect('clicked', () => {
|
btnOpenKeyboard.connect('clicked', () => {
|
||||||
const appInfo = Gio.AppInfo.create_from_commandline(
|
const appInfo = Gio.AppInfo.create_from_commandline(
|
||||||
'gnome-control-center keyboard', null, Gio.AppInfoCreateFlags.NONE
|
'gnome-control-center keyboard', null, Gio.AppInfoCreateFlags.NONE
|
||||||
|
|||||||
Reference in New Issue
Block a user