diff --git a/Bar/Modules/Battery.qml b/Bar/Modules/Battery.qml index af41d66..d4ccd2c 100644 --- a/Bar/Modules/Battery.qml +++ b/Bar/Modules/Battery.qml @@ -73,6 +73,7 @@ Item { } StyledTooltip { id: batteryTooltip + positionAbove: false text: { let lines = []; if (batteryWidget.isReady) { diff --git a/Bar/Modules/Brightness.qml b/Bar/Modules/Brightness.qml index be04cfd..6ce1158 100644 --- a/Bar/Modules/Brightness.qml +++ b/Bar/Modules/Brightness.qml @@ -128,6 +128,7 @@ Item { StyledTooltip { id: brightnessTooltip text: "Brightness: " + brightness + "%" + positionAbove: false tooltipVisible: false targetItem: pill delay: 1500 diff --git a/Bar/Modules/ClockWidget.qml b/Bar/Modules/ClockWidget.qml index f899d0d..eba2afb 100644 --- a/Bar/Modules/ClockWidget.qml +++ b/Bar/Modules/ClockWidget.qml @@ -41,6 +41,7 @@ Rectangle { StyledTooltip { id: dateTooltip text: Time.dateString + positionAbove: false tooltipVisible: showTooltip && !calendar.visible targetItem: clockWidget delay: 200 diff --git a/Bar/Modules/SystemTray.qml b/Bar/Modules/SystemTray.qml index 90d30fb..fe2ceee 100644 --- a/Bar/Modules/SystemTray.qml +++ b/Bar/Modules/SystemTray.qml @@ -138,6 +138,7 @@ Row { StyledTooltip { id: trayTooltip text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" + positionAbove: false tooltipVisible: false targetItem: trayIcon delay: 200 diff --git a/Bar/Modules/Volume.qml b/Bar/Modules/Volume.qml index f6fb87e..a7082fd 100644 --- a/Bar/Modules/Volume.qml +++ b/Bar/Modules/Volume.qml @@ -48,6 +48,7 @@ Item { StyledTooltip { id: volumeTooltip text: "Volume: " + volume + "%\nScroll up/down to change volume.\nLeft click to open the input/output selection." + positionAbove: false tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse targetItem: pillIndicator delay: 1500 diff --git a/Components/StyledTooltip.qml b/Components/StyledTooltip.qml index 3c63e93..fed3d1c 100644 --- a/Components/StyledTooltip.qml +++ b/Components/StyledTooltip.qml @@ -8,6 +8,10 @@ Window { property bool tooltipVisible: false property Item targetItem: null property int delay: 300 + + // New property to control positioning: true => above, false => below + property bool positionAbove: true + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint color: "transparent" visible: false @@ -15,11 +19,14 @@ Window { minimumWidth: tooltipText.implicitWidth + 24 minimumHeight: tooltipText.implicitHeight + 16 property var _timerObj: null + onTooltipVisibleChanged: { if (tooltipVisible) { if (delay > 0) { if (_timerObj) { _timerObj.destroy(); _timerObj = null; } - _timerObj = Qt.createQmlObject('import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', tooltipWindow); + _timerObj = Qt.createQmlObject( + 'import QtQuick 2.0; Timer { interval: ' + delay + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', + tooltipWindow); } else { _showNow(); } @@ -27,30 +34,42 @@ Window { _hideNow(); } } + function _showNow() { if (!targetItem) return; - var pos = targetItem.mapToGlobal(0, targetItem.height); - x = pos.x - width / 2 + targetItem.width / 2; - y = pos.y + 12; + + if (positionAbove) { + // Position tooltip above the target item + var pos = targetItem.mapToGlobal(0, 0); + x = pos.x - width / 2 + targetItem.width / 2; + y = pos.y - height - 12; // 12 px margin above + } else { + // Position tooltip below the target item + var pos = targetItem.mapToGlobal(0, targetItem.height); + x = pos.x - width / 2 + targetItem.width / 2; + y = pos.y + 12; // 12 px margin below + } visible = true; } + function _hideNow() { visible = false; if (_timerObj) { _timerObj.destroy(); _timerObj = null; } } + Connections { target: tooltipWindow.targetItem function onXChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } function onYChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } function onWidthChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } function onHeightChanged() { - if (tooltipWindow.visible) tooltipWindow._showNow() + if (tooltipWindow.visible) tooltipWindow._showNow(); } } @@ -63,6 +82,7 @@ Window { opacity: 0.97 z: 1 } + Text { id: tooltipText text: tooltipWindow.text @@ -76,15 +96,16 @@ Window { padding: 8 z: 2 } + MouseArea { anchors.fill: parent hoverEnabled: true onExited: tooltipWindow.tooltipVisible = false cursorShape: Qt.ArrowCursor } + onTextChanged: { width = Math.max(minimumWidth, tooltipText.implicitWidth + 24); height = Math.max(minimumHeight, tooltipText.implicitHeight + 16); } - -} \ No newline at end of file +} diff --git a/Settings/Settings.qml b/Settings/Settings.qml index 879abd2..32b7c3f 100644 --- a/Settings/Settings.qml +++ b/Settings/Settings.qml @@ -65,6 +65,9 @@ Singleton { property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) property var pinnedExecs: [] // Added for AppLauncher pinned apps + + property bool showDock: true + property bool dockExclusive: false } } diff --git a/Widgets/Dock.qml b/Widgets/Dock.qml new file mode 100644 index 0000000..6684878 --- /dev/null +++ b/Widgets/Dock.qml @@ -0,0 +1,349 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Settings +import qs.Components + +PanelWindow { + id: taskbarWindow + visible: Settings.settings.showDock + screen: (typeof modelData !== 'undefined' ? modelData : null) + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 43 + + // Auto-hide properties + property bool autoHide: true + property bool hidden: true + property int hideDelay: 500 + property int showDelay: 100 + property int hideAnimationDuration: 200 + property int showAnimationDuration: 150 + property int peekHeight: 2 + property int fullHeight: taskbarContainer.height + + // Track hover state + property bool dockHovered: false + property bool anyAppHovered: false + + // Context menu properties + property bool contextMenuVisible: false + property var contextMenuTarget: null + property var contextMenuToplevel: null + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered && !contextMenuVisible) hidden = true + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered && !contextMenuVisible) hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + Rectangle { + id: taskbarContainer + width: taskbar.width + 40 + height: Settings.settings.taskbarIconSize + 20 + topLeftRadius: 16 + topRightRadius: 16 + color: Theme.backgroundSecondary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() + } + } + + Item { + id: taskbar + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent + + StyledTooltip { id: styledTooltip } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) return ""; + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); + if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.title, true); + return icon || Quickshell.iconPath("application-x-executable", true); + } + + Row { + id: runningAppsRow + spacing: 12 + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: Settings.settings.taskbarIconSize + 8 + height: Settings.settings.taskbarIconSize + 8 + radius: Math.max(6, Settings.settings.taskbarIconSize * 0.3) + color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent") + border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent" + border.width: 1 + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { ColorAnimation { duration: 150 } } + Behavior on border.color { ColorAnimation { duration: 150 } } + + IconImage { + id: appIcon + width: Math.max(20, Settings.settings.taskbarIconSize * 0.75) + height: Math.max(20, Settings.settings.taskbarIconSize * 0.75) + anchors.centerIn: parent + source: taskbar.getAppIcon(modelData) + smooth: true + visible: source.toString() !== "" + } + + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.family: Theme.fontFamily + font.pixelSize: Math.max(14, Settings.settings.taskbarIconSize * 0.5) + font.bold: true + color: appButton.isActive ? Theme.onAccent : Theme.textPrimary + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + if (!contextMenuVisible) { + styledTooltip.text = appTitle || appId; + styledTooltip.targetItem = appButton; + styledTooltip.positionAbove = true; + styledTooltip.tooltipVisible = true; + } + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + anyAppHovered = false + if (!contextMenuVisible) { + styledTooltip.tooltipVisible = false; + } + if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() + } + onClicked: function(mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close(); + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate(); + } + if (mouse.button === Qt.RightButton) { + styledTooltip.tooltipVisible = false; + contextMenuTarget = appButton; + contextMenuToplevel = modelData; + contextMenuVisible = true; + } + } + } + + Rectangle { + visible: isActive + width: 6 + height: 6 + radius: 3 + color: Theme.onAccent + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: -8 + } + } + } + } + } + } + + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: taskbarWindow.screen + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + color: "transparent" + focusable: false + + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuVisible = false; + contextMenuTarget = null; + contextMenuToplevel = null; + hidden = true; // Hide dock when context menu closes by clicking outside + } + } + + Rectangle { + id: contextMenuContainer + width: 80 + height: contextMenuColumn.height + 0 + radius: 16 + color: Theme.backgroundPrimary + border.color: Theme.outline + border.width: 1 + + x: { + if (!contextMenuTarget) return 0; + // Get position relative to screen + const pos = contextMenuTarget.mapToItem(null, 0, 0); + // Center horizontally above the icon + let xPos = pos.x + (contextMenuTarget.width - width) / 2; + // Constrain to screen edges + return Math.max(0, Math.min(xPos, taskbarWindow.width - width)); + } + + y: { + if (!contextMenuTarget) return 0; + // Position above the dock + const pos = contextMenuTarget.mapToItem(null, 0, 0); + return pos.y - height + 32; + } + + Column { + id: contextMenuColumn + anchors.centerIn: parent + spacing: 4 + width: parent.width + + // Close + Rectangle { + width: parent.width + height: 32 + radius: 16 + color: closeMouseArea.containsMouse ? Theme.surfaceVariant : "transparent" + border.color: Theme.outline + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "close" + font.family: "Material Symbols Outlined" + font.pixelSize: 14 + color: Theme.textPrimary + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "Close" + font.family: Theme.fontFamily + font.pixelSize: 14 + color: Theme.textPrimary + } + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (contextMenuToplevel?.close) contextMenuToplevel.close(); + contextMenuVisible = false; + hidden = true; // Hide the dock here as well + } + } + } + } + + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom + + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack + } + } + + Behavior on opacity { + NumberAnimation { duration: 100 } + } + } + } +} diff --git a/Widgets/Notification/NotificationIcon.qml b/Widgets/Notification/NotificationIcon.qml index 988483a..04d37f2 100644 --- a/Widgets/Notification/NotificationIcon.qml +++ b/Widgets/Notification/NotificationIcon.qml @@ -55,6 +55,7 @@ Item { StyledTooltip { id: notificationTooltip text: "Notification History" + positionAbove: false tooltipVisible: false targetItem: bell delay: 200 diff --git a/Widgets/Sidebar/Config/ProfileSettings.qml b/Widgets/Sidebar/Config/ProfileSettings.qml index 0c74453..eac4e30 100644 --- a/Widgets/Sidebar/Config/ProfileSettings.qml +++ b/Widgets/Sidebar/Config/ProfileSettings.qml @@ -7,7 +7,7 @@ import qs.Settings Rectangle { id: profileSettingsCard Layout.fillWidth: true - Layout.preferredHeight: 650 + Layout.preferredHeight: 690 color: Theme.surface radius: 18 @@ -353,6 +353,61 @@ Rectangle { } } + // Show Dock Setting + RowLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + Text { + text: "Show Dock" + font.pixelSize: 13 + font.bold: true + color: Theme.textPrimary + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + id: dockSwitch + width: 52 + height: 32 + radius: 16 + color: Settings.settings.showDock ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Settings.settings.showDock ? Theme.accentPrimary : Theme.outline + border.width: 2 + + Rectangle { + id: dockThumb + width: 28 + height: 28 + radius: 14 + color: Theme.surface + border.color: Theme.outline + border.width: 1 + y: 2 + x: Settings.settings.showDock ? taskbarSwitch.width - width - 2 : 2 + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.settings.showDock = !Settings.settings.showDock + } + } + } + } + // Show Media In Bar Setting RowLayout { spacing: 8 diff --git a/Widgets/Sidebar/Config/SettingsModal.qml b/Widgets/Sidebar/Config/SettingsModal.qml index d4d4ccf..3b97f86 100644 --- a/Widgets/Sidebar/Config/SettingsModal.qml +++ b/Widgets/Sidebar/Config/SettingsModal.qml @@ -31,7 +31,6 @@ PanelWindow { anchors.leftMargin: 32 anchors.rightMargin: 32 anchors.topMargin: 32 - spacing: 24 // Header @@ -85,14 +84,14 @@ PanelWindow { } } - // Tabs bar (moved here) + // Tabs bar (reordered) Tabs { id: settingsTabs Layout.fillWidth: true tabsModel: [ - { icon: "cloud", label: "Weather" }, { icon: "settings", label: "System" }, - { icon: "wallpaper", label: "Wallpaper" } + { icon: "wallpaper", label: "Wallpaper" }, + { icon: "cloud", label: "Weather" } ] } @@ -115,7 +114,32 @@ PanelWindow { id: tabContentLoader anchors.top: parent.top width: parent.width - sourceComponent: settingsTabs.currentIndex === 0 ? weatherTab : settingsTabs.currentIndex === 1 ? systemTab : wallpaperTab + sourceComponent: settingsTabs.currentIndex === 0 ? systemTab : settingsTabs.currentIndex === 1 ? wallpaperTab : weatherTab + } + } + + Component { + id: systemTab + ColumnLayout { + anchors.fill: parent + ProfileSettings { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + anchors.margins: 16 + } + } + } + + Component { + id: wallpaperTab + ColumnLayout { + anchors.fill: parent + WallpaperSettings { + id: wallpaperSettings + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + anchors.margins: 16 + } } } @@ -130,29 +154,6 @@ PanelWindow { } } } - Component { - id: systemTab - ColumnLayout { - anchors.fill: parent - ProfileSettings { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - anchors.margins: 16 - } - } - } - Component { - id: wallpaperTab - ColumnLayout { - anchors.fill: parent - WallpaperSettings { - id: wallpaperSettings - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - anchors.margins: 16 - } - } - } } } } @@ -160,7 +161,6 @@ PanelWindow { // Function to open the modal and initialize temp values function openSettings() { visible = true; - // Force focus on the text input after a short delay focusTimer.start(); } @@ -174,20 +174,16 @@ PanelWindow { interval: 100 repeat: false onTriggered: { - if (visible) - // Focus will be handled by the individual components - {} - } - } - - // Release focus when modal becomes invisible - onVisibleChanged: { - if (!visible) { - // Focus will be handled by the individual components - if (typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) { - weather.fetchCityWeather(); + if (visible) { + // Focus logic can go here if needed } } } -} + // Refresh weather data when hidden + onVisibleChanged: { + if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) { + weather.fetchCityWeather(); + } + } +} diff --git a/Widgets/Sidebar/Panel/System.qml b/Widgets/Sidebar/Panel/System.qml index f2a6a21..2d8b96a 100644 --- a/Widgets/Sidebar/Panel/System.qml +++ b/Widgets/Sidebar/Panel/System.qml @@ -422,6 +422,12 @@ Rectangle { running: false } + Process { + id: logoutProcess + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false + } + function logout() { if (WorkspaceManager.isNiri) { logoutProcessNiri.running = true; diff --git a/shell.qml b/shell.qml index 737badc..4ae02a5 100644 --- a/shell.qml +++ b/shell.qml @@ -53,6 +53,10 @@ Scope { property var notificationHistoryWin: notificationHistoryWin } + Dock { + id: dock + } + Applauncher { id: appLauncherPanel visible: false