Merge branch 'noctalia-dev:main' into powermenu-ipc

This commit is contained in:
Dillon Johnson 2025-08-22 10:14:29 -10:00 committed by GitHub
commit 5cee4c234a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 815 additions and 260 deletions

View file

@ -144,6 +144,7 @@ Loader {
maskEnabled: true
maskSource: maskTexture
maskInverted: false
maskSpreadAtMax: 0.75
}
mask: Region {}

View file

@ -47,7 +47,7 @@ Variants {
layer.enabled: true
}
// Left
// Left Section - Dynamic Widgets
Row {
id: leftSection
@ -57,14 +57,25 @@ Variants {
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
SystemMonitor {}
ActiveWindow {}
MediaMini {}
Repeater {
model: Settings.data.bar.widgets.left
delegate: Loader {
id: leftWidgetLoader
sourceComponent: widgetLoader.getWidgetComponent(modelData)
active: true
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Loader.Error) {
widgetLoader.onWidgetFailed(modelData, "Loader error")
} else if (status === Loader.Ready) {
widgetLoader.onWidgetLoaded(modelData)
}
}
}
}
}
// Center
// Center Section - Dynamic Widgets
Row {
id: centerSection
@ -73,10 +84,25 @@ Variants {
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Workspace {}
Repeater {
model: Settings.data.bar.widgets.center
delegate: Loader {
id: centerWidgetLoader
sourceComponent: widgetLoader.getWidgetComponent(modelData)
active: true
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Loader.Error) {
widgetLoader.onWidgetFailed(modelData, "Loader error")
} else if (status === Loader.Ready) {
widgetLoader.onWidgetLoaded(modelData)
}
}
}
}
}
// Right
// Right Section - Dynamic Widgets
Row {
id: rightSection
@ -86,44 +112,38 @@ Variants {
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
ScreenRecorderIndicator {
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: Settings.data.bar.widgets.right
delegate: Loader {
id: rightWidgetLoader
sourceComponent: widgetLoader.getWidgetComponent(modelData)
active: true
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Loader.Error) {
widgetLoader.onWidgetFailed(modelData, "Loader error")
} else if (status === Loader.Ready) {
widgetLoader.onWidgetLoaded(modelData)
}
}
}
}
Tray {
anchors.verticalCenter: parent.verticalCenter
}
NotificationHistory {
anchors.verticalCenter: parent.verticalCenter
}
WiFi {
anchors.verticalCenter: parent.verticalCenter
}
Bluetooth {
anchors.verticalCenter: parent.verticalCenter
}
Battery {
anchors.verticalCenter: parent.verticalCenter
}
Volume {
anchors.verticalCenter: parent.verticalCenter
}
Brightness {
anchors.verticalCenter: parent.verticalCenter
}
Clock {
anchors.verticalCenter: parent.verticalCenter
}
SidePanelToggle {}
}
}
// Widget loader instance
WidgetLoader {
id: widgetLoader
onWidgetFailed: function (widgetName, error) {
Logger.error("Bar", `Widget failed: ${widgetName} - ${error}`)
}
}
// Initialize widget loading tracking
Component.onCompleted: {
const allWidgets = [...Settings.data.bar.widgets.left, ...Settings.data.bar.widgets.center, ...Settings.data.bar.widgets.right]
widgetLoader.initializeLoading(allWidgets)
}
}
}

View file

@ -1,43 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
readonly property bool wifiEnabled: Settings.data.network.wifiEnabled
sizeMultiplier: 0.8
visible: wifiEnabled
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
}
tooltipText: "WiFi Networks"
onClicked: {
wifiPanel.toggle(screen)
}
WiFiPanel {
id: wifiPanel
}
}

View file

@ -11,7 +11,7 @@ Row {
id: root
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: (Settings.data.bar.showActiveWindow && getTitle() !== "")
visible: getTitle() !== ""
property bool showingFullTitle: false
property int lastWindowIndex: -1

View file

@ -22,8 +22,6 @@ NPill {
// Choose icon based on charge and charging state
function batteryIcon() {
if (!show)
return ""
if (charging)
return "battery_android_bolt"

View file

@ -10,9 +10,7 @@ import qs.Widgets
NIconButton {
id: root
readonly property bool bluetoothEnabled: Settings.data.network.bluetoothEnabled
sizeMultiplier: 0.8
visible: bluetoothEnabled
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
@ -33,8 +31,4 @@ NIconButton {
onClicked: {
bluetoothPanel.toggle(screen)
}
BluetoothPanel {
id: bluetoothPanel
}
}

View file

@ -10,7 +10,7 @@ Item {
width: pill.width
height: pill.height
visible: Settings.data.bar.showBrightness && firstBrightnessReceived && getMonitor() !== null
visible: getMonitor() !== null
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false

View file

@ -11,7 +11,8 @@ Row {
id: root
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: Settings.data.bar.showMedia && (MediaService.canPlay || MediaService.canPause)
visible: MediaService.currentPlayer !== null
width: MediaService.currentPlayer !== null ? implicitWidth : 0
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
@ -109,14 +110,14 @@ Row {
visible: Settings.data.audio.showMiniplayerAlbumArt
Rectangle {
width: 16 * scaling
height: 16 * scaling
width: 18 * scaling
height: 18 * scaling
radius: width * 0.5
color: Color.transparent
antialiasing: true
clip: true
NImageRounded {
NImageCircled {
id: trackArt
visible: MediaService.trackArtUrl.toString() !== ""
anchors.fill: parent
@ -126,8 +127,6 @@ Row {
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
borderWidth: 0
border.color: Color.transparent
imageRadius: width
antialiasing: true
}
// Fallback icon when no album art available

View file

@ -10,7 +10,6 @@ import qs.Widgets
NIconButton {
id: root
visible: Settings.data.bar.showNotificationsHistory
sizeMultiplier: 0.8
icon: "notifications"
tooltipText: "Notification History"

View file

@ -8,7 +8,6 @@ Row {
id: root
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: (Settings.data.bar.showSystemInfo)
Rectangle {
// Let the Rectangle size itself based on its content (the Row)

View file

@ -12,9 +12,8 @@ import qs.Widgets
Rectangle {
readonly property real itemSize: 24 * scaling
visible: Settings.data.bar.showTray && (SystemTray.items.values.length > 0)
visible: SystemTray.items.values.length > 0
width: tray.width + Style.marginM * scaling * 2
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
@ -95,14 +94,14 @@ Rectangle {
return
}
if (modelData.hasMenu && modelData.menu && trayMenu) {
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.width / 2)
const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = (Style.barHeight * scaling)
trayMenu.menu = modelData.menu
trayMenu.showAt(parent, menuX, menuY)
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
}
@ -142,7 +141,7 @@ Rectangle {
function close() {
visible = false
trayMenu.hideMenu()
trayMenu.item.hideMenu()
}
// Clicking outside of the rectangle to close
@ -151,8 +150,9 @@ Rectangle {
onClicked: trayPanel.close()
}
TrayMenu {
Loader {
id: trayMenu
source: "TrayMenu.qml"
}
}
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
sizeMultiplier: 0.8
Component.onCompleted: {
Logger.log("WiFi", "Widget component completed")
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
if (NetworkService) {
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
}
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
try {
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
} catch (error) {
Logger.error("WiFi", "Error getting icon:", error)
return "wifi"
}
}
tooltipText: "WiFi Networks"
onClicked: {
try {
Logger.log("WiFi", "Button clicked, toggling panel")
wifiPanel.toggle(screen)
} catch (error) {
Logger.error("WiFi", "Error toggling panel:", error)
}
}
}

View file

@ -225,7 +225,7 @@ NPanel {
Layout.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: searchInput.activeFocus ? Color.mTertiary : Color.mOutline
border.color: searchInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling)
Item {
@ -355,7 +355,7 @@ NPanel {
height: 65 * scaling
radius: Style.radiusM * scaling
property bool isSelected: index === selectedIndex
color: (appCardArea.containsMouse || isSelected) ? Color.mTertiary : Color.mSurface
color: (appCardArea.containsMouse || isSelected) ? Color.mSecondary : Color.mSurface
Behavior on color {
ColorAnimation {

View file

@ -294,13 +294,14 @@ Loader {
// Animated avatar with glow effect or audio visualizer
Rectangle {
width: 120 * scaling
height: 120 * scaling
width: 108 * scaling
height: 108 * scaling
radius: width * 0.5
color: Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderL * scaling)
anchors.horizontalCenter: parent.horizontalCenter
z: 10
// Circular audio visualizer when music is playing
Loader {
@ -464,13 +465,12 @@ Loader {
}
}
NImageRounded {
NImageCircled {
anchors.centerIn: parent
width: 100 * scaling
height: 100 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
imageRadius: width * 0.5
}
// Hover animation

View file

@ -111,7 +111,7 @@ NPanel {
width: notificationList ? notificationList.width : 380 * scaling
height: Math.max(80, notificationContent.height + 30)
radius: Style.radiusM * scaling
color: notificationMouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
RowLayout {
anchors {

View file

@ -215,7 +215,7 @@ NPanel {
if (pending)
return Color.applyOpacity(Color.mPrimary, "20")
if (mouseArea.containsMouse)
return Color.mTertiary
return Color.mSecondary
return Color.transparent
}

View file

@ -47,6 +47,7 @@ NPanel {
id: barTab
Tabs.BarTab {}
}
Component {
id: audioTab
Tabs.AudioTab {}

View file

@ -199,7 +199,7 @@ ColumnLayout {
width: contributorsGrid.cellWidth - Style.marginL * scaling
height: contributorsGrid.cellHeight - Style.marginXS * scaling
radius: Style.radiusL * scaling
color: contributorArea.containsMouse ? Color.mTertiary : Color.transparent
color: contributorArea.containsMouse ? Color.mSecondary : Color.transparent
RowLayout {
anchors.fill: parent
@ -211,14 +211,13 @@ ColumnLayout {
Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling
NImageRounded {
NImageCircled {
imagePath: modelData.avatar_url || ""
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
imageRadius: width * 0.5
}
}

View file

@ -33,12 +33,6 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Bar Components"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
ColumnLayout {
spacing: Style.marginXXS * scaling
@ -78,70 +72,7 @@ ColumnLayout {
}
}
NToggle {
label: "Show Active Window"
description: "Display the title of the currently focused window."
checked: Settings.data.bar.showActiveWindow
onToggled: checked => {
Settings.data.bar.showActiveWindow = checked
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show System Info"
description: "Display system statistics (CPU, RAM, Temperature)."
checked: Settings.data.bar.showSystemInfo
onToggled: checked => {
Settings.data.bar.showSystemInfo = checked
}
}
NToggle {
label: "Show Media"
description: "Display media controls and information."
checked: Settings.data.bar.showMedia
onToggled: checked => {
Settings.data.bar.showMedia = checked
}
}
NToggle {
label: "Show Notifications History"
description: "Display a shortcut to the notifications history."
checked: Settings.data.bar.showNotificationsHistory
onToggled: checked => {
Settings.data.bar.showNotificationsHistory = checked
}
}
NToggle {
label: "Show Applications Tray"
description: "Display the applications tray."
checked: Settings.data.bar.showTray
onToggled: checked => {
Settings.data.bar.showTray = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Show battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
ColumnLayout {
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
@ -179,7 +110,168 @@ ColumnLayout {
}
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Show battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Add, remove, or reorder widgets in each section of the bar using the control buttons."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
// Left Section
NWidgetCard {
sectionName: "Left"
widgetModel: Settings.data.bar.widgets.left
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Center Section
NWidgetCard {
sectionName: "Center"
widgetModel: Settings.data.bar.widgets.center
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Right Section
NWidgetCard {
sectionName: "Right"
widgetModel: Settings.data.bar.widgets.right
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
}
}
}
}
}
// Helper functions
function addWidgetToSection(widgetName, section) {
console.log("Adding widget", widgetName, "to section", section)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
newArray.push(widgetName)
console.log("Widget added. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
function removeWidgetFromSection(section, index) {
console.log("Removing widget from section", section, "at index", index)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray && index >= 0 && index < sectionArray.length) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
newArray.splice(index, 1)
console.log("Widget removed. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
function reorderWidgetInSection(section, fromIndex, toIndex) {
console.log("Reordering widget in section", section, "from", fromIndex, "to", toIndex)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
&& toIndex < sectionArray.length) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
var item = newArray[fromIndex]
newArray.splice(fromIndex, 1)
newArray.splice(toIndex, 0, item)
console.log("Widget reordered. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
// Widget loader for discovering available widgets
WidgetLoader {
id: widgetLoader
}
ListModel {
id: availableWidgets
}
Component.onCompleted: {
discoverWidgets()
}
// Automatically discover available widgets using WidgetLoader
function discoverWidgets() {
availableWidgets.clear()
// Use WidgetLoader to discover available widgets
const discoveredWidgets = widgetLoader.discoverAvailableWidgets()
// Add discovered widgets to the ListModel
discoveredWidgets.forEach(widget => {
availableWidgets.append(widget)
})
}
}

View file

@ -51,13 +51,13 @@ ColumnLayout {
spacing: Style.marginL * scaling
// Avatar preview
NImageRounded {
NImageCircled {
width: 64 * scaling
height: 64 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM)
borderWidth: Math.max(1, Style.borderM * scaling)
}
NTextInput {

View file

@ -76,7 +76,7 @@ NBox {
// implicitWidth: 120 * scaling
// implicitHeight: 30 * scaling
color: Color.transparent
border.color: playerSelector.activeFocus ? Color.mTertiary : Color.mOutline
border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
}
@ -138,7 +138,7 @@ NBox {
background: Rectangle {
width: popup.width - Style.marginS * scaling * 2
color: highlighted ? Color.mTertiary : Color.transparent
color: highlighted ? Color.mSecondary : Color.transparent
radius: Style.radiusXS * scaling
}
}
@ -164,7 +164,7 @@ NBox {
border.width: Math.max(1, Style.borderS * scaling)
clip: true
NImageRounded {
NImageCircled {
id: trackArt
visible: MediaService.trackArtUrl.toString() !== ""
@ -174,7 +174,6 @@ NBox {
fallbackIcon: "music_note"
borderColor: Color.mOutline
borderWidth: Math.max(1, Style.borderS * scaling)
imageRadius: width * 0.5
}
// Fallback icon when no album art available

View file

@ -28,7 +28,7 @@ NBox {
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
NImageRounded {
NImageCircled {
width: Style.baseWidgetSize * 1.25 * scaling
height: Style.baseWidgetSize * 1.25 * scaling
imagePath: Settings.data.general.avatarImage