mirror of
https://github.com/zoriya/fairy.git
synced 2025-12-06 05:36:09 +00:00
364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const Meta = imports.gi.Meta;
|
|
const GLib = imports.gi.GLib;
|
|
const GObject = imports.gi.GObject;
|
|
const Main = imports.ui.main;
|
|
const Mainloop = imports.mainloop;
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
const Me = ExtensionUtils.getCurrentExtension();
|
|
|
|
var Renderer = GObject.registerClass(
|
|
class Renderer extends GObject.Object {
|
|
_init(state, settings) {
|
|
super._init();
|
|
this._state = state;
|
|
this._settings = settings;
|
|
this.gaps = {
|
|
smart: true,
|
|
size: 10,
|
|
outerGaps: 20,
|
|
};
|
|
log("fairy init!");
|
|
}
|
|
|
|
disable() {
|
|
this._removeSignals();
|
|
}
|
|
|
|
enable() {
|
|
this._bindSignals();
|
|
|
|
this.gaps = {
|
|
smart: this._settings.get_boolean("smart-gaps"),
|
|
size: this._settings.get_uint("gap-size"),
|
|
outerGaps: this._settings.get_uint("outer-gap-size"),
|
|
};
|
|
|
|
for (const window of global.display.list_all_windows())
|
|
this.trackWindow(window);
|
|
|
|
const workspace = global.display
|
|
.get_workspace_manager()
|
|
.get_active_workspace_index();
|
|
const tags = 0b1 << workspace;
|
|
if (Meta.prefs_get_workspaces_only_on_primary()) {
|
|
this._state.monitors[global.display.get_primary_monitor()].tags = tags;
|
|
} else {
|
|
for (let i = 0; i < this._state.monitors.length; i++)
|
|
this._state.monitors[i].tags = tags;
|
|
}
|
|
|
|
this.renderAll();
|
|
}
|
|
|
|
/**
|
|
* @params {Meta.Window} handle
|
|
* @returns {boolean}
|
|
*/
|
|
_isValidWindow(handle) {
|
|
// Check if we marked the window as invalid before (unmanaging call).
|
|
if (handle._isInvalid) return false;
|
|
|
|
let windowType = handle.get_window_type();
|
|
return (
|
|
windowType === Meta.WindowType.NORMAL ||
|
|
windowType === Meta.WindowType.MODAL_DIALOG ||
|
|
windowType === Meta.WindowType.DIALOG
|
|
);
|
|
}
|
|
|
|
// Stolen from https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/183
|
|
// Trying to move a newly-created window without using this (or waiting an arbitrary delay) crashes the gnome-shell.
|
|
// @window: the metaWindow to wait for
|
|
// @cb: the callback function called when the window is ready
|
|
//
|
|
// Waits until the actor of a metaWindow is available, it has
|
|
// an allocation and a valid gtk-application-id.
|
|
_waitForWindow(window, cb) {
|
|
let windowActor;
|
|
|
|
// Wait until window actor is available
|
|
let waitForWindowId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
|
|
// Stop if the window doesn't exist anymore or the parent is gone
|
|
if (!window.get_workspace()) return GLib.SOURCE_REMOVE;
|
|
|
|
// Continue if there's no actor available yet
|
|
if (!windowActor) {
|
|
if (!window.get_compositor_private()) return GLib.SOURCE_CONTINUE;
|
|
|
|
windowActor = window.get_compositor_private();
|
|
}
|
|
|
|
// Continue if the window is not allocated yet
|
|
if (windowActor.visible && !windowActor.has_allocation())
|
|
return GLib.SOURCE_CONTINUE;
|
|
|
|
// HACK: Still runing the callback on a timeout because the window is now setup but not placed yet.
|
|
// FIXME: Find a proper way to wait for this, I know forge is using a 220ms queue.
|
|
Mainloop.timeout_add(100, cb);
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
GLib.Source.set_name_by_id(
|
|
waitForWindowId,
|
|
"[gnome-shell] waitForWindow"
|
|
);
|
|
}
|
|
|
|
_removeSignals() {
|
|
for (const signal of this._displaySignals) {
|
|
global.display.disconnect(signal);
|
|
}
|
|
this._displaySignals = undefined;
|
|
|
|
for (const signal of this._workspaceSignals) {
|
|
global.workspace_manager.disconnect(signal);
|
|
}
|
|
this._wmSignals = undefined;
|
|
|
|
for (const window of this._state.windows) {
|
|
if (window._signals) {
|
|
for (const signal of window._signals) window.disconnect(signal);
|
|
}
|
|
let actor = window.handle.get_compositor_private();
|
|
if (actor && actor._signals) {
|
|
for (const signal of actor._signals) actor.disconnect(signal);
|
|
actor._signals = [];
|
|
}
|
|
}
|
|
this._state.windows = [];
|
|
|
|
this._settings.disconnect("changed");
|
|
}
|
|
|
|
_bindSignals() {
|
|
this._displaySignals = [
|
|
global.display.connect("window-created", (_display, window) =>
|
|
this._waitForWindow(window, () => {
|
|
this.trackWindow(window);
|
|
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
|
|
this._state.focus(window);
|
|
// Do not retrigger this idle.
|
|
return false;
|
|
});
|
|
this.renderForWindow(window);
|
|
})
|
|
),
|
|
// global.display.connect("window-entered-monitor", (_display, monitor, window) => {
|
|
//
|
|
// }),
|
|
];
|
|
this._workspaceSignals = [
|
|
global.workspace_manager.connect("active-workspace-changed", () => {
|
|
// Convert gnome workspaces to fairy's tags
|
|
const workspace = global.display
|
|
.get_workspace_manager()
|
|
.get_active_workspace_index();
|
|
const tags = 0b1 << workspace;
|
|
log("Switch to tags", tags);
|
|
if (Meta.prefs_get_workspaces_only_on_primary()) {
|
|
const primaryMon = global.display.get_primary_monitor();
|
|
this.setTags(primaryMon, tags);
|
|
} else {
|
|
for (let i = 0; i < this._state.monitors.length; i++) {
|
|
this._state.monitors[i].tags = tags;
|
|
this.render(i);
|
|
}
|
|
}
|
|
}),
|
|
];
|
|
|
|
this._settings.connect("changed", (_, key) => {
|
|
log("Proprety changed", key);
|
|
switch (key) {
|
|
case "gap-size":
|
|
this.gaps.size = this._settings.get_uint(key);
|
|
this.renderAll();
|
|
break;
|
|
case "outer-gap-size":
|
|
this.gaps.outerGaps = this._settings.get_uint(key);
|
|
this.renderAll();
|
|
break;
|
|
case "smart-gaps":
|
|
this.gaps.smart = this._settings.get_boolean(key);
|
|
this.renderAll();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {Meta.Window} handle
|
|
*/
|
|
trackWindow(handle) {
|
|
if (!this._isValidWindow(handle)) return;
|
|
// Add window signals
|
|
handle._signals = [
|
|
handle.connect("unmanaging", (handle) => {
|
|
handle._isInvalid = true;
|
|
const idx = this._state.workIndexByHandle(handle);
|
|
const faWindow = this._state.popByHandle(handle);
|
|
if (!faWindow) return;
|
|
|
|
const tags = this._state.monitors[faWindow.monitor].tags;
|
|
// Since we retrieved the idx, the window as been removed so we don't need to +1.
|
|
const newWindow = this._state.workIndex(faWindow.monitor, tags, idx);
|
|
if (newWindow) this._state.focus(newWindow.handle);
|
|
|
|
this.render(faWindow.monitor);
|
|
}),
|
|
handle.connect("workspace-changed", (handle) => {
|
|
log("Workspace changed for window");
|
|
if (handle._ignoreWorkspaceChange) {
|
|
handle._ignoreWorkspaceChange = false;
|
|
return;
|
|
}
|
|
if (!this._isValidWindow(handle)) return;
|
|
const [oldW, newW] = this._state.updateByHandle(handle);
|
|
if (oldW) this.render(oldW.monitor);
|
|
if (newW) this.render(newW.monitor);
|
|
}),
|
|
handle.connect("focus", (handle) => {
|
|
if (!this._isValidWindow(handle)) return;
|
|
this._state.monitors[handle.get_monitor()].focused = handle;
|
|
}),
|
|
];
|
|
|
|
this._state.newWindow(handle);
|
|
}
|
|
|
|
setTags(mon, tags) {
|
|
const currTags = this._state.monitors[mon].tags;
|
|
this._state.monitors[mon].tags = tags;
|
|
this._setGWorkspaceIfNeeded(mon);
|
|
|
|
for (let i = 0; i < this._state.monitors.length; i++) {
|
|
if (this._state.monitors[i] & tags && mon !== i) {
|
|
// Remove the selected tag from other monitors.
|
|
// If the other monitor had only this tag, swap monitor's tags instead.
|
|
this._state.monitors[i] = this._state.monitors[i] & ~tags || currTags;
|
|
this._setGWorkspaceIfNeeded(i);
|
|
this.render(i);
|
|
}
|
|
}
|
|
|
|
this.render(mon);
|
|
}
|
|
|
|
_setGWorkspaceIfNeeded(mon) {
|
|
if (mon !== global.display.get_primary_monitor()) return;
|
|
|
|
const tags = this._state.monitors[mon].tags;
|
|
// This retrieve the lower tag present in the tags set.
|
|
const tag = tags & ~(tags - 1);
|
|
if (tags !== tag) return;
|
|
// Retrieve the gnome workspace for the tag (inverse of 0b1 << tag)
|
|
const workspace = Math.log2(tag);
|
|
console.log("Switching to", tags, tag, workspace);
|
|
|
|
global.display
|
|
.get_workspace_manager()
|
|
.get_workspace_by_index(workspace)
|
|
.activate(global.display.get_current_time());
|
|
}
|
|
|
|
renderAll() {
|
|
const monN = global.display.get_n_monitors();
|
|
for (let mon = 0; mon < monN; mon++) {
|
|
this.render(mon);
|
|
}
|
|
}
|
|
|
|
renderForWindow(window) {
|
|
const mon = window.get_monitor();
|
|
this.render(mon);
|
|
}
|
|
|
|
/**
|
|
* @param {number} mon
|
|
*/
|
|
render(mon) {
|
|
const tags = this._state.monitors[mon].tags;
|
|
|
|
// We don't care which workspace it is, we just want the geometry
|
|
// for the current monitor without the panel.
|
|
const monGeo = global.display
|
|
.get_workspace_manager()
|
|
.get_active_workspace()
|
|
.get_work_area_for_monitor(mon);
|
|
const workIdx = global.display
|
|
.get_workspace_manager()
|
|
.get_active_workspace_index();
|
|
|
|
const windows = this._state.render(mon, tags);
|
|
for (const window of windows) {
|
|
if (window.handle.get_monitor() !== mon)
|
|
window.handle.move_to_monitor(mon);
|
|
if (window.handle.get_workspace().index() !== workIdx) {
|
|
// The window is visible because another tag as been bringed
|
|
// so we need to ask gnome to move windows (temporarly to the current workspace)
|
|
log("Invalid workspace", window.tags, 0b1 << workIdx);
|
|
window.handle._ignoreWorkspaceChange = true;
|
|
window.handle.change_workspace_by_index(workIdx, true);
|
|
}
|
|
|
|
if (window.floating) continue;
|
|
|
|
if (window.handle.minimized !== window.minimized) {
|
|
if (window.minimized) window.handle.minimize();
|
|
else window.handle.unminimize();
|
|
}
|
|
if (
|
|
window.handle["maximized-vertically"] !=
|
|
window.handle["maximized-horizontally"] ||
|
|
window.handle["maximized-vertically"] != window.maximized
|
|
) {
|
|
if (window.maximized) {
|
|
window.handle.maximize(Meta.MaximizeFlags.BOTH);
|
|
// Do not resize if maximizing to keep the overview tiled.
|
|
continue;
|
|
} else window.handle.unmaximize(Meta.MaximizeFlags.BOTH);
|
|
}
|
|
|
|
let size = {
|
|
x: (window.x * monGeo.width) / 100,
|
|
y: (window.y * monGeo.height) / 100,
|
|
width: (window.width * monGeo.width) / 100,
|
|
height: (window.height * monGeo.height) / 100,
|
|
};
|
|
|
|
if (this._state.monitors[mon].layout !== "monocle" && (windows.length > 1 || !this.gaps.smart))
|
|
size = this.addGaps(size, monGeo);
|
|
|
|
window.handle.move_resize_frame(
|
|
true,
|
|
monGeo.x + size.x,
|
|
monGeo.y + size.y,
|
|
size.width,
|
|
size.height
|
|
);
|
|
}
|
|
}
|
|
|
|
addGaps(window, monGeo) {
|
|
const gapSize = this.gaps.size;
|
|
// Inner gaps are applied two times so to make outers the same visual size we 2x
|
|
const outerGaps = this.gaps.outerGaps * 2;
|
|
|
|
return {
|
|
...window,
|
|
x: window.x === 0 ? outerGaps : window.x + gapSize,
|
|
y: window.y === 0 ? outerGaps : window.y + gapSize,
|
|
width:
|
|
window.width -
|
|
(window.x === 0 ? outerGaps : gapSize) -
|
|
(window.x + window.width === monGeo.width ? outerGaps : gapSize),
|
|
height:
|
|
window.height -
|
|
(window.y === 0 ? outerGaps : gapSize) -
|
|
(window.y + window.height === monGeo.height ? outerGaps : gapSize),
|
|
};
|
|
}
|
|
}
|
|
);
|