Add ActiveWindow, make ColorScheme look better

This commit is contained in:
Ly-sec 2025-08-15 23:15:41 +02:00
parent 52891d1fb4
commit 1d7d0752ad
5 changed files with 440 additions and 29 deletions

View file

@ -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())
}
}

View file

@ -55,6 +55,8 @@ Variants {
SystemMonitor {} SystemMonitor {}
MediaMini {} MediaMini {}
ActiveWindow {}
} }
// Center // Center

View file

@ -3,11 +3,136 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import Quickshell.Io
ColumnLayout { ColumnLayout {
id: root id: root
spacing: 0 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 { ScrollView {
id: scrollView id: scrollView
@ -32,10 +157,10 @@ ColumnLayout {
spacing: Style.marginLarge * scaling spacing: Style.marginLarge * scaling
Layout.fillWidth: true Layout.fillWidth: true
// Use Wallpaper Colors // Use Matugen
NToggle { NToggle {
label: "Use Wallpaper Colors" label: "Use Matugen"
description: "Automatically generate colors from you active wallpaper (requires Matugen)" description: "Automatically generate colors based on your active wallpaper using Matugen"
value: Settings.data.colorSchemes.useWallpaperColors value: Settings.data.colorSchemes.useWallpaperColors
onToggled: function (newValue) { onToggled: function (newValue) {
Settings.data.colorSchemes.useWallpaperColors = 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 { ColumnLayout {
spacing: Style.marginTiny * scaling spacing: Style.marginTiny * scaling
Layout.fillWidth: true Layout.fillWidth: true
ButtonGroup { // Color Schemes Grid
id: schemesGroup GridLayout {
} columns: 4
rowSpacing: Style.marginLarge * scaling
columnSpacing: Style.marginLarge * scaling
Layout.fillWidth: true
Repeater { Repeater {
model: ColorSchemes.schemes model: ColorSchemes.schemes
NRadioButton {
property string schemePath: modelData Rectangle {
ButtonGroup.group: schemesGroup id: schemeCard
text: { Layout.fillWidth: true
// Remove json and the full path Layout.preferredHeight: 120 * scaling
var chunks = schemePath.replace(".json", "").split("/") radius: 12 * scaling
return chunks[chunks.length - 1] color: getSchemeColor(modelData, "mSurface")
} border.width: 2
checked: Settings.data.colorSchemes.predefinedScheme == schemePath border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme property string schemePath: modelData
Settings.data.colorSchemes.useWallpaperColors = false
Settings.data.colorSchemes.predefinedScheme = schemePath // Mouse area for selection
ColorSchemes.applyScheme(schemePath) 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 {
} }
} }
} }
} }

View file

@ -54,6 +54,7 @@ Singleton {
} }
schemes = files schemes = files
scanning = false scanning = false
console.log("[ColorSchemes] Loaded", schemes.length, "schemes")
} }
} }
} }

View file

@ -17,7 +17,6 @@ Singleton {
property var hlWorkspaces: Hyprland.workspaces.values property var hlWorkspaces: Hyprland.workspaces.values
// Detect which compositor we're using // Detect which compositor we're using
Component.onCompleted: { Component.onCompleted: {
console.log("[Workspaces] Initializing workspaces service")
detectCompositor() detectCompositor()
} }
@ -25,25 +24,20 @@ Singleton {
try { try {
try { try {
if (Hyprland.eventSocketPath) { if (Hyprland.eventSocketPath) {
console.log("[Workspaces] Detected Hyprland compositor")
isHyprland = true isHyprland = true
isNiri = false isNiri = false
initHyprland() initHyprland()
return return
} }
} catch (e) { } catch (e) {
console.log("[Workspaces] Hyprland not available:", e)
} }
if (typeof Niri !== "undefined") { if (typeof Niri !== "undefined") {
console.log("[Workspaces] Detected Niri service")
isHyprland = false isHyprland = false
isNiri = true isNiri = true
initNiri() initNiri()
return return
} }
console.log("[Workspaces] Could not detect any supported compositor")
} catch (e) { } catch (e) {
console.error("[Workspaces] Error detecting compositor:", e) console.error("[Workspaces] Error detecting compositor:", e)
} }