Files
system-taskbar/ding/emulateX11WindowType.js

840 lines
27 KiB
JavaScript

/* Emulate X11WindowType
*
* Copyright (C) 2022 Sundeep Mediratta (smedius@gmail.com)
* Copyright (C) 2020 Sergio Costas (rastersoft@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* global global */
/* exported EmulateX11WindowType */
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import Meta from 'gi://Meta';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js';
import * as Utils from 'resource:///org/gnome/shell/misc/util.js';
export {EmulateX11WindowType};
const appID = 'com.desktop.ding';
const appPath = GLib.build_filenamev(['/', ...appID.split('.')]);
class ManageWindow {
/* This class is added to each managed window, and it's used to make it
behave like an X11 Desktop window.
Trusted windows will set in the title the characters @!, followed by
the coordinates where to put the window separated by a colon, and
ended in a semicolon. After that, it can have one or more of these
letters:
* B : put and always keep this window at the bottom of the stack of
windows on screen
* T : put and always keep this window at the top of the stack of
windows on the screen
* D : show this window in all desktops
* H : hide this window from the window list
Using the title is generally not a problem because the desktop windows
do not have a title. But some other windows may have and still need to
set a title and use this class, so adding a single blank space at the
end of the title is equivalent to @!H, and having two blank spaces at
the end of the title is equivalent to @!HTD. This allows use of these
flags for decorated or titled windows.
*/
constructor(window, waylandClient, changedStatusCB) {
this.isWayland = typeof Meta.is_wayland_compositor === 'function'
? Meta.is_wayland_compositor()
: true;
this._isX11 = !this.isWayland;
this._waylandClient = waylandClient;
this._window = window;
this._signalIDs = [];
this._onIdleChangedStatusCallback = changedStatusCB;
this._titleID = this._window.connect('notify::title', () => {
this.refreshProperties();
});
this._parseTitle();
this._attachControllers();
}
disconnect() {
this._disconnetSignalsAndTimeouts();
if (this._titleID)
this._window.disconnect(this._titleID);
this._titleID = 0;
if (this._keepAtTop)
this._window.unmake_above();
this._window = null;
this._waylandClient = null;
}
_disconnetSignalsAndTimeouts() {
for (let signalID of this._signalIDs) {
if (signalID)
this._window.disconnect(signalID);
}
this._signalIDs = [];
if (this._checkOnAllWorkspacesID)
GLib.source_remove(this._checkOnAllWorkspacesID);
this._checkOnAllWorkspacesID = 0;
if (this._moveIntoPlaceID)
GLib.source_remove(this._moveIntoPlaceID);
this._moveIntoPlaceID = 0;
if (this._restackedBottomID)
global.display.disconnect(this._restackedBottomID);
this._restackedBottomID = 0;
if (this._showDesktopID)
global.workspace_manager.disconnect(this._showDesktopID);
this._showDesktopID = 0;
if (this._restackedTopID)
global.display.disconnect(this._restackedTopID);
this._restackedTopID = 0;
}
set_wayland_client(client) {
this._waylandClient = client;
}
_parseTitle() {
this._x = null;
this._y = null;
this._keepAtBottom = false;
this._keepAtTop = false;
this._showInAllDesktops = false;
this._hideFromWindowList = false;
this._fixed = false;
this._desktopWindow = false;
let title = this._window.get_title();
if (!title && !!this._window.get_transient_for()) {
// Transient dialog window
// Does not have title, hide from windowlist
title = '@!H';
}
if (title !== null) {
if ((title.length > 0) && (title[title.length - 1] === ' ')) {
if ((title.length > 1) && (title[title.length - 2] === ' '))
title = '@!HTD';
else
title = '@!H';
}
let pos = title.search('@!');
if (pos !== -1) {
let pos2 = title.search(';', pos);
let coords;
if (pos2 !== -1)
coords = title.substring(pos + 2, pos2).trim().split(',');
else
coords = title.substring(pos + 2).trim().split(',');
try {
this._x = parseInt(coords[0]);
this._y = parseInt(coords[1]);
} catch (e) {
global.log(`Exception ${e.message}.\n${e.stack}`);
}
try {
let extraChars =
title.substring(pos + 2).trim().toUpperCase();
for (let char of extraChars) {
switch (char) {
case 'B':
this._keepAtBottom = true;
this._keepAtTop = false;
break;
case 'T':
this._keepAtTop = true;
this._keepAtBottom = false;
break;
case 'D':
this._showInAllDesktops = true;
break;
case 'H':
this._hideFromWindowList = true;
break;
case 'F':
this._fixed = true;
break;
}
}
this._desktopWindow =
this._keepAtBottom &&
!this._keepAtTop &&
this._showInAllDesktops &&
this._hideFromWindowList;
} catch (e) {
global.log(`Exception ${e.message}.\n${e.stack}`);
}
}
}
}
_attachControllers() {
if (this._fixed)
this._keepFixedWindowPosition();
if (this._hideFromWindowList)
this._keepWindowHidden();
else
this._unhideWindow();
if (this._keepAtTop)
this._keepWindowOnTop();
else if (this._window.above)
this._window.unmake_above();
if (this._keepAtBottom & !this._desktopWindow)
this._keepWindowAtBottom();
if (this._showInAllDesktops & !this._desktopWindow)
this._showWindowOnAllDesktops();
else if (this._window.on_all_workspaces)
this._window.unstick();
if (this._desktopWindow) {
if (typeof this._window.set_type === 'function') {
this._window.set_type(Meta.WindowType.DESKTOP);
console.log('Setting window type to desktop with Gnome 49 API');
// In future, Meta.WaylandClient.make_desktop(window) will not
// be necessary.
} else {
this._makeWindowTypeDesktop();
}
}
}
_keepFixedWindowPosition() {
this._signalIDs.push(
this._window.connect(
'position-changed',
() => {
if (this._fixed &&
(this._x !== null) &&
(this._y !== null)
) {
this._window.move_frame(true, this._x, this._y);
if (this._window.fullscreen)
this._window.unmake_fullscreen();
}
}
)
);
this._signalIDs.push(
this._window.connect('notify::minimized', () => {
this._window.unminimize();
})
);
this._signalIDs.push(
this._window.connect('notify::maximized-vertically',
() => {
if (typeof this._window.is_maximized === 'function' &&
!this._window.is_maximized()
)
this._window.maximize();
else if (!this._window.maximized_vertically)
this._window.maximize(Meta.MaximizeFlags.VERTICAL);
this._moveIntoPlace();
}
)
);
this._signalIDs.push(
this._window.connect('notify::maximized-horizontally',
() => {
if (typeof this._window.is_maximized === 'function' &&
!this._window.is_maximized()
)
this._window.maximize();
else if (!this._window.maximized_horizontally)
this._window.maximize(Meta.MaximizeFlags.HORIZONTAL);
this._moveIntoPlace();
}
)
);
if ((this._x !== null) && (this._y !== null))
this._window.move_frame(true, this._x, this._y);
}
_moveIntoPlace() {
if (this._moveIntoPlaceID)
GLib.source_remove(this._moveIntoPlaceID);
this._moveIntoPlaceID =
GLib.timeout_add(GLib.PRIORITY_LOW, 250, () => {
if (this._fixed && (this._x !== null) && (this._y !== null))
this._window.move_frame(true, this._x, this._y);
this._moveIntoPlaceID = 0;
return GLib.SOURCE_REMOVE;
});
}
_keepWindowHidden() {
if (!this._isX11 && this._waylandClient) {
this._waylandClient.hide_from_window_list(this._window);
} else {
const xid = this._window.xwindow;
this._setX11windowSkipTaskbar(xid);
}
}
_unhideWindow() {
if (!this._isX11 && this._waylandClient) {
this._waylandClient.show_in_window_list(this._window);
} else {
const xid = this._window.xwindow;
this._unSetX11windowSkipTaskbar(xid);
}
}
_keepWindowAtBottom() {
this._signalIDs.push(
this._window.connect(
'notify::above',
() => {
if (this._keepAtBottom && this._window.above)
this._window.unmake_above();
}
)
);
this._signalIDs.push(
this._window.connect_after(
'raised',
() => {
if (this._keepAtBottom)
this._window.lower();
}
)
);
/* If a window is lowered below us with shortcuts,
detect and fix DING window */
this._restackedBottomID = global.display.connect('restacked',
this._syncToBottomOfStack.bind(this)
);
/* If the desktop is shown with keyboard gnome shortcuts, detect and put
DING window back.
Seems to be needed for X11, works without on Wayland.
*/
if (this._isX11) {
this._showDesktopID =
global.workspace_manager.connect(
'showing-desktop-changed',
this._activateDesktopWindow.bind(this)
);
}
if (this._window.above)
this._window.unmake_above();
this._window.lower();
}
_keepWindowUnFullScreen() {
this._signalIDs.push(
this._window.connect(
'notify::fullscreen',
() => {
if (this._window.fullscreen)
this._window.unmake_fullscreen();
}
)
);
if (this._window.fullscreen)
this._window.unmake_fullscreen();
}
_activateDesktopWindow() {
if (this._desktopWindow)
this._window.activate(Meta.CURRENT_TIME);
}
_syncToBottomOfStack() {
let windows =
global.display
.get_tab_list(
Meta.TabList.NORMAL_ALL,
global.workspace_manager.get_active_workspace()
);
windows = global.display.sort_windows_by_stacking(windows);
if (windows.length > 1 && !windows[0].customJS_ding)
this._moveDesktopWindowToBottom();
}
_moveDesktopWindowToBottom() {
if (this._window.fullscreen)
this._window.unmake_fullscreen();
if (this._keepAtBottom)
this._window.lower();
}
_keepWindowOnTop() {
this._restackedTopID = global.display.connect('restacked', () => {
if (!this._window.above)
this._window.make_above();
});
if (!this._window.above)
this._window.make_above();
}
_showWindowOnAllDesktops() {
this._signalIDs.push(this._window.connect('notify::on-all-workspaces',
this._checkOnAllWorkspaces.bind(this)
));
this._signalIDs.push(this._window.connect('workspace-changed',
this._checkOnAllWorkspaces.bind(this)
));
this._window.stick();
}
_checkOnAllWorkspaces() {
if (this._checkOnAllWorkspacesID)
GLib.source_remove(this._checkOnAllWorkspacesID);
this._checkOnAllWorkspacesID =
GLib.idle_add(
GLib.PRIORITY_LOW,
() => {
if (this._showInAllDesktops &&
!this._window.on_all_workspaces
) {
this._window.stick();
this._onIdleActivateTopWindowOnActiveWorkspace();
}
this._checkOnAllWorkspacesID = null;
return GLib.SOURCE_REMOVE;
}
);
}
_makeWindowTypeDesktop() {
if (!this._isX11 && this._waylandClient) {
const desktopWindowTypeSetOnWindow =
this._waylandClient.make_desktop_window(this._window);
if (!desktopWindowTypeSetOnWindow) {
this._emulateDesktopWindow();
return;
}
} else {
const xid = this._window.xwindow;
try {
this._setX11windowTypeDesktop(xid);
} catch (e) {
logError(e);
this._emulateDesktopWindow();
return;
}
}
// Window manager bug - it treats request to resize window
// to monitor size as a fullscreen window request as well and makes
// the window fullscreen, more so for legacy X11 apps.
// This makes intellihide for docks/panels hide from desktop window
this._keepWindowUnFullScreen();
const activateTopWindowOnWorkspace = true;
this._onIdleChangedStatusCallback({activateTopWindowOnWorkspace});
}
_emulateDesktopWindow() {
console.log('Emulating window type Desktop');
this._window.get_window_type = function () {
return Meta.WindowType.DESKTOP;
};
this._keepWindowAtBottom();
this._showWindowOnAllDesktops();
const moveDesktopWindowToBottom = true;
const activateTopWindowOnWorkspace = true;
this._onIdleChangedStatusCallback(
{moveDesktopWindowToBottom, activateTopWindowOnWorkspace}
);
}
_onIdleActivateTopWindowOnActiveWorkspace() {
const activateTopWindowOnWorkspace = true;
this._onIdleChangedStatusCallback({activateTopWindowOnWorkspace});
}
_setX11windowSkipTaskbar(xid) {
// Unfortunately xprop can set only one of the properties in the state,
// not multiple.
// Stick to setting only skip-taskbar, we can otherwirse also set
// the property for pager, _NET_WM_STATE_SKIP_PAGER
const commandline = `xprop -id ${xid}` +
' -f _NET_WM_STATE 32a' +
' -set _NET_WM_STATE' +
' _NET_WM_STATE_SKIP_TASKBAR';
console.log('Making X11 windowtype type skip-taskbar');
Utils.spawnCommandLine(commandline);
}
_unSetX11windowSkipTaskbar(xid) {
const commandline = `xprop -id ${xid}` +
' -f _NET_WM_STATE 32a' +
' -remove _NET_WM_STATE' +
' _NET_WM_STATE_SKIP_TASKBAR';
console.log('Making X11 windowtype type NOT skip-taskbar');
Utils.spawnCommandLine(commandline);
}
_setX11windowTypeDesktop(xid) {
const commandline = `xprop -id ${xid}` +
' -f _NET_WM_WINDOW_TYPE 32a' +
' -set _NET_WM_WINDOW_TYPE' +
' _NET_WM_WINDOW_TYPE_DESKTOP';
console.log('Making X11 windowtype type Desktop');
Utils.trySpawnCommandLine(commandline);
}
refreshProperties() {
this._disconnetSignalsAndTimeouts();
this._parseTitle();
this._attachControllers();
}
get hideFromWindowList() {
return this._hideFromWindowList;
}
get keepAtBottom() {
return this._keepAtBottom;
}
get desktopWindow() {
return this._desktopWindow;
}
}
var EmulateX11WindowType = class {
/*
This class does all the heavy lifting for emulating WindowType.
Just make one instance of it, call enable(), and whenever a window
that you want to give "superpowers" is mapped, add it with the
"addWindowManagedCustomJS_ding" method. That's all.
*/
constructor() {
this._windowList = new Set();
this._overviewHiding = true;
this._waylandClient = null;
this.isWayland = typeof Meta.is_wayland_compositor === 'function'
? Meta.is_wayland_compositor()
: true;
this._isX11 = !this.isWayland;
}
set_wayland_client(client) {
this._waylandClient = client;
for (let window of this._windowList) {
if (window.customJS_ding)
window.customJS_ding.set_wayland_client(this._waylandClient);
}
}
enable() {
this._idMap =
global.window_manager.connect_after(
'map',
(obj, windowActor) => {
const window = windowActor.get_meta_window();
if (window.get_window_type() > Meta.WindowType.MODAL_DIALOG)
return;
const appid = window.get_gtk_application_id();
if (appid !== appID)
return;
const windowpid = window.get_pid();
const mypid = this._waylandClient
? parseInt(this._waylandClient.query_pid_of_program())
: null;
if (this._waylandClient &&
this._waylandClient.query_window_belongs_to(window)
) {
this._addWindowManagedCustomJS_ding(
window,
windowActor
);
return;
}
if (mypid !== null && windowpid === mypid) {
this._addWindowManagedCustomJS_ding(
window,
windowActor
);
}
}
);
/* But in Overview mode it is paramount to not change the workspace to
emulate "stick", or the windows will appear
*/
this._showingId = Main.overview.connect('showing', () => {
this._overviewHiding = false;
});
this._hidingId = Main.overview.connect('hiding', () => {
this._overviewHiding = true;
});
}
disable() {
if (this._activate_window_ID) {
GLib.source_remove(this._activate_window_ID);
this._activate_window_ID = null;
}
for (let window of this._windowList)
this._clearWindow(window);
this._windowList.clear();
// disconnect signals
if (this._idMap) {
global.window_manager.disconnect(this._idMap);
this._idMap = null;
}
if (this._idDestroy) {
global.window_manager.disconnect(this._idDestroy);
this._idDestroy = null;
}
if (this._showingId) {
Main.overview.disconnect(this._showingId);
this._showingId = null;
}
if (this._hidingId) {
Main.overview.disconnect(this._hidingId);
this._hidingId = null;
}
}
_addWindowManagedCustomJS_ding(window, windowActor) {
if (window.get_meta_window) { // it is a MetaWindowActor
window = window.get_meta_window();
}
if (this._windowList.has(window))
return;
window.customJS_ding =
new ManageWindow(
window,
this._waylandClient,
this.onIdleReStackActivteWindows.bind(this)
);
window.actor = windowActor;
windowActor._delegate = new HandleDragActors(windowActor);
this._windowList.add(window);
window.customJS_ding.unmanagedID =
window.connect(
'unmanaging',
win => {
this._clearWindow(win);
this._windowList.delete(window);
}
);
}
_clearWindow(window) {
window.disconnect(window.customJS_ding.unmanagedID);
window.customJS_ding.disconnect();
window.customJS_ding = null;
window.actor._delegate = null;
window.actor = null;
}
_activateTopWindowOnActiveWorkspace() {
let windows =
global.display
.get_tab_list(
Meta.TabList.NORMAL,
global.workspace_manager.get_active_workspace()
);
windows = global.display.sort_windows_by_stacking(windows);
if (windows.length) {
const topWindow = windows[windows.length - 1];
topWindow.focus(Clutter.CURRENT_TIME);
}
}
_moveDesktopWindowToBottom() {
for (let window of this._windowList)
window.customJS_ding._moveDesktopWindowToBottom();
}
onIdleReStackActivteWindows(action = {activateTopWindowOnWorkspace: true}) {
if (!this._activate_window_ID) {
this._activate_window_ID =
GLib.idle_add(
GLib.PRIORITY_LOW,
() => {
if (this._overviewHiding) {
if (action.moveDesktopWindowToBottom)
this._moveDesktopWindowToBottom();
if (action.activateTopWindowOnWorkspace)
this._activateTopWindowOnActiveWorkspace();
}
this._activate_window_ID = null;
return GLib.SOURCE_REMOVE;
}
);
}
}
// After shell unlock, window seems to lose stick property,
// refresh window properties
refreshWindows() {
for (let window of this._windowList)
window.customJS_ding.refreshProperties();
}
};
// Since Gnome Shell 48 the enumeration of the cursor is different
// the name has changed, althugh the value is the same;
// We use our own enumeration names to avoid problems with the version
// of the Gnome Shell, the enumeration integer points to the correct
// value in the Gnome Shell 48 and Meta 48 Enum and earlier.
// Using the wrong Enum Name seems to crash mutter
const ShellDropCursor = {
DEFAULT: 2, // META_CURSOR_DEFAULT Meta.Cursor.DEFAULT
NODROP: 15, // META_CURSOR_NO_DROP Meta.Cursor.DND_UNSUPPORTED_TARGET
COPY: 13, // META_CURSOR_COPY Meta.Cursor.DND_COPY
MOVE: 14, // META_CURSOR_MOVE Meta.Cursor.DND_MOVE
};
class HandleDragActors {
/* This class is added to each managed windowActor, and it's used to
make it behave like a shell Actor that can accept drops from
Gnome Shell dnd.
*/
constructor(windowActor) {
this.windowActor = windowActor;
this.remoteDingActions = Gio.DBusActionGroup.get(
Gio.DBus.session,
appID,
appPath
);
}
_getModifierKeys() {
let [, , state] = global.get_pointer();
state &= Clutter.ModifierType.MODIFIER_MASK;
this.isControl = (state & Clutter.ModifierType.CONTROL_MASK) !== 0;
this.isShift = (state & Clutter.ModifierType.SHIFT_MASK) !== 0;
}
handleDragOver(source) {
if ((source.app ?? null) === null)
return DND.DragMotionResult.NO_DROP;
this._getModifierKeys();
if (this.isShift) {
global.display.set_cursor(ShellDropCursor.COPY);
return DND.DragMotionResult.COPY_DROP;
}
if (this.isControl) {
global.display.set_cursor(ShellDropCursor.MOVE);
return DND.DragMotionResult.MOVE_DROP;
}
return DND.DragMotionResult.CONTINUE;
}
acceptDrop(source, actor, x, y) {
if ((source.app ?? null) === null)
return false;
let appFavorites = AppFavorites.getAppFavorites();
let sourceAppId = source.app.get_id();
let sourceAppPath = source.app.appInfo.get_filename();
let appIsFavorite = appFavorites.isFavorite(sourceAppId);
this._getModifierKeys();
if (appIsFavorite && !this.isShift)
appFavorites.removeFavorite(sourceAppId);
if (sourceAppPath && (this.isControl || this.isShift)) {
this.remoteDingActions.activate_action('createDesktopShortcut',
new GLib.Variant('a{sv}', {
uri: GLib.Variant.new_string(`file://${sourceAppPath}`),
X: new GLib.Variant('i', parseInt(x)),
Y: new GLib.Variant('i', parseInt(y)),
})
);
}
appFavorites.emit('changed');
return true;
}
}