From 5cf04aa941d25c813126973ec6de1e20b568ae1d Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 22 Jul 2025 19:51:16 -0400 Subject: [PATCH] settings: complete re-organize and breakout --- Modules/ControlCenter/ControlCenterPopup.qml | 80 +- Modules/PowerMenuPopup.qml | 4 + Modules/Settings/ClockTab.qml | 17 + Modules/Settings/DisplayTab.qml | 161 +++ Modules/Settings/ProfileTab.qml | 167 +++ Modules/Settings/SettingsPopup.qml | 231 ++++ Modules/{ => Settings}/SettingsSection.qml | 2 +- Modules/{ => Settings}/ThemePicker.qml | 0 Modules/Settings/WeatherTab.qml | 69 + Modules/Settings/WidgetsTab.qml | 62 + Modules/Settings/WorkspaceTab.qml | 26 + Modules/SettingsPopup.qml | 1200 ------------------ Widgets/DankDropdown.qml | 102 ++ Widgets/DankLocationSearch.qml | 309 +++++ Widgets/DankTextField.qml | 4 +- shell.qml | 7 + 16 files changed, 1217 insertions(+), 1224 deletions(-) create mode 100644 Modules/Settings/ClockTab.qml create mode 100644 Modules/Settings/DisplayTab.qml create mode 100644 Modules/Settings/ProfileTab.qml create mode 100644 Modules/Settings/SettingsPopup.qml rename Modules/{ => Settings}/SettingsSection.qml (99%) rename Modules/{ => Settings}/ThemePicker.qml (100%) create mode 100644 Modules/Settings/WeatherTab.qml create mode 100644 Modules/Settings/WidgetsTab.qml create mode 100644 Modules/Settings/WorkspaceTab.qml delete mode 100644 Modules/SettingsPopup.qml create mode 100644 Widgets/DankDropdown.qml create mode 100644 Widgets/DankLocationSearch.qml diff --git a/Modules/ControlCenter/ControlCenterPopup.qml b/Modules/ControlCenter/ControlCenterPopup.qml index 88edda5a..c4e298b9 100644 --- a/Modules/ControlCenter/ControlCenterPopup.qml +++ b/Modules/ControlCenter/ControlCenterPopup.qml @@ -18,6 +18,8 @@ PanelWindow { property string currentTab: "network" // "network", "audio", "bluetooth", "display" property bool powerOptionsExpanded: false + signal powerActionRequested(string action, string title, string message) + visible: controlCenterVisible onVisibleChanged: { // Enable/disable WiFi auto-refresh based on control center visibility @@ -293,6 +295,7 @@ PanelWindow { clip: true DankIcon { + id: dankIcon anchors.centerIn: parent name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new" size: Theme.iconSize - 2 @@ -302,7 +305,7 @@ PanelWindow { // Smooth icon transition SequentialAnimation { NumberAnimation { - target: parent + target: dankIcon property: "opacity" to: 0 duration: Theme.shortDuration / 2 @@ -310,12 +313,12 @@ PanelWindow { } PropertyAction { - target: parent + target: dankIcon property: "name" } NumberAnimation { - target: parent + target: dankIcon property: "opacity" to: 1 duration: Theme.shortDuration / 2 @@ -421,12 +424,7 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { root.powerOptionsExpanded = false; - if (typeof root !== "undefined" && root.powerConfirmDialog) { - root.powerConfirmDialog.powerConfirmAction = "logout"; - root.powerConfirmDialog.powerConfirmTitle = "Logout"; - root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to logout?"; - root.powerConfirmDialog.powerConfirmVisible = true; - } + root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?"); } } @@ -476,12 +474,57 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { root.powerOptionsExpanded = false; - if (typeof root !== "undefined" && root.powerConfirmDialog) { - root.powerConfirmDialog.powerConfirmAction = "reboot"; - root.powerConfirmDialog.powerConfirmTitle = "Restart"; - root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to restart?"; - root.powerConfirmDialog.powerConfirmVisible = true; - } + root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?"); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + + // Suspend + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: suspendButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "bedtime" + size: Theme.fontSizeSmall + color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Suspend" + font.pixelSize: Theme.fontSizeSmall + color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: suspendButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.powerOptionsExpanded = false; + root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?"); } } @@ -531,12 +574,7 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { root.powerOptionsExpanded = false; - if (typeof root !== "undefined" && root.powerConfirmDialog) { - root.powerConfirmDialog.powerConfirmAction = "poweroff"; - root.powerConfirmDialog.powerConfirmTitle = "Shutdown"; - root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to shutdown?"; - root.powerConfirmDialog.powerConfirmVisible = true; - } + root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?"); } } diff --git a/Modules/PowerMenuPopup.qml b/Modules/PowerMenuPopup.qml index 282f9732..688d2683 100644 --- a/Modules/PowerMenuPopup.qml +++ b/Modules/PowerMenuPopup.qml @@ -11,6 +11,10 @@ PanelWindow { id: root property bool powerMenuVisible: false + property bool powerConfirmVisible: false + property string powerConfirmAction: "" + property string powerConfirmTitle: "" + property string powerConfirmMessage: "" visible: powerMenuVisible implicitWidth: 400 diff --git a/Modules/Settings/ClockTab.qml b/Modules/Settings/ClockTab.qml new file mode 100644 index 00000000..0c77adb1 --- /dev/null +++ b/Modules/Settings/ClockTab.qml @@ -0,0 +1,17 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + width: parent.width + spacing: Theme.spacingM + + DankToggle { + text: "24-Hour Format" + description: "Use 24-hour time format instead of 12-hour AM/PM" + checked: Prefs.use24HourClock + onToggled: (checked) => { + return Prefs.setClockFormat(checked); + } + } +} \ No newline at end of file diff --git a/Modules/Settings/DisplayTab.qml b/Modules/Settings/DisplayTab.qml new file mode 100644 index 00000000..06aee179 --- /dev/null +++ b/Modules/Settings/DisplayTab.qml @@ -0,0 +1,161 @@ +import QtQuick +import Quickshell.Io +import qs.Common +import qs.Widgets + +Column { + id: root + width: parent.width + spacing: Theme.spacingL + + DankToggle { + text: "Night Mode" + description: "Apply warm color temperature to reduce eye strain" + checked: Prefs.nightModeEnabled + onToggled: (checked) => { + Prefs.setNightModeEnabled(checked); + if (checked) + nightModeEnableProcess.running = true; + else + nightModeDisableProcess.running = true; + } + } + + DankToggle { + text: "Light Mode" + description: "Use light theme instead of dark theme" + checked: Prefs.isLightMode + onToggled: (checked) => { + Prefs.setLightMode(checked); + Theme.isLightMode = checked; + } + } + + DankDropdown { + text: "Icon Theme" + description: "Select icon theme (requires restart)" + currentValue: Prefs.iconTheme + options: Prefs.availableIconThemes + onValueChanged: (value) => { + Prefs.setIconTheme(value); + } + } + + // Top Bar Transparency + Column { + width: parent.width + spacing: Theme.spacingS + + Text { + text: "Top Bar Transparency" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + value: Math.round(Prefs.topBarTransparency * 100) + minimum: 0 + maximum: 100 + leftIcon: "opacity" + rightIcon: "circle" + unit: "%" + showValue: true + onSliderDragFinished: (finalValue) => { + let transparencyValue = finalValue / 100; + Prefs.setTopBarTransparency(transparencyValue); + } + } + + Text { + text: "Adjust the transparency of the top bar background" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + } + + // Popup Transparency + Column { + width: parent.width + spacing: Theme.spacingS + + Text { + text: "Popup Transparency" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + value: Math.round(Prefs.popupTransparency * 100) + minimum: 0 + maximum: 100 + leftIcon: "blur_on" + rightIcon: "circle" + unit: "%" + showValue: true + onSliderDragFinished: (finalValue) => { + let transparencyValue = finalValue / 100; + Prefs.setPopupTransparency(transparencyValue); + } + } + + Text { + text: "Adjust transparency for dialogs, menus, and popups" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + } + + // Theme Picker + Column { + width: parent.width + spacing: Theme.spacingS + + Text { + text: "Theme Color" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + ThemePicker { + anchors.horizontalCenter: parent.horizontalCenter + } + + } + + // Night mode processes + Process { + id: nightModeEnableProcess + + command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"] + running: false + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("Failed to enable night mode"); + Prefs.setNightModeEnabled(false); + } + } + } + + Process { + id: nightModeDisableProcess + + command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"] + running: false + onExited: (exitCode) => { + if (exitCode !== 0) + console.warn("Failed to disable night mode"); + + } + } +} \ No newline at end of file diff --git a/Modules/Settings/ProfileTab.qml b/Modules/Settings/ProfileTab.qml new file mode 100644 index 00000000..ec891c7b --- /dev/null +++ b/Modules/Settings/ProfileTab.qml @@ -0,0 +1,167 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Widgets + +Column { + width: parent.width + spacing: Theme.spacingM + + // Profile Image Preview and Input + Column { + width: parent.width + spacing: Theme.spacingM + + Text { + text: "Profile Image" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + // Profile Image Preview with circular crop + Row { + width: parent.width + spacing: Theme.spacingM + + // Circular profile image preview + Item { + id: avatarContainer + + property bool hasImage: avatarImageSource.status === Image.Ready + + width: 54 + height: 54 + + // This rectangle provides the themed ring via its border. + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "transparent" + border.color: Theme.primary + border.width: 1 // The ring is 1px thick. + visible: parent.hasImage + } + + // Hidden Image loader. Its only purpose is to load the texture. + Image { + id: avatarImageSource + + source: { + if (profileImageInput.text === "") + return ""; + + if (profileImageInput.text.startsWith("/")) + return "file://" + profileImageInput.text; + + return profileImageInput.text; + } + smooth: true + asynchronous: true + mipmap: true + cache: true + visible: false // This item is never shown directly. + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 5 + source: avatarImageSource + maskEnabled: true + maskSource: settingsCircularMask + visible: avatarContainer.hasImage + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: settingsCircularMask + + width: 54 - 10 + height: 54 - 10 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + + } + + // Fallback for when there is no image. + Rectangle { + anchors.fill: parent + radius: width / 2 + color: Theme.primary + visible: !parent.hasImage + + DankIcon { + anchors.centerIn: parent + name: "person" + size: Theme.iconSize + 8 + color: Theme.primaryText + } + + } + + // Error icon for when the image fails to load. + DankIcon { + anchors.centerIn: parent + name: "warning" + size: Theme.iconSize + 8 + color: Theme.primaryText + visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error + } + + } + + // Input field + Column { + width: parent.width - 80 - Theme.spacingM + spacing: Theme.spacingS + + Rectangle { + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + border.color: profileImageInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + border.width: profileImageInput.activeFocus ? 2 : 1 + + DankTextField { + id: profileImageInput + + anchors.fill: parent + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + text: Prefs.profileImage + placeholderText: "Enter image path or URL..." + backgroundColor: "transparent" + normalBorderColor: "transparent" + focusedBorderColor: "transparent" + onEditingFinished: { + Prefs.setProfileImage(text); + } + + } + + } + + Text { + text: "Local filesystem path or URL to an image file." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + } + + } + + } +} \ No newline at end of file diff --git a/Modules/Settings/SettingsPopup.qml b/Modules/Settings/SettingsPopup.qml new file mode 100644 index 00000000..e7b4bb89 --- /dev/null +++ b/Modules/Settings/SettingsPopup.qml @@ -0,0 +1,231 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Widgets +import qs.Modules.Settings + +PanelWindow { + id: settingsPopup + + property bool settingsVisible: false + + signal closingPopup() + + onSettingsVisibleChanged: { + if (!settingsVisible) { + closingPopup(); + // Hide any open dropdown when settings close + if (typeof globalDropdownWindow !== 'undefined') { + globalDropdownWindow.hide(); + } + } + } + visible: settingsVisible + implicitWidth: 600 + implicitHeight: 700 + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + // Darkened background + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.5 + + MouseArea { + anchors.fill: parent + onClicked: settingsPopup.settingsVisible = false + } + + } + + // Main settings panel - spotlight-like centered appearance + Rectangle { + id: mainPanel + + width: Math.min(600, parent.width - Theme.spacingXL * 2) + height: Math.min(700, parent.height - Theme.spacingXL * 2) + anchors.centerIn: parent + color: Theme.popupBackground() + radius: Theme.cornerRadiusLarge + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + // Simple opacity and scale control tied directly to settingsVisible + opacity: settingsPopup.settingsVisible ? 1 : 0 + scale: settingsPopup.settingsVisible ? 1 : 0.95 + // Add shadow effect + layer.enabled: true + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + // Header + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "settings" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Settings" + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 175 // Spacer to push close button to the right + height: 1 + } + + // Close button + DankActionButton { + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + onClicked: settingsPopup.settingsVisible = false + } + + } + + // Settings sections + Flickable { + id: settingsScrollView + width: parent.width + height: parent.height - 80 + clip: true + contentHeight: settingsColumn.height + boundsBehavior: Flickable.DragAndOvershootBounds + flickDeceleration: 8000 + maximumFlickVelocity: 15000 + + property real wheelStepSize: 60 + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + onWheel: (wheel) => { + var delta = wheel.angleDelta.y + var steps = delta / 120 + settingsScrollView.contentY -= steps * settingsScrollView.wheelStepSize + + // Keep within bounds + if (settingsScrollView.contentY < 0) + settingsScrollView.contentY = 0 + else if (settingsScrollView.contentY > settingsScrollView.contentHeight - settingsScrollView.height) + settingsScrollView.contentY = Math.max(0, settingsScrollView.contentHeight - settingsScrollView.height) + } + } + + Column { + id: settingsColumn + width: parent.width + spacing: Theme.spacingL + + // Profile Settings + SettingsSection { + title: "Profile" + iconName: "person" + content: ProfileTab {} + } + + // Clock Settings + SettingsSection { + title: "Clock & Time" + iconName: "schedule" + content: ClockTab {} + } + + // Weather Settings + SettingsSection { + title: "Weather" + iconName: "wb_sunny" + content: WeatherTab {} + } + + // Widget Visibility Settings + SettingsSection { + title: "Top Bar Widgets" + iconName: "widgets" + content: WidgetsTab {} + } + + // Workspace Settings + SettingsSection { + title: "Workspaces" + iconName: "tab" + content: WorkspaceTab {} + } + + // Display Settings + SettingsSection { + title: "Display & Appearance" + iconName: "palette" + content: DisplayTab {} + } + + } + + } + + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + + } + + // Keyboard focus and shortcuts + FocusScope { + anchors.fill: parent + focus: settingsPopup.settingsVisible + Keys.onEscapePressed: settingsPopup.settingsVisible = false + } + +} \ No newline at end of file diff --git a/Modules/SettingsSection.qml b/Modules/Settings/SettingsSection.qml similarity index 99% rename from Modules/SettingsSection.qml rename to Modules/Settings/SettingsSection.qml index 9ad869b4..5c691bc1 100644 --- a/Modules/SettingsSection.qml +++ b/Modules/Settings/SettingsSection.qml @@ -48,4 +48,4 @@ Column { width: parent.width } -} +} \ No newline at end of file diff --git a/Modules/ThemePicker.qml b/Modules/Settings/ThemePicker.qml similarity index 100% rename from Modules/ThemePicker.qml rename to Modules/Settings/ThemePicker.qml diff --git a/Modules/Settings/WeatherTab.qml b/Modules/Settings/WeatherTab.qml new file mode 100644 index 00000000..c047815d --- /dev/null +++ b/Modules/Settings/WeatherTab.qml @@ -0,0 +1,69 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + width: parent.width + spacing: Theme.spacingM + + DankToggle { + text: "Fahrenheit" + description: "Use Fahrenheit instead of Celsius for temperature" + checked: Prefs.useFahrenheit + onToggled: (checked) => { + return Prefs.setTemperatureUnit(checked); + } + } + + // Weather Location Override + Column { + width: parent.width + spacing: Theme.spacingM + + DankToggle { + text: "Override Location" + description: "Use a specific location instead of auto-detection" + checked: Prefs.weatherLocationOverrideEnabled + onToggled: (checked) => Prefs.setWeatherLocationOverrideEnabled(checked) + } + + // Location input - only visible when override is enabled + Column { + width: parent.width + spacing: Theme.spacingS + visible: Prefs.weatherLocationOverrideEnabled + opacity: visible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Text { + text: "Location" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankLocationSearch { + width: parent.width + currentLocation: Prefs.weatherLocationOverride + placeholderText: "Search for a location..." + onLocationSelected: (displayName, coordinates) => { + Prefs.setWeatherLocationOverride(coordinates) + } + } + + Text { + text: "Examples: \"New York\", \"Tokyo\", \"44511\"" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + } +} \ No newline at end of file diff --git a/Modules/Settings/WidgetsTab.qml b/Modules/Settings/WidgetsTab.qml new file mode 100644 index 00000000..f398836c --- /dev/null +++ b/Modules/Settings/WidgetsTab.qml @@ -0,0 +1,62 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + width: parent.width + spacing: Theme.spacingM + + DankToggle { + text: "Focused Window" + description: "Show the currently focused application in the top bar" + checked: Prefs.showFocusedWindow + onToggled: (checked) => { + return Prefs.setShowFocusedWindow(checked); + } + } + + DankToggle { + text: "Weather Widget" + description: "Display weather information in the top bar" + checked: Prefs.showWeather + onToggled: (checked) => { + return Prefs.setShowWeather(checked); + } + } + + DankToggle { + text: "Media Controls" + description: "Show currently playing media in the top bar" + checked: Prefs.showMusic + onToggled: (checked) => { + return Prefs.setShowMusic(checked); + } + } + + DankToggle { + text: "Clipboard Button" + description: "Show clipboard access button in the top bar" + checked: Prefs.showClipboard + onToggled: (checked) => { + return Prefs.setShowClipboard(checked); + } + } + + DankToggle { + text: "System Resources" + description: "Display CPU and RAM usage indicators" + checked: Prefs.showSystemResources + onToggled: (checked) => { + return Prefs.setShowSystemResources(checked); + } + } + + DankToggle { + text: "System Tray" + description: "Show system tray icons in the top bar" + checked: Prefs.showSystemTray + onToggled: (checked) => { + return Prefs.setShowSystemTray(checked); + } + } +} \ No newline at end of file diff --git a/Modules/Settings/WorkspaceTab.qml b/Modules/Settings/WorkspaceTab.qml new file mode 100644 index 00000000..bd5e6470 --- /dev/null +++ b/Modules/Settings/WorkspaceTab.qml @@ -0,0 +1,26 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + width: parent.width + spacing: Theme.spacingM + + DankToggle { + text: "Workspace Index Numbers" + description: "Show workspace index numbers in the top bar workspace switcher" + checked: Prefs.showWorkspaceIndex + onToggled: (checked) => { + return Prefs.setShowWorkspaceIndex(checked); + } + } + + DankToggle { + text: "Workspace Padding" + description: "Always show a minimum of 3 workspaces, even if fewer are available" + checked: Prefs.showWorkspacePadding + onToggled: (checked) => { + return Prefs.setShowWorkspacePadding(checked); + } + } +} \ No newline at end of file diff --git a/Modules/SettingsPopup.qml b/Modules/SettingsPopup.qml deleted file mode 100644 index f05a3e44..00000000 --- a/Modules/SettingsPopup.qml +++ /dev/null @@ -1,1200 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Widgets -import qs.Common -import qs.Widgets - -PanelWindow { - id: settingsPopup - - property bool settingsVisible: false - - signal closingPopup() - - onSettingsVisibleChanged: { - if (!settingsVisible) { - closingPopup(); - // Hide any open dropdown when settings close - if (typeof globalDropdownWindow !== 'undefined') { - globalDropdownWindow.hide(); - } - } - } - visible: settingsVisible - implicitWidth: 600 - implicitHeight: 700 - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - color: "transparent" - - // SettingsDropdown component - only used within this popup - Component { - id: settingsDropdownComponent - - Rectangle { - id: dropdownRoot - - property string text: "" - property string description: "" - property string currentValue: "" - property var options: [] - - signal valueChanged(string value) - - width: parent.width - height: 60 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) - - Column { - anchors.left: parent.left - anchors.right: dropdown.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Theme.spacingM - anchors.rightMargin: Theme.spacingM - spacing: Theme.spacingXS - - Text { - text: dropdownRoot.text - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - Text { - text: dropdownRoot.description - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - visible: description.length > 0 - wrapMode: Text.WordWrap - width: parent.width - } - } - - Rectangle { - id: dropdown - - width: 180 - height: 36 - anchors.right: parent.right - anchors.rightMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - radius: Theme.cornerRadiusSmall - color: dropdownArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.contentBackground() - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - border.width: 1 - - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Theme.spacingM - anchors.rightMargin: Theme.spacingS - - Text { - text: dropdownRoot.currentValue - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 24 - elide: Text.ElideRight - } - - DankIcon { - name: "expand_more" - size: 20 - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: dropdownArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (typeof globalDropdownWindow !== 'undefined') { - // Get global position of the dropdown button - var globalPos = dropdown.mapToGlobal(0, 0); - globalDropdownWindow.showAt(dropdownRoot, globalPos.x, globalPos.y + dropdown.height + 4, dropdownRoot.options, dropdownRoot.currentValue); - - // Connect to value selection (with cleanup) - globalDropdownWindow.valueSelected.connect(function(value) { - dropdownRoot.currentValue = value; - dropdownRoot.valueChanged(value); - }); - } - } - } - } - } - } - - anchors { - top: true - left: true - right: true - bottom: true - } - - // Darkened background - Rectangle { - anchors.fill: parent - color: "black" - opacity: 0.5 - - MouseArea { - anchors.fill: parent - onClicked: settingsPopup.settingsVisible = false - } - - } - - // Main settings panel - spotlight-like centered appearance - Rectangle { - id: mainPanel - - width: Math.min(600, parent.width - Theme.spacingXL * 2) - height: Math.min(700, parent.height - Theme.spacingXL * 2) - anchors.centerIn: parent - color: Theme.popupBackground() - radius: Theme.cornerRadiusLarge - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 - // Simple opacity and scale control tied directly to settingsVisible - opacity: settingsPopup.settingsVisible ? 1 : 0 - scale: settingsPopup.settingsVisible ? 1 : 0.95 - // Add shadow effect - layer.enabled: true - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingL - - // Header - Row { - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: "settings" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: "Settings" - font.pixelSize: Theme.fontSizeXLarge - color: Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: parent.width - 175 // Spacer to push close button to the right - height: 1 - } - - // Close button - DankActionButton { - circular: false - iconName: "close" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) - onClicked: settingsPopup.settingsVisible = false - } - - } - - // Settings sections - Flickable { - id: settingsScrollView - width: parent.width - height: parent.height - 80 - clip: true - contentHeight: settingsColumn.height - boundsBehavior: Flickable.DragAndOvershootBounds - flickDeceleration: 8000 - maximumFlickVelocity: 15000 - - property real wheelStepSize: 60 - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - propagateComposedEvents: true - z: -1 - onWheel: (wheel) => { - var delta = wheel.angleDelta.y - var steps = delta / 120 - settingsScrollView.contentY -= steps * settingsScrollView.wheelStepSize - - // Keep within bounds - if (settingsScrollView.contentY < 0) - settingsScrollView.contentY = 0 - else if (settingsScrollView.contentY > settingsScrollView.contentHeight - settingsScrollView.height) - settingsScrollView.contentY = Math.max(0, settingsScrollView.contentHeight - settingsScrollView.height) - } - } - - Column { - id: settingsColumn - width: parent.width - spacing: Theme.spacingL - - // Profile Settings - SettingsSection { - title: "Profile" - iconName: "person" - - content: Column { - width: parent.width - spacing: Theme.spacingM - - // Profile Image Preview and Input - Column { - width: parent.width - spacing: Theme.spacingM - - Text { - text: "Profile Image" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - // Profile Image Preview with circular crop - Row { - width: parent.width - spacing: Theme.spacingM - - // Circular profile image preview - Item { - id: avatarContainer - - property bool hasImage: avatarImageSource.status === Image.Ready - - width: 54 - height: 54 - - // This rectangle provides the themed ring via its border. - Rectangle { - anchors.fill: parent - radius: width / 2 - color: "transparent" - border.color: Theme.primary - border.width: 1 // The ring is 1px thick. - visible: parent.hasImage - } - - // Hidden Image loader. Its only purpose is to load the texture. - Image { - id: avatarImageSource - - source: { - if (profileImageInput.text === "") - return ""; - - if (profileImageInput.text.startsWith("/")) - return "file://" + profileImageInput.text; - - return profileImageInput.text; - } - smooth: true - asynchronous: true - mipmap: true - cache: true - visible: false // This item is never shown directly. - } - - MultiEffect { - anchors.fill: parent - anchors.margins: 5 - source: avatarImageSource - maskEnabled: true - maskSource: settingsCircularMask - visible: avatarContainer.hasImage - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - - Item { - id: settingsCircularMask - - width: 54 - 10 - height: 54 - 10 - layer.enabled: true - layer.smooth: true - visible: false - - Rectangle { - anchors.fill: parent - radius: width / 2 - color: "black" - antialiasing: true - } - - } - - // Fallback for when there is no image. - Rectangle { - anchors.fill: parent - radius: width / 2 - color: Theme.primary - visible: !parent.hasImage - - DankIcon { - anchors.centerIn: parent - name: "person" - size: Theme.iconSize + 8 - color: Theme.primaryText - } - - } - - // Error icon for when the image fails to load. - DankIcon { - anchors.centerIn: parent - name: "warning" - size: Theme.iconSize + 8 - color: Theme.primaryText - visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error - } - - } - - // Input field - Column { - width: parent.width - 80 - Theme.spacingM - spacing: Theme.spacingS - - Rectangle { - width: parent.width - height: 48 - radius: Theme.cornerRadius - color: Theme.surfaceVariant - border.color: profileImageInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) - border.width: profileImageInput.activeFocus ? 2 : 1 - - DankTextField { - id: profileImageInput - - anchors.fill: parent - textColor: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - text: Prefs.profileImage - placeholderText: "Enter image path or URL..." - backgroundColor: "transparent" - normalBorderColor: "transparent" - focusedBorderColor: "transparent" - onEditingFinished: { - Prefs.setProfileImage(text); - } - - } - - } - - Text { - text: "Local filesystem path or URL to an image file." - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - width: parent.width - } - - } - - } - - } - - } - - } - - // Clock Settings - SettingsSection { - title: "Clock & Time" - iconName: "schedule" - - content: Column { - width: parent.width - spacing: Theme.spacingM - - DankToggle { - text: "24-Hour Format" - description: "Use 24-hour time format instead of 12-hour AM/PM" - checked: Prefs.use24HourClock - onToggled: (checked) => { - return Prefs.setClockFormat(checked); - } - } - - } - - } - - - // Weather Settings - SettingsSection { - title: "Weather" - iconName: "wb_sunny" - - content: Column { - width: parent.width - spacing: Theme.spacingM - - DankToggle { - text: "Fahrenheit" - description: "Use Fahrenheit instead of Celsius for temperature" - checked: Prefs.useFahrenheit - onToggled: (checked) => { - return Prefs.setTemperatureUnit(checked); - } - } - - // Weather Location Override - Column { - width: parent.width - spacing: Theme.spacingM - - DankToggle { - text: "Override Location" - description: "Use a specific location instead of auto-detection" - checked: Prefs.weatherLocationOverrideEnabled - onToggled: (checked) => Prefs.setWeatherLocationOverrideEnabled(checked) - } - - // Location input - only visible when override is enabled - Column { - width: parent.width - spacing: Theme.spacingS - visible: Prefs.weatherLocationOverrideEnabled - opacity: visible ? 1.0 : 0.0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } - - Text { - text: "Location" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - // Weather Location Search Component - Item { - id: weatherLocationSearchComponent - width: parent.width - height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0) - - property bool _internalChange: false - property bool isLoading: false - property string helperTextState: "default" // "default", "prompt", "searching", "found", "not_found" - property string currentSearchText: "" - - ListModel { - id: searchResultsModel - } - - Connections { - target: settingsPopup - function onClosingPopup() { - weatherLocationSearchComponent.resetSearchState() - } - } - - function resetSearchState() { - locationSearchTimer.stop() - dropdownHideTimer.stop() - if (locationSearcher.running) { - locationSearcher.running = false; - } - isLoading = false - searchResultsModel.clear() - helperTextState = "default" - } - - Timer { - id: locationSearchTimer - interval: 500 - running: false - repeat: false - onTriggered: { - if (weatherLocationInput.text.length > 2) { - // Stop any running search first - if (locationSearcher.running) { - locationSearcher.running = false - } - - searchResultsModel.clear() - weatherLocationSearchComponent.isLoading = true - weatherLocationSearchComponent.helperTextState = "searching" - - const searchLocation = weatherLocationInput.text - weatherLocationSearchComponent.currentSearchText = searchLocation - const encodedLocation = encodeURIComponent(searchLocation) - const curlCommand = `curl -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'` - - locationSearcher.command = ["bash", "-c", curlCommand] - locationSearcher.running = true - } - } - } - - Timer { - id: dropdownHideTimer - interval: 200 // Short delay to allow clicks - running: false - repeat: false - onTriggered: { - if (!weatherLocationInput.activeFocus && !searchDropdown.hovered) { - weatherLocationSearchComponent.resetSearchState() - } - } - } - - Process { - id: locationSearcher - command: ["bash", "-c", "echo"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - // Only process if this is still the current search - if (weatherLocationSearchComponent.currentSearchText !== weatherLocationInput.text) { - return - } - - const raw = text.trim() - weatherLocationSearchComponent.isLoading = false - searchResultsModel.clear() - - if (!raw || raw[0] !== "[") { - weatherLocationSearchComponent.helperTextState = "not_found" - return - } - - try { - const data = JSON.parse(raw) - if (data.length === 0) { - weatherLocationSearchComponent.helperTextState = "not_found" - return - } - - for (let i = 0; i < Math.min(data.length, 5); i++) { - const location = data[i] - if (location.display_name && location.lat && location.lon) { - const parts = location.display_name.split(', ') - let cleanName = parts[0] - if (parts.length > 1) { - const state = parts[parts.length - 2] - if (state && state !== cleanName) { - cleanName += `, ${state}` - } - } - const query = `${location.lat},${location.lon}` - searchResultsModel.append({ "name": cleanName, "query": query }) - } - } - weatherLocationSearchComponent.helperTextState = "found" - } catch (e) { - weatherLocationSearchComponent.helperTextState = "not_found" - } - } - } - - onExited: (exitCode) => { - weatherLocationSearchComponent.isLoading = false - if (exitCode !== 0) { - searchResultsModel.clear() - weatherLocationSearchComponent.helperTextState = "not_found" - } - } - } - - // Search input field - Rectangle { - id: searchInputField - width: parent.width - height: 48 - radius: Theme.cornerRadius - color: Theme.surfaceVariant - border.color: weatherLocationInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) - border.width: weatherLocationInput.activeFocus ? 2 : 1 - - Row { - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - DankIcon { - name: "search" - size: Theme.iconSize - 4 - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - TextInput { - id: weatherLocationInput - width: parent.width - 20 - Theme.spacingS * 3 - height: parent.height - verticalAlignment: TextInput.AlignVCenter - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - text: Prefs.weatherLocationOverride - selectByMouse: true - - onTextChanged: { - if (weatherLocationSearchComponent._internalChange) return - if (activeFocus) { - if (text.length > 2) { - weatherLocationSearchComponent.isLoading = true - weatherLocationSearchComponent.helperTextState = "searching" - locationSearchTimer.restart() - } else { - weatherLocationSearchComponent.resetSearchState() - weatherLocationSearchComponent.helperTextState = "prompt" - } - } - } - - onEditingFinished: { - if (!searchDropdown.visible) { - Prefs.setWeatherLocationOverride(text) - } - } - - onActiveFocusChanged: { - if (activeFocus) { - dropdownHideTimer.stop() - if (weatherLocationInput.text.length <= 2) { - weatherLocationSearchComponent.helperTextState = "prompt" - } - } - else { - dropdownHideTimer.start() - } - } - - // Placeholder text - Text { - anchors.verticalCenter: parent.verticalCenter - text: "Search for a location..." - color: Qt.rgba(Theme.surfaceVariantText.r, Theme.surfaceVariantText.g, Theme.surfaceVariantText.b, 0.6) - font.pixelSize: Theme.fontSizeMedium - visible: weatherLocationInput.text.length === 0 && !weatherLocationInput.activeFocus - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - } - - // Status icon - DankIcon { - name: { - if (weatherLocationSearchComponent.isLoading) return "hourglass_empty" - if (searchResultsModel.count > 0) return "check_circle" - if (weatherLocationInput.activeFocus && weatherLocationInput.text.length > 2 && !weatherLocationSearchComponent.isLoading) return "error" - return "" - } - size: Theme.iconSize - 4 - color: { - if (weatherLocationSearchComponent.isLoading) return Theme.surfaceVariantText - if (searchResultsModel.count > 0) return Theme.success || Theme.primary - if (weatherLocationInput.activeFocus && weatherLocationInput.text.length > 2) return Theme.error - return "transparent" - } - anchors.verticalCenter: parent.verticalCenter - opacity: (weatherLocationInput.activeFocus && weatherLocationInput.text.length > 2) ? 1.0 : 0.0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - } - } - - // Search results dropdown - Rectangle { - id: searchDropdown - width: parent.width - height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200) - - y: searchInputField.height - radius: Theme.cornerRadius - color: Theme.popupBackground() - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) - border.width: 1 - visible: weatherLocationInput.activeFocus && weatherLocationInput.text.length > 2 && (searchResultsModel.count > 0 || weatherLocationSearchComponent.isLoading) - - - property bool hovered: false - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - parent.hovered = true - dropdownHideTimer.stop() - } - onExited: { - parent.hovered = false - if (!weatherLocationInput.activeFocus) { - dropdownHideTimer.start() - } - } - acceptedButtons: Qt.NoButton - } - - Item { - anchors.fill: parent - anchors.margins: Theme.spacingS - - ListView { - id: searchResultsList - anchors.fill: parent - clip: true - model: searchResultsModel - spacing: 2 - - - delegate: Rectangle { - width: searchResultsList.width - height: 36 - radius: Theme.cornerRadius - color: resultMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" - - - Row { - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - DankIcon { - name: "place" - size: Theme.iconSize - 6 - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: model.name || "Unknown" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - width: parent.width - 30 - } - } - - MouseArea { - id: resultMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - weatherLocationSearchComponent._internalChange = true - const selectedName = model.name - const selectedQuery = model.query - - weatherLocationInput.text = selectedName - Prefs.setWeatherLocationOverride(selectedQuery) - - weatherLocationSearchComponent.resetSearchState() - weatherLocationInput.focus = false - weatherLocationSearchComponent._internalChange = false - } - } - } - } - - // Show message when no results - Text { - anchors.centerIn: parent - text: weatherLocationSearchComponent.isLoading ? "Searching..." : "No locations found" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - visible: searchResultsList.count === 0 && weatherLocationInput.text.length > 2 - } - } - } - } - - Text { - text: { - switch (weatherLocationSearchComponent.helperTextState) { - case "default": - return "Examples: \"New York\", \"Tokyo\", \"44511\"" - case "prompt": - return "Enter 3+ characters to search." - case "searching": - return "Searching for locations..." - case "found": - return `${searchResultsModel.count} location${searchResultsModel.count > 1 ? 's' : ''} found. Click to select.` - case "not_found": - return "No locations found. Try a different search term." - } - } - font.pixelSize: Theme.fontSizeSmall - color: { - switch (weatherLocationSearchComponent.helperTextState) { - case "found": - return Theme.success || Theme.primary - case "not_found": - return Theme.error - default: - return Theme.surfaceVariantText - } - } - wrapMode: Text.WordWrap - width: parent.width - } - } - } - - } - - } - - // Widget Visibility Settings - SettingsSection { - title: "Top Bar Widgets" - iconName: "widgets" - - content: Column { - width: parent.width - spacing: Theme.spacingM - - DankToggle { - text: "Focused Window" - description: "Show the currently focused application in the top bar" - checked: Prefs.showFocusedWindow - onToggled: (checked) => { - return Prefs.setShowFocusedWindow(checked); - } - } - - DankToggle { - text: "Weather Widget" - description: "Display weather information in the top bar" - checked: Prefs.showWeather - onToggled: (checked) => { - return Prefs.setShowWeather(checked); - } - } - - DankToggle { - text: "Media Controls" - description: "Show currently playing media in the top bar" - checked: Prefs.showMusic - onToggled: (checked) => { - return Prefs.setShowMusic(checked); - } - } - - DankToggle { - text: "Clipboard Button" - description: "Show clipboard access button in the top bar" - checked: Prefs.showClipboard - onToggled: (checked) => { - return Prefs.setShowClipboard(checked); - } - } - - DankToggle { - text: "System Resources" - description: "Display CPU and RAM usage indicators" - checked: Prefs.showSystemResources - onToggled: (checked) => { - return Prefs.setShowSystemResources(checked); - } - } - - DankToggle { - text: "System Tray" - description: "Show system tray icons in the top bar" - checked: Prefs.showSystemTray - onToggled: (checked) => { - return Prefs.setShowSystemTray(checked); - } - } - - - } - - } - - // Workspace Settings - SettingsSection { - title: "Workspaces" - iconName: "tab" - - content: Column { - width: parent.width - spacing: Theme.spacingM - - DankToggle { - text: "Workspace Index Numbers" - description: "Show workspace index numbers in the top bar workspace switcher" - checked: Prefs.showWorkspaceIndex - onToggled: (checked) => { - return Prefs.setShowWorkspaceIndex(checked); - } - } - - DankToggle { - text: "Workspace Padding" - description: "Always show a minimum of 3 workspaces, even if fewer are available" - checked: Prefs.showWorkspacePadding - onToggled: (checked) => { - return Prefs.setShowWorkspacePadding(checked); - } - } - - } - - } - - // Display Settings - SettingsSection { - title: "Display & Appearance" - iconName: "palette" - - content: Column { - width: parent.width - spacing: Theme.spacingL - - DankToggle { - text: "Night Mode" - description: "Apply warm color temperature to reduce eye strain" - checked: Prefs.nightModeEnabled - onToggled: (checked) => { - Prefs.setNightModeEnabled(checked); - if (checked) - nightModeEnableProcess.running = true; - else - nightModeDisableProcess.running = true; - } - } - - DankToggle { - text: "Light Mode" - description: "Use light theme instead of dark theme" - checked: Prefs.isLightMode - onToggled: (checked) => { - Prefs.setLightMode(checked); - Theme.isLightMode = checked; - } - } - - Loader { - width: parent.width - sourceComponent: settingsDropdownComponent - onLoaded: { - item.text = "Icon Theme" - item.description = "Select icon theme (requires restart)" - item.currentValue = Prefs.iconTheme - // Set initial options, will be updated when detection completes - item.options = Qt.binding(function() { return Prefs.availableIconThemes; }) - item.valueChanged.connect(function(value) { - Prefs.setIconTheme(value); - }) - } - - // Update options when available themes change - Connections { - target: Prefs - function onAvailableIconThemesChanged() { - if (parent.item && parent.item.hasOwnProperty('options')) { - parent.item.options = Prefs.availableIconThemes; - } - } - } - } - - // Top Bar Transparency - Column { - width: parent.width - spacing: Theme.spacingS - - Text { - text: "Top Bar Transparency" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - DankSlider { - width: parent.width - value: Math.round(Prefs.topBarTransparency * 100) - minimum: 0 - maximum: 100 - leftIcon: "opacity" - rightIcon: "circle" - unit: "%" - showValue: true - onSliderDragFinished: (finalValue) => { - let transparencyValue = finalValue / 100; - Prefs.setTopBarTransparency(transparencyValue); - } - } - - Text { - text: "Adjust the transparency of the top bar background" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - width: parent.width - } - - } - - // Popup Transparency - Column { - width: parent.width - spacing: Theme.spacingS - - Text { - text: "Popup Transparency" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - DankSlider { - width: parent.width - value: Math.round(Prefs.popupTransparency * 100) - minimum: 0 - maximum: 100 - leftIcon: "blur_on" - rightIcon: "circle" - unit: "%" - showValue: true - onSliderDragFinished: (finalValue) => { - let transparencyValue = finalValue / 100; - Prefs.setPopupTransparency(transparencyValue); - } - } - - Text { - text: "Adjust transparency for dialogs, menus, and popups" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - width: parent.width - } - - } - - // Theme Picker - Column { - width: parent.width - spacing: Theme.spacingS - - Text { - text: "Theme Color" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - ThemePicker { - anchors.horizontalCenter: parent.horizontalCenter - } - - } - - } - - } - - } - - } - - } - - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - - } - - Behavior on scale { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - - } - - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1 - shadowColor: Qt.rgba(0, 0, 0, 0.3) - shadowOpacity: 0.3 - } - - } - - // Night mode processes - Process { - id: nightModeEnableProcess - - command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"] - running: false - onExited: (exitCode) => { - if (exitCode !== 0) { - console.warn("Failed to enable night mode"); - Prefs.setNightModeEnabled(false); - } - } - } - - Process { - id: nightModeDisableProcess - - command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"] - running: false - onExited: (exitCode) => { - if (exitCode !== 0) - console.warn("Failed to disable night mode"); - - } - } - - // Keyboard focus and shortcuts - FocusScope { - anchors.fill: parent - focus: settingsPopup.settingsVisible - Keys.onEscapePressed: settingsPopup.settingsVisible = false - } - -} diff --git a/Widgets/DankDropdown.qml b/Widgets/DankDropdown.qml new file mode 100644 index 00000000..92e976f6 --- /dev/null +++ b/Widgets/DankDropdown.qml @@ -0,0 +1,102 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string text: "" + property string description: "" + property string currentValue: "" + property var options: [] + + signal valueChanged(string value) + + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) + + Column { + anchors.left: parent.left + anchors.right: dropdown.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + + Text { + text: root.text + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Text { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: description.length > 0 + wrapMode: Text.WordWrap + width: parent.width + } + } + + Rectangle { + id: dropdown + + width: 180 + height: 36 + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + radius: Theme.cornerRadiusSmall + color: dropdownArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.contentBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingS + + Text { + text: root.currentValue + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 24 + elide: Text.ElideRight + } + + DankIcon { + name: "expand_more" + size: 20 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: dropdownArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (typeof globalDropdownWindow !== 'undefined') { + // Get global position of the dropdown button + var globalPos = dropdown.mapToGlobal(0, 0); + globalDropdownWindow.showAt(root, globalPos.x, globalPos.y + dropdown.height + 4, root.options, root.currentValue); + + // Connect to value selection (with cleanup) + globalDropdownWindow.valueSelected.connect(function(value) { + root.currentValue = value; + root.valueChanged(value); + }); + } + } + } + } +} \ No newline at end of file diff --git a/Widgets/DankLocationSearch.qml b/Widgets/DankLocationSearch.qml new file mode 100644 index 00000000..185a1549 --- /dev/null +++ b/Widgets/DankLocationSearch.qml @@ -0,0 +1,309 @@ +import QtQuick +import QtQuick.Controls +import Quickshell.Io +import qs.Common +import qs.Widgets + +Item { + id: root + + property string currentLocation: "" + property string placeholderText: "Search for a location..." + + signal locationSelected(string displayName, string coordinates) + + width: parent.width + height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0) + + property bool _internalChange: false + property bool isLoading: false + property string helperTextState: "default" // "default", "prompt", "searching", "found", "not_found" + property string currentSearchText: "" + + ListModel { + id: searchResultsModel + } + + function resetSearchState() { + locationSearchTimer.stop() + dropdownHideTimer.stop() + if (locationSearcher.running) { + locationSearcher.running = false; + } + isLoading = false + searchResultsModel.clear() + helperTextState = "default" + } + + Timer { + id: locationSearchTimer + interval: 500 + running: false + repeat: false + onTriggered: { + if (locationInput.text.length > 2) { + if (locationSearcher.running) { + locationSearcher.running = false + } + + searchResultsModel.clear() + root.isLoading = true + root.helperTextState = "searching" + + const searchLocation = locationInput.text + root.currentSearchText = searchLocation + const encodedLocation = encodeURIComponent(searchLocation) + const curlCommand = `curl -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'` + + locationSearcher.command = ["bash", "-c", curlCommand] + locationSearcher.running = true + } + } + } + + Timer { + id: dropdownHideTimer + interval: 200 + running: false + repeat: false + onTriggered: { + if (!locationInput.getActiveFocus() && !searchDropdown.hovered) { + root.resetSearchState() + } + } + } + + Process { + id: locationSearcher + command: ["bash", "-c", "echo"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (root.currentSearchText !== locationInput.text) { + return + } + + const raw = text.trim() + root.isLoading = false + searchResultsModel.clear() + + if (!raw || raw[0] !== "[") { + root.helperTextState = "not_found" + return + } + + try { + const data = JSON.parse(raw) + if (data.length === 0) { + root.helperTextState = "not_found" + return + } + + for (let i = 0; i < Math.min(data.length, 5); i++) { + const location = data[i] + if (location.display_name && location.lat && location.lon) { + const parts = location.display_name.split(', ') + let cleanName = parts[0] + if (parts.length > 1) { + const state = parts[parts.length - 2] + if (state && state !== cleanName) { + cleanName += `, ${state}` + } + } + const query = `${location.lat},${location.lon}` + searchResultsModel.append({ "name": cleanName, "query": query }) + } + } + root.helperTextState = "found" + } catch (e) { + root.helperTextState = "not_found" + } + } + } + + onExited: (exitCode) => { + root.isLoading = false + if (exitCode !== 0) { + searchResultsModel.clear() + root.helperTextState = "not_found" + } + } + } + + // Search input field + Item { + id: searchInputField + width: parent.width + height: 48 + + DankTextField { + id: locationInput + width: parent.width + height: parent.height + leftIconName: "search" + placeholderText: root.placeholderText + text: root.currentLocation + backgroundColor: Theme.surfaceVariant + normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + focusedBorderColor: Theme.primary + + onTextEdited: { + if (root._internalChange) return + if (getActiveFocus()) { + if (text.length > 2) { + root.isLoading = true + root.helperTextState = "searching" + locationSearchTimer.restart() + } else { + root.resetSearchState() + root.helperTextState = "prompt" + } + } + } + + onActiveFocusChanged: (hasFocus) => { + if (hasFocus) { + dropdownHideTimer.stop() + if (text.length <= 2) { + root.helperTextState = "prompt" + } + } + else { + dropdownHideTimer.start() + } + } + } + + // Status icon overlay + DankIcon { + name: { + if (root.isLoading) return "hourglass_empty" + if (searchResultsModel.count > 0) return "check_circle" + if (locationInput.getActiveFocus() && locationInput.text.length > 2 && !root.isLoading) return "error" + return "" + } + size: Theme.iconSize - 4 + color: { + if (root.isLoading) return Theme.surfaceVariantText + if (searchResultsModel.count > 0) return Theme.success || Theme.primary + if (locationInput.getActiveFocus() && locationInput.text.length > 2) return Theme.error + return "transparent" + } + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + // Search results dropdown + Rectangle { + id: searchDropdown + width: parent.width + height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200) + + y: searchInputField.height + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + border.width: 1 + visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading) + + property bool hovered: false + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + parent.hovered = true + dropdownHideTimer.stop() + } + onExited: { + parent.hovered = false + if (!locationInput.getActiveFocus()) { + dropdownHideTimer.start() + } + } + acceptedButtons: Qt.NoButton + } + + Item { + anchors.fill: parent + anchors.margins: Theme.spacingS + + ListView { + id: searchResultsList + anchors.fill: parent + clip: true + model: searchResultsModel + spacing: 2 + + delegate: Rectangle { + width: searchResultsList.width + height: 36 + radius: Theme.cornerRadius + color: resultMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: "place" + size: Theme.iconSize - 6 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: model.name || "Unknown" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: parent.width - 30 + } + } + + MouseArea { + id: resultMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + root._internalChange = true + const selectedName = model.name + const selectedQuery = model.query + + locationInput.text = selectedName + root.locationSelected(selectedName, selectedQuery) + + root.resetSearchState() + locationInput.setFocus(false) + root._internalChange = false + } + } + } + } + + // Show message when no results + Text { + anchors.centerIn: parent + text: root.isLoading ? "Searching..." : "No locations found" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: searchResultsList.count === 0 && locationInput.text.length > 2 + } + } + } +} \ No newline at end of file diff --git a/Widgets/DankTextField.qml b/Widgets/DankTextField.qml index 88a502a1..a1656df4 100644 --- a/Widgets/DankTextField.qml +++ b/Widgets/DankTextField.qml @@ -43,7 +43,7 @@ Rectangle { signal textEdited() signal editingFinished() signal accepted() - signal focusChanged(bool hasFocus) + signal activeFocusChanged(bool hasFocus) // Access to inner TextInput properties via functions function getActiveFocus() { @@ -120,7 +120,7 @@ Rectangle { onTextChanged: root.textEdited() onEditingFinished: root.editingFinished() onAccepted: root.accepted() - onActiveFocusChanged: root.focusChanged(activeFocus) + onActiveFocusChanged: root.activeFocusChanged(activeFocus) MouseArea { anchors.fill: parent diff --git a/shell.qml b/shell.qml index 46276335..d1ee6fdd 100644 --- a/shell.qml +++ b/shell.qml @@ -4,6 +4,7 @@ import Quickshell import qs.Modules import qs.Modules.CenterCommandCenter import qs.Modules.ControlCenter +import qs.Modules.Settings import qs.Modules.TopBar ShellRoot { @@ -38,6 +39,12 @@ ShellRoot { ControlCenterPopup { id: controlCenterPopup + onPowerActionRequested: (action, title, message) => { + powerConfirmDialog.powerConfirmAction = action; + powerConfirmDialog.powerConfirmTitle = title; + powerConfirmDialog.powerConfirmMessage = message; + powerConfirmDialog.powerConfirmVisible = true; + } } WifiPasswordDialog {