89 Commits

Author SHA1 Message Date
Dome 8db89fc155 Update Makefile 2025-10-11 22:22:35 +02:00
Dome 39e8a3541b Update enterprise.js 2025-10-11 22:12:59 +02:00
Dome 47f5a734be Update interim.js 2025-10-11 22:12:33 +02:00
Dome 1d5cdf480c Update modern.js 2025-10-11 22:12:13 +02:00
Dome 5f09053e06 Update enterprise.js 2025-10-01 16:44:20 +02:00
Dome 2295756f19 Update enterprise.js 2025-10-01 16:28:43 +02:00
Dome 58b4e746b6 Update interim.js 2025-10-01 16:28:03 +02:00
Dome e37cc5f8f4 Update legacy.js 2025-10-01 16:26:50 +02:00
Dome f10d3f0adf Update prefs_enterprise.js 2025-10-01 16:26:28 +02:00
Dome 528bf11371 Update prefs_interim.js 2025-10-01 16:26:02 +02:00
Dome 0598fa615d Update prefs_legacy.js 2025-10-01 16:25:45 +02:00
Dome 2e61ade64f Update prefs_modern.js 2025-10-01 16:25:02 +02:00
Dome 37e8a149f1 Update Makefile 2025-10-01 09:56:42 +02:00
Dome ef67c2d187 Update README.md 2025-10-01 09:47:02 +02:00
Dome 56ad942f24 Create prefs_enterprise.js 2025-10-01 09:43:12 +02:00
Dome ba04453c65 Create metadata_enterprise.json.in 2025-10-01 09:42:47 +02:00
Dome fd174e084e Create enterprise.js 2025-10-01 09:42:14 +02:00
Dome 6ade2e1520 Update Makefile 2025-10-01 09:41:35 +02:00
Dome b2099a6c1e Update metadata_interim.json.in 2025-09-30 17:10:30 +02:00
Dome 034bdcb05b Update Makefile 2025-09-29 20:08:56 +02:00
Dome 6d7acefed9 Update interim.js 2025-09-29 20:06:10 +02:00
Dome ef098d8986 Update modern.js 2025-09-29 09:39:12 +02:00
Dome 18d698cf7e Update interim.js 2025-09-29 09:15:19 +02:00
Dome 0aa80fda6a Update interim.js 2025-09-29 09:13:23 +02:00
Dome 7e51b5ff62 Update modern.js 2025-09-29 09:00:15 +02:00
Dome fe41ea8312 Update modern.js
fixes error "Fails on GNOME Shell 48.3 with TypeError get_session not a function" on modern.,js
2025-09-29 08:56:34 +02:00
Dome 5020250699 Update README.md 2025-09-28 23:33:32 +02:00
Dome 16688d6996 Update README.md 2025-09-28 23:17:24 +02:00
Dome 6b8ac746aa Update modern.js 2025-09-28 23:16:42 +02:00
Dome bcc5044cf8 Update prefs_modern.js 2025-09-28 23:15:56 +02:00
Dome e2df9fb038 Delete metadata_gnome45-48.json.in 2025-09-28 23:11:46 +02:00
Dome 5e23390911 Delete prefs_gnome45-48.js 2025-09-28 23:11:27 +02:00
Dome 7ba8dd1b0f Delete gnome45-48.js 2025-09-28 23:11:18 +02:00
Dome 849e2dc177 Update metadata_modern.json.in 2025-09-28 23:11:03 +02:00
Dome 982ca71642 Update modern.js 2025-09-28 23:10:43 +02:00
Dome a62c2307f9 Merge pull request #13 from ValerioCataldo/gnome49-comp
Gnome 49 compatibility
2025-09-28 20:21:56 +00:00
Dome b1c4924276 Rename metadata_modern,json.in to metadata_modern.json.in 2025-09-28 22:20:56 +02:00
Dome 73874fbd4b Rename prefs.interim.js to prefs_interim.js 2025-09-28 22:19:08 +02:00
valerio cataldo e4af31cb90 updating metadata 2025-09-27 13:26:15 +02:00
valerio cataldo b400189b4f split files 2025-09-27 13:23:26 +02:00
ValerioCataldo 568ceb7823 Merge branch 'Domoel:main' into gnome49-comp 2025-09-27 13:12:59 +02:00
valerio cataldo 138701a33d fixed for gnome49 2025-09-27 13:03:24 +02:00
Dome bbe09af89b Update interim.js 2025-08-22 12:37:08 +02:00
Dome 3331c84f31 Update Makefile 2025-08-22 12:34:32 +02:00
Dome 6591690c69 Update interim.js 2025-08-22 12:27:05 +02:00
Dome 40bea7a937 Update prefs_modern.js 2025-08-22 12:25:30 +02:00
Dome 4ea80d27bf Update README.md 2025-08-13 03:33:25 +00:00
Dome 55dd6ca691 Update Makefile 2025-08-11 22:48:20 +02:00
Dome 573ddc2702 Update prefs_modern.js 2025-08-11 22:47:21 +02:00
Dome a4ce7f2613 Update prefs.interim.js 2025-08-11 22:47:03 +02:00
Dome f2971f2c1c Update README.md 2025-08-11 20:54:31 +02:00
Dome 72fee16254 Update README.md 2025-08-11 08:30:41 +00:00
Dome d127480261 Update README.md 2025-08-11 08:24:46 +00:00
Dome 24f5dba546 Update README.md 2025-08-11 02:17:29 +02:00
Dome c3c47dc025 Update legacy.js 2025-08-11 02:04:49 +02:00
Dome c83e987550 Update README.md 2025-08-11 01:59:15 +02:00
Dome f5c5890fe4 Create interim.js 2025-08-11 01:52:48 +02:00
Dome dd6b1d067e Create metadata_interim.json.in 2025-08-11 01:52:20 +02:00
Dome 18e5f4594c Create prefs.interim.js 2025-08-11 01:51:54 +02:00
Dome a101ad0988 Update Makefile 2025-08-11 01:51:17 +02:00
Dome 8feccbde10 Update legacy.js 2025-08-11 01:50:53 +02:00
Dome 7c9e3e8122 Update metadata_legacy.json.in 2025-08-11 01:50:23 +02:00
Dome b3e00c8f94 Update metadata_modern.json.in 2025-08-11 01:50:09 +02:00
Dome b50c2780d3 Update modern.js 2025-08-11 01:49:52 +02:00
Dome a09c934f1b Update prefs_legacy.js 2025-08-11 01:49:16 +02:00
Dome 9a9e724654 Update prefs_modern.js 2025-08-11 01:48:56 +02:00
Dome bb28c6b936 Update README.md 2025-07-31 21:58:25 +02:00
Dome 6bf3b17054 Update README.md 2025-07-31 16:47:34 +00:00
Dome 0e14ecbd18 Update README.md 2025-07-31 10:34:20 +02:00
Dome 20d9e94417 Update README.md 2025-07-31 10:33:35 +02:00
Dome 58ca81740b Update README.md 2025-07-31 10:31:47 +02:00
Dome 405871c0f3 Update README.md 2025-07-31 10:30:21 +02:00
Dome 053f5fe90a Update Makefile 2025-07-31 10:26:00 +02:00
Dome 6165bc6b62 Update Makefile 2025-07-31 10:22:17 +02:00
Dome 1289af64ce Update README.md 2025-07-31 10:08:23 +02:00
Dome 7ac6d58665 Update Makefile 2025-07-31 09:59:54 +02:00
Dome 6e03f07486 Update Makefile
added 

- make build-legacy-go   # Legacy-Ordner direkt nach ~/.local/…/extensions
- make build-modern-go   # Modern-Ordner direkt nach ~/.local/…/extensions
2025-07-31 09:58:20 +02:00
Dome f07c66101b Update README.md 2025-07-31 09:49:04 +02:00
Dome 992438d480 Update README.md 2025-07-31 09:47:03 +02:00
Dome a5b888b58f Update README.md 2025-07-31 09:44:51 +02:00
Dome 8a14b04958 Delete .gitignore 2025-07-30 15:45:31 +02:00
Dome c34cc0f48e Update README.md 2025-07-30 15:38:13 +02:00
Dome 825e35ff05 Update README.md 2025-07-30 15:37:09 +02:00
Dome cef62f2ad1 Update README.md 2025-07-30 11:08:59 +02:00
Dome 7422622b4d Update README.md 2025-07-30 11:07:27 +02:00
Dome f9cd5255c3 Update README.md 2025-07-30 11:06:37 +02:00
Dome 374f857152 Update README.md 2025-07-30 09:01:14 +02:00
Dome 81ea1db46f Update README.md 2025-07-30 00:35:37 +02:00
Dome 1b2025cb81 Update README.md 2025-07-30 00:34:55 +02:00
15 changed files with 2073 additions and 457 deletions
-6
View File
@@ -1,6 +0,0 @@
# GNOME Shell Extension specific
schemas/gschemas.compiled
# Common temporary files
*~
*.swp
+107 -48
View File
@@ -1,24 +1,30 @@
###############################################################################
# SimpleTiling  Makefile
# Simple-Tiling Makefile
#
# make build → baut beide ZIPPakete
# make build-legacy → nur LegacyZIP (Shell 3.3844)
# make build-modern → nur ModernZIP (Shell 4548)
# make clean → räumt auf
# make build → Erzeugt alle vier Versionen als Archivdatei
# make build-legacy → Erzeugt Legacy-ZIP (Shell 3.38)
# make build-enterprise → Erzeugt Enterprise-ZIP (Shell 40)
# make build-interim → Erzeugt Interim-ZIP (Shell 41-44)
# make build-modern → Erzeugt Modern-ZIP (Shell 45+)
# make install-legacy → Installiert Legacy Extension
# make install-enterprise → Installiert Enterprise Extension
# make install-interim → Installiert Interim Extension
# make install-modern → Installiert Modern Extension
# make clean → Bereinigt das Ausgangsverzeichnis
###############################################################################
UUID := simple-tiling@domoel
VERSION := 6
VERSION := 7.6
EXTDIR := $(HOME)/.local/share/gnome-shell/extensions
# Dateien/Ordner, die in *beide* Pakete gehören
COMMON_FILES := schemas exceptions.txt locale *.css README.md LICENSE
# PrefDateien (zwei Varianten)
LEGACY_PREFS := prefs_legacy.js
MODERN_PREFS := prefs_modern.js
COMMON_FILES := schemas exceptions.txt locale *.css README.md LICENSE
LEGACY_PREFS := prefs_legacy.js
ENTERPRISE_PREFS := prefs_enterprise.js
INTERIM_PREFS := prefs_interim.js
MODERN_PREFS := prefs_modern.js
###############################################################################
# Helfer: copies <file list> <dest>
# Helper: copies <file list> <dest>
###############################################################################
define copies
@for f in $(1) ; do \
@@ -28,60 +34,113 @@ define copies
done
endef
.PHONY: build build-legacy build-modern clean
build: build-legacy build-modern
.PHONY: build build-legacy build-enterprise build-interim build-modern \
install-legacy install-enterprise install-interim install-modern clean
build: build-legacy build-enterprise build-interim build-modern
###############################################################################
# LegacyBuild
# Erzeugt Legacy-ZIP (Shell 3.38)
###############################################################################
build-legacy:
@echo "==> Building LEGACY package (3.3844)…"
@echo "==> Building LEGACY zip (for GNOME 3.38)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
# Schema kompilieren
@glib-compile-schemas build/$(UUID)/schemas
# Haupt und PrefSkript
@cp legacy.js build/$(UUID)/extension.js
@cp $(LEGACY_PREFS) build/$(UUID)/prefs.js
# metadata.json anpassen
@cp $(LEGACY_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_legacy.json.in > build/$(UUID)/metadata.json
# ZipPaket
-e "s/__VERSION__/$(VERSION)/g" \
metadata_legacy.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
@rm -rf build
@echo "created $(UUID)-legacy-v$(VERSION).zip"
@echo "$(UUID)-legacy-v$(VERSION).zip created"
###############################################################################
# ModernBuild
# Erzeugt Enterprise-ZIP (Shell 40)
###############################################################################
build-modern:
@echo "==> Building MODERN package (4548)…"
build-enterprise:
@echo "==> Building ENTERPRISE zip (for GNOME 40)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
# Schema kompilieren
@glib-compile-schemas build/$(UUID)/schemas
# Haupt und PrefSkript
@cp modern.js build/$(UUID)/extension.js
@cp $(MODERN_PREFS) build/$(UUID)/prefs.js
# metadata.json anpassen
@cp enterprise.js build/$(UUID)/extension.js
@cp $(ENTERPRISE_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_modern.json.in > build/$(UUID)/metadata.json
# ZipPaket
@cd build && zip -qr ../$(UUID)-modern-v$(VERSION).zip .
-e "s/__VERSION__/$(VERSION)/g" \
metadata_enterprise.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-enterprise-v$(VERSION).zip .
@rm -rf build
@echo "created $(UUID)-modern-v$(VERSION).zip"
@echo "$(UUID)-enterprise-v$(VERSION).zip created"
###############################################################################
# Erzeugt Interim-ZIP (Shell 41-44)
###############################################################################
build-interim:
@echo "==> Building INTERIM zip (for GNOME 41-44)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
@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"
###############################################################################
# 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_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_modern.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-modern-v$(VERSION).zip .
@rm -rf build
@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)/$(UUID)
@rm -f $(UUID)-legacy-v$(VERSION).zip
@echo "✓ Legacy Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
install-enterprise: build-enterprise
@echo "==> Installing ENTERPRISE Extension..."
@rm -rf $(EXTDIR)/$(UUID)
@unzip -q $(UUID)-enterprise-v$(VERSION).zip -d $(EXTDIR)/$(UUID)
@rm -f $(UUID)-enterprise-v$(VERSION).zip
@echo "✓ Enterprise 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)/$(UUID)
@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)/$(UUID)
@rm -f $(UUID)-modern-v$(VERSION).zip
@echo "✓ Modern Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
###############################################################################
# Bereinigt das Ausgangsverzeichnis
###############################################################################
clean:
@rm -rf build $(UUID)-legacy-v$(VERSION).zip $(UUID)-modern-v$(VERSION).zip
@echo "BuildOrdner und ZIPs entfernt."
@rm -f $(UUID)-*.zip
@echo "Build directory and ZIPs removed."
+25 -35
View File
@@ -16,7 +16,7 @@ A lightweight, opinionated, and automatic tiling window manager for GNOME Shell
Simple Tiling is a GNOME Shell extension created for users who want a clean, predictable, and automatic tiling layout without the complexity of larger, more feature-heavy tiling extensions. It is designed to be simple to configure and intuitive to use, focusing on a core set of essential tiling features.
This extension was built from the ground up to be stable and performant on **GNOME Shell 3.38**. However it is now also supporting modern gnome shells up to **version 48**.
This extension was built from the ground up to be stable and performant on **GNOME Shell 3.38**. However it is now also supporting modern gnome shells up to **version 49**.
## Features
@@ -37,7 +37,7 @@ This extension was built from the ground up to be stable and performant on **GNO
Please note that this extension has been developed for a very specific environment. However, with the latest updates, I have ensured that modern Gnome Shells and Wayland are also supported.
* **GNOME Shell Version:** **3.38 - 48**
* **GNOME Shell Version:** **3.38 - 49**
* **Session Type:** **X11** (Wayland is still in beta but should be fine!).
* **Monitor Setup:** **Single monitor only.** Multi-monitor support is not yet implemented.
@@ -49,42 +49,33 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
#### Manual Installation
The repository includes a Makefile that produces readytoinstall ZIP packages for the two supported GNOMEShell lines (a legacy build (Gnome-Shell 3.38 - 44) and a modern build for Gnome-Shell 45+).
The repository includes a Makefile that produces readytoinstall ZIP packages for the three supported GnomeShell lines (a legacy build for Gnome-Shell 3.38, an enterprise build for Gnome-Shell 40, an interim build for Gnome-Shell 41 - 44 and a modern build for Gnome-Shell 45+).
1. **Clone the Source**
```bash
git clone https://github.com/YourUser/Simple-Tiling.git
cd Simple-Tiling
```
```bash
git clone https://github.com/Domoel/Simple-Tiling.git
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
```bash
make build
make install-legacy # Installs Legacy Extension (Gnome-Shell 3.38)
make install-enterprise # Installs Enterprise Extension (Gnome-Shell 40)
make install-interim # Installs Interim Extension (Gnome-Shell 41 - 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-enterprise`, `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**
```bash
ls -1 ../simple-tiling@domoel-*-v*.zip
```
4 · **Install & enable**
```bash
gnome-extensions install ../simple-tiling@domoel-legacy-v6.zip
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.
4. **Reload the shell**
```bash
Press Alt + F2, type r , hit ↩ (works for X11 and Wayland)
```
5. **Clean up (optional)**
```bash
make clean # perform this command in the downloaded folder to remove builds and generated ZIPs
```
## Configuration
@@ -98,13 +89,13 @@ All keyboard shortcuts can be configured through the Settings panel of Simple Ti
#### 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.
* 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.
@@ -132,4 +123,3 @@ This extension was built to solve a specific need. However, future enhancements
## License
This project is licensed under the MIT License - see the `LICENSE` file for details.
+604
View File
@@ -0,0 +1,604 @@
////////////////////////////////////////////////////////////////
// Simple-Tiling ENTERPRISE (GNOME Shell 40 non-ESM) //
// © 2025 domoel MIT //
////////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ───────────────────────────
const { Meta, Shell, Gio, GLib, Clutter } = imports.gi;
const Main = imports.ui.main;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
// ── 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'),
};
// ── HELPERFUNCTION ────────────────────────────────────────
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) {
Main.wm.addKeybinding(
key,
this._settings,
Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL,
(..._args) => handler(this)
);
}
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) Main.wm.removeKeybinding(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);
}
}
// ── EXTENSIONWRAPPER ──────────────────────────
let tiler;
function enable() {
tiler = new Tiler(Me);
tiler.enable();
}
function disable() {
if (tiler) {
tiler.disable();
tiler = null;
}
}
+607
View File
@@ -0,0 +1,607 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  INTERIM (GNOME Shell 41 - 44) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import { Extension } from 'resource:///org/gnome/shell/extensions/js/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
// ── 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'),
};
// ── HELPERFUNCTION ────────────────────────────────────────
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) {
Main.wm.addKeybinding(
key,
this._settings,
Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL,
(..._args) => handler(this)
);
}
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) Main.wm.removeKeybinding(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);
}
}
// ── EXTENSIONWRAPPER ───────────────────────────────────
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;
}
}
}
+461 -334
View File
@@ -1,27 +1,24 @@
///////////////////////////////////////////////////////////////
// SimpleTiling LEGACY (GNOME Shell 3.38  44) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
// Simple-Tiling LEGACY (for GNOME Shell 3.38) //
// © 2025 domoel MIT //
/////////////////////////////////////////////////////////////
'use strict';
// ── GLOBAL IMPORTS ────────────────────────────────────────
const Main = imports.ui.main;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const ExtensionUtils= imports.misc.extensionUtils;
const ByteArray = imports.byteArray;
const Main = imports.ui.main;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const ExtensionUtils = imports.misc.extensionUtils;
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 SCHEMA_NAME = 'org.gnome.shell.extensions.simple-tiling.domoel';
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 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(),
@@ -35,98 +32,77 @@ const KEYBINDINGS = {
'focus-down': (self) => self._focusInDirection('down'),
};
// ── HELPERFUNCTION ────────────────────────────────────────
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 ───────────────────────────────────
// --- INTERACTIONHANDLER ---
class InteractionHandler {
constructor(tiler) {
this.tiler = tiler;
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
this.tiler = tiler;
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
this._wmKeysToDisable = [];
this._savedWmShortcuts= {};
this._grabOpIds = [];
this._savedWmShortcuts = {};
this._grabOpIds = [];
this._settingsChangedId = null;
this._onSettingsChanged = this._onSettingsChanged.bind(this);
this._prepareWmShortcuts();
}
enable() {
if (this._wmKeysToDisable.length)
this._wmKeysToDisable.forEach(k =>
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
if (this._wmKeysToDisable.length) {
this._wmKeysToDisable.forEach((key) =>
this._wmSettings.set_value(key, new GLib.Variant("as", []))
);
}
this._bindAllShortcuts();
this._settingsChangedId =
this._settings.connect('changed', this._onSettingsChanged);
this._settingsChangedId = this._settings.connect(
"changed",
this._onSettingsChanged
);
this._grabOpIds.push(
global.display.connect('grab-op-begin',
(display, screen, win) => {
if (this.tiler.windows.includes(win))
this.tiler.grabbedWindow = win;
}));
global.display.connect(
"grab-op-begin",
(display, screen, window) => {
if (this.tiler.windows.includes(window)) {
this.tiler.grabbedWindow = window;
}
}
)
);
this._grabOpIds.push(
global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
global.display.connect("grab-op-end", this._onGrabEnd.bind(this))
);
}
disable() {
if (this._wmKeysToDisable.length)
this._wmKeysToDisable.forEach(k =>
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
if (this._wmKeysToDisable.length) {
this._wmKeysToDisable.forEach((key) =>
this._wmSettings.set_value(key, this._savedWmShortcuts[key])
);
}
this._unbindAllShortcuts();
if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = null;
}
this._grabOpIds.forEach(id => global.display.disconnect(id));
this._grabOpIds.forEach((id) => global.display.disconnect(id));
this._grabOpIds = [];
}
_bind(key, callback) {
addKeybinding(key, this._settings,
Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL,
() => callback(this));
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, 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() {
this._unbindAllShortcuts();
@@ -135,185 +111,240 @@ class InteractionHandler {
_prepareWmShortcuts() {
const schema = this._wmSettings.settings_schema;
const keys = [];
if (schema.has_key('toggle-tiled-left'))
keys.push('toggle-tiled-left','toggle-tiled-right');
else if (schema.has_key('tile-left'))
keys.push('tile-left','tile-right');
if (schema.has_key('toggle-maximized'))
keys.push('toggle-maximized');
const keys = [];
if (schema.has_key("toggle-tiled-left"))
keys.push("toggle-tiled-left", "toggle-tiled-right");
else if (schema.has_key("tile-left"))
keys.push("tile-left", "tile-right");
if (schema.has_key("toggle-maximized")) keys.push("toggle-maximized");
else {
if (schema.has_key('maximize')) keys.push('maximize');
if (schema.has_key('unmaximize')) keys.push('unmaximize');
if (schema.has_key("maximize")) keys.push("maximize");
if (schema.has_key("unmaximize")) keys.push("unmaximize");
}
if (keys.length) {
this._wmKeysToDisable = keys;
keys.forEach(k => this._savedWmShortcuts[k] =
this._wmSettings.get_value(k));
keys.forEach(
(key) => (this._savedWmShortcuts[key] = this._wmSettings.get_value(key))
);
}
}
_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());
const sourceWindow = global.display.get_focus_window();
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
const targetWindow = this._findTargetInDirection(
sourceWindow,
direction
);
if (targetWindow) {
targetWindow.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]];
const windows = this.tiler.windows;
if (windows.length < 2) return;
const focusedWindow = global.display.get_focus_window();
if (!focusedWindow || !windows.includes(focusedWindow)) return;
const focusedIndex = windows.indexOf(focusedWindow);
if (focusedIndex > 0) {
[windows[0], windows[focusedIndex]] = [
windows[focusedIndex],
windows[0],
];
} else if (focusedIndex === 0) {
[windows[0], windows[1]] = [windows[1], windows[0]];
}
this.tiler.tileNow();
w[0]?.activate(global.get_current_time());
if (windows.length > 0) windows[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 srcIdx = this.tiler.windows.indexOf(src);
if (srcIdx === 0 && direction === 'right' && this.tiler.windows.length>1)
tgt = this.tiler.windows[1];
else
tgt = this._findTargetInDirection(src, direction);
if (!tgt) return;
const tgtIdx = this.tiler.windows.indexOf(tgt);
[this.tiler.windows[srcIdx], this.tiler.windows[tgtIdx]] =
[this.tiler.windows[tgtIdx], this.tiler.windows[srcIdx]];
const sourceWindow = global.display.get_focus_window();
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
let targetWindow = null;
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
if (
sourceIndex === 0 &&
direction === "right" &&
this.tiler.windows.length > 1
) {
targetWindow = this.tiler.windows[1];
} else {
targetWindow = this._findTargetInDirection(sourceWindow, direction);
}
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();
src.activate(global.get_current_time());
sourceWindow.activate(global.get_current_time());
}
_findTargetInDirection(src, direction) {
const sRect = src.get_frame_rect();
const cands = [];
_findTargetInDirection(source, direction) {
const sourceRect = source.get_frame_rect();
let candidates = [];
for (const win of this.tiler.windows) {
if (win === src) continue;
const tRect = win.get_frame_rect();
if (win === source) continue;
const targetRect = win.get_frame_rect();
switch (direction) {
case 'left': if (tRect.x < sRect.x) cands.push(win); break;
case 'right': if (tRect.x > sRect.x) cands.push(win); break;
case 'up': if (tRect.y < sRect.y) cands.push(win); break;
case 'down': if (tRect.y > sRect.y) cands.push(win); break;
case "left":
if (targetRect.x < sourceRect.x) candidates.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;
let best=null, min=Infinity;
for (const win of cands) {
const tRect = win.get_frame_rect();
const dev = (direction==='left'||direction==='right')
? Math.abs(sRect.y - tRect.y)
: Math.abs(sRect.x - tRect.x);
if (dev < min) { min=dev; best=win; }
if (candidates.length === 0) return null;
let bestTarget = null;
let minDeviation = Infinity;
for (const win of candidates) {
const targetRect = win.get_frame_rect();
let deviation;
if (direction === "left" || direction === "right") {
deviation = Math.abs(sourceRect.y - targetRect.y);
} else {
deviation = Math.abs(sourceRect.x - targetRect.x);
}
if (deviation < minDeviation) {
minDeviation = deviation;
bestTarget = win;
}
}
return best;
return bestTarget;
}
_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]];
const grabbedWindow = this.tiler.grabbedWindow;
if (!grabbedWindow) return;
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
if (targetWindow) {
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
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.queueTile();
this.tiler.grabbedWindow = null;
}
_findTargetUnderPointer(exclude) {
const [x,y] = getPointer();
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 tRect=w.get_frame_rect();
const ovX = Math.max(0, Math.min(sRect.x+sRect.width,
tRect.x+tRect.width) -
Math.max(sRect.x, tRect.x));
const ovY = Math.max(0, Math.min(sRect.y+sRect.height,
tRect.y+tRect.height) -
Math.max(sRect.y, tRect.y));
const area = ovX*ovY;
if (area>max){ max=area; best=w; }
_findTargetUnderPointer(excludeWindow) {
let [pointerX, pointerY] = global.get_pointer();
let windows = global
.get_window_actors()
.map((actor) => actor.meta_window)
.filter((win) => {
if (
!win ||
win === excludeWindow ||
!this.tiler.windows.includes(win)
)
return false;
let frame = win.get_frame_rect();
return (
pointerX >= frame.x &&
pointerX < frame.x + frame.width &&
pointerY >= frame.y &&
pointerY < frame.y + frame.height
);
});
if (windows.length > 0) {
return windows[windows.length - 1];
}
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 {
constructor() {
this.windows = [];
this.grabbedWindow = null;
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
this.windows = [];
this.grabbedWindow = null;
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
this._signalIds = new Map();
this._tileInProgress = false;
this._signalIds = new Map();
this._tileTimeoutId = null;
this._centerTimeoutIds= [];
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._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._tilingDelay = TILING_DELAY_MS;
this._centeringDelay= CENTERING_DELAY_MS;
this._exceptions = [];
this._exceptions = [];
this._interactionHandler = new InteractionHandler(this);
this._onWindowAdded = this._onWindowAdded.bind(this);
this._onWindowRemoved= this._onWindowRemoved.bind(this);
this._onActiveWorkspaceChanged =
this._onActiveWorkspaceChanged.bind(this);
this._onWindowMinimizedStateChanged =
this._onWindowMinimizedStateChanged.bind(this);
this._tileTimeoutId = null;
this._centerTimeoutIds = [];
this._onWindowAdded = this._onWindowAdded.bind(this);
this._onWindowRemoved = this._onWindowRemoved.bind(this);
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind(
this
);
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind(
this
);
this._onSettingsChanged = this._onSettingsChanged.bind(this);
}
enable() {
this._loadExceptions();
const wm = global.workspace_manager;
this._signalIds.set('workspace-changed', {
object: wm,
id: wm.connect('active-workspace-changed',
this._onActiveWorkspaceChanged),
const workspaceManager = global.workspace_manager;
this._signalIds.set("workspace-changed", {
object: workspaceManager,
id: workspaceManager.connect(
"active-workspace-changed",
this._onActiveWorkspaceChanged
),
});
this._connectToWorkspace();
this._interactionHandler.enable();
this._signalIds.set('settings-changed', {
this._signalIds.set("settings-changed", {
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._disconnectFromWorkspace();
for (const [,sig] of this._signalIds) {
try { sig.object.disconnect(sig.id); } catch {}
for (const [, signal] of this._signalIds) {
try {
signal.object.disconnect(signal.id);
} catch (e) {}
}
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._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(Me.path + '/exceptions.txt');
if (!file.query_exists(null)) { this._exceptions=[]; return; }
const file = Gio.file_new_for_path(Me.path + "/exceptions.txt");
if (!file.query_exists(null)) {
this._exceptions = [];
return;
}
const [ok, data] = file.load_contents(null);
this._exceptions = ok ? decodeUtf8(data)
.split('\n')
.map(l => l.trim())
.filter(l => l && !l.startsWith('#'))
.map(l => l.toLowerCase())
: [];
this._exceptions = ok
? ByteArray.toString(data)
.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();
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;
return (
win &&
!win.minimized &&
!this._isException(win) &&
win.get_window_type() === Meta.WindowType.NORMAL
);
}
_centerWindow(win) {
const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
this._centeringDelay, () => {
const idx = this._centerTimeoutIds.indexOf(id);
if (idx>-1) this._centerTimeoutIds.splice(idx,1);
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())
if (win.get_maximized()) {
win.unmaximize(Meta.MaximizeFlags.BOTH);
}
const monitorIndex = win.get_monitor();
const workArea = getWorkAreaForMonitor(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));
const workArea = Main.layoutManager.getWorkAreaForMonitor(
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')
if (typeof win.set_keep_above === "function")
win.set_keep_above(true);
else if (typeof win.make_above === 'function')
else if (typeof win.make_above === "function")
win.make_above();
}
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) {
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._settings.get_string('new-window-behavior') === 'master')
if (this._settings.get_string("new-window-behavior") === "master") {
this.windows.unshift(win);
else
} 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-${id}`, {
object: win, id: win.connect('size-changed',
()=>{ if (!this.grabbedWindow) this.queueTile(); })});
this._signalIds.set(`min-${id}`, {
object: win, id: win.connect('notify::minimized',
this._onWindowMinimizedStateChanged)});
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 idx = this.windows.indexOf(win);
if (idx>-1) this.windows.splice(idx,1);
const index = this.windows.indexOf(win);
if (index > -1) {
this.windows.splice(index, 1);
}
['unmanaged','size','min'].forEach(pref=>{
const key = `${pref}-${win.get_id()}`;
["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{}
const { object, id } = this._signalIds.get(key);
try {
object.disconnect(id);
} catch (e) {}
this._signalIds.delete(key);
}
});
@@ -445,21 +511,31 @@ class Tiler {
}
_connectToWorkspace() {
const ws = global.workspace_manager.get_active_workspace();
ws.list_windows().forEach(w=>this._onWindowAdded(ws,w));
this._signalIds.set('win-add', {
object: ws, id: ws.connect('window-added', this._onWindowAdded)});
this._signalIds.set('win-rem', {
object: ws, id: ws.connect('window-removed', this._onWindowRemoved)});
const workspace = global.workspace_manager.get_active_workspace();
workspace
.list_windows()
.forEach((win) => this._onWindowAdded(workspace, win));
this._signalIds.set("window-added", {
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();
}
_disconnectFromWorkspace() {
this.windows.slice().forEach(w=>this._onWindowRemoved(null,w));
['win-add','win-rem'].forEach(k=>{
if (this._signalIds.has(k)) {
const {object,id}=this._signalIds.get(k);
try{ object.disconnect(id);}catch{}
this._signalIds.delete(k);
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);
}
});
}
@@ -467,88 +543,139 @@ class Tiler {
queueTile() {
if (this._tileInProgress || this._tileTimeoutId) return;
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._tileInProgress = false;
this._tileTimeoutId = null;
this._tileTimeoutId = null;
return GLib.SOURCE_REMOVE;
});
}
tileNow() { if (!this._tileInProgress) this._tileWindows(); }
tileNow() {
if (!this._tileInProgress) {
this._tileWindows();
}
}
_splitLayout(windows, area) {
if (!windows.length) return;
if (windows.length === 0) return;
if (windows.length === 1) {
windows[0].move_resize_frame(true,
area.x, area.y, area.width, area.height);
windows[0].move_resize_frame(
true,
area.x,
area.y,
area.width,
area.height
);
return;
}
const gap = Math.floor(this._innerGap/2);
const prim = [windows[0]];
const sec = windows.slice(1);
const gap = Math.floor(this._innerGap / 2);
const primaryWindows = [windows[0]];
const secondaryWindows = windows.slice(1);
let primaryArea, secondaryArea;
let primArea, secArea;
if (area.width > area.height) {
const pW = Math.floor(area.width/2) - gap;
primArea = {x: area.x, y: area.y,
width: pW, height: area.height};
secArea = {x: area.x+pW+this._innerGap, y: area.y,
width: area.width-pW-this._innerGap,
height: 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 pH = Math.floor(area.height/2) - gap;
primArea = {x: area.x, y: area.y,
width: area.width, height: pH};
secArea = {x: area.x, y: area.y+pH+this._innerGap,
width: area.width,
height: area.height-pH-this._innerGap};
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(prim, primArea);
this._splitLayout(sec, secArea);
this._splitLayout(primaryWindows, primaryArea);
this._splitLayout(secondaryWindows, secondaryArea);
}
_tileWindows() {
const wins = this.windows.filter(w=>!w.minimized);
if (!wins.length) return;
const windowsToTile = this.windows.filter((win) => !win.minimized);
if (windowsToTile.length === 0) return;
const monitor = Main.layoutManager.primaryMonitor;
const work = getWorkAreaForMonitor(monitor.index);
const inner = { x: work.x + this._outerGapHorizontal,
y: work.y + this._outerGapVertical,
width: work.width - 2*this._outerGapHorizontal,
height: work.height - 2*this._outerGapVertical };
const workArea = Main.layoutManager.getWorkAreaForMonitor(
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,
};
wins.forEach(w=>{ if (w.get_maximized())
w.unmaximize(Meta.MaximizeFlags.BOTH); });
windowsToTile.forEach((win) => {
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
});
if (wins.length===1) {
wins[0].move_resize_frame(true,
inner.x, inner.y, inner.width, inner.height);
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 masterW = Math.floor(inner.width/2) - gap;
const master = wins[0];
master.move_resize_frame(true,
inner.x, inner.y, masterW, inner.height);
const stack = { x: inner.x + masterW + this._innerGap,
y: inner.y,
width: inner.width - masterW - this._innerGap,
height: inner.height };
this._splitLayout(wins.slice(1), stack);
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);
}
}
// ── EXTENSIONWRAPPER ───────────────────────────────────
class SimpleTilingExtension {
enable() { this.tiler = new Tiler(); this.tiler.enable(); }
disable() { this.tiler?.disable(); this.tiler = null; }
}
// --- EXTENSION-WRAPPER (for legacy loader) ---
var LegacyExtension = class {
constructor(metadata) {
this.tiler = null;
}
enable() {
this.tiler = new Tiler();
this.tiler.enable();
}
disable() {
if (this.tiler) {
this.tiler.disable();
this.tiler = null;
}
}
};
function init() {
return new SimpleTilingExtension();
function init(metadata) {
return new LegacyExtension(metadata);
}
+13
View File
@@ -0,0 +1,13 @@
{
"uuid": "__UUID__",
"name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"shell-version": [
"40"
],
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
"preferences_ui": "prefs.js",
"url": "https://github.com/Domoel/Simple-Tiling",
"gettext-domain": "__UUID__"
}
+16
View File
@@ -0,0 +1,16 @@
{
"uuid": "__UUID__",
"name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"shell-version": [
"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__"
}
+1 -6
View File
@@ -4,12 +4,7 @@
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"shell-version": [
"3.38",
"40",
"41",
"42",
"43",
"44"
"3.38"
],
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
"preferences_ui": "prefs.js",
+2 -1
View File
@@ -7,7 +7,8 @@
"45",
"46",
"47",
"48"
"48",
"49"
],
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
"preferences_ui": "prefs.js",
+35 -8
View File
@@ -1,17 +1,18 @@
/////////////////////////////////////////////////////////////
// SimpleTiling  MODERN (GNOME Shell 45+) //
// SimpleTiling  MODERN (GNOME Shell 45+) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
import { Extension } from 'resource:///org/gnome/shell/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';
import Clutter from 'gi://Clutter';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
// ── CONST ────────────────────────────────────────────
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
@@ -31,6 +32,17 @@ const KEYBINDINGS = {
'focus-down': (self) => self._focusInDirection('down'),
};
// ── VERSION CHECK ────────────────────────────────────────────
let shellVersion;
if (Shell.get_session) {
shellVersion = Shell.get_session().get_shell_version();
} else if (Config.PACKAGE_VERSION) {
shellVersion = Config.PACKAGE_VERSION;
} else {
shellVersion = global.shell_version;
}
const SHELL_MAJOR = parseInt(shellVersion.split('.')[0]);
// ── HELPERFUNCTION ────────────────────────────────────────
function getPointerXY() {
if (global.get_pointer) {
@@ -101,7 +113,7 @@ class InteractionHandler {
}
_bind(key, handler) {
global.display.add_keybinding(
Main.wm.addKeybinding(
key,
this._settings,
Meta.KeyBindingFlags.NONE,
@@ -111,7 +123,7 @@ class InteractionHandler {
}
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) global.display.remove_keybinding(k); }
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) Main.wm.removeKeybinding(k); }
_onSettingsChanged() {
this._unbindAllShortcuts();
@@ -348,8 +360,15 @@ class Tiler {
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);
if (SHELL_MAJOR < 49) {
if (win.get_maximized()) {
win.unmaximize(Meta.MaximizeFlags.BOTH);
}
} else {
if (win.is_maximized()) {
win.unmaximize();
}
}
const monitorIndex = win.get_monitor();
const workspace = this._workspaceManager.get_active_workspace();
@@ -559,7 +578,15 @@ class Tiler {
height: workArea.height - 2 * this._outerGapVertical,
};
windowsToTile.forEach((win) => {
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
if (SHELL_MAJOR < 49) {
if (win.get_maximized()) {
win.unmaximize(Meta.MaximizeFlags.BOTH);
}
} else {
if (win.is_maximized()) {
win.unmaximize();
}
}
});
if (windowsToTile.length === 1) {
windowsToTile[0].move_resize_frame(
+91
View File
@@ -0,0 +1,91 @@
//////////////////////////////////////////////////////////////////
// Simple-Tiling ENTERPRISE MENU (GNOME Shell 40 non-ESM) //
// © 2025 domoel MIT //
//////////////////////////////////////////////////////////////////
'use strict';
// ── GLOBAL IMPORTS ────────────────────────────────────────
const { Adw, Gio, Gtk, GLib } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const _ = ExtensionUtils.gettext;
function init() {
}
function buildPrefsWidget() {
const settings = ExtensionUtils.getSettings();
const page = new Adw.PreferencesPage();
// ── 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);
// ── 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);
});
// ── 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);
return page;
}
+90
View File
@@ -0,0 +1,90 @@
///////////////////////////////////////////////////////////////
// Simple-Tiling MODERN MENU (GNOME Shell 41-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);
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  LEGACY MENU (GNOME Shell 3.38  44) //
// SimpleTiling  LEGACY MENU (GNOME Shell 3.38) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
+18 -16
View File
@@ -1,13 +1,14 @@
///////////////////////////////////////////////////////////////
// SimpleTiling MODERN MENU (GNOME Shell 45+) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
// Simple-Tiling MODERN MENU (GNOME Shell 45+) //
// © 2025 domoel MIT //
///////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
import { ExtensionPreferences } from 'resource:///org/gnome/shell/extensions/extension.js';
import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
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) {
@@ -17,13 +18,13 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
// ── WINDOW GAPS ────────────────────────────────────────────
const groupGaps = new Adw.PreferencesGroup({
title: 'Window Gaps',
title: 'Window Gaps',
description: 'Adjust spacing between windows and screen edges.'
});
page.add(groupGaps);
const rowInnerGap = new Adw.SpinRow({
title: 'Inner Gap',
title: 'Inner Gap',
subtitle: 'Space between tiled windows (pixels)',
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);
const rowOuterH = new Adw.SpinRow({
title: 'Outer Gap (horizontal)',
title: 'Outer Gap (horizontal)',
subtitle: 'Left / right screen edges (pixels)',
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);
const rowOuterV = new Adw.SpinRow({
title: 'Outer Gap (vertical)',
title: 'Outer Gap (vertical)',
subtitle: 'Top / bottom screen edges (pixels)',
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);
// ── WINDOW BEHAVIOR ────────────────────────────────────────────
const groupBehavior = new Adw.PreferencesGroup({ title: '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'],
strings: ['Stack Window (Default)', 'Master Window'],
}),
});
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', () => {
const newVal = rowNewWindow.selected === 1 ? 'master' : 'stack';
@@ -72,11 +74,11 @@ export default class SimpleTilingPrefs extends ExtensionPreferences {
const rowKeys = new Adw.ActionRow({
title: 'Configure Shortcuts',
subtitle: 'Adjust all shortcuts in GNOME Keyboard settings.',
subtitle: 'Adjust all shortcuts in GNOME Keyboard settings.',
});
groupKeys.add(rowKeys);
const btnOpenKeyboard = new Gtk.Button({ label: 'Open Keyboard Settings' });
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