diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 933d75f4..b4d78b6e 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -33,51 +33,24 @@ Item { readonly property string windowTitle: CompositorService.getFocusedWindowTitle() readonly property bool hasActiveWindow: windowTitle !== "" - readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon - - // 6% of total width - readonly property real minWidth: Math.max(1, screen.width * 0.06) - readonly property real maxWidth: minWidth * 2 - readonly property string barPosition: Settings.data.bar.position - readonly property bool isVertical: barPosition === "left" || barPosition === "right" readonly property bool compact: (Settings.data.bar.density === "compact") - readonly property real textSize: { - var base = isVertical ? width : height - return Math.max(1, compact ? base * 0.43 : base * 0.33) - } + // Widget settings - matching MediaMini pattern + readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon + readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode !== undefined ? widgetMetadata.scrollingMode : "hover") - readonly property real iconSize: textSize * 1.25 - - implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) - implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) - - visible: hasActiveWindow + // Fixed width + readonly property real widgetWidth: Math.max(1, screen.width * 0.06) function calculatedVerticalHeight() { - // Use standard widget height like other widgets - return Math.round(Style.capsuleHeight * scaling) + return Math.round(Style.baseWidgetSize * 0.8 * scaling) } - function calculatedHorizontalWidth() { - let total = Style.marginM * 2 * scaling // internal padding + implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0 + implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0 - if (showIcon) { - total += Style.capsuleHeight * 0.5 * scaling + 2 * scaling // icon + spacing - } - - // Calculate actual text width more accurately - if (windowTitle !== "") { - // Estimate text width: average character width * number of characters - const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate - const titleWidth = Math.min(windowTitle.length * avgCharWidth, 80 * scaling) - total += titleWidth - } - - // Row layout handles spacing between widgets - return Math.max(total, Style.capsuleHeight * scaling) // Minimum width - } + visible: hasActiveWindow function getAppIcon() { try { @@ -120,7 +93,7 @@ Item { } } - // A hidden text element to safely measure the full title width + // Hidden text element to measure full title width NText { id: fullTitleMetrics visible: false @@ -130,15 +103,13 @@ Item { } Rectangle { - id: windowTitleRect + id: windowActiveRect visible: root.visible - anchors.left: (barPosition === "top" || barPosition === "bottom") ? parent.left : undefined - anchors.top: (barPosition === "left" || barPosition === "right") ? parent.top : undefined + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) - height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling) - radius: width / 2 + width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling) + height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling) + radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling) color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent Item { @@ -146,21 +117,21 @@ Item { anchors.fill: parent anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling - clip: true // Horizontal layout for top/bottom bars RowLayout { - id: horizontalLayout - anchors.centerIn: parent - spacing: 2 * scaling + id: rowLayout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling visible: barPosition === "top" || barPosition === "bottom" + z: 1 // Window icon Item { - Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling - Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling + Layout.preferredWidth: Math.round(18 * scaling) + Layout.preferredHeight: Math.round(18 * scaling) Layout.alignment: Qt.AlignVCenter - visible: hasActiveWindow && showIcon + visible: showIcon && windowTitle !== "" IconImage { id: windowIcon @@ -169,39 +140,139 @@ Item { asynchronous: true smooth: true visible: source !== "" - - // Handle loading errors gracefully - onStatusChanged: { - if (status === Image.Error) { - Logger.warn("ActiveWindow", "Failed to load icon:", source) - } - } } } - NText { - id: titleText + // Title container with scrolling + Item { + id: titleContainer Layout.preferredWidth: { - try { - if (mouseArea.containsMouse) { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) - } else { - return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars + // Calculate available width based on other elements + var iconWidth = (showIcon && windowIcon.visible ? (18 * scaling + Style.marginS * scaling) : 0) + var totalMargins = Style.marginXXS * scaling * 2 + var availableWidth = mainContainer.width - iconWidth - totalMargins + return Math.max(20 * scaling, availableWidth) + } + Layout.maximumWidth: Layout.preferredWidth + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: titleText.height + + clip: true + + property bool isScrolling: false + property bool isResetting: false + property real textWidth: fullTitleMetrics.contentWidth + property real containerWidth: width + property bool needsScrolling: textWidth > containerWidth + + // Timer for "always" mode with delay + Timer { + id: scrollStartTimer + interval: 1000 + repeat: false + onTriggered: { + if (scrollingMode === "always" && titleContainer.needsScrolling) { + titleContainer.isScrolling = true + titleContainer.isResetting = false } - } catch (e) { - Logger.warn("ActiveWindow", "Error calculating width:", e) - return 80 * scaling } } - Layout.alignment: Qt.AlignVCenter - horizontalAlignment: Text.AlignLeft - text: windowTitle - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - clip: true + + // Update scrolling state based on mode + property var updateScrollingState: function() { + if (scrollingMode === "never") { + isScrolling = false + isResetting = false + } else if (scrollingMode === "always") { + if (needsScrolling) { + if (mouseArea.containsMouse) { + isScrolling = false + isResetting = true + } else { + scrollStartTimer.restart() + } + } else { + scrollStartTimer.stop() + isScrolling = false + isResetting = false + } + } else if (scrollingMode === "hover") { + if (mouseArea.containsMouse && needsScrolling) { + isScrolling = true + isResetting = false + } else { + isScrolling = false + if (needsScrolling) { + isResetting = true + } + } + } + } + + onWidthChanged: updateScrollingState() + Component.onCompleted: updateScrollingState() + + // React to hover changes + Connections { + target: mouseArea + function onContainsMouseChanged() { + titleContainer.updateScrollingState() + } + } + + // Scrolling content with seamless loop + Item { + id: scrollContainer + height: parent.height + width: childrenRect.width + + property real scrollX: 0 + x: scrollX + + Row { + spacing: 50 * scaling // Gap between text copies + + NText { + id: titleText + text: windowTitle + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + verticalAlignment: Text.AlignVCenter + color: Color.mOnSurface + } + + // Second copy for seamless scrolling + NText { + text: windowTitle + font: titleText.font + verticalAlignment: Text.AlignVCenter + color: Color.mOnSurface + visible: titleContainer.needsScrolling && titleContainer.isScrolling + } + } + + // Reset animation + NumberAnimation on scrollX { + running: titleContainer.isResetting + to: 0 + duration: 300 + easing.type: Easing.OutQuad + onFinished: { + titleContainer.isResetting = false + } + } + + // Seamless infinite scroll + NumberAnimation on scrollX { + id: infiniteScroll + running: titleContainer.isScrolling && !titleContainer.isResetting + from: 0 + to: -(titleContainer.textWidth + 50 * scaling) + duration: Math.max(4000, windowTitle.length * 100) + loops: Animation.Infinite + easing.type: Easing.Linear + } + } Behavior on Layout.preferredWidth { NumberAnimation { @@ -216,15 +287,17 @@ Item { Item { id: verticalLayout anchors.centerIn: parent - width: parent.width - Style.marginXS * scaling * 2 - height: parent.height - Style.marginXS * scaling * 2 - visible: (barPosition === "left" || barPosition === "right") && hasActiveWindow + width: parent.width - Style.marginM * scaling * 2 + height: parent.height - Style.marginM * scaling * 2 + visible: barPosition === "left" || barPosition === "right" + z: 1 // Window icon Item { - width: Style.capsuleHeight * 0.75 * scaling - height: Style.capsuleHeight * 0.75 * scaling + width: Style.baseWidgetSize * 0.5 * scaling + height: Style.baseWidgetSize * 0.5 * scaling anchors.centerIn: parent + visible: windowTitle !== "" IconImage { id: windowIconVertical @@ -233,13 +306,6 @@ Item { asynchronous: true smooth: true visible: source !== "" - - // Handle loading errors gracefully - onStatusChanged: { - if (status === Image.Error) { - Logger.warn("ActiveWindow", "Failed to load icon:", source) - } - } } } } @@ -250,30 +316,31 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton onEntered: { if (barPosition === "left" || barPosition === "right") { tooltip.show() + } else if ((tooltip.text !== "") && (scrollingMode === "never")) { + tooltip.show() } } onExited: { - if (barPosition === "left" || barPosition === "right") { - tooltip.hide() - } + tooltip.hide() } } - - // Hover tooltip with full title (only for vertical bars) - NTooltip { - id: tooltip - target: verticalLayout - text: windowTitle - positionLeft: barPosition === "right" - positionRight: barPosition === "left" - delay: 500 - } } } + NTooltip { + id: tooltip + text: windowTitle + target: (barPosition === "left" || barPosition === "right") ? verticalLayout : windowActiveRect + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" + delay: Style.tooltipDelay + } + Connections { target: CompositorService function onActiveWindowChanged() { @@ -293,4 +360,4 @@ Item { } } } -} +} \ No newline at end of file diff --git a/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml index 72366ffd..c7916109 100644 --- a/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml +++ b/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml @@ -15,10 +15,12 @@ ColumnLayout { // Local state property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon + property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode function saveSettings() { var settings = Object.assign({}, widgetData || {}) settings.showIcon = valueShowIcon + settings.scrollingMode = valueScrollingMode return settings } @@ -29,4 +31,20 @@ ColumnLayout { checked: root.valueShowIcon onToggled: checked => root.valueShowIcon = checked } + NComboBox { + label: I18n.tr("bar.widget-settings.active-window.scrolling-mode") + model: [{ + "key": "always", + "name": I18n.tr("options.scrolling-modes.always") + }, { + "key": "hover", + "name": I18n.tr("options.scrolling-modes.hover") + }, { + "key": "never", + "name": I18n.tr("options.scrolling-modes.never") + }] + currentKey: valueScrollingMode + onSelected: key => valueScrollingMode = key + minimumWidth: 200 * scaling + } } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 08435025..a0e0e765 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -40,7 +40,8 @@ Singleton { property var widgetMetadata: ({ "ActiveWindow": { "allowUserSettings": true, - "showIcon": true + "showIcon": true, + "scrollingMode": "hover" }, "Battery": { "allowUserSettings": true,