/* 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 . */ 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 = ` `; 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(); } };