import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower import Quickshell.Wayland import qs.Commons import qs.Modules.Bar.Extras import qs.Modules.Notification import qs.Modules.Panels.Settings import qs.Services.Compositor import qs.Services.UI import qs.Widgets // Bar Component Item { id: root // This property will be set by MainScreen property ShellScreen screen: null // Filter widgets to only include those that exist in the registry // This prevents errors when plugins are missing or widgets are being cleaned up function filterValidWidgets(widgets: list): list { if (!widgets) return []; return widgets.filter(function (w) { return w && w.id && BarWidgetRegistry.hasWidget(w.id); }); } // Hot corner: trigger click on first widget in a section function triggerFirstWidgetInSection(sectionName: string) { var widgets = BarService.getWidgetsBySection(sectionName, screen?.name); for (var i = 0; i < widgets.length; i++) { var widget = widgets[i]; if (widget && widget.visible && widget.widgetId !== "Spacer") { if (typeof widget.clicked === "function") { widget.clicked(); } return; } } } // Hot corner: trigger click on last widget in a section function triggerLastWidgetInSection(sectionName: string) { var widgets = BarService.getWidgetsBySection(sectionName, screen?.name); for (var i = widgets.length - 1; i >= 0; i--) { var widget = widgets[i]; if (widget && widget.visible && widget.widgetId !== "Spacer") { if (typeof widget.clicked === "function") { widget.clicked(); } return; } } } // Expose bar region for click-through mask readonly property var barRegion: barContentLoader.item?.children[0] || null // Expose the actual bar Item for unified background system readonly property var barItem: barRegion // Bar positioning properties (per-screen) readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" readonly property bool barFloating: Settings.data.bar.floating || false // Bar density (per-screen) readonly property string barDensity: Settings.getBarDensityForScreen(screen?.name) // Bar sizing based on per-screen density readonly property real barHeight: Style.getBarHeightForDensity(barDensity, barIsVertical) readonly property real capsuleHeight: Style.getCapsuleHeightForDensity(barDensity, barHeight) readonly property real barFontSize: Style.getBarFontSizeForDensity(barHeight, capsuleHeight, barIsVertical) // Bar widgets (per-screen) - initial configuration // Note: Updates are handled via Connections to BarService.widgetsRevisionChanged readonly property var barWidgets: Settings.getBarWidgetsForScreen(screen?.name) // Stable ListModels for each section - prevents Repeater recreation on settings changes property ListModel leftWidgetsModel: ListModel {} property ListModel centerWidgetsModel: ListModel {} property ListModel rightWidgetsModel: ListModel {} // Sync a ListModel with widget data, preserving delegates when only settings change function syncWidgetModel(model, newWidgets) { var validWidgets = filterValidWidgets(newWidgets); // Build list of current IDs in model var currentIds = []; for (var i = 0; i < model.count; i++) { currentIds.push(model.get(i).id); } // Build list of new IDs var newIds = validWidgets.map(w => w.id); // Check if structure changed (different IDs or order) var structureChanged = currentIds.length !== newIds.length; if (!structureChanged) { for (var i = 0; i < currentIds.length; i++) { if (currentIds[i] !== newIds[i]) { structureChanged = true; break; } } } Logger.d("Bar", "syncWidgetModel:", currentIds.join("|"), "→", newIds.join("|"), "changed:", structureChanged); if (structureChanged) { // Rebuild model - IDs changed model.clear(); for (var i = 0; i < validWidgets.length; i++) { model.append(validWidgets[i]); } } // If structure didn't change, delegates are preserved and will read fresh settings } // Sync models when widget revision changes // Note: We use Connections instead of onBarWidgetsChanged because getBarWidgetsForScreen // returns the same object reference (Settings.data.bar.widgets) even when content changes, // so QML won't detect the change via property binding. Connections { target: BarService function onWidgetsRevisionChanged() { Logger.d("Bar", "onWidgetsRevisionChanged, revision:", BarService.widgetsRevision, "screen:", root.screen?.name); Qt.callLater(root._syncFromRevision); } } function _syncFromRevision() { var widgets = Settings.getBarWidgetsForScreen(screen?.name); if (widgets) { syncWidgetModel(leftWidgetsModel, widgets.left); syncWidgetModel(centerWidgetsModel, widgets.center); syncWidgetModel(rightWidgetsModel, widgets.right); } } // Initialize models — deferred to next event-loop tick via Qt.callLater to avoid // re-entrant incubation: Component.onCompleted fires during QQmlObjectCreator::finalize, // and ListModel.append synchronously creates Repeater delegates whose own finalization // can corrupt the V4 heap (SIGSEGV in QV4::Object::insertMember). Component.onCompleted: { Logger.d("Bar", "Bar Component.onCompleted for screen:", screen?.name); Qt.callLater(root._initModels); } function _initModels() { var widgets = Settings.getBarWidgetsForScreen(screen?.name); if (widgets) { syncWidgetModel(leftWidgetsModel, widgets.left); syncWidgetModel(centerWidgetsModel, widgets.center); syncWidgetModel(rightWidgetsModel, widgets.right); } } // Fill the parent (the Loader) anchors.fill: parent // Register bar when screen becomes available onScreenChanged: { if (screen && screen.name) { Logger.d("Bar", "Bar screen set to:", screen.name); Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating); BarService.registerBar(screen.name); } } // Wait for screen to be set before loading bar content Loader { id: barContentLoader anchors.fill: parent active: { if (root.screen === null || root.screen === undefined) { return false; } var monitors = Settings.data.bar.monitors || []; var result = monitors.length === 0 || monitors.includes(root.screen.name); return result; } sourceComponent: Item { anchors.fill: parent // Bar container - Content Item { id: bar // Wheel scroll handling (empty bar area) property int barWheelAccumulatedDelta: 0 property bool barWheelCooldown: false readonly property string barWheelAction: { return Settings.data.bar.mouseWheelAction || "none"; } readonly property string barRightClickAction: Settings.data.bar.rightClickAction || "controlCenter" // Position and size the bar content based on orientation x: (root.barPosition === "right") ? (parent.width - root.barHeight) : 0 y: (root.barPosition === "bottom") ? (parent.height - root.barHeight) : 0 width: root.barIsVertical ? root.barHeight : parent.width height: root.barIsVertical ? parent.height : root.barHeight // Corner states for new unified background system // State -1: No radius (flat/square corner) // State 0: Normal (inner curve) // State 1: Horizontal inversion (outer curve on X-axis) // State 2: Vertical inversion (outer curve on Y-axis) readonly property int topLeftCornerState: { // Floating bar: always simple rounded corners if (barFloating) return 0; // Top bar: top corners against screen edge = no radius if (barPosition === "top") return -1; // Left bar: top-left against screen edge = no radius if (barPosition === "left") return -1; // Bottom/Right bar with outerCorners: inverted corner if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) { return barIsVertical ? 1 : 2; // horizontal invert for vertical bars, vertical invert for horizontal } // No outerCorners = square return -1; } readonly property int topRightCornerState: { // Floating bar: always simple rounded corners if (barFloating) return 0; // Top bar: top corners against screen edge = no radius if (barPosition === "top") return -1; // Right bar: top-right against screen edge = no radius if (barPosition === "right") return -1; // Bottom/Left bar with outerCorners: inverted corner if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) { return barIsVertical ? 1 : 2; } // No outerCorners = square return -1; } readonly property int bottomLeftCornerState: { // Floating bar: always simple rounded corners if (barFloating) return 0; // Bottom bar: bottom corners against screen edge = no radius if (barPosition === "bottom") return -1; // Left bar: bottom-left against screen edge = no radius if (barPosition === "left") return -1; // Top/Right bar with outerCorners: inverted corner if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) { return barIsVertical ? 1 : 2; } // No outerCorners = square return -1; } readonly property int bottomRightCornerState: { // Floating bar: always simple rounded corners if (barFloating) return 0; // Bottom bar: bottom corners against screen edge = no radius if (barPosition === "bottom") return -1; // Right bar: bottom-right against screen edge = no radius if (barPosition === "right") return -1; // Top/Left bar with outerCorners: inverted corner if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) { return barIsVertical ? 1 : 2; } // No outerCorners = square return -1; } function isPointOverWidget(xPos, yPos) { var widgets = BarService.getAllWidgetInstances(null, screen.name); for (var i = 0; i < widgets.length; i++) { var widget = widgets[i]; if (!widget || !widget.visible || widget.widgetId === "Spacer") { continue; } var localPos = mapToItem(widget, xPos, yPos); if (root.barIsVertical) { if (localPos.y >= -Style.marginS && localPos.y <= widget.height + Style.marginS) { return true; } } else { if (localPos.x >= -Style.marginS && localPos.x <= widget.width + Style.marginS) { return true; } } } return false; } function switchWorkspaceByOffset(offset) { if (!root.screen || CompositorService.workspaces.count === 0) return; var screenName = root.screen.name.toLowerCase(); var candidates = []; for (var i = 0; i < CompositorService.workspaces.count; i++) { var ws = CompositorService.workspaces.get(i); var matchesScreen = CompositorService.globalWorkspaces || (ws.output && ws.output.toLowerCase() === screenName); if (matchesScreen) candidates.push(ws); } if (candidates.length <= 1) return; var current = -1; for (var j = 0; j < candidates.length; j++) { if (candidates[j].isFocused) { current = j; break; } } if (current < 0) current = 0; var next = current + offset; if (Settings.data.bar.mouseWheelWrap) { next = next % candidates.length; if (next < 0) next = candidates.length - 1; } else { if (next < 0 || next >= candidates.length) return; } if (next === current) return; CompositorService.switchToWorkspace(candidates[next]); } function handleEmptyBarClick(action, followMouse, command, mouse) { if (action === "none") return; if (action === "controlCenter") { var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen); controlCenterPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : "ControlCenter"); mouse.accepted = true; } else if (action === "settings") { var settingsPanel = PanelService.getPanel("settingsPanel", screen); settingsPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : null); mouse.accepted = true; } else if (action === "launcherPanel") { var launcherPanel = PanelService.getPanel("launcherPanel", screen); launcherPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : null); mouse.accepted = true; } else if (action === "command") { runCustomCommand(command); mouse.accepted = true; } } function runCustomCommand(command) { if (!command || command.trim() === "") return; const processString = "import QtQuick; import Quickshell.Io; Process { command: [\"sh\", \"-lc\", \"\"] }"; try { const processObj = Qt.createQmlObject(processString, root, "BarCommandProcess_" + Date.now()); processObj.command = ["sh", "-lc", command]; processObj.exited.connect(function (exitCode) { if (exitCode !== 0) { ToastService.showError(I18n.tr("toast.custom-command-failed.title"), I18n.tr("toast.custom-command-failed.description", { command: command, code: exitCode })); } processObj.destroy(); }); processObj.running = true; } catch (e) { Logger.e("Bar", "Failed to start custom command:", e); ToastService.showError(I18n.tr("toast.custom-command-failed.title"), I18n.tr("toast.custom-command-failed.description", { command: command, code: "start_error" })); } } MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton | Qt.MiddleButton enabled: bar.barRightClickAction !== "none" || Settings.data.bar.middleClickAction !== "none" hoverEnabled: false preventStealing: true onClicked: mouse => { if (mouse.button === Qt.RightButton) { if (bar.isPointOverWidget(mouse.x, mouse.y)) return; bar.handleEmptyBarClick(bar.barRightClickAction, Settings.data.bar.rightClickFollowMouse, Settings.data.bar.rightClickCommand, mouse); return; } if (mouse.button === Qt.MiddleButton) { if (bar.isPointOverWidget(mouse.x, mouse.y)) return; bar.handleEmptyBarClick(Settings.data.bar.middleClickAction || "none", Settings.data.bar.middleClickFollowMouse, Settings.data.bar.middleClickCommand, mouse); return; } } } // Debounce timer for wheel interactions Timer { id: barWheelDebounce interval: 150 repeat: false onTriggered: { bar.barWheelCooldown = false; bar.barWheelAccumulatedDelta = 0; } } // Scroll on empty bar area to switch workspaces WheelHandler { id: barWheelHandler target: bar acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad enabled: bar.barWheelAction !== "none" onWheel: function (event) { if (bar.barWheelCooldown) return; if (bar.isPointOverWidget(event.x, event.y)) return; var dy = event.angleDelta.y; var dx = event.angleDelta.x; var useDy = Math.abs(dy) >= Math.abs(dx); var delta = useDy ? dy : dx; bar.barWheelAccumulatedDelta += delta; var step = 120; if (Math.abs(bar.barWheelAccumulatedDelta) >= step) { var direction = bar.barWheelAccumulatedDelta > 0 ? -1 : 1; if (Settings.data.bar.reverseScroll) direction *= -1; if (bar.barWheelAction === "workspace") { bar.switchWorkspaceByOffset(direction); } else if (bar.barWheelAction === "content") { CompositorService.scrollWorkspaceContent(direction); } bar.barWheelCooldown = true; barWheelDebounce.restart(); bar.barWheelAccumulatedDelta = 0; event.accepted = true; } } } Loader { anchors.fill: parent sourceComponent: root.barIsVertical ? verticalBarComponent : horizontalBarComponent } } } } // For vertical bars Component { id: verticalBarComponent Item { anchors.fill: parent clip: true // Top edge hot corner - triggers first widget in left (top) section MouseArea { width: parent.width height: Style.marginS x: 0 y: 0 onClicked: root.triggerFirstWidgetInSection("left") } // Bottom edge hot corner - triggers last widget in right (bottom) section MouseArea { width: parent.width height: Style.marginS x: 0 anchors.bottom: parent.bottom onClicked: root.triggerLastWidgetInSection("right") } // Calculate margin to center widgets vertically within the bar height readonly property real verticalBarMargin: Math.round((root.barHeight - root.capsuleHeight) / 2) // Top section (left widgets) ColumnLayout { x: Style.pixelAlignCenter(parent.width, width) anchors.top: parent.top anchors.topMargin: verticalBarMargin + Settings.data.bar.contentPadding spacing: Settings.data.bar.widgetSpacing Repeater { model: root.leftWidgetsModel delegate: BarWidgetLoader { required property var model required property int index widgetId: model.id || "" widgetScreen: root.screen widgetProps: ({ "widgetId": model.id, "section": "left", "sectionWidgetIndex": index, "sectionWidgetsCount": root.leftWidgetsModel.count }) Layout.alignment: Qt.AlignHCenter } } } // Center section (center widgets) ColumnLayout { x: Style.pixelAlignCenter(parent.width, width) anchors.verticalCenter: parent.verticalCenter spacing: Settings.data.bar.widgetSpacing Repeater { model: root.centerWidgetsModel delegate: BarWidgetLoader { required property var model required property int index widgetId: model.id || "" widgetScreen: root.screen widgetProps: ({ "widgetId": model.id, "section": "center", "sectionWidgetIndex": index, "sectionWidgetsCount": root.centerWidgetsModel.count }) Layout.alignment: Qt.AlignHCenter } } } // Bottom section (right widgets) ColumnLayout { x: Style.pixelAlignCenter(parent.width, width) anchors.bottom: parent.bottom anchors.bottomMargin: verticalBarMargin + Settings.data.bar.contentPadding spacing: Settings.data.bar.widgetSpacing Repeater { model: root.rightWidgetsModel delegate: BarWidgetLoader { required property var model required property int index widgetId: model.id || "" widgetScreen: root.screen widgetProps: ({ "widgetId": model.id, "section": "right", "sectionWidgetIndex": index, "sectionWidgetsCount": root.rightWidgetsModel.count }) Layout.alignment: Qt.AlignHCenter } } } } } // For horizontal bars Component { id: horizontalBarComponent Item { anchors.fill: parent clip: true // Left edge hot corner - triggers first widget in left section MouseArea { width: Style.marginS height: parent.height x: 0 y: 0 onClicked: root.triggerFirstWidgetInSection("left") } // Right edge hot corner - triggers last widget in right section MouseArea { width: Style.marginS height: parent.height anchors.right: parent.right y: 0 onClicked: root.triggerLastWidgetInSection("right") } // Calculate margin to center widgets horizontally within the bar height readonly property real horizontalBarMargin: Math.round((root.barHeight - root.capsuleHeight) / 2) // Left Section RowLayout { id: leftSection objectName: "leftSection" anchors.left: parent.left anchors.leftMargin: horizontalBarMargin + Settings.data.bar.contentPadding y: Style.pixelAlignCenter(parent.height, height) spacing: Settings.data.bar.widgetSpacing Repeater { model: root.leftWidgetsModel delegate: BarWidgetLoader { required property var model required property int index widgetId: model.id || "" widgetScreen: root.screen widgetProps: ({ "widgetId": model.id, "section": "left", "sectionWidgetIndex": index, "sectionWidgetsCount": root.leftWidgetsModel.count }) Layout.alignment: Qt.AlignVCenter } } } // Center Section RowLayout { id: centerSection objectName: "centerSection" anchors.horizontalCenter: parent.horizontalCenter y: Style.pixelAlignCenter(parent.height, height) spacing: Settings.data.bar.widgetSpacing Repeater { model: root.centerWidgetsModel delegate: BarWidgetLoader { required property var model required property int index widgetId: model.id || "" widgetScreen: root.screen widgetProps: ({ "widgetId": model.id, "section": "center", "sectionWidgetIndex": index, "sectionWidgetsCount": root.centerWidgetsModel.count }) Layout.alignment: Qt.AlignVCenter } } } // Right Section RowLayout { id: rightSection objectName: "rightSection" anchors.right: parent.right anchors.rightMargin: horizontalBarMargin + Settings.data.bar.contentPadding y: Style.pixelAlignCenter(parent.height, height) spacing: Settings.data.bar.widgetSpacing Repeater { model: root.rightWidgetsModel delegate: BarWidgetLoader { required property var model required property int index widgetId: model.id || "" widgetScreen: root.screen widgetProps: ({ "widgetId": model.id, "section": "right", "sectionWidgetIndex": index, "sectionWidgetsCount": root.rightWidgetsModel.count }) Layout.alignment: Qt.AlignVCenter } } } } } }