Files
system-taskbar/ding/app/windowManager.js

608 lines
19 KiB
JavaScript

/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw/Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@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/>.
*/
import {
DesktopGrid
} from '../dependencies/localFiles.js';
import {Gio, GLib, DesktopWidgetCapability} from '../dependencies/gi.js';
export {WindowManager};
const WindowManager = class {
constructor(desktopManager, desktopList, asDesktop, primaryIndex) {
this._desktopManager = desktopManager;
this._Prefs = desktopManager.Prefs;
this.mainApp = desktopManager.mainApp;
this._desktopList = desktopList;
this._primaryIndex = primaryIndex;
if (primaryIndex < desktopList.length)
this._primaryScreen = desktopList[primaryIndex];
else
this._primaryScreen = null;
this._priorDesktopList = [];
this._desktops = [];
this._asDesktop = asDesktop;
this._zoom = 1;
this._primaryMonitorIndex = null;
this._priorPrimaryIndex = null;
this._priorPrimaryMonitorIndex = null;
this._differentZooms = false;
this._hidden = false;
this._gridWindowsUpdateInProgress = false;
this._pendingDesktopList = null;
this._registerWidgetLayerAction();
this._dbusAdvertiseUpdate();
}
_dbusAdvertiseUpdate() {
const updateGridWindows = new Gio.SimpleAction({
name: 'updateGridWindows',
parameter_type: new GLib.VariantType('av'),
});
updateGridWindows.connect('activate', (action, parameter) => {
this.updateGridWindows(parameter.recursiveUnpack())
.catch(e => logError(e));
});
this.mainApp.add_action(updateGridWindows);
const busObjectPath = this.mainApp.get_dbus_object_path();
const busName = this.mainApp.get_application_id();
const connection = Gio.DBus.session;
const signalName = 'updategeometry';
const signalXml = `
<node>
<interface name="${busName}">
<signal name="${signalName}">
<arg name="type" type="s"/>
<arg name="value" type="b"/>
</signal>
</interface>
</node>`;
this._dbusGeometryIface =
Gio.DBusExportedObject.wrapJSObject(signalXml, this);
this._dbusGeometryIface.export(
connection,
busObjectPath
);
}
requestGeometryUpdate() {
const variant = new GLib.Variant('(sb)', ['updategeometry', true]);
const busObjectPath = this.mainApp.get_dbus_object_path();
const busName = this.mainApp.get_application_id();
const connection = Gio.DBus.session;
const signalName = 'updategeometry';
connection.emit_signal(
null,
busObjectPath,
busName,
signalName,
variant
);
}
async updateGridWindows(newdesktoplist) {
if (this._gridWindowsUpdateInProgress) {
this._pendingDesktopList = newdesktoplist;
return;
}
const changeInfo =
this._computeDesktopChangeInfo(newdesktoplist);
const {
firstDesktop,
monitorCountChanged,
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
} = changeInfo;
// Allow initial startup if no desktops defined on initiation
if (firstDesktop) {
await this._handleFirstDesktop();
return;
}
// If any new monitors plugged in or removed
// by creating new desktops
if (monitorCountChanged) {
await this._handleMonitorCountChange();
return;
}
if (redisplay) {
await this._handleRedisplay({
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
});
}
}
async _handleFirstDesktop() {
this._desktopManager.clearAllLayersFromGrids();
await this.createGridWindows();
// sanity checks and icons placement on grid will be done by
// desktopManager in sync startup
}
async _handleMonitorCountChange() {
this._gridWindowsUpdateInProgress = true;
// monitor has been plugged in or removed.
this._desktopManager.clearAllLayersFromGrids();
await this.createGridWindows();
await this._desktopManager.applyDesktopLayoutChange({
redisplay: true,
monitorschanged: true,
gridschanged: true,
});
this._gridWindowsUpdateInProgress = false;
await this._drainPendingUpdates();
}
async _drainPendingUpdates() {
if (this._gridWindowsUpdateInProgress)
return;
const next = this._pendingDesktopList;
this._pendingDesktopList = null;
if (next != null)
await this.updateGridWindows(next);
this.queue_draw();
}
async _handleRedisplay({
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
}) {
if (!redisplay)
return;
this._gridWindowsUpdateInProgress = true;
await this._displayDesktopSnapShots();
this._desktopManager.clearAllLayersFromGrids();
this._desktops.forEach((desktop, index) => {
desktop.updateGridDescription(this._desktopList[index]);
if (monitorschangedList.includes(index)) {
desktop.resizeWindow();
desktop.resizeGrid();
} else if (gridschangedList.includes(index)) {
desktop.resizeGrid();
}
});
// There is a subtle difference here, all information is needed
//
// gridschanged implies prior grid information is available.
// Therefore write mode is 'PRESERVE' initially
//
// monitors changed implies that all coordintes are rewritten to the
// new monitor relative coordinates with a write mode of 'OVERWRITE'
//
// redisplay re-arranges all the icons on the new desktop monitor,
// essential for proper sorting/stacking of icons and arranging of
// icons
//
// For keep arranged new coordinates are automatically written to
// grid. However for stacked co-ordinates- we will neeed to redo the
// old coordinates seperately in do stacks with nonitorschanged info
await this._desktopManager.applyDesktopLayoutChange({
redisplay,
monitorschanged,
gridschanged,
});
// animate to the new margins and positions
// force a queue draw of all windows now that we have drawn the desktop,
// and poke mutter to map the meta window.
this._displayAnimationToLive();
this._gridWindowsUpdateInProgress = false;
await this._drainPendingUpdates();
}
_updatePrimaryStateAndZoomInfo(newdesktoplist) {
// Save prior primary state
this._priorPrimaryIndex = this._primaryIndex ?? null;
this._priorPrimaryMonitorIndex = this._primaryMonitorIndex ?? 0;
// Compute new primary index
let newPrimaryIndex;
if (newdesktoplist.length > 0 &&
('primaryMonitor' in newdesktoplist[0]))
newPrimaryIndex = newdesktoplist[0].primaryMonitor ?? null;
// Update primary index if changed
if (newPrimaryIndex !== this._priorPrimaryIndex)
this._primaryIndex = newPrimaryIndex;
// Find the new primary monitor
this._primaryScreen = this._desktopList[this._primaryIndex] ?? null;
this._primaryMonitorIndex = this._primaryScreen.monitorIndex ?? null;
// See if there are different zooms in the desktops
this._differentZooms = this._desktopList.some((d, index) => {
const nextd = this._desktopList[index + 1];
if (nextd != null)
return d.zoom !== nextd.zoom;
return false;
});
}
_computeDesktopChangeInfo(newDesktopList) {
const priorDesktopList = this._desktopList;
this._priorDesktopList = priorDesktopList;
this._desktopList = newDesktopList;
this._updatePrimaryStateAndZoomInfo(newDesktopList);
// Allow initial startup if no desktops defined on initiation
const firstDesktop =
priorDesktopList.some(d => typeof d !== 'object' || d == null) ||
priorDesktopList.length === 0;
const monitorCountChanged =
priorDesktopList.length !== newDesktopList.length;
const monitorschangedList = [];
const gridschangedList = [];
// If this is the first desktop, we don't need finer diffing;
if (firstDesktop) {
return {
firstDesktop,
monitorCountChanged,
monitorschangedList,
gridschangedList,
monitorschanged: false,
gridschanged: false,
redisplay: false,
};
}
// if no change in monitors, check if any change in monitor geometry
// or if any change in grid geometry
newDesktopList.forEach((area, index) => {
const area2 = priorDesktopList[index];
if (!area || !area2) {
// Monitor count changed; mark this index as changed and skip diff
monitorschangedList.push(index);
gridschangedList.push(index);
return;
}
if ((area.x !== area2.x) ||
(area.y !== area2.y) ||
(area.width !== area2.width) ||
(area.height !== area2.height) ||
(area.zoom !== area2.zoom) ||
(area.monitorIndex !== area2.monitorIndex)
) {
monitorschangedList.push(index);
gridschangedList.push(index);
return;
}
if ((area.marginTop !== area2.marginTop) ||
(area.marginBottom !== area2.marginBottom) ||
(area.marginLeft !== area2.marginLeft) ||
(area.marginRight !== area2.marginRight)
) {
if (!gridschangedList.includes(index))
gridschangedList.push(index);
}
});
const indexChanged =
this._priorPrimaryMonitorIndex !== this._primaryMonitorIndex;
// indexChanged implies monitors have changed
// monitors changed or index changed implies grids have changed
// as there may be other actors on the new monitor edge
const monitorschanged = !!monitorschangedList.length || indexChanged;
// only the grids have changed, no monitor changes
const gridschanged = gridschangedList.length
? gridschangedList.some(i => !monitorschangedList.includes(i))
: false;
// redisplay is needed for sorting and stacking. Icons
// need to be redisplayed if anything changes - the actual fileList
// has not changed
const redisplay = monitorschanged || gridschanged;
return {
firstDesktop,
monitorCountChanged,
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
};
}
async _displayDesktopSnapShots() {
const array = this._desktops.map(
d => d.displaySnapshot()
);
await Promise.all(array).catch(e => logError(e));
}
_displayAnimationToLive() {
this._desktops.forEach(d => d.requestAnimatedRelayout());
}
async createGridWindows() {
// Allow startup with no desktops from constructor
// even if no desktops are defined when started by the extension
// desktops can be defined later from updateGridWindows(), dbus
// activation
if (!this._desktopList.length ||
this._desktopList.some(d => {
return typeof d !== 'object' || d == null;
}))
return;
this._desktops.forEach(desktop => desktop.destroy());
this._desktops = [];
this._desktopList.forEach((desktop, desktopIndex) => {
const desktopName =
this._asDesktop
? `@!${desktop.x},${desktop.y};BDHF`
: `DING ${desktopIndex}`;
this._desktops.push(
new DesktopGrid.DesktopGrid({
desktopManager: this._desktopManager,
desktopName,
desktopDescription: desktop,
asDesktop: this._asDesktop,
hidden: this._hidden,
desktopIndex,
})
);
});
const displayPromises =
this._desktops.map(desktop => desktop.ensureMapped());
const allocatedPromises =
this._desktops.map(d => d.ensureAllocationComplete());
let safegaurd;
try {
safegaurd = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000,
() => {
throw new Error(
'Timeout while waiting for desktop windows to map'
);
}
);
await Promise.all(displayPromises);
await Promise.all(allocatedPromises);
} catch (e) {
logError(e);
// if the windows fail to map, we should still proceed
// and poke the desktop windows later.
this.show();
}
if (safegaurd)
GLib.source_remove(safegaurd);
if (this._desktopManager.windowsPromiseResolve)
this._desktopManager.windowsPromiseResolve(true);
}
hide() {
this._desktops.forEach(desktop => desktop.hide());
this._hidden = true;
}
show() {
this._hidden = false;
this._desktops.forEach(desktop => {
desktop.show();
desktop.set_visible(true);
});
}
queue_draw() {
this._desktops.forEach(desktop => desktop.queue_draw());
}
toggleVisibility() {
if (this._hidden)
this.show();
else
this.hide();
}
toggleWidgetLayers() {
if (!this._Prefs.showDesktopWidgets)
return;
this._desktops.forEach(desktop => desktop.toggleWidgetLayer());
}
lowerWidgetLayers() {
this._desktops.forEach(desktop => desktop.lowerWidgetContainer());
}
raiseWidgetLayers() {
this._desktops.forEach(desktop => desktop.raiseWidgetContainer());
}
_registerWidgetLayerAction() {
const action = new Gio.SimpleAction({name: 'toggleWidgetLayer'});
action.connect('activate', () => {
this.toggleWidgetLayers();
});
action.set_enabled(DesktopWidgetCapability);
this._desktopManager.mainApp.add_action(action);
const lowerAction = new Gio.SimpleAction({name: 'lowerWidgetLayer'});
lowerAction.connect('activate', () => {
this.lowerWidgetLayers();
});
lowerAction.set_enabled(DesktopWidgetCapability);
this._desktopManager.mainApp.add_action(lowerAction);
const raiseAction = new Gio.SimpleAction({name: 'raiseWidgetLayer'});
raiseAction.connect('activate', () => {
this.raiseWidgetLayers();
});
raiseAction.set_enabled(DesktopWidgetCapability);
this._desktopManager.mainApp.add_action(raiseAction);
}
_getPreferredDisplayDesktop() {
if (!this._desktops.length)
return null;
if (this._desktops.length === 1)
return this._desktops[0];
if (!this._Prefs.showOnSecondaryMonitor &&
this._primaryMonitorIndex !== null) {
return this._desktops.filter(d => {
return d.monitorIndex === this._primaryMonitorIndex;
})[0];
}
const tempDesktops = this._desktops.filter((desktop, index) =>
index !== this._primaryMonitorIndex
);
if (this._desktops.length > 1) {
if (tempDesktops.length === 1)
return tempDesktops[0];
// Positional algorithms here depending on new geomertry
// of the placed monitors, -FIX ME- currently rudimentary
// only going by position in the index, not by placement geometry.
if (tempDesktops.length <= this._primaryMonitorIndex)
return tempDesktops[0];
else
return tempDesktops[tempDesktops.length - 1];
}
// Catch All if everything fails
return this._desktops[0];
}
destroyDesktops() {
this._desktops.forEach(desktop => desktop.destroy());
this._desktops = [];
}
onMutterSettingsChanged() {
for (let desktop of this._desktops)
desktop._premultiplied = this._premultiplied;
this.requestGeometryUpdate();
}
getClosestDesktop(itempositionX) {
let closestDesktop = null;
let closestDistance = 100000000000;
for (let desktop of this._desktops) {
if (!desktop.isAvailable())
continue;
const distance = desktop.getDistance(itempositionX);
if (distance < closestDistance) {
closestDesktop = desktop;
closestDistance = distance;
}
}
return closestDesktop;
}
get desktops() {
return this._desktops;
}
get desktopList() {
return this._desktopList;
}
get primaryMonitorIndex() {
return this._primaryMonitorIndex;
}
get primaryMonitor() {
return this._primaryScreen;
}
get primaryIndex() {
return this._primaryIndex;
}
get priorDesktopList() {
return this._priorDesktopList;
}
get priorPrimaryMonitorIndex() {
return this._priorPrimaryMonitorIndex;
}
get differentZooms() {
return this._differentZooms;
}
get preferredDisplayDesktop() {
return this._getPreferredDisplayDesktop();
}
};