diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index b33d0dd..0877655 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -6,10 +6,6 @@ import Quickshell.Wayland import qs.Services import qs.Widgets - -/* - An experiment/demo panel to tweaks widgets -*/ NLoader { id: root diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml new file mode 100644 index 0000000..389e239 --- /dev/null +++ b/Modules/Settings/SettingsWindow.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets +import "Tabs" as Tabs + +NLoader { + id: root + + content: Component { + NPanel { + id: settingsPanel + + readonly property real scaling: Scaling.scale(screen) + // Single source of truth for tabs + // Each tab points to a QML file path. The content stack simply loads the file via Loader.source. + property var tabsModel: [ + { icon: "tune", label: "General", source: "Tabs/General.qml" }, + { icon: "web_asset", label: "Bar", source: "Tabs/Bar.qml" }, + { icon: "schedule", label: "Time & Weather", source: "Tabs/TimeWeather.qml" }, + { icon: "videocam", label: "Screen Recorder", source: "Tabs/ScreenRecorder.qml" }, + { icon: "wifi", label: "Network", source: "Tabs/Network.qml" }, + { icon: "monitor", label: "Display", source: "Tabs/Display.qml" }, + { icon: "image", label: "Wallpaper", source: "Tabs/Wallpaper.qml" }, + { icon: "more_horiz", label: "Misc", source: "Tabs/Misc.qml" }, + { icon: "info", label: "About", source: "Tabs/About.qml" } + ] + + // Always default to the first tab (General) when the panel becomes visible + onVisibleChanged: function () { + if (visible) { + Qt.callLater(function () { + if (typeof stack !== 'undefined' && stack) { + stack.currentIndex = 0 + } + }) + } + } + + // Ensure panel shows itself once created + Component.onCompleted: show() + + Rectangle { + id: bgRect + color: Colors.backgroundPrimary + radius: Style.radiusLarge * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + layer.enabled: true + width: 1040 * scaling + height: 640 * scaling + anchors.centerIn: parent + + // Prevent closing when clicking in the panel bg + MouseArea { anchors.fill: parent } + + + // Main two-pane layout + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginLarge * scaling + + // Sidebar + Rectangle { + id: sidebar + Layout.preferredWidth: 260 * scaling + Layout.fillHeight: true + radius: Style.radiusMedium * scaling + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + Repeater { + id: sections + model: settingsPanel.tabsModel + + delegate: Rectangle { + readonly property bool selected: index === stack.currentIndex + Layout.fillWidth: true + height: 44 * scaling + radius: Style.radiusSmall * scaling + color: selected ? Colors.highlight : "transparent" + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + NText { + text: modelData.icon + font.family: "Material Symbols Outlined" + font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } + color: selected ? Colors.onAccent : Colors.textSecondary + } + NText { text: modelData.label; color: selected ? Colors.onAccent : Colors.textPrimary; Layout.fillWidth: true } + } + MouseArea { anchors.fill: parent; onClicked: stack.currentIndex = index } + } + } + } + } + + // Content + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusMedium * scaling + color: Colors.surface + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + // Content layout: header + divider + pages + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginSmall * scaling + + // Header row + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + NText { + text: settingsPanel.tabsModel[stack.currentIndex].label + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.fillWidth: true + } + NIconButton { + id: demoPanelToggle + icon: "close" + tooltipText: "Open demo panel" + Layout.alignment: Qt.AlignVCenter + onClicked: function () { settingsWindow.isLoaded = !settingsWindow.isLoaded } + } + } + + NDivider { Layout.fillWidth: true } + + // Stacked pages + StackLayout { + id: stack + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: 0 + Component.onCompleted: currentIndex = 0 + + // Pages generated from tabsModel + Repeater { + model: settingsPanel.tabsModel + delegate: Loader { + active: index === stack.currentIndex + visible: active + source: modelData.source + } + } + } + } + } + } + } + } + } +} + diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml new file mode 100644 index 0000000..18142c3 --- /dev/null +++ b/Modules/Settings/Tabs/About.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "About"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml new file mode 100644 index 0000000..da8669f --- /dev/null +++ b/Modules/Settings/Tabs/Bar.qml @@ -0,0 +1,19 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + // Optional scaling prop to match other tabs + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Bar"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml new file mode 100644 index 0000000..ac2f8e1 --- /dev/null +++ b/Modules/Settings/Tabs/Display.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Display"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml new file mode 100644 index 0000000..e0800db --- /dev/null +++ b/Modules/Settings/Tabs/General.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + id: generalPage + + // Public API + // Scaling factor provided by the parent settings window + property real scaling: 1 + + anchors.fill: parent + implicitWidth: parent ? parent.width : 0 + implicitHeight: parent ? parent.height : 0 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 0 + spacing: Style.marginMedium * scaling + + // Profile section + NText { text: "Profile"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + // Avatar preview + Rectangle { + width: 40 * scaling + height: 40 * scaling + radius: 20 * scaling + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + Image { + anchors.fill: parent + anchors.margins: 2 * scaling + source: Settings.data.general.avatarImage + fillMode: Image.PreserveAspectCrop + asynchronous: true + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + NText { text: "Profile Image"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Your profile picture displayed in various places throughout the shell"; color: Colors.textSecondary } + NTextBox { + text: Settings.data.general.avatarImage + placeholderText: "/home/user/.face" + Layout.fillWidth: true + onEditingFinished: Settings.data.general.avatarImage = text + } + } + } + + NDivider { Layout.fillWidth: true; Layout.topMargin: Style.marginSmall * scaling; Layout.bottomMargin: Style.marginSmall * scaling } + + // UI section + NText { text: "User Interface"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Show Corners" + description: "Display rounded corners on the edge of the screen" + value: Settings.data.general.showScreenCorners + onToggled: function (v) { Settings.data.general.showScreenCorners = v } + } + + NToggle { + label: "Show Dock" + description: "Display a dock at the bottom of the screen for quick access to applications" + value: Settings.data.general.showDock + onToggled: function (v) { Settings.data.general.showDock = v } + } + + NToggle { + label: "Dim Desktop" + description: "Dim the desktop when panels or menus are open" + value: Settings.data.general.dimDesktop + onToggled: function (v) { Settings.data.general.dimDesktop = v } + } + + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml new file mode 100644 index 0000000..9d26a4c --- /dev/null +++ b/Modules/Settings/Tabs/Misc.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Misc"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml new file mode 100644 index 0000000..408922d --- /dev/null +++ b/Modules/Settings/Tabs/Network.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Network"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml new file mode 100644 index 0000000..ea70889 --- /dev/null +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Screen Recorder"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml new file mode 100644 index 0000000..57bece5 --- /dev/null +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Time & Weather"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml new file mode 100644 index 0000000..c495075 --- /dev/null +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Wallpaper"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index 441e68f..b715b7d 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -10,6 +10,8 @@ NBox { id: root readonly property real scaling: Scaling.scale(screen) + // Hold a single instance of the Settings window (root is NLoader) + property var settingsWindow: null Layout.fillWidth: true // Height driven by content @@ -71,6 +73,23 @@ NBox { NIconButton { icon: "settings" sizeMultiplier: 0.9 + onClicked: function () { + if (!root.settingsWindow) { + const comp = Qt.createComponent("../Settings/SettingsWindow.qml") + if (comp.status === Component.Ready) { + root.settingsWindow = comp.createObject(root) + } else { + comp.statusChanged.connect(function () { + if (comp.status === Component.Ready) { + root.settingsWindow = comp.createObject(root) + } + }) + } + } + if (root.settingsWindow) { + root.settingsWindow.isLoaded = !root.settingsWindow.isLoaded + } + } } NIconButton { icon: "power_settings_new" diff --git a/Services/Settings.qml b/Services/Settings.qml index 2c034f3..7b33d9a 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -65,6 +65,7 @@ Singleton { property string avatarImage: Quickshell.env("HOME") + "/.face" property bool dimDesktop: true property bool showScreenCorners: false + property bool showDock: false } // location diff --git a/Widgets/NTextBox.qml b/Widgets/NTextBox.qml new file mode 100644 index 0000000..85b89f7 --- /dev/null +++ b/Widgets/NTextBox.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Services + +Item { + id: root + + readonly property real scaling: Scaling.scale(screen) + + // API + property alias text: input.text + property alias placeholderText: input.placeholderText + property bool readOnly: false + property bool enabled: true + property var onEditingFinished: function () {} + property var onTextChanged: function (value) {} + + // Sizing + implicitHeight: Style.baseWidgetSize * 1.25 * scaling + implicitWidth: 320 * scaling + + // Container + Rectangle { + id: frame + anchors.fill: parent + radius: Style.radiusMedium * scaling + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + // Focus ring + Rectangle { + anchors.fill: parent + radius: frame.radius + color: "transparent" + border.color: input.activeFocus ? Colors.accentPrimary : "transparent" + border.width: input.activeFocus ? Math.max(1, Style.borderMedium * scaling) : 0 + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + // Optional leading icon slot in the future + // Item { Layout.preferredWidth: 0 } + + TextField { + id: input + Layout.fillWidth: true + echoMode: TextInput.Normal + readOnly: root.readOnly + enabled: root.enabled + color: Colors.textPrimary + placeholderTextColor: Colors.textSecondary + background: null + font.pointSize: Colors.fontSizeSmall * scaling + onEditingFinished: root.onEditingFinished() + onTextChanged: root.onTextChanged(text) + } + } + } +} +