Update legacy.js
This commit is contained in:
@@ -1,27 +1,24 @@
|
|||||||
///////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
// Simple‑Tiling – LEGACY (GNOME Shell 3.38 ‑ 44) //
|
// Simple-Tiling – LEGACY (for GNOME Shell 3.38) //
|
||||||
// © 2025 domoel – MIT //
|
// © 2025 domoel – MIT //
|
||||||
/////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
const Main = imports.ui.main;
|
||||||
const Main = imports.ui.main;
|
const Meta = imports.gi.Meta;
|
||||||
const Meta = imports.gi.Meta;
|
const Shell = imports.gi.Shell;
|
||||||
const Shell = imports.gi.Shell;
|
const Gio = imports.gi.Gio;
|
||||||
const Gio = imports.gi.Gio;
|
const GLib = imports.gi.GLib;
|
||||||
const GLib = imports.gi.GLib;
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
const ExtensionUtils= imports.misc.extensionUtils;
|
const ByteArray = imports.byteArray;
|
||||||
const ByteArray = imports.byteArray;
|
|
||||||
|
|
||||||
const Me = ExtensionUtils.getCurrentExtension();
|
const Me = ExtensionUtils.getCurrentExtension();
|
||||||
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
|
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
||||||
|
|
||||||
// ── CONST ────────────────────────────────────────────
|
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
||||||
const SCHEMA_NAME = 'org.gnome.shell.extensions.simple-tiling.domoel';
|
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
||||||
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
|
|
||||||
|
|
||||||
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
|
||||||
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
|
||||||
|
|
||||||
const KEYBINDINGS = {
|
const KEYBINDINGS = {
|
||||||
'swap-master-window': (self) => self._swapWithMaster(),
|
'swap-master-window': (self) => self._swapWithMaster(),
|
||||||
@@ -35,98 +32,77 @@ const KEYBINDINGS = {
|
|||||||
'focus-down': (self) => self._focusInDirection('down'),
|
'focus-down': (self) => self._focusInDirection('down'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
// --- INTERACTIONHANDLER ---
|
||||||
function addKeybinding(name, settings, flags, mode, handler) {
|
|
||||||
if (Main.wm?.addKeybinding)
|
|
||||||
Main.wm.addKeybinding(name, settings, flags, mode, handler);
|
|
||||||
else
|
|
||||||
global.display.add_keybinding(name, settings, flags, mode, handler);
|
|
||||||
}
|
|
||||||
function removeKeybinding(name) {
|
|
||||||
if (Main.wm?.removeKeybinding)
|
|
||||||
Main.wm.removeKeybinding(name);
|
|
||||||
else
|
|
||||||
global.display.remove_keybinding(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkAreaForMonitor(monitorIndex) {
|
|
||||||
if (Main.layoutManager?.getWorkAreaForMonitor)
|
|
||||||
return Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
|
|
||||||
|
|
||||||
return global.workspace_manager
|
|
||||||
.get_active_workspace()
|
|
||||||
.get_work_area_for_monitor(monitorIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeUtf8(bytes) {
|
|
||||||
if (typeof ByteArray !== 'undefined')
|
|
||||||
return ByteArray.toString(bytes);
|
|
||||||
return new TextDecoder('utf-8').decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointer() {
|
|
||||||
return global.get_pointer ? global.get_pointer()
|
|
||||||
: global.display.get_pointer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── INTERACTIONHANDLER ───────────────────────────────────
|
|
||||||
class InteractionHandler {
|
class InteractionHandler {
|
||||||
constructor(tiler) {
|
constructor(tiler) {
|
||||||
this.tiler = tiler;
|
this.tiler = tiler;
|
||||||
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
this._wmKeysToDisable = [];
|
this._wmKeysToDisable = [];
|
||||||
this._savedWmShortcuts= {};
|
this._savedWmShortcuts = {};
|
||||||
this._grabOpIds = [];
|
this._grabOpIds = [];
|
||||||
this._settingsChangedId = null;
|
this._settingsChangedId = null;
|
||||||
|
|
||||||
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
|
|
||||||
this._prepareWmShortcuts();
|
this._prepareWmShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
if (this._wmKeysToDisable.length)
|
if (this._wmKeysToDisable.length) {
|
||||||
this._wmKeysToDisable.forEach(k =>
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
this._wmSettings.set_value(key, new GLib.Variant("as", []))
|
||||||
|
);
|
||||||
|
}
|
||||||
this._bindAllShortcuts();
|
this._bindAllShortcuts();
|
||||||
this._settingsChangedId =
|
this._settingsChangedId = this._settings.connect(
|
||||||
this._settings.connect('changed', this._onSettingsChanged);
|
"changed",
|
||||||
|
this._onSettingsChanged
|
||||||
|
);
|
||||||
this._grabOpIds.push(
|
this._grabOpIds.push(
|
||||||
global.display.connect('grab-op-begin',
|
global.display.connect(
|
||||||
(display, screen, win) => {
|
"grab-op-begin",
|
||||||
if (this.tiler.windows.includes(win))
|
(display, screen, window) => {
|
||||||
this.tiler.grabbedWindow = win;
|
if (this.tiler.windows.includes(window)) {
|
||||||
}));
|
this.tiler.grabbedWindow = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
this._grabOpIds.push(
|
this._grabOpIds.push(
|
||||||
global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
|
global.display.connect("grab-op-end", this._onGrabEnd.bind(this))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
if (this._wmKeysToDisable.length)
|
if (this._wmKeysToDisable.length) {
|
||||||
this._wmKeysToDisable.forEach(k =>
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
this._wmSettings.set_value(key, this._savedWmShortcuts[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
this._unbindAllShortcuts();
|
this._unbindAllShortcuts();
|
||||||
|
|
||||||
if (this._settingsChangedId) {
|
if (this._settingsChangedId) {
|
||||||
this._settings.disconnect(this._settingsChangedId);
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
this._settingsChangedId = null;
|
this._settingsChangedId = null;
|
||||||
}
|
}
|
||||||
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
this._grabOpIds.forEach((id) => global.display.disconnect(id));
|
||||||
this._grabOpIds = [];
|
this._grabOpIds = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_bind(key, callback) {
|
_bind(key, callback) {
|
||||||
addKeybinding(key, this._settings,
|
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL,
|
||||||
Meta.KeyBindingFlags.NONE,
|
() => callback(this));
|
||||||
Shell.ActionMode.NORMAL,
|
}
|
||||||
() => callback(this));
|
|
||||||
|
_bindAllShortcuts() {
|
||||||
|
for (const [key, handler] of Object.entries(KEYBINDINGS)) {
|
||||||
|
this._bind(key, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_unbindAllShortcuts() {
|
||||||
|
for (const key in KEYBINDINGS) {
|
||||||
|
Main.wm.removeKeybinding(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k,h); }
|
|
||||||
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) removeKeybinding(k); }
|
|
||||||
|
|
||||||
_onSettingsChanged() {
|
_onSettingsChanged() {
|
||||||
this._unbindAllShortcuts();
|
this._unbindAllShortcuts();
|
||||||
@@ -135,185 +111,240 @@ class InteractionHandler {
|
|||||||
|
|
||||||
_prepareWmShortcuts() {
|
_prepareWmShortcuts() {
|
||||||
const schema = this._wmSettings.settings_schema;
|
const schema = this._wmSettings.settings_schema;
|
||||||
const keys = [];
|
const keys = [];
|
||||||
|
if (schema.has_key("toggle-tiled-left"))
|
||||||
if (schema.has_key('toggle-tiled-left'))
|
keys.push("toggle-tiled-left", "toggle-tiled-right");
|
||||||
keys.push('toggle-tiled-left','toggle-tiled-right');
|
else if (schema.has_key("tile-left"))
|
||||||
else if (schema.has_key('tile-left'))
|
keys.push("tile-left", "tile-right");
|
||||||
keys.push('tile-left','tile-right');
|
if (schema.has_key("toggle-maximized")) keys.push("toggle-maximized");
|
||||||
|
|
||||||
if (schema.has_key('toggle-maximized'))
|
|
||||||
keys.push('toggle-maximized');
|
|
||||||
else {
|
else {
|
||||||
if (schema.has_key('maximize')) keys.push('maximize');
|
if (schema.has_key("maximize")) keys.push("maximize");
|
||||||
if (schema.has_key('unmaximize')) keys.push('unmaximize');
|
if (schema.has_key("unmaximize")) keys.push("unmaximize");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.length) {
|
if (keys.length) {
|
||||||
this._wmKeysToDisable = keys;
|
this._wmKeysToDisable = keys;
|
||||||
keys.forEach(k => this._savedWmShortcuts[k] =
|
keys.forEach(
|
||||||
this._wmSettings.get_value(k));
|
(key) => (this._savedWmShortcuts[key] = this._wmSettings.get_value(key))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_focusInDirection(direction) {
|
_focusInDirection(direction) {
|
||||||
const src = global.display.get_focus_window();
|
const sourceWindow = global.display.get_focus_window();
|
||||||
if (!src || !this.tiler.windows.includes(src)) return;
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
const tgt = this._findTargetInDirection(src, direction);
|
|
||||||
if (tgt) tgt.activate(global.get_current_time());
|
const targetWindow = this._findTargetInDirection(
|
||||||
|
sourceWindow,
|
||||||
|
direction
|
||||||
|
);
|
||||||
|
if (targetWindow) {
|
||||||
|
targetWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_swapWithMaster() {
|
_swapWithMaster() {
|
||||||
const w = this.tiler.windows;
|
const windows = this.tiler.windows;
|
||||||
if (w.length < 2) return;
|
if (windows.length < 2) return;
|
||||||
const foc = global.display.get_focus_window();
|
const focusedWindow = global.display.get_focus_window();
|
||||||
if (!foc || !w.includes(foc)) return;
|
if (!focusedWindow || !windows.includes(focusedWindow)) return;
|
||||||
const idx = w.indexOf(foc);
|
const focusedIndex = windows.indexOf(focusedWindow);
|
||||||
if (idx > 0)
|
if (focusedIndex > 0) {
|
||||||
[w[0], w[idx]] = [w[idx], w[0]];
|
[windows[0], windows[focusedIndex]] = [
|
||||||
else
|
windows[focusedIndex],
|
||||||
[w[0], w[1]] = [w[1], w[0]];
|
windows[0],
|
||||||
|
];
|
||||||
|
} else if (focusedIndex === 0) {
|
||||||
|
[windows[0], windows[1]] = [windows[1], windows[0]];
|
||||||
|
}
|
||||||
this.tiler.tileNow();
|
this.tiler.tileNow();
|
||||||
w[0]?.activate(global.get_current_time());
|
if (windows.length > 0) windows[0].activate(global.get_current_time());
|
||||||
}
|
}
|
||||||
|
|
||||||
_swapInDirection(direction) {
|
_swapInDirection(direction) {
|
||||||
const src = global.display.get_focus_window();
|
const sourceWindow = global.display.get_focus_window();
|
||||||
if (!src || !this.tiler.windows.includes(src)) return;
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
let targetWindow = null;
|
||||||
let tgt = null;
|
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
|
||||||
const srcIdx = this.tiler.windows.indexOf(src);
|
if (
|
||||||
if (srcIdx === 0 && direction === 'right' && this.tiler.windows.length>1)
|
sourceIndex === 0 &&
|
||||||
tgt = this.tiler.windows[1];
|
direction === "right" &&
|
||||||
else
|
this.tiler.windows.length > 1
|
||||||
tgt = this._findTargetInDirection(src, direction);
|
) {
|
||||||
|
targetWindow = this.tiler.windows[1];
|
||||||
if (!tgt) return;
|
} else {
|
||||||
const tgtIdx = this.tiler.windows.indexOf(tgt);
|
targetWindow = this._findTargetInDirection(sourceWindow, direction);
|
||||||
[this.tiler.windows[srcIdx], this.tiler.windows[tgtIdx]] =
|
}
|
||||||
[this.tiler.windows[tgtIdx], this.tiler.windows[srcIdx]];
|
if (!targetWindow) return;
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
this.tiler.tileNow();
|
this.tiler.tileNow();
|
||||||
src.activate(global.get_current_time());
|
sourceWindow.activate(global.get_current_time());
|
||||||
}
|
}
|
||||||
|
|
||||||
_findTargetInDirection(src, direction) {
|
_findTargetInDirection(source, direction) {
|
||||||
const sRect = src.get_frame_rect();
|
const sourceRect = source.get_frame_rect();
|
||||||
const cands = [];
|
let candidates = [];
|
||||||
|
|
||||||
for (const win of this.tiler.windows) {
|
for (const win of this.tiler.windows) {
|
||||||
if (win === src) continue;
|
if (win === source) continue;
|
||||||
const tRect = win.get_frame_rect();
|
const targetRect = win.get_frame_rect();
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case 'left': if (tRect.x < sRect.x) cands.push(win); break;
|
case "left":
|
||||||
case 'right': if (tRect.x > sRect.x) cands.push(win); break;
|
if (targetRect.x < sourceRect.x) candidates.push(win);
|
||||||
case 'up': if (tRect.y < sRect.y) cands.push(win); break;
|
break;
|
||||||
case 'down': if (tRect.y > sRect.y) cands.push(win); break;
|
case "right":
|
||||||
|
if (targetRect.x > sourceRect.x) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "up":
|
||||||
|
if (targetRect.y < sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
if (targetRect.y > sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!cands.length) return null;
|
if (candidates.length === 0) return null;
|
||||||
|
let bestTarget = null;
|
||||||
let best=null, min=Infinity;
|
let minDeviation = Infinity;
|
||||||
for (const win of cands) {
|
for (const win of candidates) {
|
||||||
const tRect = win.get_frame_rect();
|
const targetRect = win.get_frame_rect();
|
||||||
const dev = (direction==='left'||direction==='right')
|
let deviation;
|
||||||
? Math.abs(sRect.y - tRect.y)
|
if (direction === "left" || direction === "right") {
|
||||||
: Math.abs(sRect.x - tRect.x);
|
deviation = Math.abs(sourceRect.y - targetRect.y);
|
||||||
if (dev < min) { min=dev; best=win; }
|
} else {
|
||||||
|
deviation = Math.abs(sourceRect.x - targetRect.x);
|
||||||
|
}
|
||||||
|
if (deviation < minDeviation) {
|
||||||
|
minDeviation = deviation;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return best;
|
return bestTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onGrabEnd() {
|
_onGrabEnd() {
|
||||||
const grabbed = this.tiler.grabbedWindow;
|
const grabbedWindow = this.tiler.grabbedWindow;
|
||||||
if (!grabbed) return;
|
if (!grabbedWindow) return;
|
||||||
|
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
|
||||||
const tgt = this._findTargetUnderPointer(grabbed);
|
if (targetWindow) {
|
||||||
if (tgt) {
|
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
|
||||||
const a = this.tiler.windows.indexOf(grabbed);
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
const b = this.tiler.windows.indexOf(tgt);
|
[
|
||||||
[this.tiler.windows[a], this.tiler.windows[b]] =
|
this.tiler.windows[sourceIndex],
|
||||||
[this.tiler.windows[b], this.tiler.windows[a]];
|
this.tiler.windows[targetIndex],
|
||||||
|
] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
this.tiler.queueTile();
|
this.tiler.queueTile();
|
||||||
this.tiler.grabbedWindow = null;
|
this.tiler.grabbedWindow = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_findTargetUnderPointer(exclude) {
|
_findTargetUnderPointer(excludeWindow) {
|
||||||
const [x,y] = getPointer();
|
let [pointerX, pointerY] = global.get_pointer();
|
||||||
const wins = global.get_window_actors()
|
let windows = global
|
||||||
.map(a => a.meta_window)
|
.get_window_actors()
|
||||||
.filter(w => w && w!==exclude &&
|
.map((actor) => actor.meta_window)
|
||||||
this.tiler.windows.includes(w) &&
|
.filter((win) => {
|
||||||
((()=>{ const f=w.get_frame_rect();
|
if (
|
||||||
return x>=f.x && x<f.x+f.width &&
|
!win ||
|
||||||
y>=f.y && y<f.y+f.height;})()));
|
win === excludeWindow ||
|
||||||
if (wins.length) return wins[wins.length-1];
|
!this.tiler.windows.includes(win)
|
||||||
|
)
|
||||||
let best=null, max=0, sRect=exclude.get_frame_rect();
|
return false;
|
||||||
for (const w of this.tiler.windows) {
|
let frame = win.get_frame_rect();
|
||||||
if (w===exclude) continue;
|
return (
|
||||||
const tRect=w.get_frame_rect();
|
pointerX >= frame.x &&
|
||||||
const ovX = Math.max(0, Math.min(sRect.x+sRect.width,
|
pointerX < frame.x + frame.width &&
|
||||||
tRect.x+tRect.width) -
|
pointerY >= frame.y &&
|
||||||
Math.max(sRect.x, tRect.x));
|
pointerY < frame.y + frame.height
|
||||||
const ovY = Math.max(0, Math.min(sRect.y+sRect.height,
|
);
|
||||||
tRect.y+tRect.height) -
|
});
|
||||||
Math.max(sRect.y, tRect.y));
|
if (windows.length > 0) {
|
||||||
const area = ovX*ovY;
|
return windows[windows.length - 1];
|
||||||
if (area>max){ max=area; best=w; }
|
|
||||||
}
|
}
|
||||||
return best;
|
|
||||||
|
let bestTarget = null;
|
||||||
|
let maxOverlap = 0;
|
||||||
|
const sourceFrame = excludeWindow.get_frame_rect();
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === excludeWindow) continue;
|
||||||
|
const targetFrame = win.get_frame_rect();
|
||||||
|
const overlapX = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.x + sourceFrame.width,
|
||||||
|
targetFrame.x + targetFrame.width
|
||||||
|
) - Math.max(sourceFrame.x, targetFrame.x)
|
||||||
|
);
|
||||||
|
const overlapY = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.y + sourceFrame.height,
|
||||||
|
targetFrame.y + targetFrame.height
|
||||||
|
) - Math.max(sourceFrame.y, targetFrame.y)
|
||||||
|
);
|
||||||
|
const overlapArea = overlapX * overlapY;
|
||||||
|
if (overlapArea > maxOverlap) {
|
||||||
|
maxOverlap = overlapArea;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TILER ────────────────────────────────────────────────
|
// --- TILER ---
|
||||||
class Tiler {
|
class Tiler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
this.grabbedWindow = null;
|
this.grabbedWindow = null;
|
||||||
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
this._signalIds = new Map();
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
this._tileTimeoutId = null;
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
this._centerTimeoutIds= [];
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
this._tileInProgress = false;
|
|
||||||
|
|
||||||
this._innerGap = this._settings.get_int('inner-gap');
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
this._outerGapVertical= this._settings.get_int('outer-gap-vertical');
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
this._outerGapHorizontal = this._settings.get_int('outer-gap-horizontal');
|
|
||||||
|
|
||||||
this._tilingDelay = TILING_DELAY_MS;
|
this._exceptions = [];
|
||||||
this._centeringDelay= CENTERING_DELAY_MS;
|
|
||||||
|
|
||||||
this._exceptions = [];
|
|
||||||
this._interactionHandler = new InteractionHandler(this);
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
this._onWindowAdded = this._onWindowAdded.bind(this);
|
this._tileTimeoutId = null;
|
||||||
this._onWindowRemoved= this._onWindowRemoved.bind(this);
|
this._centerTimeoutIds = [];
|
||||||
this._onActiveWorkspaceChanged =
|
|
||||||
this._onActiveWorkspaceChanged.bind(this);
|
this._onWindowAdded = this._onWindowAdded.bind(this);
|
||||||
this._onWindowMinimizedStateChanged =
|
this._onWindowRemoved = this._onWindowRemoved.bind(this);
|
||||||
this._onWindowMinimizedStateChanged.bind(this);
|
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
this._loadExceptions();
|
this._loadExceptions();
|
||||||
|
const workspaceManager = global.workspace_manager;
|
||||||
const wm = global.workspace_manager;
|
this._signalIds.set("workspace-changed", {
|
||||||
this._signalIds.set('workspace-changed', {
|
object: workspaceManager,
|
||||||
object: wm,
|
id: workspaceManager.connect(
|
||||||
id: wm.connect('active-workspace-changed',
|
"active-workspace-changed",
|
||||||
this._onActiveWorkspaceChanged),
|
this._onActiveWorkspaceChanged
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._connectToWorkspace();
|
this._connectToWorkspace();
|
||||||
|
|
||||||
this._interactionHandler.enable();
|
this._interactionHandler.enable();
|
||||||
|
this._signalIds.set("settings-changed", {
|
||||||
this._signalIds.set('settings-changed', {
|
|
||||||
object: this._settings,
|
object: this._settings,
|
||||||
id: this._settings.connect('changed', this._onSettingsChanged),
|
id: this._settings.connect("changed", this._onSettingsChanged),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,112 +358,147 @@ class Tiler {
|
|||||||
|
|
||||||
this._interactionHandler.disable();
|
this._interactionHandler.disable();
|
||||||
this._disconnectFromWorkspace();
|
this._disconnectFromWorkspace();
|
||||||
|
for (const [, signal] of this._signalIds) {
|
||||||
for (const [,sig] of this._signalIds) {
|
try {
|
||||||
try { sig.object.disconnect(sig.id); } catch {}
|
signal.object.disconnect(signal.id);
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
this._signalIds.clear();
|
this._signalIds.clear();
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSettingsChanged() {
|
_onSettingsChanged() {
|
||||||
this._innerGap = this._settings.get_int('inner-gap');
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
this._outerGapVertical = this._settings.get_int('outer-gap-vertical');
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
this._outerGapHorizontal= this._settings.get_int('outer-gap-horizontal');
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadExceptions() {
|
_loadExceptions() {
|
||||||
const file = Gio.File.new_for_path(Me.path + '/exceptions.txt');
|
const file = Gio.file_new_for_path(Me.path + "/exceptions.txt");
|
||||||
if (!file.query_exists(null)) { this._exceptions=[]; return; }
|
if (!file.query_exists(null)) {
|
||||||
|
this._exceptions = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
const [ok, data] = file.load_contents(null);
|
const [ok, data] = file.load_contents(null);
|
||||||
this._exceptions = ok ? decodeUtf8(data)
|
this._exceptions = ok
|
||||||
.split('\n')
|
? ByteArray.toString(data)
|
||||||
.map(l => l.trim())
|
.split("\n")
|
||||||
.filter(l => l && !l.startsWith('#'))
|
.map((l) => l.trim())
|
||||||
.map(l => l.toLowerCase())
|
.filter((l) => l && !l.startsWith("#"))
|
||||||
: [];
|
.map((l) => l.toLowerCase())
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_isException(win) {
|
_isException(win) {
|
||||||
if (!win) return false;
|
if (!win) return false;
|
||||||
const wmClass = (win.get_wm_class() || '').toLowerCase();
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
const appId = (win.get_gtk_application_id() || '').toLowerCase();
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isTileable(win) {
|
_isTileable(win) {
|
||||||
return win && !win.minimized && !this._isException(win) &&
|
return (
|
||||||
win.get_window_type() === Meta.WindowType.NORMAL;
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_centerWindow(win) {
|
_centerWindow(win) {
|
||||||
const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
|
const timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._centeringDelay, () => {
|
||||||
this._centeringDelay, () => {
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
const idx = this._centerTimeoutIds.indexOf(id);
|
if (index > -1) {
|
||||||
if (idx>-1) this._centerTimeoutIds.splice(idx,1);
|
this._centerTimeoutIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
if (win.get_maximized())
|
if (win.get_maximized()) {
|
||||||
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
}
|
||||||
const monitorIndex = win.get_monitor();
|
const monitorIndex = win.get_monitor();
|
||||||
const workArea = getWorkAreaForMonitor(monitorIndex);
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(
|
||||||
const frame = win.get_frame_rect();
|
monitorIndex
|
||||||
win.move_frame(true,
|
);
|
||||||
workArea.x + Math.floor((workArea.width - frame.width )/2),
|
const frame = win.get_frame_rect();
|
||||||
workArea.y + Math.floor((workArea.height - frame.height)/2));
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y + Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
if (win.get_display()) {
|
if (win.get_display()) {
|
||||||
if (typeof win.set_keep_above === 'function')
|
if (typeof win.set_keep_above === "function")
|
||||||
win.set_keep_above(true);
|
win.set_keep_above(true);
|
||||||
else if (typeof win.make_above === 'function')
|
else if (typeof win.make_above === "function")
|
||||||
win.make_above();
|
win.make_above();
|
||||||
}
|
}
|
||||||
return GLib.SOURCE_REMOVE;
|
return GLib.SOURCE_REMOVE;
|
||||||
});
|
});
|
||||||
return GLib.SOURCE_REMOVE;
|
return GLib.SOURCE_REMOVE;
|
||||||
});
|
});
|
||||||
this._centerTimeoutIds.push(id);
|
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWindowMinimizedStateChanged(){ this.queueTile(); }
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
_onWindowAdded(workspace, win) {
|
_onWindowAdded(workspace, win) {
|
||||||
if (this.windows.includes(win)) return;
|
if (this.windows.includes(win)) return;
|
||||||
|
|
||||||
if (this._isException(win)) { this._centerWindow(win); return; }
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._isTileable(win)) {
|
if (this._isTileable(win)) {
|
||||||
if (this._settings.get_string('new-window-behavior') === 'master')
|
if (this._settings.get_string("new-window-behavior") === "master") {
|
||||||
this.windows.unshift(win);
|
this.windows.unshift(win);
|
||||||
else
|
} else {
|
||||||
this.windows.push(win);
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
|
||||||
const id = win.get_id();
|
const id = win.get_id();
|
||||||
this._signalIds.set(`unmanaged-${id}`, {
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
object: win, id: win.connect('unmanaged',
|
object: win,
|
||||||
()=>this._onWindowRemoved(null, win))});
|
id: win.connect("unmanaged", () =>
|
||||||
this._signalIds.set(`size-${id}`, {
|
this._onWindowRemoved(null, win)
|
||||||
object: win, id: win.connect('size-changed',
|
),
|
||||||
()=>{ if (!this.grabbedWindow) this.queueTile(); })});
|
});
|
||||||
this._signalIds.set(`min-${id}`, {
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
object: win, id: win.connect('notify::minimized',
|
object: win,
|
||||||
this._onWindowMinimizedStateChanged)});
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect(
|
||||||
|
"notify::minimized",
|
||||||
|
this._onWindowMinimizedStateChanged
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWindowRemoved(workspace, win) {
|
_onWindowRemoved(workspace, win) {
|
||||||
const idx = this.windows.indexOf(win);
|
const index = this.windows.indexOf(win);
|
||||||
if (idx>-1) this.windows.splice(idx,1);
|
if (index > -1) {
|
||||||
|
this.windows.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
['unmanaged','size','min'].forEach(pref=>{
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
const key = `${pref}-${win.get_id()}`;
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
if (this._signalIds.has(key)) {
|
if (this._signalIds.has(key)) {
|
||||||
const {object,id} = this._signalIds.get(key);
|
const { object, id } = this._signalIds.get(key);
|
||||||
try{ object.disconnect(id);}catch{}
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
this._signalIds.delete(key);
|
this._signalIds.delete(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -445,21 +511,31 @@ class Tiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_connectToWorkspace() {
|
_connectToWorkspace() {
|
||||||
const ws = global.workspace_manager.get_active_workspace();
|
const workspace = global.workspace_manager.get_active_workspace();
|
||||||
ws.list_windows().forEach(w=>this._onWindowAdded(ws,w));
|
workspace
|
||||||
this._signalIds.set('win-add', {
|
.list_windows()
|
||||||
object: ws, id: ws.connect('window-added', this._onWindowAdded)});
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
this._signalIds.set('win-rem', {
|
this._signalIds.set("window-added", {
|
||||||
object: ws, id: ws.connect('window-removed', this._onWindowRemoved)});
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", this._onWindowAdded),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", this._onWindowRemoved),
|
||||||
|
});
|
||||||
this.queueTile();
|
this.queueTile();
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnectFromWorkspace() {
|
_disconnectFromWorkspace() {
|
||||||
this.windows.slice().forEach(w=>this._onWindowRemoved(null,w));
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
['win-add','win-rem'].forEach(k=>{
|
|
||||||
if (this._signalIds.has(k)) {
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
const {object,id}=this._signalIds.get(k);
|
if (this._signalIds.has(key)) {
|
||||||
try{ object.disconnect(id);}catch{}
|
const { object, id } = this._signalIds.get(key);
|
||||||
this._signalIds.delete(k);
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -467,88 +543,135 @@ class Tiler {
|
|||||||
queueTile() {
|
queueTile() {
|
||||||
if (this._tileInProgress || this._tileTimeoutId) return;
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
this._tileInProgress = true;
|
this._tileInProgress = true;
|
||||||
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
|
|
||||||
this._tilingDelay, () => {
|
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._tilingDelay, () => {
|
||||||
this._tileWindows();
|
this._tileWindows();
|
||||||
this._tileInProgress = false;
|
this._tileInProgress = false;
|
||||||
this._tileTimeoutId = null;
|
this._tileTimeoutId = null;
|
||||||
return GLib.SOURCE_REMOVE;
|
return GLib.SOURCE_REMOVE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
tileNow() { if (!this._tileInProgress) this._tileWindows(); }
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_splitLayout(windows, area) {
|
_splitLayout(windows, area) {
|
||||||
if (!windows.length) return;
|
if (windows.length === 0) return;
|
||||||
if (windows.length === 1) {
|
if (windows.length === 1) {
|
||||||
windows[0].move_resize_frame(true,
|
windows[0].move_resize_frame(
|
||||||
area.x, area.y, area.width, area.height);
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gap = Math.floor(this._innerGap/2);
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
const prim = [windows[0]];
|
const primaryWindows = [windows[0]];
|
||||||
const sec = windows.slice(1);
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
|
||||||
let primArea, secArea;
|
|
||||||
if (area.width > area.height) {
|
if (area.width > area.height) {
|
||||||
const pW = Math.floor(area.width/2) - gap;
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
primArea = {x: area.x, y: area.y,
|
primaryArea = {
|
||||||
width: pW, height: area.height};
|
x: area.x,
|
||||||
secArea = {x: area.x+pW+this._innerGap, y: area.y,
|
y: area.y,
|
||||||
width: area.width-pW-this._innerGap,
|
width: primaryWidth,
|
||||||
height: area.height};
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const pH = Math.floor(area.height/2) - gap;
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
primArea = {x: area.x, y: area.y,
|
primaryArea = {
|
||||||
width: area.width, height: pH};
|
x: area.x,
|
||||||
secArea = {x: area.x, y: area.y+pH+this._innerGap,
|
y: area.y,
|
||||||
width: area.width,
|
width: area.width,
|
||||||
height: area.height-pH-this._innerGap};
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
this._splitLayout(prim, primArea);
|
|
||||||
this._splitLayout(sec, secArea);
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
_tileWindows() {
|
_tileWindows() {
|
||||||
const wins = this.windows.filter(w=>!w.minimized);
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
if (!wins.length) return;
|
if (windowsToTile.length === 0) return;
|
||||||
|
|
||||||
const monitor = Main.layoutManager.primaryMonitor;
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
const work = getWorkAreaForMonitor(monitor.index);
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(
|
||||||
const inner = { x: work.x + this._outerGapHorizontal,
|
monitor.index
|
||||||
y: work.y + this._outerGapVertical,
|
);
|
||||||
width: work.width - 2*this._outerGapHorizontal,
|
const innerArea = {
|
||||||
height: work.height - 2*this._outerGapVertical };
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
|
||||||
wins.forEach(w=>{ if (w.get_maximized())
|
windowsToTile.forEach((win) => {
|
||||||
w.unmaximize(Meta.MaximizeFlags.BOTH); });
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
|
||||||
if (wins.length===1) {
|
if (windowsToTile.length === 1) {
|
||||||
wins[0].move_resize_frame(true,
|
windowsToTile[0].move_resize_frame(
|
||||||
inner.x, inner.y, inner.width, inner.height);
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gap = Math.floor(this._innerGap/2);
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
const masterW = Math.floor(inner.width/2) - gap;
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
const master = wins[0];
|
const master = windowsToTile[0];
|
||||||
master.move_resize_frame(true,
|
master.move_resize_frame(
|
||||||
inner.x, inner.y, masterW, inner.height);
|
true,
|
||||||
|
innerArea.x,
|
||||||
const stack = { x: inner.x + masterW + this._innerGap,
|
innerArea.y,
|
||||||
y: inner.y,
|
masterWidth,
|
||||||
width: inner.width - masterW - this._innerGap,
|
innerArea.height
|
||||||
height: inner.height };
|
);
|
||||||
this._splitLayout(wins.slice(1), stack);
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
// --- EXTENSION-WRAPPER (for legacy loader) ---
|
||||||
class SimpleTilingExtension {
|
var LegacyExtension = class {
|
||||||
enable() { this.tiler = new Tiler(); this.tiler.enable(); }
|
constructor(metadata) {
|
||||||
disable() { this.tiler?.disable(); this.tiler = null; }
|
this.tiler = null;
|
||||||
}
|
}
|
||||||
|
enable() {
|
||||||
function init() {
|
this.tiler = new Tiler();
|
||||||
return new SimpleTilingExtension();
|
this.tiler.enable();
|
||||||
}
|
}
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user