import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets import Quickshell.Io ColumnLayout { id: root spacing: 0 // Helper function to get color from scheme file (supports dark/light variants) function getSchemeColor(schemePath, colorKey) { // Extract scheme name from path var schemeName = schemePath.split("/").pop().replace(".json", "") // Try to get from cached data first if (schemeColorsCache[schemeName]) { var entry = schemeColorsCache[schemeName] var variant = entry if (entry.dark || entry.light) { variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark) } if (variant && variant[colorKey]) return variant[colorKey] } // Return a default color if not cached yet return "#000000" } // Cache for scheme JSON (can be flat or {dark, light}) property var schemeColorsCache: ({}) // Scale properties for card animations property real cardScaleLow: 0.95 property real cardScaleHigh: 1.0 // This function is called by the FileView Repeater when a scheme file is loaded function schemeLoaded(schemeName, jsonData) { var value = jsonData || {} var newCache = schemeColorsCache newCache[schemeName] = value schemeColorsCache = newCache } // When the list of available schemes changes, clear the cache. // The Repeater below will automatically re-create the FileViews. Connections { target: ColorSchemeService function onSchemesChanged() { schemeColorsCache = {} } } // A non-visual Item to host the Repeater that loads the color scheme files. Item { visible: false id: fileLoaders Repeater { model: ColorSchemeService.schemes // The delegate is a Component, which correctly wraps the non-visual FileView delegate: Item { FileView { path: modelData blockLoading: true onLoaded: { var schemeName = path.split("/").pop().replace(".json", "") try { var jsonData = JSON.parse(text()) root.schemeLoaded(schemeName, jsonData) } catch (e) { Logger.warn("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e) root.schemeLoaded(schemeName, null) // Load defaults on parse error } } } } } } // UI Code ScrollView { id: scrollView Layout.fillWidth: true Layout.fillHeight: true padding: Style.marginM * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded ColumnLayout { width: scrollView.availableWidth spacing: 0 Item { Layout.fillWidth: true Layout.preferredHeight: 0 } ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true // Use Matugen NToggle { label: "Enable Matugen" description: "Automatically generate colors based on your active wallpaper." checked: Settings.data.colorSchemes.useWallpaperColors onToggled: checked => { Settings.data.colorSchemes.useWallpaperColors = checked if (Settings.data.colorSchemes.useWallpaperColors) { ColorSchemeService.changedWallpaper() } } } // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) NToggle { label: "Dark Mode" description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available." checked: Settings.data.colorSchemes.darkMode enabled: true onToggled: checked => { Settings.data.colorSchemes.darkMode = checked if (Settings.data.colorSchemes.useWallpaperColors) { ColorSchemeService.changedWallpaper() } else if (Settings.data.colorSchemes.predefinedScheme) { // Re-apply current scheme to pick the right variant ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) // Force refresh of previews var tmp = schemeColorsCache schemeColorsCache = {} schemeColorsCache = tmp } } } // GTK/QT theming NToggle { label: "Theme external apps (GTK & Qt)" description: "Writes GTK (gtk.css) and Qt (qt6ct) themes based on your colors." checked: Settings.data.colorSchemes.themeApps onToggled: checked => { Settings.data.colorSchemes.themeApps = checked if (Settings.data.colorSchemes.useWallpaperColors) { ColorSchemeService.changedWallpaper() } } } NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginL * scaling Layout.bottomMargin: Style.marginL * scaling } ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true NText { text: "Predefined Color Schemes" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true } NText { text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead. You can toggle between light and dark themes when using Matugen." font.pointSize: Style.fontSizeXS * scaling color: Color.mOnSurface Layout.fillWidth: true wrapMode: Text.WordWrap } } ColumnLayout { spacing: Style.marginXS * scaling Layout.fillWidth: true Layout.topMargin: Style.marginL * scaling // Color Schemes Grid GridLayout { columns: 4 rowSpacing: Style.marginL * scaling columnSpacing: Style.marginL * scaling Layout.fillWidth: true Repeater { model: ColorSchemeService.schemes Rectangle { id: schemeCard property string schemePath: modelData Layout.fillWidth: true Layout.preferredHeight: 120 * scaling radius: Style.radiusM * scaling color: getSchemeColor(modelData, "mSurface") border.width: Math.max(1, Style.borderL * scaling) border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Color.mPrimary : Color.mOutline scale: root.cardScaleLow // Mouse area for selection MouseArea { anchors.fill: parent onClicked: { // Disable useWallpaperColors when picking a predefined color scheme Settings.data.colorSchemes.useWallpaperColors = false Settings.data.colorSchemes.predefinedScheme = schemePath ColorSchemeService.applyScheme(schemePath) } hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: { schemeCard.scale = root.cardScaleHigh } onExited: { schemeCard.scale = root.cardScaleLow } } // Card content ColumnLayout { anchors.fill: parent anchors.margins: Style.marginXL * scaling spacing: Style.marginS * scaling // Scheme name NText { text: { // Remove json and the full path var chunks = schemePath.replace(".json", "").split("/") return chunks[chunks.length - 1] } font.pointSize: Style.fontSizeM * scaling font.weight: Style.fontWeightBold color: getSchemeColor(modelData, "mOnSurface") Layout.fillWidth: true elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter } // Color swatches RowLayout { spacing: Style.marginS * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter // Primary color swatch Rectangle { width: 28 * scaling height: 28 * scaling radius: width * 0.5 color: getSchemeColor(modelData, "mPrimary") } // Secondary color swatch Rectangle { width: 28 * scaling height: 28 * scaling radius: width * 0.5 color: getSchemeColor(modelData, "mSecondary") } // Tertiary color swatch Rectangle { width: 28 * scaling height: 28 * scaling radius: width * 0.5 color: getSchemeColor(modelData, "mTertiary") } // Error color swatch Rectangle { width: 28 * scaling height: 28 * scaling radius: width * 0.5 color: getSchemeColor(modelData, "mError") } } } // Selection indicator Rectangle { visible: Settings.data.colorSchemes.predefinedScheme === schemePath anchors.right: parent.right anchors.top: parent.top anchors.margins: Style.marginS * scaling width: 24 * scaling height: 24 * scaling radius: width * 0.5 color: Color.mPrimary NText { anchors.centerIn: parent text: "✓" font.pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightBold color: Color.mOnPrimary } } // Smooth animations Behavior on scale { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutCubic } } Behavior on border.color { ColorAnimation { duration: Style.animationNormal } } Behavior on border.width { NumberAnimation { duration: Style.animationFast } } } } } } } } } }