add initial code base.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# --- Ausnahmeliste für den Tiler ---
|
||||||
|
# Jede Zeile enthält die WM_CLASS einer Anwendung, die ignoriert werden soll.
|
||||||
|
# Die Groß- und Kleinschreibung wird ignoriert.
|
||||||
|
|
||||||
|
# Befehl zum Finden der WM_CLASS:
|
||||||
|
# 1. Terminal öffnen
|
||||||
|
# 2. 'xprop WM_CLASS' eingeben und Enter drücken
|
||||||
|
# 3. Mit dem Kreuz auf das gewünschte Fenster klicken
|
||||||
|
|
||||||
|
gnome-screenshot
|
||||||
|
|
||||||
+446
@@ -0,0 +1,446 @@
|
|||||||
|
// Simple-Tiling – GNOME Shell 3.38 (X11) - Version 1.0
|
||||||
|
// Features: Fibonacci-Stack-Layout (50/50) · Tiling-Lock · Drag- & Keyboard-Swap
|
||||||
|
// Pop-ups: zentriert + Always-on-Top + Exception-List
|
||||||
|
// © 2025 domoel – MIT
|
||||||
|
|
||||||
|
// Global Imports
|
||||||
|
const Main = imports.ui.main;
|
||||||
|
const Meta = imports.gi.Meta;
|
||||||
|
const Shell = imports.gi.Shell;
|
||||||
|
const Mainloop = imports.mainloop;
|
||||||
|
const Gio = imports.gi.Gio;
|
||||||
|
const GLib = imports.gi.GLib;
|
||||||
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
|
const ByteArray = imports.byteArray;
|
||||||
|
|
||||||
|
const Me = ExtensionUtils.getCurrentExtension();
|
||||||
|
const SCHEMA_NAME = 'org.gnome.shell.extensions.simple-tiling.domoel';
|
||||||
|
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
|
||||||
|
|
||||||
|
// InteractionHandler
|
||||||
|
// Verwaltung aller Nutzerinteraktionen (Maus & Tastatur)
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
|
||||||
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
if (this._wmKeysToDisable.length) {
|
||||||
|
this._wmKeysToDisable.forEach(key => this._wmSettings.set_value(key, new GLib.Variant('as', [])));
|
||||||
|
}
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId = this._settings.connect('changed', this._onSettingsChanged);
|
||||||
|
this._grabOpIds.push(global.display.connect('grab-op-begin', (display, screen, window) => {
|
||||||
|
if (this.tiler.windows.includes(window)) { this.tiler.grabbedWindow = window; }
|
||||||
|
}));
|
||||||
|
this._grabOpIds.push(global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length) {
|
||||||
|
this._wmKeysToDisable.forEach(key => this._wmSettings.set_value(key, this._savedWmShortcuts[key]));
|
||||||
|
}
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, callback) {
|
||||||
|
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, callback.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAllShortcuts() {
|
||||||
|
this._bind('swap-master-window', this._swapWithMaster);
|
||||||
|
this._bind('swap-left-window', () => this._swapInDirection('left'));
|
||||||
|
this._bind('swap-right-window', () => this._swapInDirection('right'));
|
||||||
|
this._bind('swap-up-window', () => this._swapInDirection('up'));
|
||||||
|
this._bind('swap-down-window', () => this._swapInDirection('down'));
|
||||||
|
}
|
||||||
|
|
||||||
|
_unbindAllShortcuts() {
|
||||||
|
['swap-master-window', 'swap-left-window', 'swap-right-window', 'swap-up-window', 'swap-down-window']
|
||||||
|
.forEach(key => Main.wm.removeKeybinding(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
const keys = [];
|
||||||
|
if (schema.has_key('toggle-tiled-left')) keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
||||||
|
else if (schema.has_key('tile-left')) keys.push('tile-left', 'tile-right');
|
||||||
|
if (schema.has_key('toggle-maximized')) keys.push('toggle-maximized');
|
||||||
|
else {
|
||||||
|
if (schema.has_key('maximize')) keys.push('maximize');
|
||||||
|
if (schema.has_key('unmaximize')) keys.push('unmaximize');
|
||||||
|
}
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(key => this._savedWmShortcuts[key] = this.tiler.wmSettings.get_value(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const windows = this.tiler.windows;
|
||||||
|
if (windows.length < 2) return;
|
||||||
|
const focusedWindow = global.display.get_focus_window();
|
||||||
|
if (!focusedWindow || !windows.includes(focusedWindow)) return;
|
||||||
|
const focusedIndex = windows.indexOf(focusedWindow);
|
||||||
|
if (focusedIndex > 0) {
|
||||||
|
[windows[0], windows[focusedIndex]] = [windows[focusedIndex], windows[0]];
|
||||||
|
} else if (focusedIndex === 0) {
|
||||||
|
[windows[0], windows[1]] = [windows[1], windows[0]];
|
||||||
|
}
|
||||||
|
this.tiler.tileNow();
|
||||||
|
if (windows.length > 0) windows[0].activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const sourceWindow = global.display.get_focus_window();
|
||||||
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
let targetWindow = null;
|
||||||
|
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
|
||||||
|
if (sourceIndex === 0 && direction === 'right' && this.tiler.windows.length > 1) {
|
||||||
|
targetWindow = this.tiler.windows[1];
|
||||||
|
} else {
|
||||||
|
targetWindow = this._findTargetInDirection(sourceWindow, direction);
|
||||||
|
}
|
||||||
|
if (!targetWindow) return;
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [this.tiler.windows[targetIndex], this.tiler.windows[sourceIndex]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
sourceWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(source, direction) {
|
||||||
|
const sourceRect = source.get_frame_rect();
|
||||||
|
let candidates = [];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === source) continue;
|
||||||
|
const targetRect = win.get_frame_rect();
|
||||||
|
switch (direction) {
|
||||||
|
case 'left': if (targetRect.x < sourceRect.x) candidates.push(win); break;
|
||||||
|
case 'right': if (targetRect.x > sourceRect.x) candidates.push(win); break;
|
||||||
|
case 'up': if (targetRect.y < sourceRect.y) candidates.push(win); break;
|
||||||
|
case 'down': if (targetRect.y > sourceRect.y) candidates.push(win); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
let bestTarget = null;
|
||||||
|
let minDeviation = Infinity;
|
||||||
|
for (const win of candidates) {
|
||||||
|
const targetRect = win.get_frame_rect();
|
||||||
|
let deviation;
|
||||||
|
if (direction === 'left' || direction === 'right') {
|
||||||
|
deviation = Math.abs(sourceRect.y - targetRect.y);
|
||||||
|
} else {
|
||||||
|
deviation = Math.abs(sourceRect.x - targetRect.x);
|
||||||
|
}
|
||||||
|
if (deviation < minDeviation) {
|
||||||
|
minDeviation = deviation;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbedWindow = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbedWindow) return;
|
||||||
|
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
|
||||||
|
if (targetWindow) {
|
||||||
|
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [this.tiler.windows[targetIndex], this.tiler.windows[sourceIndex]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(excludeWindow) {
|
||||||
|
let [pointerX, pointerY] = global.get_pointer();
|
||||||
|
let windows = global.get_window_actors().map(actor => actor.meta_window).filter(win => {
|
||||||
|
if (!win || win === excludeWindow || !this.tiler.windows.includes(win)) return false;
|
||||||
|
let frame = win.get_frame_rect();
|
||||||
|
return pointerX >= frame.x && pointerX < frame.x + frame.width &&
|
||||||
|
pointerY >= frame.y && pointerY < frame.y + frame.height;
|
||||||
|
});
|
||||||
|
if (windows.length > 0) { return windows[windows.length - 1]; }
|
||||||
|
|
||||||
|
let bestTarget = null;
|
||||||
|
let maxOverlap = 0;
|
||||||
|
const sourceFrame = excludeWindow.get_frame_rect();
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === excludeWindow) continue;
|
||||||
|
const targetFrame = win.get_frame_rect();
|
||||||
|
const overlapX = Math.max(0, Math.min(sourceFrame.x + sourceFrame.width, targetFrame.x + targetFrame.width) - Math.max(sourceFrame.x, targetFrame.x));
|
||||||
|
const overlapY = Math.max(0, Math.min(sourceFrame.y + sourceFrame.height, targetFrame.y + targetFrame.height) - Math.max(sourceFrame.y, targetFrame.y));
|
||||||
|
const overlapArea = overlapX * overlapY;
|
||||||
|
if (overlapArea > maxOverlap) {
|
||||||
|
maxOverlap = overlapArea;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tiler
|
||||||
|
// Die Hauptklasse für die Tiling-Logik.
|
||||||
|
class Tiler {
|
||||||
|
constructor() {
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this.wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
// Layout-Konfiguration
|
||||||
|
this._innerGap = 10;
|
||||||
|
this._outerGapVertical = 5;
|
||||||
|
this._outerGapHorizontal = 10;
|
||||||
|
|
||||||
|
// Delay-Zeiten für das Tiling und Exception Windows
|
||||||
|
this._tilingDelay = 20;
|
||||||
|
this._centeringDelay = 5;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._onWindowAdded = this._onWindowAdded.bind(this);
|
||||||
|
this._onWindowRemoved = this._onWindowRemoved.bind(this);
|
||||||
|
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind(this);
|
||||||
|
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
const workspaceManager = global.workspace_manager;
|
||||||
|
this._signalIds.set('workspace-changed', { object: workspaceManager, id: workspaceManager.connect('active-workspace-changed', this._onActiveWorkspaceChanged) });
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
for (const [, signal] of this._signalIds) {
|
||||||
|
try { signal.object.disconnect(signal.id); } catch(e) {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.file_new_for_path(Me.path + '/exceptions.txt');
|
||||||
|
if (!file.query_exists(null)) { this._exceptions = []; return; }
|
||||||
|
const [ok, data] = file.load_contents(null);
|
||||||
|
this._exceptions = ok ? ByteArray.toString(data).split('\n')
|
||||||
|
.map(l => l.trim()).filter(l => l && !l.startsWith('#'))
|
||||||
|
.map(l => l.toLowerCase()) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
return !!win && this._exceptions.includes((win.get_wm_class() || '').toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return win && !win.minimized && !this._isException(win) && win.get_window_type() === Meta.WindowType.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
Mainloop.timeout_add(this._centeringDelay, () => {
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (win.get_maximized()) { win.unmaximize(Meta.MaximizeFlags.BOTH); }
|
||||||
|
const monitorIndex = win.get_monitor();
|
||||||
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
|
||||||
|
const frame = win.get_frame_rect();
|
||||||
|
win.move_frame(true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y + Math.floor((workArea.height - frame.height) / 2));
|
||||||
|
Mainloop.idle_add(() => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowAdded(workspace, win) {
|
||||||
|
if (this.windows.includes(win)) return;
|
||||||
|
|
||||||
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isTileable(win)) {
|
||||||
|
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 = global.workspace_manager.get_active_workspace();
|
||||||
|
workspace.list_windows().forEach(win => this._onWindowAdded(workspace, win));
|
||||||
|
this._signalIds.set('window-added', { object: workspace, id: workspace.connect('window-added', this._onWindowAdded) });
|
||||||
|
this._signalIds.set('window-removed', { object: workspace, id: workspace.connect('window-removed', this._onWindowRemoved) });
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach(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) return;
|
||||||
|
this._tileInProgress = true;
|
||||||
|
Mainloop.timeout_add(this._tilingDelay, () => {
|
||||||
|
this._tileWindows();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
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 workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index);
|
||||||
|
const innerArea = {
|
||||||
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
class SimpleTilingExtension {
|
||||||
|
constructor() {
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
enable() {
|
||||||
|
this.tiler = new Tiler();
|
||||||
|
this.tiler.enable();
|
||||||
|
}
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
return new SimpleTilingExtension();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"uuid": "simple-tiling@domoel",
|
||||||
|
"name": "Simple Tiling",
|
||||||
|
"description": "A Simple Tiling Extension for Gnome Shell 3.38.",
|
||||||
|
"version": 1,
|
||||||
|
"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,121 @@
|
|||||||
|
// prefs.js - Finale Version nach dem Vorbild von Focus-Switcher
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Gtk, GObject } = imports.gi;
|
||||||
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
|
|
||||||
|
// Definiere die Spalten für unser Datenmodell
|
||||||
|
const COLUMN_ID = 0; // z.B. 'swap-master-window'
|
||||||
|
const COLUMN_DESC = 1; // z.B. 'Master-Fenster tauschen'
|
||||||
|
const COLUMN_KEY = 2; // Der Key-Code (eine Zahl)
|
||||||
|
const COLUMN_MODS = 3; // Die Modifier-Maske (eine Zahl)
|
||||||
|
|
||||||
|
function init() {}
|
||||||
|
|
||||||
|
function buildPrefsWidget() {
|
||||||
|
const settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.simple-tiling.domoel');
|
||||||
|
|
||||||
|
const prefsWidget = new Gtk.Box({
|
||||||
|
orientation: Gtk.Orientation.VERTICAL,
|
||||||
|
margin: 20,
|
||||||
|
spacing: 12,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = new Gtk.Label({
|
||||||
|
label: '<b>Tastenkürzel für Simple-Tiling</b>',
|
||||||
|
use_markup: true,
|
||||||
|
halign: Gtk.Align.START,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
prefsWidget.add(title);
|
||||||
|
|
||||||
|
// 1. Das Datenmodell (ListStore) erstellen
|
||||||
|
let store = new Gtk.ListStore();
|
||||||
|
store.set_column_types([
|
||||||
|
GObject.TYPE_STRING, // COLUMN_ID
|
||||||
|
GObject.TYPE_STRING, // COLUMN_DESC
|
||||||
|
GObject.TYPE_INT, // COLUMN_KEY
|
||||||
|
GObject.TYPE_INT, // COLUMN_MODS
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Fülle das Datenmodell mit unseren Einstellungen
|
||||||
|
addKeybinding(store, settings, 'swap-master-window', 'Master-Fenster tauschen');
|
||||||
|
addKeybinding(store, settings, 'swap-left-window', 'Fenster nach links tauschen');
|
||||||
|
addKeybinding(store, settings, 'swap-right-window', 'Fenster nach rechts tauschen');
|
||||||
|
addKeybinding(store, settings, 'swap-up-window', 'Fenster nach oben tauschen');
|
||||||
|
addKeybinding(store, settings, 'swap-down-window', 'Fenster nach unten tauschen');
|
||||||
|
|
||||||
|
// 2. Die Ansicht (TreeView) erstellen, die das Modell anzeigt
|
||||||
|
let treeView = new Gtk.TreeView({
|
||||||
|
model: store,
|
||||||
|
headers_visible: false,
|
||||||
|
hexpand: true,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstelle die Spalte für die Beschreibung
|
||||||
|
let descRenderer = new Gtk.CellRendererText();
|
||||||
|
let descColumn = new Gtk.TreeViewColumn({ expand: true });
|
||||||
|
descColumn.pack_start(descRenderer, true);
|
||||||
|
descColumn.add_attribute(descRenderer, 'text', COLUMN_DESC);
|
||||||
|
treeView.append_column(descColumn);
|
||||||
|
|
||||||
|
// 3. Erstelle die Spalte für das Tastenkürzel mit dem Spezialisten Gtk.CellRendererAccel
|
||||||
|
let accelRenderer = new Gtk.CellRendererAccel({
|
||||||
|
'accel-mode': Gtk.CellRendererAccelMode.GTK,
|
||||||
|
'editable': true
|
||||||
|
});
|
||||||
|
let accelColumn = new Gtk.TreeViewColumn();
|
||||||
|
accelColumn.pack_end(accelRenderer, false);
|
||||||
|
accelColumn.add_attribute(accelRenderer, 'accel-key', COLUMN_KEY);
|
||||||
|
accelColumn.add_attribute(accelRenderer, 'accel-mods', COLUMN_MODS);
|
||||||
|
treeView.append_column(accelColumn);
|
||||||
|
|
||||||
|
prefsWidget.add(treeView);
|
||||||
|
|
||||||
|
// Verbinde die Events, die ausgelöst werden, wenn der Nutzer ein Kürzel ändert
|
||||||
|
accelRenderer.connect('accel-edited', (renderer, path_string, key, mods, hw_code) => {
|
||||||
|
let [ok, iter] = store.get_iter_from_string(path_string);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
// Aktualisiere das Datenmodell...
|
||||||
|
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]);
|
||||||
|
|
||||||
|
// ...und speichere die Änderung in den GSettings
|
||||||
|
let id = store.get_value(iter, COLUMN_ID);
|
||||||
|
let accelString = Gtk.accelerator_name(key, mods);
|
||||||
|
settings.set_strv(id, [accelString]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event für das Löschen eines Kürzels (z.B. mit Backspace)
|
||||||
|
accelRenderer.connect('accel-cleared', (renderer, path_string) => {
|
||||||
|
let [ok, iter] = store.get_iter_from_string(path_string);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]);
|
||||||
|
let id = store.get_value(iter, COLUMN_ID);
|
||||||
|
settings.set_strv(id, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return prefsWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion zum Befüllen des Datenmodells
|
||||||
|
function addKeybinding(model, settings, id, description) {
|
||||||
|
let [key, mods] = [0, 0];
|
||||||
|
|
||||||
|
// KORREKTUR: Leerzeichen zwischen const und strv eingefügt
|
||||||
|
const strv = settings.get_strv(id);
|
||||||
|
if (strv && strv.length > 0 && strv[0]) {
|
||||||
|
[key, mods] = Gtk.accelerator_parse(strv[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Füge eine neue Zeile zum Datenmodell hinzu
|
||||||
|
let iter = model.append();
|
||||||
|
model.set(iter,
|
||||||
|
[COLUMN_ID, COLUMN_DESC, COLUMN_KEY, COLUMN_MODS],
|
||||||
|
[id, description, key, mods]
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user