197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
import Meta from 'gi://Meta';
|
|
import Clutter from "gi://Clutter";
|
|
import {IWindowManager} from "./windowManager.js";
|
|
import {Logger} from "../utils/logger.js";
|
|
import {Rect} from "../utils/rect.js";
|
|
import WindowContainer from "./container.js";
|
|
import queueEvent from "../utils/events.js";
|
|
|
|
|
|
type WindowMinimizedHandler = (window: WindowWrapper) => void;
|
|
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
|
|
|
|
export class WindowWrapper {
|
|
private static readonly RESIZE_TOLERANCE = 2;
|
|
|
|
readonly _window: Meta.Window;
|
|
readonly _windowMinimizedHandler: WindowMinimizedHandler;
|
|
readonly _signals: number[] = [];
|
|
_parent: WindowContainer | null = null;
|
|
_dragging: boolean = false;
|
|
|
|
constructor(
|
|
window: Meta.Window,
|
|
winMinimized: WindowMinimizedHandler
|
|
) {
|
|
this._window = window;
|
|
this._windowMinimizedHandler = winMinimized;
|
|
}
|
|
|
|
getWindow(): Meta.Window {
|
|
return this._window;
|
|
}
|
|
|
|
getWindowId(): number {
|
|
return this._window.get_id();
|
|
}
|
|
|
|
getWorkspace(): number {
|
|
return this._window.get_workspace().index();
|
|
}
|
|
|
|
getMonitor(): number {
|
|
return this._window.get_monitor();
|
|
}
|
|
|
|
getRect(): Rect {
|
|
return this._window.get_frame_rect();
|
|
}
|
|
|
|
isFullscreen(): boolean {
|
|
return this._window.is_fullscreen();
|
|
}
|
|
|
|
getTabLabel(): string {
|
|
const rawAppName = this._window.get_wm_class() ?? '';
|
|
// Strip reverse-domain prefix (e.g. "org.gnome.Nautilus" -> "Nautilus")
|
|
const lastName = rawAppName.includes('.')
|
|
? (rawAppName.split('.').pop() ?? rawAppName)
|
|
: rawAppName;
|
|
// Capitalize first letter
|
|
const appName = lastName.charAt(0).toUpperCase() + lastName.slice(1);
|
|
const title = this._window.get_title() ?? 'Untitled';
|
|
if (appName && appName.toLowerCase() !== title.toLowerCase()) {
|
|
return `${appName} | ${title}`;
|
|
}
|
|
return title;
|
|
}
|
|
|
|
hideWindow(): void {
|
|
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
|
|
if (actor) actor.hide();
|
|
}
|
|
|
|
showWindow(): void {
|
|
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
|
|
if (actor) actor.show();
|
|
}
|
|
|
|
startDragging(): void {
|
|
this._dragging = true;
|
|
}
|
|
|
|
stopDragging(): void {
|
|
Logger.log("STOPPED DRAGGING")
|
|
this._dragging = false;
|
|
}
|
|
|
|
connectWindowSignals(windowManager: IWindowManager): void {
|
|
const windowId = this._window.get_id();
|
|
this._signals.push(
|
|
this._window.connect('unmanaging', () => {
|
|
Logger.log("REMOVING WINDOW", windowId);
|
|
windowManager.handleWindowClosed(this);
|
|
}),
|
|
this._window.connect('notify::minimized', () => {
|
|
if (this._window.minimized) {
|
|
Logger.log(`Window minimized: ${windowId}`);
|
|
windowManager.handleWindowMinimized(this);
|
|
} else {
|
|
Logger.log(`Window unminimized: ${windowId}`);
|
|
windowManager.handleWindowUnminimized(this);
|
|
}
|
|
}),
|
|
this._window.connect('notify::maximized-horizontally', () => {
|
|
if (this._window.is_maximized()) {
|
|
Logger.log(`Window maximized: ${windowId}`);
|
|
} else {
|
|
Logger.log(`Window unmaximized: ${windowId}`);
|
|
}
|
|
}),
|
|
this._window.connect("workspace-changed", () => {
|
|
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
|
|
windowManager.handleWindowChangedWorkspace(this);
|
|
}),
|
|
this._window.connect("position-changed", () => {
|
|
windowManager.handleWindowPositionChanged(this);
|
|
}),
|
|
this._window.connect("size-changed", () => {
|
|
windowManager.handleWindowPositionChanged(this);
|
|
}),
|
|
this._window.connect('notify::title', () => {
|
|
windowManager.handleWindowTitleChanged(this);
|
|
}),
|
|
);
|
|
}
|
|
|
|
disconnectWindowSignals(): void {
|
|
if (this._signals) {
|
|
this._signals.forEach(signal => {
|
|
try {
|
|
if (this._window != null) {
|
|
this._window.disconnect(signal);
|
|
}
|
|
} catch (e) {
|
|
Logger.warn("error disconnecting signal", signal, e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
safelyResizeWindow(rect: Rect, _retry: number = 3): void {
|
|
if (this._dragging) {
|
|
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED");
|
|
return;
|
|
}
|
|
|
|
if (this.isFullscreen()) {
|
|
Logger.info("STOPPED RESIZE BECAUSE WINDOW IS FULLSCREEN");
|
|
return;
|
|
}
|
|
|
|
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
|
|
if (!actor) {
|
|
Logger.log("No actor available, can't resize safely yet");
|
|
return;
|
|
}
|
|
|
|
actor.remove_all_transitions();
|
|
|
|
// Move first to guarantee the window reaches the correct position even
|
|
// if the subsequent resize is clamped by minimum-size hints.
|
|
this._window.move_frame(true, rect.x, rect.y);
|
|
this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height);
|
|
|
|
const new_rect = this._window.get_frame_rect();
|
|
const mismatch =
|
|
Math.abs(new_rect.x - rect.x) > WindowWrapper.RESIZE_TOLERANCE ||
|
|
Math.abs(new_rect.y - rect.y) > WindowWrapper.RESIZE_TOLERANCE ||
|
|
Math.abs(new_rect.width - rect.width) > WindowWrapper.RESIZE_TOLERANCE ||
|
|
Math.abs(new_rect.height - rect.height) > WindowWrapper.RESIZE_TOLERANCE;
|
|
|
|
if (_retry > 0 && mismatch) {
|
|
// If the window's actual size is larger than requested, it has a
|
|
// minimum-size constraint — retrying won't help. Just make sure
|
|
// it's at the correct position with its actual size.
|
|
const sizeConstrained =
|
|
new_rect.width > rect.width + WindowWrapper.RESIZE_TOLERANCE ||
|
|
new_rect.height > rect.height + WindowWrapper.RESIZE_TOLERANCE;
|
|
|
|
if (sizeConstrained) {
|
|
Logger.info("Window has min-size constraint, accepting actual size",
|
|
`want(${rect.x},${rect.y},${rect.width},${rect.height})`,
|
|
`actual(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
|
|
this._window.move_frame(true, rect.x, rect.y);
|
|
} else {
|
|
Logger.warn("RESIZE MISMATCH, retrying",
|
|
`want(${rect.x},${rect.y},${rect.width},${rect.height})`,
|
|
`got(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
|
|
queueEvent({
|
|
name: `delayed_resize_${this.getWindowId()}`,
|
|
callback: () => this.safelyResizeWindow(rect, _retry - 1),
|
|
}, 50);
|
|
}
|
|
}
|
|
}
|
|
}
|