1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

Modularlize the TopBar

This commit is contained in:
bbedward
2025-07-11 22:07:54 -04:00
parent ecbc835f10
commit 962c56c0ce
12 changed files with 1060 additions and 870 deletions

View File

@@ -1,869 +0,0 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import "../Common"
import "../Services"
PanelWindow {
id: topBar
// modelData contains the screen from Quickshell.screens
property var modelData
screen: modelData
// Get the screen name (e.g., "DP-1", "DP-2")
property string screenName: modelData.name
anchors {
top: true
left: true
right: true
}
implicitHeight: Theme.barHeight - 4
color: "transparent"
// Audio visualization data
property list<real> audioLevels: [0, 0, 0, 0]
// Real-time audio visualization using cava (with fallback)
property bool cavaAvailable: false
Process {
id: cavaCheck
command: ["which", "cava"]
running: true
onExited: (exitCode) => {
topBar.cavaAvailable = exitCode === 0
if (topBar.cavaAvailable) {
console.log("cava found - creating config and enabling real audio visualization")
configWriter.running = true
} else {
console.log("cava not found - using fallback animation")
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
// Create temporary config file for cava
Process {
id: configWriter
running: topBar.cavaAvailable
command: [
"sh", "-c",
`cat > /tmp/quickshell_cava_config << 'EOF'
[general]
mode = normal
framerate = 30
autosens = 0
sensitivity = 50
bars = 4
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
channels = mono
mono_option = average
[smoothing]
noise_reduction = 20
EOF`
]
onExited: {
// Start cava after config is written
if (topBar.cavaAvailable) {
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
Process {
id: cavaProcess
running: false
command: ["cava", "-p", "/tmp/quickshell_cava_config"]
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (data.trim()) {
// Parse semicolon-separated values from cava
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
if (points.length >= 4) {
topBar.audioLevels = [points[0], points[1], points[2], points[3]]
}
}
}
}
onRunningChanged: {
if (!running) {
topBar.audioLevels = [0, 0, 0, 0]
}
}
}
// Fallback animation when cava is not available
Timer {
id: fallbackTimer
running: false
interval: 100
repeat: true
onTriggered: {
// Generate smooth random values for fallback (0-100 range)
topBar.audioLevels = [
Math.random() * 40 + 10, // 10-50
Math.random() * 60 + 20, // 20-80
Math.random() * 50 + 15, // 15-65
Math.random() * 35 + 20 // 20-55
]
}
}
// Floating panel container with margins
Item {
anchors.fill: parent
anchors.margins: 2
anchors.topMargin: 6
anchors.bottomMargin: 0
anchors.leftMargin: 8
anchors.rightMargin: 8
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadiusXLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.75)
// Material 3 elevation shadow
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 4
radius: 16
samples: 33
color: Qt.rgba(0, 0, 0, 0.15)
transparentBorder: true
}
// Subtle border for definition
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
radius: parent.radius
}
// Subtle surface tint overlay with animation
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
anchors.bottomMargin: Theme.spacingXS
Row {
id: leftSection
height: parent.height
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: archLauncher
width: 40
height: 32
radius: Theme.cornerRadius
color: launcherArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: root.osLogo || "apps" // Use OS logo if detected, fallback to apps icon
font.family: root.osLogo ? "NerdFont" : Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
LauncherService.toggleAppLauncher()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
id: workspaceSwitcher
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
visible: NiriWorkspaceService.niriAvailable
// Use reactive properties from NiriWorkspaceService
property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: getDisplayWorkspaces()
// Get workspaces for this display
function getDisplayWorkspaces() {
// Always return something for now, even if service isn't ready
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return [1, 2] // Fallback
}
if (!topBar.screenName) {
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
}
// Filter workspaces for this specific display
var displayWorkspaces = []
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === topBar.screenName) {
displayWorkspaces.push(ws.idx + 1) // Convert to 1-based
}
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
}
// Get active workspace for this display
function getDisplayActiveWorkspace() {
// Always return something for now, even if service isn't ready
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return 1 // Fallback
}
if (!topBar.screenName) {
return NiriWorkspaceService.getCurrentWorkspaceNumber()
}
// Find active workspace for this display (is_active, not is_focused)
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === topBar.screenName && ws.is_active) {
return ws.idx + 1 // Convert to 1-based
}
}
return 1
}
// React to workspace changes
Connections {
target: NiriWorkspaceService
function onAllWorkspacesChanged() {
workspaceSwitcher.workspaceList = workspaceSwitcher.getDisplayWorkspaces()
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
}
function onFocusedWorkspaceIndexChanged() {
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
}
function onNiriAvailableChanged() {
if (NiriWorkspaceService.niriAvailable) {
workspaceSwitcher.workspaceList = workspaceSwitcher.getDisplayWorkspaces()
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
}
}
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: workspaceSwitcher.workspaceList
Rectangle {
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
property bool isHovered: mouseArea.containsMouse
property int sequentialNumber: index + 1 // 1-based sequential number per monitor
width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
height: Theme.spacingS
radius: height / 2
color: isActive ? Theme.primary :
isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Use sequential workspace number directly - niri focus-workspace uses 1,2,3,etc per monitor
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", sequentialNumber.toString()])
}
}
}
}
}
}
}
Rectangle {
id: clockContainer
width: {
let baseWidth = 200
if (root.hasActiveMedia) {
// Calculate width needed for media info + time/date + spacing + padding
let mediaWidth = 24 + Theme.spacingXS + mediaTitleText.implicitWidth + Theme.spacingM + 180
return Math.min(Math.max(mediaWidth, 300), parent.width - Theme.spacingL * 2)
} else if (root.weather.available) {
return Math.min(280, parent.width - Theme.spacingL * 2)
} else {
return Math.min(baseWidth, parent.width - Theme.spacingL * 2)
}
}
height: 32
radius: Theme.cornerRadius
color: clockMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
anchors.centerIn: parent
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
property date currentDate: new Date()
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
// Media info or Weather info
Row {
spacing: Theme.spacingXS
visible: root.hasActiveMedia || root.weather.available
anchors.verticalCenter: parent.verticalCenter
// Animated equalizer when media is playing
Item {
width: 20
height: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia
Row {
anchors.centerIn: parent
spacing: 2
Repeater {
model: 4
Rectangle {
width: 3
height: {
if (root.activePlayer?.playbackState === MprisPlaybackState.Playing && topBar.audioLevels.length > index) {
// Scale and compress audio data for better visual range
const rawLevel = topBar.audioLevels[index] || 0
// Use square root to compress high values and expand low values
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
const maxHeight = Theme.iconSize - 2
const minHeight = 3
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
}
return 3 // Minimum height when not playing
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: 80 // Slightly slower for smoother movement
easing.type: Easing.OutQuad
}
}
}
}
}
}
// Song title when media is playing
Text {
id: mediaTitleText
text: root.activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia
width: Math.min(implicitWidth, clockContainer.width - 100)
elide: Text.ElideRight
}
// Weather icon when no media but weather available
Text {
text: WeatherService.getWeatherIcon(root.weather.wCode)
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !root.hasActiveMedia && root.weather.available
}
// Weather temp when no media but weather available
Text {
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: !root.hasActiveMedia && root.weather.available
}
}
// Separator
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia || root.weather.available
}
// Time and date
Row {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: Qt.formatTime(clockContainer.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Qt.formatDate(clockContainer.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
clockContainer.currentDate = new Date()
}
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.calendarVisible = !root.calendarVisible
}
}
}
Row {
id: rightSection
height: parent.height
spacing: Theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: systemTrayRow.children.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
width: 24
height: 24
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon || "";
if (!icon) return "";
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
customTrayMenu.showMenu(mouse.x, mouse.y)
} else {
console.log("No menu available for:", trayItem.title || "Unknown")
}
}
}
}
// Custom Material 3 styled menu
QtObject {
id: customTrayMenu
property bool menuVisible: false
function showMenu(x, y) {
root.currentTrayMenu = customTrayMenu
root.currentTrayItem = trayItem
// Simple positioning: right side of screen, below the panel
root.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL
root.trayMenuY = Theme.barHeight + Theme.spacingS
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
menuVisible = true
root.showTrayMenu = true
}
function hideMenu() {
menuVisible = false
root.showTrayMenu = false
root.currentTrayMenu = null
root.currentTrayItem = null
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Clipboard History Button
Rectangle {
width: 40
height: 32
radius: Theme.cornerRadius
color: clipboardArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "content_paste" // Material icon for clipboard
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clipboardHistoryPopup.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// System Monitor Widgets
CpuMonitorWidget {
anchors.verticalCenter: parent.verticalCenter
}
RamMonitorWidget {
anchors.verticalCenter: parent.verticalCenter
}
// Notification Center Button
Rectangle {
width: 40
height: 32
radius: Theme.cornerRadius
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
property bool hasUnread: notificationHistory.count > 0
Text {
anchors.centerIn: parent
text: "notifications" // Material icon for notifications
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
Theme.primary : Theme.surfaceText
}
// Notification dot indicator
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: parent.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.notificationHistoryVisible = !root.notificationHistoryVisible
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Battery Widget
BatteryWidget {
anchors.verticalCenter: parent.verticalCenter
}
// Control Center Indicators
Rectangle {
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
// Network Status Icon
Text {
text: {
if (root.networkStatus === "ethernet") return "lan"
else if (root.networkStatus === "wifi") {
switch (root.wifiSignalStrength) {
case "excellent": return "wifi"
case "good": return "wifi_2_bar"
case "fair": return "wifi_1_bar"
case "poor": return "wifi_calling_3"
default: return "wifi"
}
}
else return "wifi_off"
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: root.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: true
}
// Audio Icon
Text {
text: root.volumeLevel === 0 ? "volume_off" :
root.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
// Microphone Icon (when active)
Text {
text: "mic"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
// Bluetooth Icon (when available and enabled)
Text {
text: "bluetooth"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: root.bluetoothEnabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.bluetoothAvailable && root.bluetoothEnabled
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.controlCenterVisible = !root.controlCenterVisible
if (root.controlCenterVisible) {
// Refresh data when opening control center
WifiService.scanWifi()
BluetoothService.scanDevices()
// Audio sink info is automatically refreshed by AudioService
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Power Button
PowerButton {
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import "../../Common"
Item {
id: root
property list<real> audioLevels: [0, 0, 0, 0]
property bool hasActiveMedia: false
property var activePlayer: null
property bool cavaAvailable: false
width: 20
height: Theme.iconSize
Process {
id: cavaCheck
command: ["which", "cava"]
running: true
onExited: (exitCode) => {
root.cavaAvailable = exitCode === 0
if (root.cavaAvailable) {
console.log("cava found - creating config and enabling real audio visualization")
configWriter.running = true
} else {
console.log("cava not found - using fallback animation")
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
Process {
id: configWriter
running: root.cavaAvailable
command: [
"sh", "-c",
`cat > /tmp/quickshell_cava_config << 'EOF'
[general]
mode = normal
framerate = 30
autosens = 0
sensitivity = 50
bars = 4
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
channels = mono
mono_option = average
[smoothing]
noise_reduction = 20
EOF`
]
onExited: {
if (root.cavaAvailable) {
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
Process {
id: cavaProcess
running: false
command: ["cava", "-p", "/tmp/quickshell_cava_config"]
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (data.trim()) {
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
if (points.length >= 4) {
root.audioLevels = [points[0], points[1], points[2], points[3]]
}
}
}
}
onRunningChanged: {
if (!running) {
root.audioLevels = [0, 0, 0, 0]
}
}
}
Timer {
id: fallbackTimer
running: false
interval: 100
repeat: true
onTriggered: {
root.audioLevels = [
Math.random() * 40 + 10,
Math.random() * 60 + 20,
Math.random() * 50 + 15,
Math.random() * 35 + 20
]
}
}
Row {
anchors.centerIn: parent
spacing: 2
Repeater {
model: 4
Rectangle {
width: 3
height: {
if (root.activePlayer?.playbackState === MprisPlaybackState.Playing && root.audioLevels.length > index) {
const rawLevel = root.audioLevels[index] || 0
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
const maxHeight = Theme.iconSize - 2
const minHeight = 3
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
}
return 3
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: 80
easing.type: Easing.OutQuad
}
}
}
}
}
}

View File

@@ -0,0 +1,155 @@
import QtQuick
import Quickshell.Services.Mpris
import "../../Common"
import "../../Services"
Rectangle {
id: root
property bool hasActiveMedia: false
property var activePlayer: null
property bool weatherAvailable: false
property string weatherCode: ""
property int weatherTemp: 0
property int weatherTempF: 0
property bool useFahrenheit: false
property date currentDate: new Date()
signal clockClicked()
width: {
let baseWidth = 200
if (root.hasActiveMedia) {
let mediaWidth = 24 + Theme.spacingXS + mediaTitleText.implicitWidth + Theme.spacingM + 180
return Math.min(Math.max(mediaWidth, 300), parent.width - Theme.spacingL * 2)
} else if (root.weatherAvailable) {
return Math.min(280, parent.width - Theme.spacingL * 2)
} else {
return Math.min(baseWidth, parent.width - Theme.spacingL * 2)
}
}
height: 32
radius: Theme.cornerRadius
color: clockMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
// Media info or Weather info
Row {
spacing: Theme.spacingXS
visible: root.hasActiveMedia || root.weatherAvailable
anchors.verticalCenter: parent.verticalCenter
// Audio visualization placeholder - will be replaced by parent
Item {
id: audioVisualizationPlaceholder
width: 20
height: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia
}
// Song title when media is playing
Text {
id: mediaTitleText
text: root.activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia
width: Math.min(implicitWidth, root.width - 100)
elide: Text.ElideRight
}
// Weather icon when no media but weather available
Text {
text: WeatherService.getWeatherIcon(root.weatherCode)
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !root.hasActiveMedia && root.weatherAvailable
}
// Weather temp when no media but weather available
Text {
text: (root.useFahrenheit ? root.weatherTempF : root.weatherTemp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: !root.hasActiveMedia && root.weatherAvailable
}
}
// Separator
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia || root.weatherAvailable
}
// Time and date
Row {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: Qt.formatTime(root.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Qt.formatDate(root.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
root.currentDate = new Date()
}
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clockClicked()
}
}
}

View File

@@ -0,0 +1,104 @@
import QtQuick
import "../../Common"
import "../../Services"
Rectangle {
id: root
property string networkStatus: "disconnected"
property string wifiSignalStrength: "good"
property int volumeLevel: 50
property bool bluetoothAvailable: false
property bool bluetoothEnabled: false
property bool isActive: false
signal clicked()
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: controlCenterArea.containsMouse || root.isActive ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
// Network Status Icon
Text {
text: {
if (root.networkStatus === "ethernet") return "lan"
else if (root.networkStatus === "wifi") {
switch (root.wifiSignalStrength) {
case "excellent": return "wifi"
case "good": return "wifi_2_bar"
case "fair": return "wifi_1_bar"
case "poor": return "wifi_calling_3"
default: return "wifi"
}
}
else return "wifi_off"
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: root.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: true
}
// Audio Icon
Text {
text: root.volumeLevel === 0 ? "volume_off" :
root.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: controlCenterArea.containsMouse || root.isActive ?
Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
// Microphone Icon (when active)
Text {
text: "mic"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
// Bluetooth Icon (when available and enabled)
Text {
text: "bluetooth"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: root.bluetoothEnabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.bluetoothAvailable && root.bluetoothEnabled
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -0,0 +1,41 @@
import QtQuick
import "../../Common"
import "../../Services"
Rectangle {
id: root
width: 40
height: 32
radius: Theme.cornerRadius
color: launcherArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
property string osLogo: ""
Text {
anchors.centerIn: parent
text: root.osLogo || "apps"
font.family: root.osLogo ? "NerdFont" : Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
LauncherService.toggleAppLauncher()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -0,0 +1,58 @@
import QtQuick
import "../../Common"
Rectangle {
id: root
property bool hasUnread: false
property bool isActive: false
signal clicked()
width: 40
height: 32
radius: Theme.cornerRadius
color: notificationArea.containsMouse || root.isActive ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: notificationArea.containsMouse || root.isActive ?
Theme.primary : Theme.surfaceText
}
// Notification dot indicator
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: root.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -0,0 +1,100 @@
import QtQuick
import Quickshell.Services.SystemTray
import "../../Common"
Rectangle {
id: root
signal menuRequested(var menu, var item, real x, real y)
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: systemTrayRow.children.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
width: 24
height: 24
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon || "";
if (!icon) return "";
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
customTrayMenu.showMenu(mouse.x, mouse.y)
} else {
console.log("No menu available for:", trayItem.title || "Unknown")
}
}
}
}
QtObject {
id: customTrayMenu
property bool menuVisible: false
function showMenu(x, y) {
root.menuRequested(customTrayMenu, trayItem, x, y)
menuVisible = true
}
function hideMenu() {
menuVisible = false
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}

292
Widgets/TopBar/TopBar.qml Normal file
View File

@@ -0,0 +1,292 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import "../../Common"
import "../../Services"
import ".."
PanelWindow {
id: topBar
property var modelData
screen: modelData
property string screenName: modelData.name
// Properties exposed to shell
property bool hasActiveMedia: false
property var activePlayer: null
property bool weatherAvailable: false
property string weatherCode: ""
property int weatherTemp: 0
property int weatherTempF: 0
property bool useFahrenheit: false
property string osLogo: ""
property string networkStatus: "disconnected"
property string wifiSignalStrength: "good"
property int volumeLevel: 50
property bool bluetoothAvailable: false
property bool bluetoothEnabled: false
// Notification properties
property bool notificationHistoryVisible: false
property int notificationCount: 0
// Control center properties
property bool controlCenterVisible: false
// Calendar properties
property bool calendarVisible: false
// Clipboard properties
signal clipboardRequested()
// Tray menu properties
property bool showTrayMenu: false
property var currentTrayMenu: null
property var currentTrayItem: null
property real trayMenuX: 0
property real trayMenuY: 0
// Proxy objects for external connections
QtObject {
id: notificationHistory
property int count: 0
}
// Battery widget and other widgets are imported from their original locations
// These will be handled by the parent shell
property alias batteryWidget: batteryWidgetProxy
property alias cpuMonitorWidget: cpuMonitorWidgetProxy
property alias ramMonitorWidget: ramMonitorWidgetProxy
property alias powerButton: powerButtonProxy
QtObject { id: batteryWidgetProxy }
QtObject { id: cpuMonitorWidgetProxy }
QtObject { id: ramMonitorWidgetProxy }
QtObject { id: powerButtonProxy }
anchors {
top: true
left: true
right: true
}
implicitHeight: Theme.barHeight - 4
color: "transparent"
// Floating panel container with margins
Item {
anchors.fill: parent
anchors.margins: 2
anchors.topMargin: 6
anchors.bottomMargin: 0
anchors.leftMargin: 8
anchors.rightMargin: 8
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadiusXLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.75)
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 4
radius: 16
samples: 33
color: Qt.rgba(0, 0, 0, 0.15)
transparentBorder: true
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
radius: parent.radius
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
anchors.bottomMargin: Theme.spacingXS
Row {
id: leftSection
height: parent.height
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
LauncherButton {
anchors.verticalCenter: parent.verticalCenter
osLogo: topBar.osLogo
}
WorkspaceSwitcher {
anchors.verticalCenter: parent.verticalCenter
screenName: topBar.screenName
}
}
ClockWidget {
id: clockWidget
anchors.centerIn: parent
hasActiveMedia: topBar.hasActiveMedia
activePlayer: topBar.activePlayer
weatherAvailable: topBar.weatherAvailable
weatherCode: topBar.weatherCode
weatherTemp: topBar.weatherTemp
weatherTempF: topBar.weatherTempF
useFahrenheit: topBar.useFahrenheit
onClockClicked: {
topBar.calendarVisible = !topBar.calendarVisible
}
// Insert audio visualization into the clock widget
AudioVisualization {
parent: clockWidget.children[0].children[0] // Row -> Row (media info)
anchors.verticalCenter: parent.verticalCenter
hasActiveMedia: topBar.hasActiveMedia
activePlayer: topBar.activePlayer
visible: topBar.hasActiveMedia
}
}
Row {
id: rightSection
height: parent.height
spacing: Theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
SystemTrayWidget {
anchors.verticalCenter: parent.verticalCenter
onMenuRequested: (menu, item, x, y) => {
topBar.currentTrayMenu = menu
topBar.currentTrayItem = item
topBar.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL
topBar.trayMenuY = Theme.barHeight + Theme.spacingS
console.log("Showing menu at:", topBar.trayMenuX, topBar.trayMenuY)
menu.menuVisible = true
topBar.showTrayMenu = true
}
}
Rectangle {
width: 40
height: 32
radius: Theme.cornerRadius
color: clipboardArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "content_paste"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
topBar.clipboardRequested()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// System Monitor Widgets
CpuMonitorWidget {
anchors.verticalCenter: parent.verticalCenter
}
RamMonitorWidget {
anchors.verticalCenter: parent.verticalCenter
}
NotificationCenterButton {
anchors.verticalCenter: parent.verticalCenter
hasUnread: topBar.notificationCount > 0
isActive: topBar.notificationHistoryVisible
onClicked: {
topBar.notificationHistoryVisible = !topBar.notificationHistoryVisible
}
}
// Battery Widget
BatteryWidget {
anchors.verticalCenter: parent.verticalCenter
}
ControlCenterButton {
anchors.verticalCenter: parent.verticalCenter
networkStatus: topBar.networkStatus
wifiSignalStrength: topBar.wifiSignalStrength
volumeLevel: topBar.volumeLevel
bluetoothAvailable: topBar.bluetoothAvailable
bluetoothEnabled: topBar.bluetoothEnabled
isActive: topBar.controlCenterVisible
onClicked: {
topBar.controlCenterVisible = !topBar.controlCenterVisible
if (topBar.controlCenterVisible) {
WifiService.scanWifi()
BluetoothService.scanDevices()
}
}
}
// Power Button
PowerButton {
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
import QtQuick
import Quickshell
import "../../Common"
import "../../Services"
Rectangle {
id: root
property string screenName: ""
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
visible: NiriWorkspaceService.niriAvailable
property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: getDisplayWorkspaces()
function getDisplayWorkspaces() {
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return [1, 2]
}
if (!root.screenName) {
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
}
var displayWorkspaces = []
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === root.screenName) {
displayWorkspaces.push(ws.idx + 1)
}
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
}
function getDisplayActiveWorkspace() {
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return 1
}
if (!root.screenName) {
return NiriWorkspaceService.getCurrentWorkspaceNumber()
}
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === root.screenName && ws.is_active) {
return ws.idx + 1
}
}
return 1
}
Connections {
target: NiriWorkspaceService
function onAllWorkspacesChanged() {
root.workspaceList = root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
}
function onFocusedWorkspaceIndexChanged() {
root.currentWorkspace = root.getDisplayActiveWorkspace()
}
function onNiriAvailableChanged() {
if (NiriWorkspaceService.niriAvailable) {
root.workspaceList = root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
}
}
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: root.workspaceList
Rectangle {
property bool isActive: modelData === root.currentWorkspace
property bool isHovered: mouseArea.containsMouse
property int sequentialNumber: index + 1
width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
height: Theme.spacingS
radius: height / 2
color: isActive ? Theme.primary :
isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", sequentialNumber.toString()])
}
}
}
}
}
}

8
Widgets/TopBar/qmldir Normal file
View File

@@ -0,0 +1,8 @@
TopBar 1.0 TopBar.qml
LauncherButton 1.0 LauncherButton.qml
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
ClockWidget 1.0 ClockWidget.qml
SystemTrayWidget 1.0 SystemTrayWidget.qml
NotificationCenterButton 1.0 NotificationCenterButton.qml
ControlCenterButton 1.0 ControlCenterButton.qml
AudioVisualization 1.0 AudioVisualization.qml

View File

@@ -1,4 +1,4 @@
TopBar 1.0 TopBar.qml
TopBar 1.0 TopBar/TopBar.qml
TrayMenuPopup 1.0 TrayMenuPopup.qml
NotificationPopup 1.0 NotificationPopup.qml
NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml

View File

@@ -283,6 +283,47 @@ ShellRoot {
model: Quickshell.screens
delegate: TopBar {
modelData: item
// Connect shell properties
hasActiveMedia: root.hasActiveMedia
activePlayer: root.activePlayer
weatherAvailable: root.weather.available
weatherCode: root.weather.wCode
weatherTemp: root.weather.temp
weatherTempF: root.weather.tempF
useFahrenheit: root.useFahrenheit
osLogo: root.osLogo
networkStatus: root.networkStatus
wifiSignalStrength: root.wifiSignalStrength
volumeLevel: root.volumeLevel
bluetoothAvailable: root.bluetoothAvailable
bluetoothEnabled: root.bluetoothEnabled
notificationHistoryVisible: root.notificationHistoryVisible
notificationCount: notificationHistory.count
controlCenterVisible: root.controlCenterVisible
calendarVisible: root.calendarVisible
// Connect tray menu properties
showTrayMenu: root.showTrayMenu
currentTrayMenu: root.currentTrayMenu
currentTrayItem: root.currentTrayItem
trayMenuX: root.trayMenuX
trayMenuY: root.trayMenuY
// Connect clipboard
onClipboardRequested: {
clipboardHistoryPopup.toggle()
}
// Property change handlers
onCalendarVisibleChanged: root.calendarVisible = calendarVisible
onControlCenterVisibleChanged: root.controlCenterVisible = controlCenterVisible
onNotificationHistoryVisibleChanged: root.notificationHistoryVisible = notificationHistoryVisible
onShowTrayMenuChanged: root.showTrayMenu = showTrayMenu
onCurrentTrayMenuChanged: root.currentTrayMenu = currentTrayMenu
onCurrentTrayItemChanged: root.currentTrayItem = currentTrayItem
onTrayMenuXChanged: root.trayMenuX = trayMenuX
onTrayMenuYChanged: root.trayMenuY = trayMenuY
}
}