Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f09053e06 | |||
| 2295756f19 | |||
| 58b4e746b6 | |||
| e37cc5f8f4 | |||
| f10d3f0adf | |||
| 528bf11371 | |||
| 0598fa615d | |||
| 2e61ade64f | |||
| 37e8a149f1 | |||
| ef67c2d187 | |||
| 56ad942f24 | |||
| ba04453c65 | |||
| fd174e084e | |||
| 6ade2e1520 | |||
| b2099a6c1e | |||
| 034bdcb05b | |||
| 6d7acefed9 | |||
| ef098d8986 | |||
| 18d698cf7e | |||
| 0aa80fda6a | |||
| 7e51b5ff62 | |||
| fe41ea8312 | |||
| 5020250699 | |||
| 16688d6996 | |||
| 6b8ac746aa | |||
| bcc5044cf8 | |||
| e2df9fb038 | |||
| 5e23390911 | |||
| 7ba8dd1b0f | |||
| 849e2dc177 | |||
| 982ca71642 | |||
| a62c2307f9 | |||
| b1c4924276 | |||
| 73874fbd4b | |||
| e4af31cb90 | |||
| b400189b4f | |||
| 568ceb7823 | |||
| 138701a33d | |||
| bbe09af89b | |||
| 3331c84f31 | |||
| 6591690c69 | |||
| 40bea7a937 | |||
| 4ea80d27bf | |||
| 55dd6ca691 | |||
| 573ddc2702 | |||
| a4ce7f2613 | |||
| f2971f2c1c | |||
| 72fee16254 | |||
| d127480261 | |||
| 24f5dba546 | |||
| c3c47dc025 | |||
| c83e987550 | |||
| f5c5890fe4 | |||
| dd6b1d067e | |||
| 18e5f4594c | |||
| a101ad0988 | |||
| 8feccbde10 | |||
| 7c9e3e8122 | |||
| b3e00c8f94 | |||
| b50c2780d3 | |||
| a09c934f1b | |||
| 9a9e724654 | |||
| bb28c6b936 | |||
| 6bf3b17054 | |||
| 0e14ecbd18 | |||
| 20d9e94417 | |||
| 58ca81740b | |||
| 405871c0f3 | |||
| 053f5fe90a | |||
| 6165bc6b62 | |||
| 1289af64ce | |||
| 7ac6d58665 | |||
| 6e03f07486 | |||
| f07c66101b | |||
| 992438d480 | |||
| a5b888b58f | |||
| 8a14b04958 | |||
| c34cc0f48e | |||
| 825e35ff05 | |||
| cef62f2ad1 | |||
| 7422622b4d | |||
| f9cd5255c3 | |||
| 374f857152 | |||
| 81ea1db46f | |||
| 1b2025cb81 | |||
| 0a2c273a2b | |||
| 14357adb1e | |||
| 31a61478ad | |||
| 2eb304a16a | |||
| 8480e6ccaf | |||
| f7a86e51b1 | |||
| 1afeb816c4 | |||
| 83ceb4ce67 | |||
| 6e90d12ee9 | |||
| 717ef0b16b | |||
| 1896d992d0 | |||
| 749e3a0275 | |||
| 1daeb0e100 | |||
| defa7255df | |||
| 6031a36664 | |||
| 021c51040f | |||
| 98bfaaa3d5 | |||
| cb26f3aebb | |||
| d9bacba373 | |||
| 4e26db91ea | |||
| 6f56a5c7c8 | |||
| 6549e5bca3 | |||
| 553a1599c6 | |||
| 921928bd2a | |||
| 07f1d40726 | |||
| 40f7ca64ef | |||
| 7d864878b4 | |||
| 0c79ce6da1 | |||
| 843ea1d819 | |||
| c7cf481e33 | |||
| 54172118e1 |
@@ -1,6 +0,0 @@
|
|||||||
# GNOME Shell Extension specific
|
|
||||||
schemas/gschemas.compiled
|
|
||||||
|
|
||||||
# Common temporary files
|
|
||||||
*~
|
|
||||||
*.swp
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Dome
|
Copyright (c) 2025 Domoel (https://github.com/Domoel/)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Simple-Tiling – Makefile
|
||||||
|
#
|
||||||
|
# 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 := 7.5
|
||||||
|
EXTDIR := $(HOME)/.local/share/gnome-shell/extensions
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Helper: copies <file list> <dest>
|
||||||
|
###############################################################################
|
||||||
|
define copies
|
||||||
|
@for f in $(1) ; do \
|
||||||
|
if [ -e $$f ] ; then \
|
||||||
|
cp -r $$f $(2)/ ; \
|
||||||
|
fi ; \
|
||||||
|
done
|
||||||
|
endef
|
||||||
|
|
||||||
|
.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
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Erzeugt Legacy-ZIP (Shell 3.38)
|
||||||
|
###############################################################################
|
||||||
|
build-legacy:
|
||||||
|
@echo "==> Building LEGACY zip (for GNOME 3.38)..."
|
||||||
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
|
@cp legacy.js build/$(UUID)/extension.js
|
||||||
|
@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
|
||||||
|
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
|
||||||
|
@rm -rf build
|
||||||
|
@echo "✓ $(UUID)-legacy-v$(VERSION).zip created"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Erzeugt Enterprise-ZIP (Shell 40)
|
||||||
|
###############################################################################
|
||||||
|
build-enterprise:
|
||||||
|
@echo "==> Building ENTERPRISE zip (for GNOME 40)..."
|
||||||
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
|
@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_enterprise.json.in > build/$(UUID)/metadata.json
|
||||||
|
@cd build && zip -qr ../$(UUID)-enterprise-v$(VERSION).zip .
|
||||||
|
@rm -rf build
|
||||||
|
@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 -f $(UUID)-*.zip
|
||||||
|
@echo "Build directory and ZIPs removed."
|
||||||
@@ -4,20 +4,19 @@ Simple Tiling
|
|||||||
</span>
|
</span>
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<span style="display:inline-flex; align-items:center; gap:12px;">
|
<span style="display:inline-flex; align-items:center; gap:12px;">
|
||||||
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell 3.38.
|
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||

|
|
||||||
|
|
||||||
<img width="2560" height="1440" alt="Simple Tiling v4" src="https://github.com/user-attachments/assets/b080483e-40fe-4ea2-b0dd-56fcb587f9b8" />
|
<img width="2560" height="1440" alt="Simple-Tiling-v6" src="https://github.com/user-attachments/assets/eb0f7cc3-6a5a-4036-8a1e-8f945c52e55c" />
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
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.
|
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**.
|
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
|
## Features
|
||||||
|
|
||||||
@@ -36,10 +35,10 @@ This extension was built from the ground up to be stable and performant on **GNO
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Please note that this extension has been developed for a very specific environment:
|
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**
|
* **GNOME Shell Version:** **3.38 - 49**
|
||||||
* **Session Type:** **X11** (Wayland is not supported).
|
* **Session Type:** **X11** (Wayland is still in beta but should be fine!).
|
||||||
* **Monitor Setup:** **Single monitor only.** Multi-monitor support is not yet implemented.
|
* **Monitor Setup:** **Single monitor only.** Multi-monitor support is not yet implemented.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -50,17 +49,33 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
|
|||||||
|
|
||||||
#### Manual Installation
|
#### Manual Installation
|
||||||
|
|
||||||
1. **Clone the repository** into your local extensions directory:
|
The repository includes a Makefile that produces ready‑to‑install ZIP packages for the three supported Gnome‑Shell lines (a legacy build for Gnome-Shell 3.38, an 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
|
```bash
|
||||||
git clone https://github.com/Domoel/Simple-Tiling.git
|
git clone https://github.com/Domoel/Simple-Tiling.git
|
||||||
|
cd Simple-Tiling
|
||||||
```
|
```
|
||||||
2. **Compile the GSettings schema.** This is a mandatory step for the keyboard shortcuts to work.
|
|
||||||
|
2. **Install the package that matches your GNOME-Shell version**
|
||||||
|
|
||||||
|
Open the Terminal within the Simple-Tiling directory and run
|
||||||
```bash
|
```bash
|
||||||
cd ~/.local/share/gnome-shell/extensions/simple-tiling@domoel/
|
make install-legacy # Installs Legacy Extension (Gnome-Shell 3.38)
|
||||||
glib-compile-schemas schemas/
|
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 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.
|
||||||
|
|
||||||
|
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
|
||||||
```
|
```
|
||||||
3. **Restart GNOME Shell.** Press `Alt` + `F2`, type `r`, and press `Enter`.
|
|
||||||
4. **Enable the extension** using the GNOME Extensions app or GNOME Tweaks.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -74,13 +89,13 @@ All keyboard shortcuts can be configured through the Settings panel of Simple Ti
|
|||||||
|
|
||||||
#### Ignoring Applications (`exceptions.txt`)
|
#### Ignoring Applications (`exceptions.txt`)
|
||||||
|
|
||||||
To prevent an application from being tiled, you can add its `WM_CLASS` to the `exceptions.txt` file in the extension's directory.
|
To prevent an application from being tiled, you can add its `WM_CLASS` (x11) or `App ID` (Wayland) to the `exceptions.txt` file in the extension's directory.
|
||||||
|
|
||||||
* Each application's `WM_CLASS` should be on a new line.
|
* Each application's `WM_CLASS` or `App ID` should be on a new line.
|
||||||
* Lines starting with `#` are treated as comments and are ignored.
|
* Lines starting with `#` are treated as comments and are ignored.
|
||||||
* The check is case-insensitive.
|
* The check is case-insensitive.
|
||||||
|
|
||||||
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude.
|
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude. To find the `App ID`, Press Alt + F2, type 'lg', and press Enter. In the Looking Glass window, click the "Windows" tab. Click on the desired window to see its details. Find the value for "app id" and add it to a new line below.
|
||||||
|
|
||||||
An Example of an exceptions.txt can be found in the repo.
|
An Example of an exceptions.txt can be found in the repo.
|
||||||
|
|
||||||
@@ -102,11 +117,9 @@ If you have race condition issues between mutter (Gnome WM) and the Simple Tilin
|
|||||||
|
|
||||||
This extension was built to solve a specific need. However, future enhancements could include:
|
This extension was built to solve a specific need. However, future enhancements could include:
|
||||||
* Multi-monitor support.
|
* Multi-monitor support.
|
||||||
* Support for newer Gnome shells
|
|
||||||
* Additional layout algorithms.
|
* Additional layout algorithms.
|
||||||
* A more detailed settings panel to configure other options via a GUI.
|
* A more detailed settings panel to configure other options via a GUI.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
||||||
|
|
||||||
|
|||||||
+604
@@ -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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
||||||
|
function getPointerXY() {
|
||||||
|
if (global.get_pointer) {
|
||||||
|
const [x, y] = global.get_pointer();
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = Clutter.get_current_event();
|
||||||
|
if (ev) {
|
||||||
|
const coords = ev.get_coords();
|
||||||
|
if (Array.isArray(coords))
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = Clutter.get_default_backend()
|
||||||
|
.get_default_seat()
|
||||||
|
.get_pointer();
|
||||||
|
return device ? device.get_position() : [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INTERACTIONHANDLER ───────────────────────────────────
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = this.tiler.settings;
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
||||||
|
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId =
|
||||||
|
this._settings.connect('changed', () => this._onSettingsChanged());
|
||||||
|
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-begin',
|
||||||
|
(_, __, win) => { if (this.tiler.windows.includes(win))
|
||||||
|
this.tiler.grabbedWindow = win; })
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-end', () => this._onGrabEnd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
||||||
|
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
||||||
|
this._grabOpIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, handler) {
|
||||||
|
global.display.add_keybinding(
|
||||||
|
key,
|
||||||
|
this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
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) global.display.remove_keybinding(k); }
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
|
||||||
|
const add = key => { if (schema.has_key(key)) keys.push(key); };
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-tiled-left'))
|
||||||
|
keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
||||||
|
else {
|
||||||
|
add('tile-left'); add('tile-right');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-maximized'))
|
||||||
|
keys.push('toggle-maximized');
|
||||||
|
else {
|
||||||
|
add('maximize'); add('unmaximize');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(k => this._savedWmShortcuts[k] =
|
||||||
|
this._wmSettings.get_value(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
const tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (tgt) tgt.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const w = this.tiler.windows;
|
||||||
|
if (w.length < 2) return;
|
||||||
|
const foc = global.display.get_focus_window();
|
||||||
|
if (!foc || !w.includes(foc)) return;
|
||||||
|
const idx = w.indexOf(foc);
|
||||||
|
if (idx > 0) [w[0], w[idx]] = [w[idx], w[0]];
|
||||||
|
else [w[0], w[1]] = [w[1], w[0]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
w[0]?.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
let tgt = null;
|
||||||
|
const idx = this.tiler.windows.indexOf(src);
|
||||||
|
if (idx === 0 && direction==='right' && this.tiler.windows.length>1)
|
||||||
|
tgt = this.tiler.windows[1];
|
||||||
|
else
|
||||||
|
tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (!tgt) return;
|
||||||
|
const tidx = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[idx], this.tiler.windows[tidx]] =
|
||||||
|
[this.tiler.windows[tidx], this.tiler.windows[idx]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
src.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(src, dir) {
|
||||||
|
const sRect = src.get_frame_rect(), cand=[];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win===src) continue;
|
||||||
|
const r=win.get_frame_rect();
|
||||||
|
if (dir==='left' && r.x<sRect.x) cand.push(win);
|
||||||
|
if (dir==='right'&& r.x>sRect.x) cand.push(win);
|
||||||
|
if (dir==='up' && r.y<sRect.y) cand.push(win);
|
||||||
|
if (dir==='down' && r.y>sRect.y) cand.push(win);
|
||||||
|
}
|
||||||
|
if (!cand.length) return null;
|
||||||
|
let best=null, min=Infinity;
|
||||||
|
for (const w of cand) {
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const dev = (dir==='left'||dir==='right')
|
||||||
|
? Math.abs(sRect.y - r.y)
|
||||||
|
: Math.abs(sRect.x - r.x);
|
||||||
|
if (dev<min){min=dev; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbed = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbed) return;
|
||||||
|
const tgt = this._findTargetUnderPointer(grabbed);
|
||||||
|
if (tgt) {
|
||||||
|
const a = this.tiler.windows.indexOf(grabbed);
|
||||||
|
const b = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[a], this.tiler.windows[b]] =
|
||||||
|
[this.tiler.windows[b], this.tiler.windows[a]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(exclude) {
|
||||||
|
const [x,y] = getPointerXY();
|
||||||
|
const wins = global.get_window_actors()
|
||||||
|
.map(a=>a.meta_window)
|
||||||
|
.filter(w=>w && w!==exclude &&
|
||||||
|
this.tiler.windows.includes(w) && (()=>{const f=w.get_frame_rect();
|
||||||
|
return x>=f.x && x<f.x+f.width &&
|
||||||
|
y>=f.y && y<f.y+f.height;})());
|
||||||
|
if (wins.length) return wins[wins.length-1];
|
||||||
|
|
||||||
|
let best=null, max=0, sRect=exclude.get_frame_rect();
|
||||||
|
for (const w of this.tiler.windows) {
|
||||||
|
if (w===exclude) continue;
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const ovX=Math.max(0, Math.min(sRect.x+sRect.width, r.x+r.width)-Math.max(sRect.x,r.x));
|
||||||
|
const ovY=Math.max(0, Math.min(sRect.y+sRect.height,r.y+r.height)-Math.max(sRect.y,r.y));
|
||||||
|
const area=ovX*ovY;
|
||||||
|
if (area>max){max=area; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TILER ────────────────────────────────────────────────
|
||||||
|
class Tiler {
|
||||||
|
constructor(extension) {
|
||||||
|
this._extension = extension;
|
||||||
|
this.settings = this._extension.getSettings();
|
||||||
|
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical= this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal = this.settings.get_int('outer-gap-horizontal');
|
||||||
|
|
||||||
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
this._workspaceManager = global.workspace_manager;
|
||||||
|
|
||||||
|
this._signalIds.set('workspace-changed', {
|
||||||
|
object: this._workspaceManager,
|
||||||
|
id: this._workspaceManager.connect('active-workspace-changed',
|
||||||
|
()=>this._onActiveWorkspaceChanged())
|
||||||
|
});
|
||||||
|
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
|
||||||
|
this._signalIds.set('settings-changed', {
|
||||||
|
object: this.settings,
|
||||||
|
id: this.settings.connect('changed', ()=>this._onSettingsChanged())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._tileTimeoutId) {
|
||||||
|
GLib.source_remove(this._tileTimeoutId);
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
}
|
||||||
|
this._centerTimeoutIds.forEach(id=>GLib.source_remove(id));
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
|
||||||
|
for (const [,sig] of this._signalIds) {
|
||||||
|
try { sig.object.disconnect(sig.id); } catch {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical = this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal= this.settings.get_int('outer-gap-horizontal');
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.File.new_for_path(this._extension.path + '/exceptions.txt');
|
||||||
|
if (!file.query_exists(null)) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const [ok,data] = file.load_contents(null);
|
||||||
|
if (!ok) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const txt = new TextDecoder('utf-8').decode(data);
|
||||||
|
this._exceptions = txt.split('\n')
|
||||||
|
.map(l=>l.trim())
|
||||||
|
.filter(l=>l && !l.startsWith('#'))
|
||||||
|
.map(l=>l.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
if (!win) return false;
|
||||||
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return (
|
||||||
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
const timeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._centeringDelay,
|
||||||
|
() => {
|
||||||
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index > -1) this._centerTimeoutIds.splice(index, 1);
|
||||||
|
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (win.get_maximized())
|
||||||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
|
||||||
|
const monitorIndex = win.get_monitor();
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(
|
||||||
|
monitorIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = win.get_frame_rect();
|
||||||
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y +
|
||||||
|
Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
|
if (win.get_display()) {
|
||||||
|
if (typeof win.set_keep_above === "function")
|
||||||
|
win.set_keep_above(true);
|
||||||
|
else if (typeof win.make_above === "function")
|
||||||
|
win.make_above();
|
||||||
|
}
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowAdded(workspace, win) {
|
||||||
|
if (this.windows.includes(win)) return;
|
||||||
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._isTileable(win)) {
|
||||||
|
if (this.settings.get_string("new-window-behavior") === "master") {
|
||||||
|
this.windows.unshift(win);
|
||||||
|
} else {
|
||||||
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
const id = win.get_id();
|
||||||
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("unmanaged", () =>
|
||||||
|
this._onWindowRemoved(null, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("notify::minimized", () =>
|
||||||
|
this._onWindowMinimizedStateChanged()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowRemoved(workspace, win) {
|
||||||
|
const index = this.windows.indexOf(win);
|
||||||
|
if (index > -1) this.windows.splice(index, 1);
|
||||||
|
|
||||||
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActiveWorkspaceChanged() {
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
this._connectToWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToWorkspace() {
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
workspace
|
||||||
|
.list_windows()
|
||||||
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
|
this._signalIds.set("window-added", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", (ws, win) =>
|
||||||
|
this._onWindowAdded(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", (ws, win) =>
|
||||||
|
this._onWindowRemoved(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTile() {
|
||||||
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
|
this._tileInProgress = true;
|
||||||
|
this._tileTimeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._tilingDelay,
|
||||||
|
() => {
|
||||||
|
this._tileWindows();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_splitLayout(windows, area) {
|
||||||
|
if (windows.length === 0) return;
|
||||||
|
if (windows.length === 1) {
|
||||||
|
windows[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const primaryWindows = [windows[0]];
|
||||||
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
if (area.width > area.height) {
|
||||||
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: primaryWidth,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tileWindows() {
|
||||||
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
|
if (windowsToTile.length === 0) return;
|
||||||
|
|
||||||
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(monitor.index);
|
||||||
|
|
||||||
|
const innerArea = {
|
||||||
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
windowsToTile.forEach((win) => {
|
||||||
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
if (windowsToTile.length === 1) {
|
||||||
|
windowsToTile[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
|
const master = windowsToTile[0];
|
||||||
|
master.move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
masterWidth,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EXTENSION‑WRAPPER ──────────────────────────
|
||||||
|
let tiler;
|
||||||
|
|
||||||
|
function enable() {
|
||||||
|
tiler = new Tiler(Me);
|
||||||
|
tiler.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable() {
|
||||||
|
if (tiler) {
|
||||||
|
tiler.disable();
|
||||||
|
tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-8
@@ -1,11 +1,35 @@
|
|||||||
# --- Ausnahmeliste für den Tiler ---
|
# --- Exception List for Tiling Windows ---
|
||||||
# Jede Zeile enthält die WM_CLASS einer Anwendung, die ignoriert werden soll.
|
# Each line contains an application identifier (WM_CLASS for X11, or App ID for Wayland)
|
||||||
# Die Groß- und Kleinschreibung wird ignoriert.
|
# that should be ignored by the tiling manager.
|
||||||
|
# For best results, add both identifiers for an application if they differ.
|
||||||
|
# Uppercase and lowercase letters are ignored.
|
||||||
|
|
||||||
# Befehl zum Finden der WM_CLASS:
|
# -----------------------------------------------------------
|
||||||
# 1. Terminal öffnen
|
# Finding the App ID (for Wayland & modern apps)
|
||||||
# 2. 'xprop WM_CLASS' eingeben und Enter drücken
|
# -----------------------------------------------------------
|
||||||
# 3. Mit dem Kreuz auf das gewünschte Fenster klicken
|
# 1. Press Alt + F2, type 'lg', and press Enter.
|
||||||
|
# 2. In the Looking Glass window, click the "Windows" tab.
|
||||||
|
# 3. Click on the desired window to see its details.
|
||||||
|
# 4. Find the value for "app id" and add it to a new line below.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Finding the WM_CLASS (for X11)
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# 1. Open a terminal.
|
||||||
|
# 2. Type 'xprop WM_CLASS' and press Enter.
|
||||||
|
# 3. Your cursor will turn into a crosshair. Click on the desired window.
|
||||||
|
# 4. The terminal will output a line like: WM_CLASS(STRING) = "navigator", "Firefox".
|
||||||
|
# 5. Add one of these values (e.g., "firefox") to a new line below.
|
||||||
|
|
||||||
|
# --- Start of the Exception List ---
|
||||||
|
|
||||||
|
ulauncher
|
||||||
|
steam
|
||||||
|
element
|
||||||
|
totem
|
||||||
|
extension-manager
|
||||||
|
timeshift-gtk
|
||||||
gnome-screenshot
|
gnome-screenshot
|
||||||
|
org.gnome.NautilusPreviewer
|
||||||
|
org.gnome.Shell.Extensions
|
||||||
|
evolution-alarm-notify
|
||||||
|
|||||||
+607
@@ -0,0 +1,607 @@
|
|||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// Simple‑Tiling – INTERIM (GNOME Shell 41 - 44) //
|
||||||
|
// © 2025 domoel – 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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
||||||
|
function getPointerXY() {
|
||||||
|
if (global.get_pointer) {
|
||||||
|
const [x, y] = global.get_pointer();
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = Clutter.get_current_event();
|
||||||
|
if (ev) {
|
||||||
|
const coords = ev.get_coords();
|
||||||
|
if (Array.isArray(coords))
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = Clutter.get_default_backend()
|
||||||
|
.get_default_seat()
|
||||||
|
.get_pointer();
|
||||||
|
return device ? device.get_position() : [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INTERACTIONHANDLER ───────────────────────────────────
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = this.tiler.settings;
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
||||||
|
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId =
|
||||||
|
this._settings.connect('changed', () => this._onSettingsChanged());
|
||||||
|
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-begin',
|
||||||
|
(_, __, win) => { if (this.tiler.windows.includes(win))
|
||||||
|
this.tiler.grabbedWindow = win; })
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-end', () => this._onGrabEnd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
||||||
|
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
||||||
|
this._grabOpIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, handler) {
|
||||||
|
global.display.add_keybinding(
|
||||||
|
key,
|
||||||
|
this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
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) global.display.remove_keybinding(k); }
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
|
||||||
|
const add = key => { if (schema.has_key(key)) keys.push(key); };
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-tiled-left'))
|
||||||
|
keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
||||||
|
else {
|
||||||
|
add('tile-left'); add('tile-right');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-maximized'))
|
||||||
|
keys.push('toggle-maximized');
|
||||||
|
else {
|
||||||
|
add('maximize'); add('unmaximize');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(k => this._savedWmShortcuts[k] =
|
||||||
|
this._wmSettings.get_value(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
const tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (tgt) tgt.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const w = this.tiler.windows;
|
||||||
|
if (w.length < 2) return;
|
||||||
|
const foc = global.display.get_focus_window();
|
||||||
|
if (!foc || !w.includes(foc)) return;
|
||||||
|
const idx = w.indexOf(foc);
|
||||||
|
if (idx > 0) [w[0], w[idx]] = [w[idx], w[0]];
|
||||||
|
else [w[0], w[1]] = [w[1], w[0]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
w[0]?.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
let tgt = null;
|
||||||
|
const idx = this.tiler.windows.indexOf(src);
|
||||||
|
if (idx === 0 && direction==='right' && this.tiler.windows.length>1)
|
||||||
|
tgt = this.tiler.windows[1];
|
||||||
|
else
|
||||||
|
tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (!tgt) return;
|
||||||
|
const tidx = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[idx], this.tiler.windows[tidx]] =
|
||||||
|
[this.tiler.windows[tidx], this.tiler.windows[idx]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
src.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(src, dir) {
|
||||||
|
const sRect = src.get_frame_rect(), cand=[];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win===src) continue;
|
||||||
|
const r=win.get_frame_rect();
|
||||||
|
if (dir==='left' && r.x<sRect.x) cand.push(win);
|
||||||
|
if (dir==='right'&& r.x>sRect.x) cand.push(win);
|
||||||
|
if (dir==='up' && r.y<sRect.y) cand.push(win);
|
||||||
|
if (dir==='down' && r.y>sRect.y) cand.push(win);
|
||||||
|
}
|
||||||
|
if (!cand.length) return null;
|
||||||
|
let best=null, min=Infinity;
|
||||||
|
for (const w of cand) {
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const dev = (dir==='left'||dir==='right')
|
||||||
|
? Math.abs(sRect.y - r.y)
|
||||||
|
: Math.abs(sRect.x - r.x);
|
||||||
|
if (dev<min){min=dev; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbed = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbed) return;
|
||||||
|
const tgt = this._findTargetUnderPointer(grabbed);
|
||||||
|
if (tgt) {
|
||||||
|
const a = this.tiler.windows.indexOf(grabbed);
|
||||||
|
const b = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[a], this.tiler.windows[b]] =
|
||||||
|
[this.tiler.windows[b], this.tiler.windows[a]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(exclude) {
|
||||||
|
const [x,y] = getPointerXY();
|
||||||
|
const wins = global.get_window_actors()
|
||||||
|
.map(a=>a.meta_window)
|
||||||
|
.filter(w=>w && w!==exclude &&
|
||||||
|
this.tiler.windows.includes(w) && (()=>{const f=w.get_frame_rect();
|
||||||
|
return x>=f.x && x<f.x+f.width &&
|
||||||
|
y>=f.y && y<f.y+f.height;})());
|
||||||
|
if (wins.length) return wins[wins.length-1];
|
||||||
|
|
||||||
|
let best=null, max=0, sRect=exclude.get_frame_rect();
|
||||||
|
for (const w of this.tiler.windows) {
|
||||||
|
if (w===exclude) continue;
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const ovX=Math.max(0, Math.min(sRect.x+sRect.width, r.x+r.width)-Math.max(sRect.x,r.x));
|
||||||
|
const ovY=Math.max(0, Math.min(sRect.y+sRect.height,r.y+r.height)-Math.max(sRect.y,r.y));
|
||||||
|
const area=ovX*ovY;
|
||||||
|
if (area>max){max=area; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TILER ────────────────────────────────────────────────
|
||||||
|
class Tiler {
|
||||||
|
constructor(extension) {
|
||||||
|
this._extension = extension;
|
||||||
|
this.settings = this._extension.getSettings();
|
||||||
|
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical= this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal = this.settings.get_int('outer-gap-horizontal');
|
||||||
|
|
||||||
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
this._workspaceManager = global.workspace_manager;
|
||||||
|
|
||||||
|
this._signalIds.set('workspace-changed', {
|
||||||
|
object: this._workspaceManager,
|
||||||
|
id: this._workspaceManager.connect('active-workspace-changed',
|
||||||
|
()=>this._onActiveWorkspaceChanged())
|
||||||
|
});
|
||||||
|
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
|
||||||
|
this._signalIds.set('settings-changed', {
|
||||||
|
object: this.settings,
|
||||||
|
id: this.settings.connect('changed', ()=>this._onSettingsChanged())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._tileTimeoutId) {
|
||||||
|
GLib.source_remove(this._tileTimeoutId);
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
}
|
||||||
|
this._centerTimeoutIds.forEach(id=>GLib.source_remove(id));
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
|
||||||
|
for (const [,sig] of this._signalIds) {
|
||||||
|
try { sig.object.disconnect(sig.id); } catch {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical = this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal= this.settings.get_int('outer-gap-horizontal');
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.File.new_for_path(this._extension.path + '/exceptions.txt');
|
||||||
|
if (!file.query_exists(null)) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const [ok,data] = file.load_contents(null);
|
||||||
|
if (!ok) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const txt = new TextDecoder('utf-8').decode(data);
|
||||||
|
this._exceptions = txt.split('\n')
|
||||||
|
.map(l=>l.trim())
|
||||||
|
.filter(l=>l && !l.startsWith('#'))
|
||||||
|
.map(l=>l.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
if (!win) return false;
|
||||||
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return (
|
||||||
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
const timeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._centeringDelay,
|
||||||
|
() => {
|
||||||
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index > -1) this._centerTimeoutIds.splice(index, 1);
|
||||||
|
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (win.get_maximized())
|
||||||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
|
||||||
|
const monitorIndex = win.get_monitor();
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(
|
||||||
|
monitorIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = win.get_frame_rect();
|
||||||
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y +
|
||||||
|
Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
|
if (win.get_display()) {
|
||||||
|
if (typeof win.set_keep_above === "function")
|
||||||
|
win.set_keep_above(true);
|
||||||
|
else if (typeof win.make_above === "function")
|
||||||
|
win.make_above();
|
||||||
|
}
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowAdded(workspace, win) {
|
||||||
|
if (this.windows.includes(win)) return;
|
||||||
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._isTileable(win)) {
|
||||||
|
if (this.settings.get_string("new-window-behavior") === "master") {
|
||||||
|
this.windows.unshift(win);
|
||||||
|
} else {
|
||||||
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
const id = win.get_id();
|
||||||
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("unmanaged", () =>
|
||||||
|
this._onWindowRemoved(null, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("notify::minimized", () =>
|
||||||
|
this._onWindowMinimizedStateChanged()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowRemoved(workspace, win) {
|
||||||
|
const index = this.windows.indexOf(win);
|
||||||
|
if (index > -1) this.windows.splice(index, 1);
|
||||||
|
|
||||||
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActiveWorkspaceChanged() {
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
this._connectToWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToWorkspace() {
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
workspace
|
||||||
|
.list_windows()
|
||||||
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
|
this._signalIds.set("window-added", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", (ws, win) =>
|
||||||
|
this._onWindowAdded(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", (ws, win) =>
|
||||||
|
this._onWindowRemoved(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTile() {
|
||||||
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
|
this._tileInProgress = true;
|
||||||
|
this._tileTimeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._tilingDelay,
|
||||||
|
() => {
|
||||||
|
this._tileWindows();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_splitLayout(windows, area) {
|
||||||
|
if (windows.length === 0) return;
|
||||||
|
if (windows.length === 1) {
|
||||||
|
windows[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const primaryWindows = [windows[0]];
|
||||||
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
if (area.width > area.height) {
|
||||||
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: primaryWidth,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tileWindows() {
|
||||||
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
|
if (windowsToTile.length === 0) return;
|
||||||
|
|
||||||
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(monitor.index);
|
||||||
|
|
||||||
|
const innerArea = {
|
||||||
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
windowsToTile.forEach((win) => {
|
||||||
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
if (windowsToTile.length === 1) {
|
||||||
|
windowsToTile[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
|
const master = windowsToTile[0];
|
||||||
|
master.move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
masterWidth,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
||||||
|
export default class InterimExtension extends Extension {
|
||||||
|
enable() {
|
||||||
|
this.tiler = new Tiler(this);
|
||||||
|
this.tiler.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-47
@@ -1,11 +1,10 @@
|
|||||||
// ---------------------------------------------------- //
|
/////////////////////////////////////////////////////////////
|
||||||
// Simple-Tiling – GNOME Shell 3.38 (X11) - Version 5 //
|
// Simple-Tiling – LEGACY (for GNOME Shell 3.38) //
|
||||||
// © 2025 domoel – MIT //
|
// © 2025 domoel – MIT //
|
||||||
// ---------------------------------------------------- //
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
// Global Imports //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const Main = imports.ui.main;
|
const Main = imports.ui.main;
|
||||||
const Meta = imports.gi.Meta;
|
const Meta = imports.gi.Meta;
|
||||||
const Shell = imports.gi.Shell;
|
const Shell = imports.gi.Shell;
|
||||||
@@ -13,29 +12,27 @@ const Gio = imports.gi.Gio;
|
|||||||
const GLib = imports.gi.GLib;
|
const GLib = imports.gi.GLib;
|
||||||
const ExtensionUtils = imports.misc.extensionUtils;
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
const ByteArray = imports.byteArray;
|
const ByteArray = imports.byteArray;
|
||||||
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
|
||||||
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
|
||||||
|
|
||||||
const Me = ExtensionUtils.getCurrentExtension();
|
const Me = ExtensionUtils.getCurrentExtension();
|
||||||
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
||||||
|
|
||||||
const KEYBINDINGS = {
|
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
||||||
"swap-master-window": (self) => self._swapWithMaster(),
|
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
||||||
"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"),
|
const KEYBINDINGS = {
|
||||||
"focus-right": (self) => self._focusInDirection("right"),
|
'swap-master-window': (self) => self._swapWithMaster(),
|
||||||
"focus-up": (self) => self._focusInDirection("up"),
|
'swap-left-window': (self) => self._swapInDirection('left'),
|
||||||
"focus-down": (self) => self._focusInDirection("down"),
|
'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'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// --- INTERACTIONHANDLER ---
|
||||||
// InteractionHandler //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
class InteractionHandler {
|
class InteractionHandler {
|
||||||
constructor(tiler) {
|
constructor(tiler) {
|
||||||
this.tiler = tiler;
|
this.tiler = tiler;
|
||||||
@@ -100,6 +97,7 @@ class InteractionHandler {
|
|||||||
this._bind(key, handler);
|
this._bind(key, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_unbindAllShortcuts() {
|
_unbindAllShortcuts() {
|
||||||
for (const key in KEYBINDINGS) {
|
for (const key in KEYBINDINGS) {
|
||||||
Main.wm.removeKeybinding(key);
|
Main.wm.removeKeybinding(key);
|
||||||
@@ -110,6 +108,7 @@ class InteractionHandler {
|
|||||||
this._unbindAllShortcuts();
|
this._unbindAllShortcuts();
|
||||||
this._bindAllShortcuts();
|
this._bindAllShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
_prepareWmShortcuts() {
|
_prepareWmShortcuts() {
|
||||||
const schema = this._wmSettings.settings_schema;
|
const schema = this._wmSettings.settings_schema;
|
||||||
const keys = [];
|
const keys = [];
|
||||||
@@ -125,10 +124,7 @@ class InteractionHandler {
|
|||||||
if (keys.length) {
|
if (keys.length) {
|
||||||
this._wmKeysToDisable = keys;
|
this._wmKeysToDisable = keys;
|
||||||
keys.forEach(
|
keys.forEach(
|
||||||
(key) =>
|
(key) => (this._savedWmShortcuts[key] = this._wmSettings.get_value(key))
|
||||||
(this._savedWmShortcuts[
|
|
||||||
key
|
|
||||||
] = this._wmSettings.get_value(key))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,10 +297,7 @@ class InteractionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// --- TILER ---
|
||||||
// Tiler //
|
|
||||||
// Main Classes for Tiling Logic //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
class Tiler {
|
class Tiler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
@@ -315,9 +308,7 @@ class Tiler {
|
|||||||
|
|
||||||
this._innerGap = this._settings.get_int("inner-gap");
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
this._outerGapHorizontal = this._settings.get_int(
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
"outer-gap-horizontal"
|
|
||||||
);
|
|
||||||
|
|
||||||
this._tilingDelay = TILING_DELAY_MS;
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
this._centeringDelay = CENTERING_DELAY_MS;
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
@@ -379,9 +370,7 @@ class Tiler {
|
|||||||
_onSettingsChanged() {
|
_onSettingsChanged() {
|
||||||
this._innerGap = this._settings.get_int("inner-gap");
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
this._outerGapHorizontal = this._settings.get_int(
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
"outer-gap-horizontal"
|
|
||||||
);
|
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,10 +391,10 @@ class Tiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isException(win) {
|
_isException(win) {
|
||||||
return (
|
if (!win) return false;
|
||||||
!!win &&
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
this._exceptions.includes((win.get_wm_class() || "").toLowerCase())
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
);
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isTileable(win) {
|
_isTileable(win) {
|
||||||
@@ -670,11 +659,9 @@ class Tiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// --- EXTENSION-WRAPPER (for legacy loader) ---
|
||||||
// Extension Wrapper //
|
var LegacyExtension = class {
|
||||||
// ---------------------------------------------------- //
|
constructor(metadata) {
|
||||||
class SimpleTilingExtension {
|
|
||||||
constructor() {
|
|
||||||
this.tiler = null;
|
this.tiler = null;
|
||||||
}
|
}
|
||||||
enable() {
|
enable() {
|
||||||
@@ -687,8 +674,8 @@ class SimpleTilingExtension {
|
|||||||
this.tiler = null;
|
this.tiler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function init() {
|
function init(metadata) {
|
||||||
return new SimpleTilingExtension();
|
return new LegacyExtension(metadata);
|
||||||
}
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"uuid": "simple-tiling@domoel",
|
|
||||||
"name": "Simple Tiling",
|
|
||||||
"description": "A Simple Tiling Extension for Gnome Shell 3.38.",
|
|
||||||
"version": 5,
|
|
||||||
"shell-version": [ "3.38" ],
|
|
||||||
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
|
||||||
"preferences_ui": "prefs.js",
|
|
||||||
"url": "https://github.com/Domoel/Simple-Tiling",
|
|
||||||
"gettext-domain": "simple-tiling-domoel"
|
|
||||||
}
|
|
||||||
@@ -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__"
|
||||||
|
}
|
||||||
@@ -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__"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"uuid": "__UUID__",
|
||||||
|
"name": "Simple Tiling",
|
||||||
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
|
"version": __VERSION__,
|
||||||
|
"shell-version": [
|
||||||
|
"3.38"
|
||||||
|
],
|
||||||
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
|
"preferences_ui": "prefs.js",
|
||||||
|
"url": "https://github.com/Domoel/Simple-Tiling",
|
||||||
|
"gettext-domain": "__UUID__"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"uuid": "__UUID__",
|
||||||
|
"name": "Simple Tiling",
|
||||||
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
|
"version": __VERSION__,
|
||||||
|
"shell-version": [
|
||||||
|
"45",
|
||||||
|
"46",
|
||||||
|
"47",
|
||||||
|
"48",
|
||||||
|
"49"
|
||||||
|
],
|
||||||
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
|
"preferences_ui": "prefs.js",
|
||||||
|
"url": "https://github.com/Domoel/Simple-Tiling",
|
||||||
|
"gettext-domain": "__UUID__"
|
||||||
|
}
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// Simple‑Tiling – MODERN (GNOME Shell 45+) //
|
||||||
|
// © 2025 domoel – 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/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';
|
||||||
|
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 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]);
|
||||||
|
|
||||||
|
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
||||||
|
function getPointerXY() {
|
||||||
|
if (global.get_pointer) {
|
||||||
|
const [x, y] = global.get_pointer();
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = Clutter.get_current_event();
|
||||||
|
if (ev) {
|
||||||
|
const coords = ev.get_coords();
|
||||||
|
if (Array.isArray(coords))
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = Clutter.get_default_backend()
|
||||||
|
.get_default_seat()
|
||||||
|
.get_pointer();
|
||||||
|
return device ? device.get_position() : [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INTERACTIONHANDLER ───────────────────────────────────
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = this.tiler.settings;
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
||||||
|
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId =
|
||||||
|
this._settings.connect('changed', () => this._onSettingsChanged());
|
||||||
|
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-begin',
|
||||||
|
(_, __, win) => { if (this.tiler.windows.includes(win))
|
||||||
|
this.tiler.grabbedWindow = win; })
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-end', () => this._onGrabEnd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
||||||
|
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
||||||
|
this._grabOpIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, handler) {
|
||||||
|
global.display.add_keybinding(
|
||||||
|
key,
|
||||||
|
this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
(..._args) => handler(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
|
||||||
|
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) global.display.remove_keybinding(k); }
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
|
||||||
|
const add = key => { if (schema.has_key(key)) keys.push(key); };
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-tiled-left'))
|
||||||
|
keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
||||||
|
else {
|
||||||
|
add('tile-left'); add('tile-right');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-maximized'))
|
||||||
|
keys.push('toggle-maximized');
|
||||||
|
else {
|
||||||
|
add('maximize'); add('unmaximize');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(k => this._savedWmShortcuts[k] =
|
||||||
|
this._wmSettings.get_value(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
const tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (tgt) tgt.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const w = this.tiler.windows;
|
||||||
|
if (w.length < 2) return;
|
||||||
|
const foc = global.display.get_focus_window();
|
||||||
|
if (!foc || !w.includes(foc)) return;
|
||||||
|
const idx = w.indexOf(foc);
|
||||||
|
if (idx > 0) [w[0], w[idx]] = [w[idx], w[0]];
|
||||||
|
else [w[0], w[1]] = [w[1], w[0]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
w[0]?.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
let tgt = null;
|
||||||
|
const idx = this.tiler.windows.indexOf(src);
|
||||||
|
if (idx === 0 && direction==='right' && this.tiler.windows.length>1)
|
||||||
|
tgt = this.tiler.windows[1];
|
||||||
|
else
|
||||||
|
tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (!tgt) return;
|
||||||
|
const tidx = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[idx], this.tiler.windows[tidx]] =
|
||||||
|
[this.tiler.windows[tidx], this.tiler.windows[idx]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
src.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(src, dir) {
|
||||||
|
const sRect = src.get_frame_rect(), cand=[];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win===src) continue;
|
||||||
|
const r=win.get_frame_rect();
|
||||||
|
if (dir==='left' && r.x<sRect.x) cand.push(win);
|
||||||
|
if (dir==='right'&& r.x>sRect.x) cand.push(win);
|
||||||
|
if (dir==='up' && r.y<sRect.y) cand.push(win);
|
||||||
|
if (dir==='down' && r.y>sRect.y) cand.push(win);
|
||||||
|
}
|
||||||
|
if (!cand.length) return null;
|
||||||
|
let best=null, min=Infinity;
|
||||||
|
for (const w of cand) {
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const dev = (dir==='left'||dir==='right')
|
||||||
|
? Math.abs(sRect.y - r.y)
|
||||||
|
: Math.abs(sRect.x - r.x);
|
||||||
|
if (dev<min){min=dev; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbed = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbed) return;
|
||||||
|
const tgt = this._findTargetUnderPointer(grabbed);
|
||||||
|
if (tgt) {
|
||||||
|
const a = this.tiler.windows.indexOf(grabbed);
|
||||||
|
const b = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[a], this.tiler.windows[b]] =
|
||||||
|
[this.tiler.windows[b], this.tiler.windows[a]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(exclude) {
|
||||||
|
const [x,y] = getPointerXY();
|
||||||
|
const wins = global.get_window_actors()
|
||||||
|
.map(a=>a.meta_window)
|
||||||
|
.filter(w=>w && w!==exclude &&
|
||||||
|
this.tiler.windows.includes(w) && (()=>{const f=w.get_frame_rect();
|
||||||
|
return x>=f.x && x<f.x+f.width &&
|
||||||
|
y>=f.y && y<f.y+f.height;})());
|
||||||
|
if (wins.length) return wins[wins.length-1];
|
||||||
|
|
||||||
|
let best=null, max=0, sRect=exclude.get_frame_rect();
|
||||||
|
for (const w of this.tiler.windows) {
|
||||||
|
if (w===exclude) continue;
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const ovX=Math.max(0, Math.min(sRect.x+sRect.width, r.x+r.width)-Math.max(sRect.x,r.x));
|
||||||
|
const ovY=Math.max(0, Math.min(sRect.y+sRect.height,r.y+r.height)-Math.max(sRect.y,r.y));
|
||||||
|
const area=ovX*ovY;
|
||||||
|
if (area>max){max=area; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TILER ────────────────────────────────────────────────
|
||||||
|
class Tiler {
|
||||||
|
constructor(extension) {
|
||||||
|
this._extension = extension;
|
||||||
|
this.settings = this._extension.getSettings();
|
||||||
|
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical= this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal = this.settings.get_int('outer-gap-horizontal');
|
||||||
|
|
||||||
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
this._workspaceManager = global.workspace_manager;
|
||||||
|
|
||||||
|
this._signalIds.set('workspace-changed', {
|
||||||
|
object: this._workspaceManager,
|
||||||
|
id: this._workspaceManager.connect('active-workspace-changed',
|
||||||
|
()=>this._onActiveWorkspaceChanged())
|
||||||
|
});
|
||||||
|
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
|
||||||
|
this._signalIds.set('settings-changed', {
|
||||||
|
object: this.settings,
|
||||||
|
id: this.settings.connect('changed', ()=>this._onSettingsChanged())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._tileTimeoutId) {
|
||||||
|
GLib.source_remove(this._tileTimeoutId);
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
}
|
||||||
|
this._centerTimeoutIds.forEach(id=>GLib.source_remove(id));
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
|
||||||
|
for (const [,sig] of this._signalIds) {
|
||||||
|
try { sig.object.disconnect(sig.id); } catch {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._innerGap = this.settings.get_int('inner-gap');
|
||||||
|
this._outerGapVertical = this.settings.get_int('outer-gap-vertical');
|
||||||
|
this._outerGapHorizontal= this.settings.get_int('outer-gap-horizontal');
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.File.new_for_path(this._extension.path + '/exceptions.txt');
|
||||||
|
if (!file.query_exists(null)) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const [ok,data] = file.load_contents(null);
|
||||||
|
if (!ok) { this._exceptions=[]; return; }
|
||||||
|
|
||||||
|
const txt = new TextDecoder('utf-8').decode(data);
|
||||||
|
this._exceptions = txt.split('\n')
|
||||||
|
.map(l=>l.trim())
|
||||||
|
.filter(l=>l && !l.startsWith('#'))
|
||||||
|
.map(l=>l.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
if (!win) return false;
|
||||||
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return (
|
||||||
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
const timeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._centeringDelay,
|
||||||
|
() => {
|
||||||
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index > -1) this._centerTimeoutIds.splice(index, 1);
|
||||||
|
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (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();
|
||||||
|
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 (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(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
|
const master = windowsToTile[0];
|
||||||
|
master.move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
masterWidth,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
||||||
|
export default class ModernExtension extends Extension {
|
||||||
|
enable() { this.tiler = new Tiler(this); this.tiler.enable(); }
|
||||||
|
disable() { this.tiler?.disable(); this.tiler = null; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-50
@@ -1,11 +1,10 @@
|
|||||||
// ------------------------------------------------------ //
|
///////////////////////////////////////////////////////////////
|
||||||
// Extension Settings Menu for Simple Tiling - Version 5 //
|
// Simple‑Tiling – LEGACY MENU (GNOME Shell 3.38) //
|
||||||
// © 2025 domoel – MIT //
|
// © 2025 domoel – MIT //
|
||||||
// ------------------------------------------------------ //
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
// Global Imports //
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
// ---------------------------------------------------- //
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { Gtk, GObject, Gio } = imports.gi;
|
const { Gtk, GObject, Gio } = imports.gi;
|
||||||
@@ -13,9 +12,7 @@ const ExtensionUtils = imports.misc.extensionUtils;
|
|||||||
|
|
||||||
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── DEFINITIONS ────────────────────────────────────────────
|
||||||
// Definition of Row Model //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const COLUMN_ID = 0;
|
const COLUMN_ID = 0;
|
||||||
const COLUMN_DESC = 1;
|
const COLUMN_DESC = 1;
|
||||||
const COLUMN_KEY = 2;
|
const COLUMN_KEY = 2;
|
||||||
@@ -36,11 +33,9 @@ function buildPrefsWidget() {
|
|||||||
visible: true,
|
visible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── KEYBINDINGS ────────────────────────────────────────────
|
||||||
// Section for Keybindings //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const keysTitle = new Gtk.Label({
|
const keysTitle = new Gtk.Label({
|
||||||
label: "<b>Tastenkürzel</b>",
|
label: "<b>Keybindings</b>",
|
||||||
use_markup: true,
|
use_markup: true,
|
||||||
halign: Gtk.Align.START,
|
halign: Gtk.Align.START,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -66,17 +61,17 @@ function buildPrefsWidget() {
|
|||||||
GObject.TYPE_INT,
|
GObject.TYPE_INT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
addKeybinding(store, settings, "swap-master-window", "Master-Fenster tauschen");
|
addKeybinding(store, settings, "swap-master-window", "Swap current window with master");
|
||||||
|
|
||||||
addKeybinding(store, settings, "swap-up-window", "Fenster nach oben tauschen");
|
addKeybinding(store, settings, "swap-up-window", "Swap current window with window above");
|
||||||
addKeybinding(store, settings, "swap-down-window", "Fenster nach unten tauschen");
|
addKeybinding(store, settings, "swap-down-window", "Swap current window with window below");
|
||||||
addKeybinding(store, settings, "swap-left-window", "Fenster nach links tauschen");
|
addKeybinding(store, settings, "swap-left-window", "Swap current window with window to the left");
|
||||||
addKeybinding(store, settings, "swap-right-window", "Fenster nach rechts tauschen");
|
addKeybinding(store, settings, "swap-right-window", "Swap current window with window to the right");
|
||||||
|
|
||||||
addKeybinding(store, settings, "focus-up", "Fokus nach oben wechseln");
|
addKeybinding(store, settings, "focus-up", "Focus window above");
|
||||||
addKeybinding(store, settings, "focus-down", "Fokus nach unten wechseln");
|
addKeybinding(store, settings, "focus-down", "Focus window below");
|
||||||
addKeybinding(store, settings, "focus-left", "Fokus nach links wechseln");
|
addKeybinding(store, settings, "focus-left", "Focus window to the left");
|
||||||
addKeybinding(store, settings, "focus-right", "Fokus nach rechts wechseln");
|
addKeybinding(store, settings, "focus-right", "Focus window to the right");
|
||||||
|
|
||||||
let treeView = new Gtk.TreeView({
|
let treeView = new Gtk.TreeView({
|
||||||
model: store,
|
model: store,
|
||||||
@@ -122,11 +117,9 @@ function buildPrefsWidget() {
|
|||||||
|
|
||||||
prefsWidget.add(keysFrame);
|
prefsWidget.add(keysFrame);
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── WINDOW GAPS ────────────────────────────────────────────
|
||||||
// Section for Window Gaps //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const gapsTitle = new Gtk.Label({
|
const gapsTitle = new Gtk.Label({
|
||||||
label: "<b>Fensterabstände (Gaps)</b>",
|
label: "<b>Window Gaps</b>",
|
||||||
use_markup: true,
|
use_markup: true,
|
||||||
halign: Gtk.Align.START,
|
halign: Gtk.Align.START,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -144,29 +137,15 @@ function buildPrefsWidget() {
|
|||||||
});
|
});
|
||||||
gapsFrame.add(gapsGrid);
|
gapsFrame.add(gapsGrid);
|
||||||
|
|
||||||
addSpinButtonRow(gapsGrid, settings, "Innerer Abstand", "inner-gap", 0);
|
addSpinButtonRow(gapsGrid, settings, "Inner Gap", "inner-gap", 0);
|
||||||
addSpinButtonRow(
|
addSpinButtonRow(gapsGrid, settings, "Outer Gap (horizontal)", "outer-gap-horizontal", 1);
|
||||||
gapsGrid,
|
addSpinButtonRow(gapsGrid, settings, "Outer Gap (vertical)", "outer-gap-vertical", 2);
|
||||||
settings,
|
|
||||||
"Äußerer Abstand (horizontal)",
|
|
||||||
"outer-gap-horizontal",
|
|
||||||
1
|
|
||||||
);
|
|
||||||
addSpinButtonRow(
|
|
||||||
gapsGrid,
|
|
||||||
settings,
|
|
||||||
"Äußerer Abstand (vertikal)",
|
|
||||||
"outer-gap-vertical",
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
prefsWidget.add(gapsFrame);
|
prefsWidget.add(gapsFrame);
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── WINDOW BEHAVIOR ────────────────────────────────────────────
|
||||||
// Section for Window Behavior (Master vs. Stack) //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const behaviorTitle = new Gtk.Label({
|
const behaviorTitle = new Gtk.Label({
|
||||||
label: "<b>Fensterverhalten</b>",
|
label: "<b>Window Behavior</b>",
|
||||||
use_markup: true,
|
use_markup: true,
|
||||||
halign: Gtk.Align.START,
|
halign: Gtk.Align.START,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -186,12 +165,13 @@ function buildPrefsWidget() {
|
|||||||
addComboBoxRow(
|
addComboBoxRow(
|
||||||
behaviorGrid,
|
behaviorGrid,
|
||||||
settings,
|
settings,
|
||||||
"Neues Fenster öffnen als",
|
"Open new windows as",
|
||||||
"new-window-behavior",
|
"new-window-behavior",
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
prefsWidget.add(behaviorFrame);
|
prefsWidget.add(behaviorFrame);
|
||||||
|
|
||||||
|
prefsWidget.show_all();
|
||||||
return prefsWidget;
|
return prefsWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +195,7 @@ function addSpinButtonRow(grid, settings, desc, key, pos) {
|
|||||||
visible: true,
|
visible: true,
|
||||||
});
|
});
|
||||||
grid.attach(label, 0, pos, 1, 1);
|
grid.attach(label, 0, pos, 1, 1);
|
||||||
const adj = new Gtk.Adjustment({ lower: 0, upper: 50, step_increment: 1 });
|
const adj = new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 });
|
||||||
const spin = new Gtk.SpinButton({
|
const spin = new Gtk.SpinButton({
|
||||||
adjustment: adj,
|
adjustment: adj,
|
||||||
climb_rate: 1,
|
climb_rate: 1,
|
||||||
@@ -237,8 +217,8 @@ function addComboBoxRow(grid, settings, desc, key, pos) {
|
|||||||
visible: true,
|
visible: true,
|
||||||
halign: Gtk.Align.END,
|
halign: Gtk.Align.END,
|
||||||
});
|
});
|
||||||
combo.append("stack", "Stack-Fenster (Standard)");
|
combo.append("stack", "Stack Window (Default)");
|
||||||
combo.append("master", "Master-Fenster");
|
combo.append("master", "Master Window");
|
||||||
combo.set_active_id(settings.get_string(key));
|
combo.set_active_id(settings.get_string(key));
|
||||||
combo.connect("changed", () => {
|
combo.connect("changed", () => {
|
||||||
settings.set_string(key, combo.get_active_id());
|
settings.set_string(key, combo.get_active_id());
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// Simple-Tiling – MODERN MENU (GNOME Shell 45+) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
|
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);
|
||||||
|
|
||||||
|
// ── WINDOW GAPS ────────────────────────────────────────────
|
||||||
|
const groupGaps = new Adw.PreferencesGroup({
|
||||||
|
title: 'Window Gaps',
|
||||||
|
description: 'Adjust spacing between windows and screen edges.'
|
||||||
|
});
|
||||||
|
page.add(groupGaps);
|
||||||
|
|
||||||
|
const rowInnerGap = new Adw.SpinRow({
|
||||||
|
title: 'Inner Gap',
|
||||||
|
subtitle: 'Space between tiled windows (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,59 +4,59 @@
|
|||||||
|
|
||||||
<key name="swap-master-window" type="as">
|
<key name="swap-master-window" type="as">
|
||||||
<default><![CDATA[['<Super>Return']]]></default>
|
<default><![CDATA[['<Super>Return']]]></default>
|
||||||
<summary>Tauscht das fokussierte Fenster mit dem Master.</summary>
|
<summary>Swap current window with master.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="swap-up-window" type="as">
|
<key name="swap-up-window" type="as">
|
||||||
<default><![CDATA[['<Super>Up']]]></default>
|
<default><![CDATA[['<Super>Up']]]></default>
|
||||||
<summary>Tauscht das Fenster mit dem oberen Nachbarn.</summary>
|
<summary>Swap current window with window above.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="swap-down-window" type="as">
|
<key name="swap-down-window" type="as">
|
||||||
<default><![CDATA[['<Super>Down']]]></default>
|
<default><![CDATA[['<Super>Down']]]></default>
|
||||||
<summary>Tauscht das Fenster mit dem unteren Nachbarn.</summary>
|
<summary>Swap current window with window below.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="swap-left-window" type="as">
|
<key name="swap-left-window" type="as">
|
||||||
<default><![CDATA[['<Super>Left']]]></default>
|
<default><![CDATA[['<Super>Left']]]></default>
|
||||||
<summary>Tauscht das Fenster mit dem linken Nachbarn.</summary>
|
<summary>Swap current window with window to the left.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="swap-right-window" type="as">
|
<key name="swap-right-window" type="as">
|
||||||
<default><![CDATA[['<Super>Right']]]></default>
|
<default><![CDATA[['<Super>Right']]]></default>
|
||||||
<summary>Tauscht das Fenster mit dem rechten Nachbarn.</summary>
|
<summary>Swap current window with window to the right.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="focus-up" type="as">
|
<key name="focus-up" type="as">
|
||||||
<default><![CDATA[['<Alt>Up']]]></default>
|
<default><![CDATA[['<Alt>Up']]]></default>
|
||||||
<summary>Fokus zum oberen Fenster wechseln.</summary>
|
<summary>Focus window above.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="focus-down" type="as">
|
<key name="focus-down" type="as">
|
||||||
<default><![CDATA[['<Alt>Down']]]></default>
|
<default><![CDATA[['<Alt>Down']]]></default>
|
||||||
<summary>Fokus zum unteren Fenster wechseln.</summary>
|
<summary>Focus window below.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="focus-left" type="as">
|
<key name="focus-left" type="as">
|
||||||
<default><![CDATA[['<Alt>Left']]]></default>
|
<default><![CDATA[['<Alt>Left']]]></default>
|
||||||
<summary>Fokus zum linken Fenster wechseln.</summary>
|
<summary>Focus window to the left.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="focus-right" type="as">
|
<key name="focus-right" type="as">
|
||||||
<default><![CDATA[['<Alt>Right']]]></default>
|
<default><![CDATA[['<Alt>Right']]]></default>
|
||||||
<summary>Fokus zum rechten Fenster wechseln.</summary>
|
<summary>Focus window to the right.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="inner-gap" type="i">
|
<key name="inner-gap" type="i">
|
||||||
<default>10</default>
|
<default>10</default>
|
||||||
<summary>Der Abstand zwischen den Fenstern in Pixeln.</summary>
|
<summary>The gap between windows in pixels.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="outer-gap-horizontal" type="i">
|
<key name="outer-gap-horizontal" type="i">
|
||||||
<default>5</default>
|
<default>5</default>
|
||||||
<summary>Der Abstand zum linken und rechten Bildschirmrand.</summary>
|
<summary>The gap to the left and right screen edges.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="outer-gap-vertical" type="i">
|
<key name="outer-gap-vertical" type="i">
|
||||||
<default>5</default>
|
<default>5</default>
|
||||||
<summary>Der Abstand zum oberen und unteren Bildschirmrand.</summary>
|
<summary>The gap to the top and bottom screen edges.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="new-window-behavior" type="s">
|
<key name="new-window-behavior" type="s">
|
||||||
<default>'stack'</default>
|
<default>'stack'</default>
|
||||||
<summary>Verhalten für neu geöffnete Fenster.</summary>
|
<summary>Behavior for newly opened windows.</summary>
|
||||||
<description>Legt fest, ob ein neues Fenster als Master oder als Teil des Stacks hinzugefügt wird.</description>
|
<description>Determines if a new window is added as master or stack window.</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
</schema>
|
</schema>
|
||||||
|
|||||||
Reference in New Issue
Block a user