diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml new file mode 100644 index 0000000..194ec21 --- /dev/null +++ b/Modules/Bar/ActiveWindow.qml @@ -0,0 +1,151 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Services +import qs.Widgets + +Row { + id: layout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + visible: Settings.data.bar.showActiveWindow + + property bool showingFullTitle: false + property int lastWindowIndex: -1 + + // Timer to hide full title after window switch + Timer { + id: fullTitleTimer + interval: 2000 // Show full title for 2 seconds + repeat: false + onTriggered: { + showingFullTitle = false + titleText.text = getDisplayText() + } + } + + // Update text when window changes + Connections { + target: typeof Niri !== "undefined" ? Niri : null + function onFocusedWindowIndexChanged() { + // Check if window actually changed + if (Niri.focusedWindowIndex !== lastWindowIndex) { + lastWindowIndex = Niri.focusedWindowIndex + showingFullTitle = true + fullTitleTimer.restart() + } + titleText.text = getDisplayText() + } + } + + // Window icon + NText { + id: windowIcon + text: "desktop_windows" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + color: Colors.mPrimary + visible: getDisplayText() !== "" + } + + // Window title container + Item { + id: titleContainer + width: titleText.width + height: titleText.height + anchors.verticalCenter: parent.verticalCenter + + Behavior on width { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + + NText { + id: titleText + text: getDisplayText() + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + // Mouse area for hover detection + MouseArea { + id: titleContainerMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + onEntered: { + titleText.text = getDisplayText() + } + onExited: { + titleText.text = getDisplayText() + } + } + } + + + + function getDisplayText() { + // Check if Niri service is available + if (typeof Niri === "undefined") { + return "" + } + + // Get the focused window data + const focusedWindow = Niri.focusedWindowIndex >= 0 && Niri.focusedWindowIndex < Niri.windows.length + ? Niri.windows[Niri.focusedWindowIndex] + : null + + if (!focusedWindow) { + return "" + } + + const appId = focusedWindow.appId || "" + const title = focusedWindow.title || "" + + // If no appId, fall back to title processing + if (!appId) { + if (!title || title === "(No active window)" || title === "(Unnamed window)") { + return "" + } + + // Extract program name from title (before first space or special characters) + const programName = title.split(/[\s\-_]/)[0] + + if (programName.length <= 2 || programName === title) { + return truncateTitle(title) + } + + if (showingFullTitle || titleContainerMouseArea.containsMouse || isGenericName(programName)) { + return truncateTitle(title) + } + + return programName + } + + // Use appId for program name, show full title on hover or window switch + if (showingFullTitle || titleContainerMouseArea.containsMouse) { + return truncateTitle(title || appId) + } + + return appId + } + + function truncateTitle(title) { + if (title.length > 50) { + return title.substring(0, 47) + "..." + } + return title + } + + function isGenericName(name) { + const genericNames = ["window", "application", "app", "program", "process", "unknown"] + return genericNames.includes(name.toLowerCase()) + } +} \ No newline at end of file diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 2959a5e..14907dd 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -55,6 +55,8 @@ Variants { SystemMonitor {} MediaMini {} + + ActiveWindow {} } // Center diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 8e6b895..35a583c 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -3,11 +3,136 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets +import Quickshell.Io ColumnLayout { id: root spacing: 0 + + // Helper function to get color from scheme file + 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] && schemeColorsCache[schemeName][colorKey]) { + return schemeColorsCache[schemeName][colorKey] + } + + // Return a default color if not cached yet + return "#000000" + } + + // Cache for scheme colors + property var schemeColorsCache: ({}) + + // Array to hold FileView objects + property var fileViews: [] + + // Load color scheme data when schemes are available + Connections { + target: ColorSchemes + function onSchemesChanged() { + loadSchemeColors() + } + } + + function loadSchemeColors() { + // Clear existing cache + schemeColorsCache = {} + + // Destroy existing FileViews + for (var i = 0; i < fileViews.length; i++) { + if (fileViews[i]) { + fileViews[i].destroy() + } + } + fileViews = [] + + // Create FileViews for each scheme + for (var i = 0; i < ColorSchemes.schemes.length; i++) { + var schemePath = ColorSchemes.schemes[i] + var schemeName = schemePath.split("/").pop().replace(".json", "") + + // Create FileView component + var component = Qt.createComponent("SchemeFileView.qml") + if (component.status === Component.Ready) { + var fileView = component.createObject(root, { + "path": schemePath, + "schemeName": schemeName + }) + fileViews.push(fileView) + } else { + // Fallback: create inline FileView + createInlineFileView(schemePath, schemeName) + } + } + } + + function createInlineFileView(schemePath, schemeName) { + var fileViewQml = ` + import QtQuick + import Quickshell.Io + + FileView { + property string schemeName: "${schemeName}" + path: "${schemePath}" + blockLoading: true + + onLoaded: { + try { + var jsonData = JSON.parse(text()) + root.schemeLoaded(schemeName, jsonData) + } catch (e) { + console.warn("Failed to parse JSON for scheme:", schemeName, e) + } + } + } + ` + + try { + var fileView = Qt.createQmlObject(fileViewQml, root, "dynamicFileView_" + schemeName) + fileViews.push(fileView) + } catch (e) { + console.warn("Failed to create FileView for scheme:", schemeName, e) + } + } + + function schemeLoaded(schemeName, jsonData) { + console.log("Loading scheme colors for:", schemeName) + + var colors = {} + + // Extract colors from JSON data + if (jsonData && typeof jsonData === 'object') { + colors.mPrimary = jsonData.mPrimary || jsonData.primary || "#000000" + colors.mSecondary = jsonData.mSecondary || jsonData.secondary || "#000000" + colors.mTertiary = jsonData.mTertiary || jsonData.tertiary || "#000000" + colors.mError = jsonData.mError || jsonData.error || "#ff0000" + colors.mSurface = jsonData.mSurface || jsonData.surface || "#ffffff" + colors.mOnSurface = jsonData.mOnSurface || jsonData.onSurface || "#000000" + colors.mOutline = jsonData.mOutline || jsonData.outline || "#666666" + } else { + // Default colors + colors = { + mPrimary: "#000000", + mSecondary: "#000000", + mTertiary: "#000000", + mError: "#ff0000", + mSurface: "#ffffff", + mOnSurface: "#000000", + mOutline: "#666666" + } + } + + // Update cache + var newCache = schemeColorsCache + newCache[schemeName] = colors + schemeColorsCache = newCache + + console.log("Cached colors for", schemeName, ":", JSON.stringify(colors)) + } ScrollView { id: scrollView @@ -32,10 +157,10 @@ ColumnLayout { spacing: Style.marginLarge * scaling Layout.fillWidth: true - // Use Wallpaper Colors + // Use Matugen NToggle { - label: "Use Wallpaper Colors" - description: "Automatically generate colors from you active wallpaper (requires Matugen)" + label: "Use Matugen" + description: "Automatically generate colors based on your active wallpaper using Matugen" value: Settings.data.colorSchemes.useWallpaperColors onToggled: function (newValue) { Settings.data.colorSchemes.useWallpaperColors = newValue @@ -45,30 +170,168 @@ ColumnLayout { } } + NDivider { + Layout.fillWidth: true + } + + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.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." + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + Layout.fillWidth: true + wrapMode: Text.WordWrap + Layout.topMargin: -16 * scaling + } + ColumnLayout { spacing: Style.marginTiny * scaling Layout.fillWidth: true - ButtonGroup { - id: schemesGroup - } + // Color Schemes Grid + GridLayout { + columns: 4 + rowSpacing: Style.marginLarge * scaling + columnSpacing: Style.marginLarge * scaling + Layout.fillWidth: true - Repeater { - model: ColorSchemes.schemes - NRadioButton { - property string schemePath: modelData - ButtonGroup.group: schemesGroup - text: { - // Remove json and the full path - var chunks = schemePath.replace(".json", "").split("/") - return chunks[chunks.length - 1] - } - checked: Settings.data.colorSchemes.predefinedScheme == schemePath - onClicked: { - // Disable useWallpaperColors when picking a predefined color scheme - Settings.data.colorSchemes.useWallpaperColors = false - Settings.data.colorSchemes.predefinedScheme = schemePath - ColorSchemes.applyScheme(schemePath) + Repeater { + model: ColorSchemes.schemes + + Rectangle { + id: schemeCard + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: 12 * scaling + color: getSchemeColor(modelData, "mSurface") + border.width: 2 + border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline + + property string schemePath: modelData + + // 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 + ColorSchemes.applyScheme(schemePath) + } + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: { + schemeCard.scale = 1.05 + schemeCard.border.width = 3 + } + + onExited: { + schemeCard.scale = 1.0 + schemeCard.border.width = 2 + } + } + + // Card content + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 * scaling + spacing: 8 * 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.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + + // Color swatches + RowLayout { + spacing: 8 * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + // Primary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mPrimary") + } + + // Secondary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mSecondary") + } + + // Tertiary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mTertiary") + } + + // Error color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mError") + } + } + } + + // Selection indicator + Rectangle { + visible: Settings.data.colorSchemes.predefinedScheme === schemePath + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 8 * scaling + width: 24 * scaling + height: 24 * scaling + radius: 12 * scaling + color: Colors.mPrimary + + NText { + anchors.centerIn: parent + text: "✓" + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnPrimary + } + } + + // Smooth animations + Behavior on scale { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + + Behavior on border.color { + ColorAnimation { duration: 300 } + } + + Behavior on border.width { + NumberAnimation { duration: 200 } + } } } } @@ -76,4 +339,4 @@ ColumnLayout { } } } -} +} \ No newline at end of file diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml index eddd275..4ef659e 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemes.qml @@ -54,6 +54,7 @@ Singleton { } schemes = files scanning = false + console.log("[ColorSchemes] Loaded", schemes.length, "schemes") } } } diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index f513b37..574a577 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -17,7 +17,6 @@ Singleton { property var hlWorkspaces: Hyprland.workspaces.values // Detect which compositor we're using Component.onCompleted: { - console.log("[Workspaces] Initializing workspaces service") detectCompositor() } @@ -25,25 +24,20 @@ Singleton { try { try { if (Hyprland.eventSocketPath) { - console.log("[Workspaces] Detected Hyprland compositor") isHyprland = true isNiri = false initHyprland() return } } catch (e) { - console.log("[Workspaces] Hyprland not available:", e) } if (typeof Niri !== "undefined") { - console.log("[Workspaces] Detected Niri service") isHyprland = false isNiri = true initNiri() return } - - console.log("[Workspaces] Could not detect any supported compositor") } catch (e) { console.error("[Workspaces] Error detecting compositor:", e) }