mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-12 06:17:22 -04:00
355 lines
13 KiB
QML
355 lines
13 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Modules.DankBar
|
|
import qs.Modules.DankIsland
|
|
import qs.Modules.Frame
|
|
import qs.Modules.WorkspaceOverlays
|
|
import qs.Services
|
|
|
|
Item {
|
|
id: root
|
|
|
|
readonly property var log: Log.scoped("ShellCore")
|
|
|
|
property bool barSurfacesLoaded: true
|
|
property int pendingFrameTransitionRevision: 0
|
|
property bool frameSurfacesLoaded: true
|
|
|
|
property alias dankBarRepeater: dankBarRepeater
|
|
property alias hyprlandOverviewLoader: hyprlandOverviewLoader
|
|
|
|
signal surfaceRecoveryPass
|
|
|
|
property string _barLayoutStateJson: {
|
|
if (!barSurfacesLoaded)
|
|
return "[]";
|
|
const configs = SettingsData.getBarKindConfigs();
|
|
const mapped = configs.map(c => ({
|
|
id: c.id,
|
|
position: c.position,
|
|
autoHide: c.autoHide,
|
|
visible: c.visible
|
|
})).sort((a, b) => {
|
|
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
|
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
|
if (aVertical !== bVertical) {
|
|
return aVertical - bVertical;
|
|
}
|
|
return String(a.id).localeCompare(String(b.id));
|
|
});
|
|
return JSON.stringify(mapped);
|
|
}
|
|
|
|
function recreateBarSurfaces() {
|
|
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","));
|
|
dankBarRepeater.horizontalReady = 0;
|
|
if (barSurfacesLoaded)
|
|
barSurfacesLoaded = false;
|
|
barSurfaceReloadAction.schedule();
|
|
}
|
|
|
|
// Holds the bar rebuild until the compositor applies the layout, so the swap lands in one pass
|
|
function runPendingFrameTransition() {
|
|
if (pendingFrameTransitionRevision <= 0 || !CompositorService.frameCompositorLayoutReady)
|
|
return;
|
|
recreateBarSurfaces();
|
|
}
|
|
|
|
DeferredAction {
|
|
id: barSurfaceReloadAction
|
|
onTriggered: {
|
|
// Ack first so the latch flips and new bars build directly in the post-transition state
|
|
if (root.pendingFrameTransitionRevision > 0 && CompositorService.frameCompositorLayoutReady) {
|
|
FrameTransitionState.acknowledge(root.pendingFrameTransitionRevision);
|
|
root.pendingFrameTransitionRevision = 0;
|
|
}
|
|
root.barSurfacesLoaded = true;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: FrameTransitionState
|
|
function onTransitionRequested(revision) {
|
|
root.pendingFrameTransitionRevision = Math.max(root.pendingFrameTransitionRevision, revision);
|
|
root.runPendingFrameTransition();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: CompositorService
|
|
function onFrameCompositorLayoutReadyChanged() {
|
|
root.runPendingFrameTransition();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
function onForceDankBarLayoutRefresh() {
|
|
root.recreateBarSurfaces();
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
active: root.frameSurfacesLoaded
|
|
asynchronous: false
|
|
sourceComponent: Frame {}
|
|
}
|
|
|
|
Loader {
|
|
active: FrameTransitionState.effectiveFrameEnabled && SettingsData.frameLauncherEdgeHover
|
|
asynchronous: false
|
|
sourceComponent: FrameLauncherHoverZone {}
|
|
}
|
|
|
|
DeferredAction {
|
|
id: frameSurfaceReloadAction
|
|
onTriggered: root.frameSurfacesLoaded = true
|
|
}
|
|
|
|
Repeater {
|
|
id: dankBarRepeater
|
|
model: ScriptModel {
|
|
id: barRepeaterModel
|
|
values: JSON.parse(root._barLayoutStateJson)
|
|
}
|
|
|
|
Component.onCompleted: BarWidgetService.dankBarRepeater = dankBarRepeater
|
|
|
|
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
|
|
|
// Horizontal bars must claim their exclusive zones first, so vertical bars wait for every enabled horizontal bar to load
|
|
readonly property int horizontalWanted: SettingsData.getBarKindConfigs().filter(c => (c.enabled ?? false) && c.position !== SettingsData.Position.Left && c.position !== SettingsData.Position.Right).length
|
|
property int horizontalReady: 0
|
|
onHorizontalWantedChanged: recountHorizontalReady()
|
|
|
|
function recountHorizontalReady() {
|
|
let ready = 0;
|
|
for (let i = 0; i < count; i++) {
|
|
const loader = itemAt(i);
|
|
if (loader?.item && !loader.isVertical)
|
|
ready++;
|
|
}
|
|
horizontalReady = ready;
|
|
}
|
|
|
|
delegate: Loader {
|
|
id: barLoader
|
|
required property var modelData
|
|
property var barConfig: SettingsData.barConfigs.find(cfg => cfg.id === modelData.id) || null
|
|
readonly property bool isVertical: modelData.position === SettingsData.Position.Left || modelData.position === SettingsData.Position.Right
|
|
active: root.barSurfacesLoaded && (barConfig?.enabled ?? false) && (!isVertical || dankBarRepeater.horizontalReady >= dankBarRepeater.horizontalWanted)
|
|
asynchronous: false
|
|
onItemChanged: dankBarRepeater.recountHorizontalReady()
|
|
|
|
sourceComponent: DankBar {
|
|
barConfig: barLoader.barConfig
|
|
hyprlandOverviewLoader: dankBarRepeater.hyprlandOverviewLoaderRef
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
active: SettingsData.dankIslandEnabled
|
|
asynchronous: false
|
|
sourceComponent: DankIsland {
|
|
hyprlandOverviewLoader: root.hyprlandOverviewLoader
|
|
}
|
|
}
|
|
|
|
property bool hadRealScreen: true
|
|
property var previousRealScreenNames: []
|
|
// Guards for the screen-reconnect recovery path (see scheduleScreenReconnectRecovery).
|
|
property bool _screenRecoveryCooldown: false
|
|
property bool _screenRecoveryPending: false
|
|
|
|
function _getRealScreenNames() {
|
|
const names = [];
|
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
|
if (Quickshell.screens[i].name.length > 0)
|
|
names.push(Quickshell.screens[i].name);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function _hasRealScreen() {
|
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
|
if (Quickshell.screens[i].name.length > 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function triggerSurfaceRecovery(source) {
|
|
log.info("Surface recovery triggered by:", source, "screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","), "barLoaded:", root.barSurfacesLoaded, "frameLoaded:", root.frameSurfacesLoaded);
|
|
surfaceResumeRecoveryTimer.pass = 0;
|
|
surfaceResumeRecoveryTimer.interval = 800;
|
|
surfaceResumeRecoveryTimer.restart();
|
|
}
|
|
|
|
Connections {
|
|
target: Quickshell
|
|
function onScreensChanged() {
|
|
const hasReal = root._hasRealScreen();
|
|
const currentNames = root._getRealScreenNames();
|
|
log.info("Screens changed:", Quickshell.screens.length, Quickshell.screens.map(s => "'" + s.name + "'").join(","), "hasReal:", hasReal, "hadReal:", root.hadRealScreen);
|
|
const fullReconnect = !root.hadRealScreen && hasReal;
|
|
const partialReconnect = root.previousRealScreenNames.length > 0 && currentNames.some(name => !root.previousRealScreenNames.includes(name));
|
|
const removed = hasReal && root.previousRealScreenNames.some(name => !currentNames.includes(name));
|
|
if (fullReconnect || partialReconnect || removed) {
|
|
log.info("Screen change detected, scheduling surface recovery", "full:", fullReconnect, "partial:", partialReconnect, "removed:", removed);
|
|
root.scheduleScreenReconnectRecovery();
|
|
}
|
|
root.hadRealScreen = hasReal;
|
|
root.previousRealScreenNames = currentNames;
|
|
}
|
|
}
|
|
|
|
// Quickshell.screensChanged only fires on add/remove. Removing or reconfiguring one output
|
|
// reflows the survivors, and the compositor may leave their layer surfaces stale at the
|
|
// old placement until something forces a repaint (#3135), so watch geometry per screen.
|
|
Instantiator {
|
|
model: Quickshell.screens
|
|
delegate: Connections {
|
|
required property ShellScreen modelData
|
|
target: modelData
|
|
function onGeometryChanged() {
|
|
if (modelData.name.length === 0)
|
|
return;
|
|
root.log.info("Screen geometry changed:", modelData.name, modelData.x, modelData.y, modelData.width, modelData.height);
|
|
root.scheduleScreenReconnectRecovery();
|
|
}
|
|
}
|
|
}
|
|
|
|
// A DPMS off/on cycle removes an output from the screen list and re-adds it,
|
|
// which is indistinguishable here from a hotplug. Recovering immediately on
|
|
// every such event lets a flapping monitor (or a recovery that itself perturbs
|
|
// the output) drive an endless recovery storm that power-cycles the display
|
|
// (#2642). Debounce a burst of changes into a single pass, then hold a cooldown
|
|
// so repeated flaps trigger at most one recovery per window. Recovery still runs
|
|
// once per resume, so a partial DPMS resume keeps redrawing its surfaces (#2579).
|
|
function scheduleScreenReconnectRecovery() {
|
|
if (root._screenRecoveryCooldown) {
|
|
root._screenRecoveryPending = true;
|
|
return;
|
|
}
|
|
screenReconnectDebounce.restart();
|
|
}
|
|
|
|
function refreshScreenSurfaces() {
|
|
if (!_hasRealScreen()) {
|
|
log.info("Surface refresh skipped: no real screen");
|
|
return;
|
|
}
|
|
log.info("Refreshing layer surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","));
|
|
SurfaceRecovery.refreshAll();
|
|
surfaceRefreshVerifyTimer.restart();
|
|
}
|
|
|
|
Timer {
|
|
id: screenReconnectDebounce
|
|
// Wide enough to collapse the output-remove + output-re-add pair that one
|
|
// DPMS off/on cycle emits as two near-simultaneous events into one recovery.
|
|
interval: 450
|
|
repeat: false
|
|
onTriggered: {
|
|
root._screenRecoveryCooldown = true;
|
|
root._screenRecoveryPending = false;
|
|
screenReconnectCooldown.restart();
|
|
root.refreshScreenSurfaces();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: surfaceRefreshVerifyTimer
|
|
interval: 800
|
|
repeat: false
|
|
onTriggered: {
|
|
const stale = SurfaceRecovery.staleWindows();
|
|
if (stale.length === 0)
|
|
return;
|
|
log.warn("Layer surfaces still stale after refresh:", stale.map(w => (w.screen?.name ?? "?") + ":" + w.width + "x" + w.height).join(","));
|
|
root.triggerSurfaceRecovery("stale-surfaces");
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: screenReconnectCooldown
|
|
// Must exceed surfaceRefreshVerifyTimer plus the two-pass
|
|
// surfaceResumeRecoveryTimer sequence (800 + 800 + 2000 ms).
|
|
interval: 4000
|
|
repeat: false
|
|
onTriggered: {
|
|
root._screenRecoveryCooldown = false;
|
|
if (root._screenRecoveryPending) {
|
|
root._screenRecoveryPending = false;
|
|
screenReconnectDebounce.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: surfaceResumeRecoveryTimer
|
|
interval: 800
|
|
repeat: false
|
|
property int pass: 0
|
|
onTriggered: {
|
|
// Rebuilding against a placeholder-only screen list feeds a dangling screen to the per-screen delegate models and segfaults (#3057); onScreensChanged reschedules once outputs return.
|
|
if (!root._hasRealScreen()) {
|
|
log.info("Surface recovery skipped: no real screen");
|
|
pass = 0;
|
|
interval = 800;
|
|
return;
|
|
}
|
|
|
|
pass++;
|
|
log.info("Surface recovery pass", pass, "screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","));
|
|
|
|
root.recreateBarSurfaces();
|
|
|
|
if (root.frameSurfacesLoaded) {
|
|
root.frameSurfacesLoaded = false;
|
|
frameSurfaceReloadAction.schedule();
|
|
}
|
|
|
|
root.surfaceRecoveryPass();
|
|
|
|
if (pass < 2) {
|
|
interval = 2000;
|
|
restart();
|
|
} else {
|
|
pass = 0;
|
|
interval = 800;
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SessionService
|
|
|
|
function onSessionResumed() {
|
|
log.info("Session resumed: screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","), "barLoaded:", root.barSurfacesLoaded, "frameLoaded:", root.frameSurfacesLoaded);
|
|
|
|
// This path runs its own recovery directly, so drop any queued or
|
|
// in-flight screen-reconnect recovery to avoid a redundant pass once
|
|
// its cooldown expires.
|
|
screenReconnectDebounce.stop();
|
|
screenReconnectCooldown.stop();
|
|
surfaceRefreshVerifyTimer.stop();
|
|
root._screenRecoveryCooldown = false;
|
|
root._screenRecoveryPending = false;
|
|
|
|
root.triggerSurfaceRecovery("sessionResumed");
|
|
}
|
|
}
|
|
|
|
LazyLoader {
|
|
id: hyprlandOverviewLoader
|
|
active: CompositorService.isHyprland
|
|
component: HyprlandOverview {
|
|
id: hyprlandOverview
|
|
}
|
|
}
|
|
}
|