mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
chore(merge): bar autohide + hyprland fixes
This commit is contained in:
@@ -85,6 +85,12 @@ Item {
|
||||
property ListModel centerWidgetsModel: ListModel {}
|
||||
property ListModel rightWidgetsModel: ListModel {}
|
||||
|
||||
// Guard: set when Bar is destroyed; prevents Qt.callLater callbacks from running
|
||||
// during/after teardown (avoids SIGSEGV in QV4::Object::insertMember when rapid
|
||||
// workspace switch causes load/unload overlap with async widget incubation)
|
||||
property bool _destroyed: false
|
||||
Component.onDestruction: root._destroyed = true
|
||||
|
||||
// Sync a ListModel with widget data, preserving delegates when only settings change
|
||||
function syncWidgetModel(model, newWidgets) {
|
||||
var validWidgets = filterValidWidgets(newWidgets);
|
||||
@@ -134,6 +140,8 @@ Item {
|
||||
}
|
||||
|
||||
function _syncFromRevision() {
|
||||
if (root._destroyed)
|
||||
return;
|
||||
var widgets = Settings.getBarWidgetsForScreen(screen?.name);
|
||||
if (widgets) {
|
||||
syncWidgetModel(leftWidgetsModel, widgets.left);
|
||||
@@ -152,6 +160,8 @@ Item {
|
||||
}
|
||||
|
||||
function _initModels() {
|
||||
if (root._destroyed)
|
||||
return;
|
||||
var widgets = Settings.getBarWidgetsForScreen(screen?.name);
|
||||
if (widgets) {
|
||||
syncWidgetModel(leftWidgetsModel, widgets.left);
|
||||
|
||||
@@ -72,33 +72,8 @@ Item {
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
|
||||
// Deferred activation to prevent re-entrant incubation crash:
|
||||
// When ListModel.append() creates this delegate, the Repeater is mid-incubation.
|
||||
// If this Loader activates synchronously (asynchronous: false) during delegate
|
||||
// finalization, it triggers nested QQmlIncubatorPrivate::incubate which corrupts
|
||||
// the V4 heap (SIGSEGV in QV4::Object::insertMember).
|
||||
// Deferring to the next event loop iteration breaks the nesting.
|
||||
property bool _ready: false
|
||||
active: _ready && root.checkWidgetExists() && (root.reloadCounter >= 0)
|
||||
|
||||
Timer {
|
||||
id: activateTimer
|
||||
interval: 0
|
||||
onTriggered: loader._ready = true
|
||||
}
|
||||
|
||||
Component.onCompleted: activateTimer.start()
|
||||
|
||||
// Reset _ready when reloadCounter changes to force a deferred re-activation
|
||||
Connections {
|
||||
target: root
|
||||
function onReloadCounterChanged() {
|
||||
loader._ready = false;
|
||||
activateTimer.restart();
|
||||
}
|
||||
}
|
||||
asynchronous: true
|
||||
active: root.checkWidgetExists() && (root.reloadCounter >= 0)
|
||||
|
||||
sourceComponent: {
|
||||
// Depend on reloadCounter to force re-fetch of component
|
||||
|
||||
@@ -50,7 +50,7 @@ NIconButton {
|
||||
// If using distro logo, don't use theme icon.
|
||||
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("controlCenterPanel", screen)?.isPanelOpen) {
|
||||
if (!screen || PanelService.getPanel("controlCenterPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.open-control-center");
|
||||
|
||||
@@ -408,7 +408,7 @@ Item {
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if ((isVertical || scrollingMode === "never") && !PanelService.getPanel("mediaPlayerPanel", screen)?.isPanelOpen) {
|
||||
if (screen && (isVertical || scrollingMode === "never") && !PanelService.getPanel("mediaPlayerPanel", screen)?.isPanelOpen) {
|
||||
TooltipService.show(root, title, BarService.getTooltipDirection(root.screen?.name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ Item {
|
||||
|
||||
function onVolumeAtMaximum() {
|
||||
if (!firstVolumeReceived) {
|
||||
firstVolumeReceived = true;
|
||||
firstVolumeReceived = true;
|
||||
} else {
|
||||
// Hide any tooltip while the pill is visible / being updated
|
||||
TooltipService.hide();
|
||||
@@ -87,7 +87,7 @@ Item {
|
||||
|
||||
function onVolumeAtMinimum() {
|
||||
if (!firstVolumeReceived) {
|
||||
firstVolumeReceived = true;
|
||||
firstVolumeReceived = true;
|
||||
} else {
|
||||
// Hide any tooltip while the pill is visible / being updated
|
||||
TooltipService.hide();
|
||||
|
||||
@@ -271,20 +271,12 @@ Item {
|
||||
Settings.data.dock.pinnedApps = pinnedApps;
|
||||
}
|
||||
|
||||
// Deferred to next event-loop iteration via Timer { interval: 0 } to avoid
|
||||
// re-entrant incubation: Qt.callLater() can still fire within the same event
|
||||
// processing cycle, so it is not sufficient. localWorkspaces.append() inside
|
||||
// refreshWorkspaces() causes the Repeater to create WorkspacePill delegates
|
||||
// mid-incubation, corrupting the V4 heap (SIGSEGV in QV4::Object::insertMember).
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 0
|
||||
onTriggered: root.refreshWorkspaces()
|
||||
}
|
||||
|
||||
// Deferred via Qt.callLater to avoid synchronous ListModel mutations during
|
||||
// signal cascades. Qt.callLater deduplicates by function identity, so rapid
|
||||
// calls from multiple signal handlers coalesce into a single refresh.
|
||||
function scheduleRefresh() {
|
||||
if (!root.isDestroying)
|
||||
refreshTimer.restart();
|
||||
Qt.callLater(root.refreshWorkspaces);
|
||||
}
|
||||
|
||||
Component.onCompleted: scheduleRefresh()
|
||||
|
||||
@@ -20,8 +20,10 @@ PanelWindow {
|
||||
// Note: screen property is inherited from PanelWindow and should be set by parent
|
||||
color: "transparent" // Transparent - background is in MainScreen below
|
||||
|
||||
// Make window pass-through when content is unloaded or bar is hidden via IPC
|
||||
visible: contentLoaded && BarService.effectivelyVisible
|
||||
// Window invisible when auto-hidden (blocks input) or toggled off via IPC.
|
||||
// windowVisible stays true briefly after isHidden to allow fade-out animation.
|
||||
property bool windowVisible: !isHidden
|
||||
visible: contentLoaded && windowVisible && BarService.effectivelyVisible
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name);
|
||||
@@ -146,66 +148,41 @@ PanelWindow {
|
||||
right: barPosition === "right" || !barIsVertical
|
||||
}
|
||||
|
||||
// Track if content should be loaded (stays true during fade-out animation).
|
||||
// Must NOT be a binding to isHidden — on the first hide cycle the binding
|
||||
// would flip contentLoaded false synchronously (before onIsHiddenChanged can
|
||||
// start the unload timer), unmapping the Wayland surface mid-animation.
|
||||
// Content stays loaded once initialized — never unloaded during auto-hide.
|
||||
// Destroying and recreating widgets on every hide/show cycle caused nested
|
||||
// QML incubation crashes (SIGSEGV in QV4::Object::insertMember) because
|
||||
// async widget Loaders complete during incubateFor() and their onLoaded
|
||||
// handlers trigger signal cascades mid-incubation.
|
||||
// The bar is hidden via opacity + window visibility instead.
|
||||
property bool contentLoaded: false
|
||||
|
||||
// Timer to delay unload until after fade animation
|
||||
// Delay window hide to allow fade-out animation to complete
|
||||
Timer {
|
||||
id: unloadTimer
|
||||
interval: Style.animationFast + 50
|
||||
id: windowHideTimer
|
||||
interval: Style.animationFast
|
||||
onTriggered: {
|
||||
// Only unload if still hidden AND not about to show (prevents unload/reload race)
|
||||
if (barWindow.isHidden && !showTimer.running) {
|
||||
// Clear hover state before unloading to prevent issues during destruction
|
||||
barWindow.barHovered = false;
|
||||
barWindow.contentLoaded = false;
|
||||
}
|
||||
if (barWindow.isHidden)
|
||||
barWindow.windowVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// When hidden changes, handle load/unload
|
||||
onIsHiddenChanged: {
|
||||
if (isHidden) {
|
||||
// Start fade out, then unload after animation
|
||||
unloadTimer.restart();
|
||||
// Delay window hide so fade-out is visible
|
||||
windowHideTimer.restart();
|
||||
} else {
|
||||
// Load immediately when showing
|
||||
unloadTimer.stop();
|
||||
deferredUnloadTimer.stop();
|
||||
contentLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced content unload when bar visibility is toggled.
|
||||
// Rapid toggles keep widgets alive; content is only unloaded after the bar
|
||||
// has been continuously hidden for the debounce period.
|
||||
Timer {
|
||||
id: deferredUnloadTimer
|
||||
interval: 1000
|
||||
onTriggered: {
|
||||
if (!BarService.effectivelyVisible) {
|
||||
barWindow.barHovered = false;
|
||||
barWindow.contentLoaded = false;
|
||||
Logger.d("BarContentWindow", "Debounced content unload for screen:", barWindow.screen?.name);
|
||||
}
|
||||
windowHideTimer.stop();
|
||||
windowVisible = true;
|
||||
if (!contentLoaded)
|
||||
contentLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onEffectivelyVisibleChanged() {
|
||||
if (!BarService.effectivelyVisible) {
|
||||
// Bar hidden — start debounced unload
|
||||
deferredUnloadTimer.restart();
|
||||
} else {
|
||||
// Bar shown — cancel pending unload, ensure content is loaded
|
||||
deferredUnloadTimer.stop();
|
||||
if (!barWindow.isHidden) {
|
||||
barWindow.contentLoaded = true;
|
||||
}
|
||||
if (BarService.effectivelyVisible && !barWindow.isHidden && !barWindow.contentLoaded) {
|
||||
barWindow.contentLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +199,7 @@ PanelWindow {
|
||||
implicitWidth: barIsVertical ? barHeight : barWindow.screen.width
|
||||
implicitHeight: barIsVertical ? barWindow.screen.height : barHeight
|
||||
|
||||
// Bar content loader - unloads when hidden to prevent input
|
||||
// Bar content loader - loaded once, stays active for lifetime
|
||||
Loader {
|
||||
id: barLoader
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -28,11 +28,9 @@ Variants {
|
||||
|
||||
property ListModel notificationModel: NotificationService.popupModel
|
||||
|
||||
// Deferred activation to prevent re-entrant QML incubation crash.
|
||||
// Direct binding to notificationModel.count would activate the Loader
|
||||
// synchronously during ListModel.insert(), causing nested incubation
|
||||
// (Loader + inner Repeater both processing the model) which crashes
|
||||
// the V4 engine in QV4::Object::insertMember.
|
||||
// Deferred activation via Qt.callLater to avoid activating the Loader
|
||||
// synchronously during ListModel.insert() (which would cause nested
|
||||
// incubation with the inner Repeater).
|
||||
property bool shouldBeActive: false
|
||||
active: shouldBeActive || delayTimer.running
|
||||
|
||||
@@ -43,12 +41,8 @@ Variants {
|
||||
repeat: false
|
||||
}
|
||||
|
||||
// Deferred activation timer - activates Loader on next event loop iteration
|
||||
Timer {
|
||||
id: activationTimer
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: root.shouldBeActive = true
|
||||
function activate() {
|
||||
shouldBeActive = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -56,7 +50,7 @@ Variants {
|
||||
function onCountChanged() {
|
||||
if (notificationModel.count > 0) {
|
||||
if (!root.shouldBeActive) {
|
||||
activationTimer.restart();
|
||||
Qt.callLater(root.activate);
|
||||
}
|
||||
} else if (root.shouldBeActive) {
|
||||
root.shouldBeActive = false;
|
||||
|
||||
@@ -249,11 +249,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function syncWindows() {
|
||||
const ws = backend && backend.windows ? backend.windows : [];
|
||||
Logger.d("CompositorService", "syncWindows() from backend length:", ws.length);
|
||||
|
||||
windows.clear();
|
||||
const ws = backend.windows;
|
||||
for (var i = 0; i < ws.length; i++) {
|
||||
windows.append(ws[i]);
|
||||
}
|
||||
Logger.d("CompositorService", "syncWindows() model count:", windows.count);
|
||||
|
||||
// Emit signal to notify listeners that window list has been updated
|
||||
windowListChanged();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ Item {
|
||||
property var workspaceCache: ({})
|
||||
property var windowCache: ({})
|
||||
|
||||
// Debounce timer for updates
|
||||
// Debounce timer for window updates
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 50
|
||||
@@ -32,6 +32,15 @@ Item {
|
||||
onTriggered: safeUpdate()
|
||||
}
|
||||
|
||||
// Deferred via Qt.callLater to coalesce workspace updates: onRawEvent calls
|
||||
// refreshWorkspaces() which triggers onValuesChanged synchronously in the
|
||||
// same call stack — without deferral the ListModel gets cleared+repopulated
|
||||
// twice per event. Qt.callLater deduplicates by function identity.
|
||||
function _deferredWorkspaceUpdate() {
|
||||
safeUpdateWorkspaces();
|
||||
workspaceChanged();
|
||||
}
|
||||
|
||||
// Initialization
|
||||
function initialize() {
|
||||
if (initialized)
|
||||
@@ -236,6 +245,7 @@ Item {
|
||||
// Safe window update
|
||||
function safeUpdateWindows() {
|
||||
try {
|
||||
Logger.d("HyprlandService", "safeUpdateWindows() start");
|
||||
const windowsList = [];
|
||||
windowCache = {};
|
||||
|
||||
@@ -294,6 +304,7 @@ Item {
|
||||
}
|
||||
|
||||
windows = toSortedWindowList(windowsList);
|
||||
Logger.d("HyprlandService", "safeUpdateWindows() windows after sort:", windows.length);
|
||||
|
||||
// Resolve focused index from sorted list (order changes after sort)
|
||||
let newFocusedIndex = -1;
|
||||
@@ -479,8 +490,7 @@ Item {
|
||||
target: Hyprland.workspaces
|
||||
enabled: initialized
|
||||
function onValuesChanged() {
|
||||
safeUpdateWorkspaces();
|
||||
workspaceChanged();
|
||||
Qt.callLater(_deferredWorkspaceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,8 +508,10 @@ Item {
|
||||
function onRawEvent(event) {
|
||||
Hyprland.refreshWorkspaces();
|
||||
Hyprland.refreshToplevels();
|
||||
safeUpdateWorkspaces();
|
||||
workspaceChanged();
|
||||
// Workspace and window updates are deferred — refreshWorkspaces()/
|
||||
// refreshToplevels() trigger onValuesChanged which also calls
|
||||
// Qt.callLater, so the deduplication coalesces into one update.
|
||||
Qt.callLater(_deferredWorkspaceUpdate);
|
||||
updateTimer.restart();
|
||||
|
||||
const monitorsEvents = ["configreloaded", "monitoradded", "monitorremoved", "monitoraddedv2", "monitorremovedv2"];
|
||||
|
||||
@@ -29,8 +29,8 @@ Singleton {
|
||||
property real wpctlOutputVolume: 0
|
||||
property bool wpctlOutputMuted: true
|
||||
|
||||
signal volumeAtMaximum()
|
||||
signal volumeAtMinimum()
|
||||
signal volumeAtMaximum
|
||||
signal volumeAtMinimum
|
||||
|
||||
function clampOutputVolume(vol: real): real {
|
||||
if (vol === undefined || isNaN(vol)) {
|
||||
|
||||
@@ -196,6 +196,25 @@ Singleton {
|
||||
// Track last workspace ID to detect actual workspace changes
|
||||
property var lastWorkspaceId: null
|
||||
|
||||
// Debounce rapid workspace switches to reduce load/unload races (SIGSEGV in QV4)
|
||||
property string _pendingWorkspaceScreen: ""
|
||||
|
||||
Timer {
|
||||
id: workspaceDebounceTimer
|
||||
interval: 80
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
var screen = root._pendingWorkspaceScreen;
|
||||
root._pendingWorkspaceScreen = "";
|
||||
if (screen) {
|
||||
setScreenHidden(screen, false);
|
||||
if (!root.isBarHovered(screen)) {
|
||||
barHoverStateChanged(screen, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace switch handler - directly show bar on the focused workspace screen
|
||||
Connections {
|
||||
target: CompositorService
|
||||
@@ -220,13 +239,10 @@ Singleton {
|
||||
var screenName = ws.output || "";
|
||||
Logger.d("BarService", "Workspace switched to:", currentWsId, "on screen:", screenName);
|
||||
|
||||
// Show bar immediately
|
||||
setScreenHidden(screenName, false);
|
||||
|
||||
// Only trigger hideTimer if not already hovered (e.g., mouse on trigger zone)
|
||||
if (!root.isBarHovered(screenName)) {
|
||||
barHoverStateChanged(screenName, false);
|
||||
}
|
||||
// Debounce: rapid switches (e.g. external monitor ↔ laptop) cause overlapping
|
||||
// bar load/unload; 80ms delay coalesces them and reduces QV4 incubation races
|
||||
root._pendingWorkspaceScreen = screenName;
|
||||
workspaceDebounceTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user