-
-
Notifications
You must be signed in to change notification settings - Fork 457
Expand file tree
/
Copy pathShellCore.qml
More file actions
305 lines (264 loc) · 11.1 KB
/
Copy pathShellCore.qml
File metadata and controls
305 lines (264 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.DankBar
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.barConfigs;
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(","));
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.barConfigs.filter(c => (c.enabled ?? false) && c.position !== SettingsData.Position.Left && c.position !== SettingsData.Position.Right).length
property int horizontalReady: 0
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
onColorPickerRequested: {
const modal = PopoutService.colorPickerModal;
if (!modal)
return;
if (modal.shouldBeVisible) {
modal.close();
} else {
modal.show();
}
}
}
}
}
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));
if (fullReconnect || partialReconnect) {
log.info("Screen reconnect detected, scheduling surface recovery", "full:", fullReconnect, "partial:", partialReconnect);
root.scheduleScreenReconnectRecovery();
}
root.hadRealScreen = hasReal;
root.previousRealScreenNames = currentNames;
}
}
// 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();
}
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.triggerSurfaceRecovery("screen-reconnect");
}
}
Timer {
id: screenReconnectCooldown
// Must exceed the full two-pass surfaceResumeRecoveryTimer sequence
// (800 + 2000 ms) so the cooldown still covers an in-flight recovery;
// raise this if those passes are lengthened.
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: {
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();
root._screenRecoveryCooldown = false;
root._screenRecoveryPending = false;
root.triggerSurfaceRecovery("sessionResumed");
}
}
LazyLoader {
id: hyprlandOverviewLoader
active: CompositorService.isHyprland
component: HyprlandOverview {
id: hyprlandOverview
}
}
}