Refactor: Panels and Bar background are now drawn separately with Shapes.

This commit is contained in:
ItsLemmy
2025-11-06 10:41:48 -05:00
parent 76a182a90c
commit e29c6ee1a6
109 changed files with 2381 additions and 2192 deletions
-253
View File
@@ -1,253 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
property real localOutputVolume: AudioService.volume || 0
property bool localOutputVolumeChanging: false
property real localInputVolume: AudioService.inputVolume || 0
property bool localInputVolumeChanging: false
preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 420 * Style.uiScaleRatio
// Connections to update local volumes when AudioService changes
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
if (!localOutputVolumeChanging) {
localOutputVolume = AudioService.volume
}
}
}
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
if (!localInputVolumeChanging) {
localInputVolume = AudioService.inputVolume
}
}
}
// Timer to debounce volume changes
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
if (Math.abs(localOutputVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localOutputVolume)
}
if (Math.abs(localInputVolume - AudioService.inputVolume) >= 0.01) {
AudioService.setInputVolume(localInputVolume)
}
}
}
panelContent: Rectangle {
color: Color.transparent
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2
// property real contentPreferredHeight: Math.min(screen.height * 0.42, mainColumn.implicitHeight) + Style.marginL * 2
ColumnLayout {
id: mainColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
icon: "settings-audio"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("settings.audio.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: AudioService.getOutputIcon()
tooltipText: I18n.tr("tooltips.output-muted")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
AudioService.setOutputMuted(!AudioService.muted)
}
}
NIconButton {
icon: AudioService.getInputIcon()
tooltipText: I18n.tr("tooltips.input-muted")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
}
}
}
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// AudioService Devices
ColumnLayout {
spacing: Style.marginM
width: parent.width
// -------------------------------
// Output Devices
ButtonGroup {
id: sinks
}
NBox {
Layout.fillWidth: true
Layout.preferredHeight: outputColumn.implicitHeight + (Style.marginM * 2)
ColumnLayout {
id: outputColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM
spacing: Style.marginS
NText {
text: I18n.tr("settings.audio.devices.output-device.label")
pointSize: Style.fontSizeL
color: Color.mPrimary
}
// Output Volume Slider
NValueSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localOutputVolume
stepSize: 0.01
heightRatio: 0.5
onMoved: value => localOutputVolume = value
onPressedChanged: (pressed, value) => localOutputVolumeChanging = pressed
text: Math.round(localOutputVolume * 100) + "%"
Layout.bottomMargin: Style.marginM
}
Repeater {
model: AudioService.sinks
NRadioButton {
ButtonGroup.group: sinks
required property PwNode modelData
pointSize: Style.fontSizeS
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData)
localOutputVolume = AudioService.volume
}
Layout.fillWidth: true
}
}
}
}
// -------------------------------
// Input Devices
ButtonGroup {
id: sources
}
NBox {
Layout.fillWidth: true
Layout.preferredHeight: inputColumn.implicitHeight + (Style.marginM * 2)
ColumnLayout {
id: inputColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Style.marginM
spacing: Style.marginS
NText {
text: I18n.tr("settings.audio.devices.input-device.label")
pointSize: Style.fontSizeL
color: Color.mPrimary
}
// Input Volume Slider
NValueSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localInputVolume
stepSize: 0.01
heightRatio: 0.5
onMoved: value => localInputVolume = value
onPressedChanged: (pressed, value) => localInputVolumeChanging = pressed
text: Math.round(localInputVolume * 100) + "%"
Layout.bottomMargin: Style.marginM
}
Repeater {
model: AudioService.sources
NRadioButton {
ButtonGroup.group: sources
required property PwNode modelData
pointSize: Style.fontSizeS
text: modelData.description
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
Layout.fillWidth: true
}
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
+79 -20
View File
@@ -14,12 +14,15 @@ import qs.Modules.Bar.Extras
Item {
id: root
// This property will be set by NFullScreenWindow
// This property will be set by MainScreen
property ShellScreen screen: null
// Expose bar region for click-through mask
readonly property var barRegion: barContentLoader.item?.children[0] || null
// Expose the actual bar Item for unified background system
readonly property var barItem: barRegion
// Bar positioning properties
readonly property string barPosition: Settings.data.bar.position || "top"
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
@@ -61,8 +64,8 @@ Item {
sourceComponent: Item {
anchors.fill: parent
// Background fill
NShapedRectangle {
// Bar container (background rendered by AllBackgrounds)
Item {
id: bar
// Position and size the bar based on orientation and floating margins
@@ -92,26 +95,82 @@ Item {
return baseHeight // Vertical bars extend via width, not height
}
backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
// Corner states for new unified background system
// State -1: No radius (flat/square corner)
// State 0: Normal (inner curve)
// State 1: Horizontal inversion (outer curve on X-axis)
// State 2: Vertical inversion (outer curve on Y-axis)
readonly property int topLeftCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Top bar: top corners against screen edge = no radius
if (barPosition === "top")
return -1
// Left bar: top-left against screen edge = no radius
if (barPosition === "left")
return -1
// Bottom/Right bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
return barIsVertical ? 1 : 2 // horizontal invert for vertical bars, vertical invert for horizontal
}
// No outerCorners = square
return -1
}
// Floating bar rounded corners
topLeftRadius: Settings.data.bar.floating || topLeftInverted ? Style.radiusL : 0
topRightRadius: Settings.data.bar.floating || topRightInverted ? Style.radiusL : 0
bottomLeftRadius: Settings.data.bar.floating || bottomLeftInverted ? Style.radiusL : 0
bottomRightRadius: Settings.data.bar.floating || bottomRightInverted ? Style.radiusL : 0
readonly property int topRightCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Top bar: top corners against screen edge = no radius
if (barPosition === "top")
return -1
// Right bar: top-right against screen edge = no radius
if (barPosition === "right")
return -1
// Bottom/Left bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
return barIsVertical ? 1 : 2
}
// No outerCorners = square
return -1
}
topLeftInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")
topLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical"
topRightInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")
topRightInvertedDirection: barIsVertical ? "horizontal" : "vertical"
readonly property int bottomLeftCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Bottom bar: bottom corners against screen edge = no radius
if (barPosition === "bottom")
return -1
// Left bar: bottom-left against screen edge = no radius
if (barPosition === "left")
return -1
// Top/Right bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
return barIsVertical ? 1 : 2
}
// No outerCorners = square
return -1
}
bottomLeftInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")
bottomLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical"
bottomRightInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")
bottomRightInvertedDirection: barIsVertical ? "horizontal" : "vertical"
// No border on the bar
borderWidth: 0
readonly property int bottomRightCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Bottom bar: bottom corners against screen edge = no radius
if (barPosition === "bottom")
return -1
// Right bar: bottom-right against screen edge = no radius
if (barPosition === "right")
return -1
// Top/Left bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
return barIsVertical ? 1 : 2
}
// No outerCorners = square
return -1
}
MouseArea {
anchors.fill: parent
-132
View File
@@ -1,132 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
property var optionsModel: []
function updateOptionsModel() {
let newOptions = [{
"id": BatteryService.ChargingMode.Full,
"label": "battery.panel.full"
}, {
"id": BatteryService.ChargingMode.Balanced,
"label": "battery.panel.balanced"
}, {
"id": BatteryService.ChargingMode.Lifespan,
"label": "battery.panel.lifespan"
}]
root.optionsModel = newOptions
}
onOpened: {
updateOptionsModel()
}
ButtonGroup {
id: batteryGroup
}
Component {
id: optionsComponent
ColumnLayout {
spacing: Style.marginM
Repeater {
model: root.optionsModel
delegate: NRadioButton {
ButtonGroup.group: batteryGroup
required property var modelData
text: I18n.tr(modelData.label, {
"percent": BatteryService.getThresholdValue(modelData.id)
})
checked: BatteryService.chargingMode === modelData.id
onClicked: {
BatteryService.setChargingMode(modelData.id)
}
Layout.fillWidth: true
}
}
}
}
Component {
id: disabledComponent
NText {
text: I18n.tr("battery.panel.disabled")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
panelContent: Item {
anchors.fill: parent
property real contentPreferredHeight: mainLayout.implicitHeight + Style.marginM * 2
property real contentPreferredWidth: 350 * Style.uiScaleRatio
ColumnLayout {
id: mainLayout
anchors.centerIn: parent
width: parent.contentPreferredWidth - Style.marginM * 2
anchors.margins: Style.marginM
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
Layout.preferredHeight: header.implicitHeight + Style.marginM * 2
RowLayout {
id: header
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NText {
text: I18n.tr("battery.panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
id: batteryManagerSwitch
checked: BatteryService.chargingMode !== BatteryService.ChargingMode.Disabled
onToggled: checked => BatteryService.toggleEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
}
}
}
NBox {
Layout.fillWidth: true
Layout.preferredHeight: loader.implicitHeight + Style.marginM * 2
Loader {
id: loader
anchors.centerIn: parent
width: parent.width - Style.marginM * 2
sourceComponent: BatteryService.chargingMode === BatteryService.ChargingMode.Disabled ? disabledComponent : optionsComponent
}
}
}
}
}
@@ -1,190 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NBox {
id: root
property string label: ""
property string tooltipText: ""
property var model: {
}
Layout.fillWidth: true
Layout.preferredHeight: column.implicitHeight + Style.marginM * 2
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NText {
text: root.label
pointSize: Style.fontSizeL
color: Color.mSecondary
font.weight: Style.fontWeightMedium
visible: root.model.length > 0
Layout.fillWidth: true
Layout.leftMargin: Style.marginM
}
Repeater {
id: deviceList
Layout.fillWidth: true
model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Rectangle {
id: device
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
function getContentColor(defaultColor = Color.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return defaultColor
}
Layout.fillWidth: true
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * 2)
radius: Style.radiusM
color: Color.mSurface
border.width: Style.borderS
border.color: getContentColor(Color.mOutline)
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
icon: BluetoothService.getDeviceIcon(modelData)
pointSize: Style.fontSizeXXL
color: getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
// Device name
NText {
text: modelData.name || modelData.deviceName
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
color: getContentColor(Color.mOnSurface)
Layout.fillWidth: true
}
// Status
NText {
text: BluetoothService.getStatusString(modelData)
visible: text !== ""
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
}
// Signal Strength
RowLayout {
visible: modelData.signalStrength !== undefined
Layout.fillWidth: true
spacing: Style.marginXS
// Device signal strength - "Unknown" when not connected
NText {
text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
}
NIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
}
NText {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
}
}
// Battery
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
}
}
// Spacer to push connect button to the right
Item {
Layout.fillWidth: true
}
// Call to action
NButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
enabled: (canConnect || canDisconnect) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeXS
fontWeight: Style.fontWeightMedium
backgroundColor: {
if (device.canDisconnect && !isBusy) {
return Color.mError
}
return Color.mPrimary
}
tooltipText: root.tooltipText
text: {
if (modelData.pairing) {
return I18n.tr("bluetooth.panel.pairing")
}
if (modelData.blocked) {
return I18n.tr("bluetooth.panel.blocked")
}
if (modelData.connected) {
return I18n.tr("bluetooth.panel.disconnect")
}
return I18n.tr("bluetooth.panel.connect")
}
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData)
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
onRightClicked: {
BluetoothService.forgetDevice(modelData)
}
}
}
}
}
}
}
-236
View File
@@ -1,236 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
preferredWidth: 420 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio
panelContent: Rectangle {
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// Header
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
spacing: Style.marginM
NIcon {
icon: "bluetooth"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("bluetooth.panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
id: bluetoothSwitch
checked: BluetoothService.enabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65
}
NIconButton {
enabled: BluetoothService.enabled
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
tooltipText: I18n.tr("tooltips.refresh-devices")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
}
}
}
// Adapter not available of disabled
NBox {
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
// Center the content within this rectangle
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM
NIcon {
icon: "bluetooth-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("bluetooth.panel.disabled")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("bluetooth.panel.enable-message")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
NScrollView {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
ColumnLayout {
width: parent.width
spacing: Style.marginM
// Connected devices
BluetoothDevicesList {
label: I18n.tr("bluetooth.panel.connected-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected)
return BluetoothService.sortDevices(filtered)
}
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Known devices
BluetoothDevicesList {
label: I18n.tr("bluetooth.panel.known-devices")
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted))
return BluetoothService.sortDevices(filtered)
}
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Available devices
BluetoothDevicesList {
label: I18n.tr("bluetooth.panel.available-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted)
return BluetoothService.sortDevices(filtered)
}
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Fallback - No devices, scanning
NBox {
Layout.fillWidth: true
Layout.preferredHeight: columnScanning.implicitHeight + Style.marginM * 2
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
return false
}
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0)
}).length
return (availableCount === 0)
}
ColumnLayout {
id: columnScanning
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginXS
NIcon {
icon: "refresh"
pointSize: Style.fontSizeXXL * 1.5
color: Color.mPrimary
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: Style.animationSlow * 4
}
}
NText {
text: I18n.tr("bluetooth.panel.scanning")
pointSize: Style.fontSizeL
color: Color.mOnSurface
}
}
NText {
text: I18n.tr("bluetooth.panel.pairing-mode")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
-651
View File
@@ -1,651 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.ControlCenter.Cards
import qs.Services
import qs.Widgets
NPanel {
id: root
property ShellScreen screen
readonly property var now: Time.now
preferredWidth: 500 * Style.uiScaleRatio
preferredHeight: 700 * Style.uiScaleRatio
// Helper function to calculate ISO week number
function getISOWeekNumber(date) {
const target = new Date(date.valueOf())
const dayNr = (date.getDay() + 6) % 7
target.setDate(target.getDate() - dayNr + 3)
const firstThursday = new Date(target.getFullYear(), 0, 4)
const diff = target - firstThursday
const oneWeek = 1000 * 60 * 60 * 24 * 7
const weekNumber = 1 + Math.round(diff / oneWeek)
return weekNumber
}
panelContent: Item {
anchors.fill: parent
// Dynamic sizing properties that NPanel will bind to
property real contentPreferredWidth: (Settings.data.location.showWeekNumberInCalendar ? 400 : 380) * Style.uiScaleRatio
property real calendarGridHeight: {
// Calculate number of weeks in the calendar grid
const numWeeks = grid.daysModel ? Math.ceil(grid.daysModel.length / 7) : 5
// Calendar grid height (dynamic based on number of weeks)
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS
return numWeeks * rowHeight
}
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: Style.marginL
width: parent.contentPreferredWidth - Style.marginL * 2
spacing: Style.marginM
readonly property int firstDayOfWeek: Settings.data.location.firstDayOfWeek === -1 ? I18n.locale.firstDayOfWeek : Settings.data.location.firstDayOfWeek
property bool isCurrentMonth: true
readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
function checkIsCurrentMonth() {
return (now.getMonth() === grid.month) && (now.getFullYear() === grid.year)
}
Component.onCompleted: {
isCurrentMonth = checkIsCurrentMonth()
}
Connections {
target: Time
function onNowChanged() {
content.isCurrentMonth = content.checkIsCurrentMonth()
}
}
Connections {
target: I18n
function onLanguageChanged() {
// Force update of day names when language changes
grid.month = grid.month
}
}
// Banner with date/time/clock
Rectangle {
id: banner
Layout.fillWidth: true
Layout.preferredHeight: capsuleColumn.implicitHeight + Style.marginM * 2
radius: Style.radiusL
color: Color.mPrimary
ColumnLayout {
id: capsuleColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.topMargin: Style.marginM
anchors.bottomMargin: Style.marginM
anchors.rightMargin: clockLoader.width + (Style.marginXL * 2)
anchors.leftMargin: Style.marginXL
spacing: 0
// Combined layout for date, month year, locatio and time-zone
RowLayout {
Layout.fillWidth: true
height: 60 * Style.uiScaleRatio
clip: true
spacing: Style.marginS
// Today day number - with simple, stable animation
NText {
opacity: content.isCurrentMonth ? 1.0 : 0.0
Layout.preferredWidth: content.isCurrentMonth ? implicitWidth : 0
elide: Text.ElideNone
clip: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: now.getDate()
pointSize: Style.fontSizeXXXL * 1.5
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
// Month, year, location
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.bottomMargin: Style.marginXXS
Layout.topMargin: -Style.marginXXS
spacing: -Style.marginXS
RowLayout {
spacing: Style.marginS
NText {
text: I18n.locale.monthName(grid.month, Locale.LongFormat).toUpperCase()
pointSize: Style.fontSizeXL * 1.1
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
Layout.alignment: Qt.AlignBaseline
elide: Text.ElideRight
}
NText {
text: `${grid.year}`
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Qt.alpha(Color.mOnPrimary, 0.7)
Layout.alignment: Qt.AlignBaseline
}
}
RowLayout {
spacing: 0
NText {
text: {
if (!Settings.data.location.weatherEnabled)
return ""
if (!content.weatherReady)
return I18n.tr("calendar.weather.loading")
const chunks = Settings.data.location.name.split(",")
return chunks[0]
}
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
color: Color.mOnPrimary
Layout.maximumWidth: 150
elide: Text.ElideRight
}
NText {
text: content.weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
pointSize: Style.fontSizeXS
font.weight: Style.fontWeightMedium
color: Qt.alpha(Color.mOnPrimary, 0.7)
}
}
}
// Spacer to push content left
Item {
Layout.fillWidth: true
}
}
}
// Analog clock
NClock {
id: clockLoader
anchors.right: parent.right
anchors.rightMargin: Style.marginXL
anchors.verticalCenter: parent.verticalCenter
clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital"
progressColor: Color.mOnPrimary
Layout.alignment: Qt.AlignVCenter
now: root.now
}
}
// Calendar itself
NBox {
id: calendar
Layout.fillWidth: true
Layout.preferredHeight: {
const navigationHeight = Style.baseWidgetSize // Navigation buttons row
const dayNamesHeight = Style.baseWidgetSize * 0.6 // Day names header row
const innerMargins = Style.marginM * 2 // Top and bottom margins inside NBox
const innerSpacing = Style.marginS * 2 // Spacing between nav, dayNames, and grid (2 gaps)
return navigationHeight + dayNamesHeight + calendarGridHeight + innerMargins + innerSpacing
}
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NDivider {
Layout.fillWidth: true
}
NIconButton {
icon: "chevron-left"
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
content.isCurrentMonth = content.checkIsCurrentMonth()
const now = new Date()
const monthStart = new Date(grid.year, grid.month, 1)
const monthEnd = new Date(grid.year, grid.month + 1, 0)
const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000)))
const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000)))
CalendarService.loadEvents(daysAhead + 30, daysBehind + 30)
}
}
NIconButton {
icon: "calendar"
onClicked: {
grid.month = now.getMonth()
grid.year = now.getFullYear()
content.isCurrentMonth = true
CalendarService.loadEvents()
}
}
NIconButton {
icon: "chevron-right"
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
content.isCurrentMonth = content.checkIsCurrentMonth()
const now = new Date()
const monthStart = new Date(grid.year, grid.month, 1)
const monthEnd = new Date(grid.year, grid.month + 1, 0)
const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000)))
const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000)))
CalendarService.loadEvents(daysAhead + 30, daysBehind + 30)
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
Item {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
}
GridLayout {
Layout.fillWidth: true
columns: 7
rows: 1
columnSpacing: 0
rowSpacing: 0
Repeater {
model: 7
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.fontSizeS * 2
NText {
anchors.centerIn: parent
text: {
let dayIndex = (content.firstDayOfWeek + index) % 7
const dayName = I18n.locale.dayName(dayIndex, Locale.ShortFormat)
return dayName.substring(0, 2).toUpperCase()
}
color: Color.mPrimary
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
// Helper function to check if a date has events
function hasEventsOnDate(year, month, day) {
if (!CalendarService.available || CalendarService.events.length === 0)
return false
const targetDate = new Date(year, month, day)
const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000
const targetEnd = targetStart + 86400 // +24 hours
return CalendarService.events.some(event => {
// Check if event starts or overlaps with this day
return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd)
})
}
// Helper function to get events for a specific date
function getEventsForDate(year, month, day) {
if (!CalendarService.available || CalendarService.events.length === 0)
return []
const targetDate = new Date(year, month, day)
const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000)
const targetEnd = targetStart + 86400 // +24 hours
return CalendarService.events.filter(event => {
return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd)
})
}
// Helper function to check if an event is all-day
function isAllDayEvent(event) {
const duration = event.end - event.start
const startDate = new Date(event.start * 1000)
const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0
return duration === 86400 && isAtMidnight
}
// Helper function to check if an event is multi-day
function isMultiDayEvent(event) {
if (isAllDayEvent(event)) {
return false
}
const startDate = new Date(event.start * 1000)
const endDate = new Date(event.end * 1000)
const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
return startDateOnly.getTime() !== endDateOnly.getTime()
}
// Helper function to get color for a specific event
function getEventColor(event, isToday) {
if (isMultiDayEvent(event)) {
return isToday ? Color.mOnSecondary : Color.mTertiary
} else if (isAllDayEvent(event)) {
return isToday ? Color.mOnSecondary : Color.mSecondary
} else {
return isToday ? Color.mOnSecondary : Color.mPrimary
}
}
// Column of week numbers
ColumnLayout {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
Layout.preferredHeight: {
const numWeeks = weekNumbers ? weekNumbers.length : 5
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS
return numWeeks * rowHeight
}
spacing: Style.marginXXS
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
property var weekNumbers: {
if (!grid.daysModel || grid.daysModel.length === 0)
return []
const weeks = []
const numWeeks = Math.ceil(grid.daysModel.length / 7)
for (var i = 0; i < numWeeks; i++) {
const dayIndex = i * 7
if (dayIndex < grid.daysModel.length) {
const weekDay = grid.daysModel[dayIndex]
const date = new Date(weekDay.year, weekDay.month, weekDay.day)
// Get Thursday of this week for ISO week calculation
const firstDayOfWeek = content.firstDayOfWeek
let thursday = new Date(date)
if (firstDayOfWeek === 0) {
thursday.setDate(date.getDate() + 4)
} else if (firstDayOfWeek === 1) {
thursday.setDate(date.getDate() + 3)
} else {
let daysToThursday = (4 - firstDayOfWeek + 7) % 7
thursday.setDate(date.getDate() + daysToThursday)
}
weeks.push(root.getISOWeekNumber(thursday))
}
}
return weeks
}
Repeater {
model: parent.weekNumbers
Item {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.9
NText {
anchors.centerIn: parent
color: Qt.alpha(Color.mPrimary, 0.7)
pointSize: Style.fontSizeXXS
font.weight: Style.fontWeightMedium
text: modelData
}
}
}
}
GridLayout {
id: grid
Layout.fillWidth: true
Layout.preferredHeight: {
const numWeeks = daysModel ? Math.ceil(daysModel.length / 7) : 5
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS
return numWeeks * rowHeight
}
columns: 7
columnSpacing: Style.marginXXS
rowSpacing: Style.marginXXS
property int month: now.getMonth()
property int year: now.getFullYear()
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
// Calculate days to display
property var daysModel: {
const firstOfMonth = new Date(year, month, 1)
const lastOfMonth = new Date(year, month + 1, 0)
const daysInMonth = lastOfMonth.getDate()
// Get first day of week (0 = Sunday, 1 = Monday, etc.)
const firstDayOfWeek = content.firstDayOfWeek
const firstOfMonthDayOfWeek = firstOfMonth.getDay()
// Calculate days before first of month
let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7
// Calculate days after last of month to complete the week
const lastOfMonthDayOfWeek = lastOfMonth.getDay()
const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7
// Build array of day objects
const days = []
const today = new Date()
// Previous month days
const prevMonth = new Date(year, month, 0)
const prevMonthDays = prevMonth.getDate()
for (var i = daysBefore - 1; i >= 0; i--) {
const day = prevMonthDays - i
const date = new Date(year, month - 1, day)
days.push({
"day": day,
"month": month - 1,
"year": month === 0 ? year - 1 : year,
"today": false,
"currentMonth": false
})
}
// Current month days
for (var day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day)
const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate()
days.push({
"day": day,
"month": month,
"year": year,
"today": isToday,
"currentMonth": true
})
}
// Next month days (only if needed to complete the week)
for (var i = 1; i <= daysAfter; i++) {
days.push({
"day": i,
"month": month + 1,
"year": month === 11 ? year + 1 : year,
"today": false,
"currentMonth": false
})
}
return days
}
Repeater {
model: grid.daysModel
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 0.9
Rectangle {
width: Style.baseWidgetSize * 0.9
height: Style.baseWidgetSize * 0.9
anchors.centerIn: parent
radius: Style.radiusM
color: modelData.today ? Color.mSecondary : Color.transparent
NText {
anchors.centerIn: parent
text: modelData.day
color: {
if (modelData.today)
return Color.mOnSecondary
if (modelData.currentMonth)
return Color.mOnSurface
return Color.mOnSurfaceVariant
}
opacity: modelData.currentMonth ? 1.0 : 0.4
pointSize: Style.fontSizeM
font.weight: modelData.today ? Style.fontWeightBold : Style.fontWeightMedium
}
// Event indicator dots
Row {
visible: Settings.data.location.showCalendarEvents && parent.parent.parent.parent.hasEventsOnDate(modelData.year, modelData.month, modelData.day)
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginXS
Repeater {
model: parent.parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day)
Rectangle {
width: 4
height: width
radius: width / 2
color: parent.parent.parent.parent.parent.getEventColor(modelData, modelData.today)
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
enabled: Settings.data.location.showCalendarEvents
onEntered: {
const events = parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day)
if (events.length > 0) {
const summaries = events.map(e => e.summary).join('\n')
TooltipService.show(Screen, parent, summaries)
TooltipService.updateText(summaries)
}
}
onClicked: {
const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`
if (ProgramCheckerService.gnomeCalendarAvailable) {
Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes])
root.close()
}
}
onExited: {
TooltipService.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
}
Loader {
id: weatherLoader
active: Settings.data.location.weatherEnabled && Settings.data.location.showCalendarWeather
Layout.fillWidth: true
sourceComponent: WeatherCard {
Layout.fillWidth: true
forecastDays: 6
showLocation: false
}
}
}
}
}
-183
View File
@@ -1,183 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import qs.Commons
import qs.Services
import qs.Widgets
// A compact grid panel listing all tray items, opened from the Tray widget
NPanel {
id: root
objectName: "trayDropdownPanel"
// Widget info for menu functionality
property string widgetSection: ""
property int widgetIndex: -1
// Trigger refresh when settings change
property int settingsVersion: 0
// Read widget settings for reactivity
readonly property var widgetSettings: {
// Reference settingsVersion to force recalculation when it changes
var _ = root.settingsVersion
if (widgetSection === "" || widgetIndex < 0)
return {}
var widgets = Settings.data.bar.widgets[widgetSection]
if (!widgets || widgetIndex >= widgets.length)
return {}
var settings = widgets[widgetIndex]
if (!settings || settings.id !== "Tray")
return {}
return settings
}
// Read favorites directly from settings for reactivity
readonly property var favoritesList: widgetSettings.favorites || []
function wildCardMatch(str, rule) {
if (!str || !rule)
return false
let escaped = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
let pattern = '^' + escaped.replace(/\\\*/g, '.*') + '$'
try {
return new RegExp(pattern, 'i').test(str)
} catch (e) {
return false
}
}
function isFavorite(item) {
if (!favoritesList || favoritesList.length === 0)
return false
const title = item?.tooltipTitle || item?.name || item?.id || ""
for (var i = 0; i < favoritesList.length; i++) {
if (wildCardMatch(title, favoritesList[i]))
return true
}
return false
}
// Dynamic sizing based on item count
// Show items that are NOT favorites (non-favorites go to dropdown)
readonly property var trayValuesAll: (SystemTray.items && SystemTray.items.values) ? SystemTray.items.values : []
readonly property var trayValues: trayValuesAll.filter(function (it) {
return !root.isFavorite(it)
})
readonly property int itemCount: trayValues.length
readonly property int maxColumns: 8
readonly property real cellSize: Math.round(Style.capsuleHeight * 0.65)
readonly property real outerPadding: Style.marginM
readonly property real innerSpacing: Style.marginM
readonly property int columns: Math.max(1, Math.min(maxColumns, itemCount))
readonly property int rows: Math.max(1, Math.ceil(itemCount / Math.max(1, columns)))
// Add 2*gap margins around the grid
preferredWidth: (columns * cellSize) + ((columns - 1) * innerSpacing) + (2 * outerPadding)
preferredHeight: (rows * cellSize) + ((rows - 1) * innerSpacing) + (2 * outerPadding)
// Positioning is handled automatically by NPanel when toggle(buttonItem) is called
// Watch for settings changes to refresh the dropdown
Connections {
target: Settings
function onSettingsSaved() {
// Force refresh by incrementing settingsVersion, which triggers recalculation of favoritesList
root.settingsVersion++
}
}
panelContent: Item {
id: content
Grid {
id: grid
anchors.fill: parent
anchors.margins: outerPadding
spacing: innerSpacing
columns: root.columns
rowSpacing: innerSpacing
columnSpacing: innerSpacing
Repeater {
id: repeater
model: root.trayValues
delegate: Item {
width: root.cellSize
height: root.cellSize
IconImage {
id: trayIcon
anchors.fill: parent
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
source: {
let icon = modelData?.icon || ""
if (!icon)
return ""
if (icon.includes("?path=")) {
const chunks = icon.split("?path=")
const name = chunks[0]
const path = chunks[1]
const fileName = name.substring(name.lastIndexOf("/") + 1)
return `file://${path}/${fileName}`
}
return icon
}
layer.enabled: root.widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 1.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!modelData)
return
if (mouse.button === Qt.RightButton && modelData.hasMenu && modelData.menu) {
const panel = PanelService.getPanel("trayMenu", root.screen)
if (panel) {
panel.menu = modelData.menu
panel.trayItem = modelData
panel.widgetSection = root.widgetSection
panel.widgetIndex = root.widgetIndex
panel.openAt(trayIcon)
}
} else if (mouse.button === Qt.LeftButton) {
modelData.activate()
// Close the dropdown after activation
PanelService.getPanel("trayDropdownPanel", root.screen)?.close()
} else if (mouse.button === Qt.MiddleButton) {
modelData.secondaryActivate()
PanelService.getPanel("trayDropdownPanel", root.screen)?.close()
}
}
onWheel: wheel => {
if (wheel.angleDelta.y > 0)
modelData?.scrollUp()
else if (wheel.angleDelta.y < 0)
modelData?.scrollDown()
}
onEntered: TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection())
onExited: TooltipService.hide()
}
}
}
}
}
// (Tray menu now uses dedicated TrayMenu via PanelService)
}
}
-623
View File
@@ -1,623 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
objectName: "trayMenu"
// Avoid bouncy feel: keep fade but disable scale/slide transforms
disableScaleAnimation: true
disableSlideAnimation: false
customSlideDistance: 100
// Inputs
property QsMenuHandle menu
property var trayItem: null
property string widgetSection: ""
property int widgetIndex: -1
// Internal
readonly property int menuWidth: 280
preferredWidth: menuWidth
// Height is content-driven via panelContent
// Open positioned relative to button
function openAt(buttonItem) {
open(buttonItem)
}
panelContent: Item {
id: content
// Track currently open submenu
property var activeSubMenu: null
property var activeSubMenuEntry: null
// Only show submenu in place (replace main menu)
property bool inPlaceSubmenu: true
// Let NPanel size to our content
readonly property real contentPreferredWidth: root.menuWidth
readonly property real contentPreferredHeight: {
// If showing submenu in-place, size to the submenu's flickable content height
if (activeSubMenu && inPlaceSubmenu) {
const subFlickable = inPlaceSubMenuLoader.item?.children[1] // Flickable is the second child (after MouseArea)
const subHeight = subFlickable?.contentHeight || 0
return Math.min(root.screen ? root.screen.height * 0.9 : Screen.height * 0.9, subHeight + (Style.marginS * 2))
}
const mainHeight = mainFlickable.contentHeight
return Math.min(root.screen ? root.screen.height * 0.9 : Screen.height * 0.9, mainHeight + (Style.marginS * 2))
}
// Expose mask region for click-through
property alias maskRegion: background
Rectangle {
id: background
x: 0
y: 0
width: content.contentPreferredWidth
height: content.contentPreferredHeight
color: Color.mSurface
radius: Style.radiusM
}
QsMenuOpener {
id: opener
menu: root.menu
}
Component {
id: subMenuComponent
Item {
id: subMenuContainer
// MouseArea to track hover for submenu (covers entire submenu area)
MouseArea {
id: subMenuMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: content.activeSubMenu !== null && !content.inPlaceSubmenu
visible: !content.inPlaceSubmenu
z: 1
acceptedButtons: Qt.NoButton // Don't intercept clicks, just track hover
onExited: {
if (content.inPlaceSubmenu)
return
Qt.callLater(() => {
if (!subMenuMouseArea.containsMouse && content.activeSubMenuEntry) {
// Find the mouseArea in the entry (it's in the second Rectangle's MouseArea)
let entryMouseArea = null
for (var i = 0; i < content.activeSubMenuEntry.children.length; i++) {
const child = content.activeSubMenuEntry.children[i]
if (child && child.children && child.children.length > 0) {
for (var j = 0; j < child.children.length; j++) {
const grandchild = child.children[j]
if (grandchild && grandchild.hoverEnabled !== undefined) {
entryMouseArea = grandchild
break
}
}
}
if (entryMouseArea)
break
}
if (!entryMouseArea || !entryMouseArea.containsMouse) {
content.activeSubMenu = null
content.activeSubMenuEntry = null
}
}
})
}
}
Flickable {
id: subMenuFlickable
anchors.fill: parent
contentHeight: subMenuColumnLayout.implicitHeight
interactive: true
z: 0
ColumnLayout {
id: subMenuColumnLayout
width: subMenuFlickable.width
spacing: 0
QsMenuOpener {
id: subMenuOpener
menu: content.activeSubMenu
}
// Back button (only shown when submenu is in-place)
Rectangle {
id: backEntry
visible: content.inPlaceSubmenu
Layout.preferredWidth: parent.width
Layout.preferredHeight: visible ? 28 : 0
color: Color.transparent
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 0
anchors.bottomMargin: 0
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - (Style.marginM * 2)
color: backMouseArea.containsMouse ? Color.mHover : Color.transparent
radius: Style.radiusS
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
spacing: Style.marginS
NIcon {
icon: "arrow-left"
pointSize: Style.fontSizeS
applyUiScale: false
verticalAlignment: Text.AlignVCenter
color: backMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
}
NText {
Layout.fillWidth: true
color: backMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
text: I18n.tr("settings.bar.tray.back")
pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
MouseArea {
id: backMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
content.activeSubMenu = null
content.activeSubMenuEntry = null
}
}
}
}
Rectangle {
visible: content.inPlaceSubmenu
Layout.preferredWidth: parent.width
Layout.preferredHeight: visible ? 8 : 0
color: Color.transparent
NDivider {
anchors.centerIn: parent
width: parent.width - (Style.marginM * 2)
visible: parent.visible
}
}
Repeater {
model: subMenuOpener.children ? [...subMenuOpener.children.values] : []
delegate: Rectangle {
id: subEntry
required property var modelData
Layout.preferredWidth: parent.width
Layout.preferredHeight: {
if (modelData?.isSeparator) {
return 8
} else {
return 28
}
}
color: Color.transparent
NDivider {
anchors.centerIn: parent
width: parent.width - (Style.marginM * 2)
visible: modelData?.isSeparator ?? false
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 0
anchors.bottomMargin: 0
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - (Style.marginM * 2)
color: subMouseArea.containsMouse ? Color.mHover : Color.transparent
radius: Style.radiusS
visible: !(modelData?.isSeparator ?? false)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
spacing: Style.marginS
NText {
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? (subMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Image {
Layout.preferredWidth: Style.marginL
Layout.preferredHeight: Style.marginL
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
NIcon {
icon: modelData?.hasChildren ? "menu" : ""
pointSize: Style.fontSizeS
applyUiScale: false
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false
Layout.rightMargin: Style.marginL
color: (subMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface)
}
}
MouseArea {
id: subMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false)
onClicked: {
if (!modelData || modelData.isSeparator)
return
if (modelData.hasChildren) {
// Toggle nested submenu on click
if (content.activeSubMenu === modelData) {
// If already open, close it on second click
content.activeSubMenu = null
content.activeSubMenuEntry = null
return
}
// Open nested submenu in place
content.activeSubMenu = modelData
content.activeSubMenuEntry = null // No entry for nested submenus
content.inPlaceSubmenu = true
} else {
modelData.triggered()
root.close()
}
}
}
}
}
}
}
}
}
}
// Main menu and in-place submenu
RowLayout {
id: rowLayout
anchors.fill: background
anchors.margins: Style.marginS
spacing: 0
// Submenu replacing main menu (in-place)
Loader {
id: inPlaceSubMenuLoader
Layout.preferredWidth: (content.activeSubMenu && content.inPlaceSubmenu) ? root.menuWidth : 0
Layout.fillHeight: true
visible: content.activeSubMenu !== null && content.inPlaceSubmenu
sourceComponent: subMenuComponent
}
// Main menu
Flickable {
id: mainFlickable
Layout.preferredWidth: root.menuWidth
Layout.fillHeight: true
contentHeight: mainColumnLayout.implicitHeight
interactive: true
visible: !(content.activeSubMenu !== null && content.inPlaceSubmenu)
ColumnLayout {
id: mainColumnLayout
width: mainFlickable.width
spacing: 0
Repeater {
model: opener.children ? [...opener.children.values] : []
delegate: Rectangle {
id: entry
required property var modelData
Layout.preferredWidth: parent.width
Layout.preferredHeight: {
if (modelData?.isSeparator) {
return 8
} else {
return 28
}
}
color: Color.transparent
NDivider {
anchors.centerIn: parent
width: parent.width - Style.marginL
visible: modelData?.isSeparator ?? false
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 0
anchors.bottomMargin: 0
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - (Style.marginM * 2)
color: mouseArea.containsMouse ? Color.mHover : Color.transparent
radius: Style.radiusS
visible: !(modelData?.isSeparator ?? false)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
spacing: Style.marginS
NText {
id: text
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Image {
Layout.preferredWidth: Style.marginL
Layout.preferredHeight: Style.marginL
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
NIcon {
icon: modelData?.hasChildren ? "menu" : ""
pointSize: Style.fontSizeS
applyUiScale: false
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false
Layout.rightMargin: Style.marginS
color: (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface)
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false)
onClicked: {
if (!modelData || modelData.isSeparator)
return
if (modelData.hasChildren) {
// Toggle submenu on click
if (content.activeSubMenuEntry === entry && content.inPlaceSubmenu) {
// If already open in-place, close it on second click
content.activeSubMenu = null
content.activeSubMenuEntry = null
return
}
// Close any other open submenu
if (content.activeSubMenuEntry && content.activeSubMenuEntry !== entry) {
content.activeSubMenu = null
content.activeSubMenuEntry = null
}
// Open submenu in place (replace main menu)
content.activeSubMenu = modelData
content.activeSubMenuEntry = entry
content.inPlaceSubmenu = true
} else {
modelData.triggered()
root.close()
}
}
onExited: {
}
}
}
}
}
Rectangle {
visible: root.trayItem !== null && root.widgetSection !== "" && root.widgetIndex >= 0
Layout.preferredWidth: parent.width
Layout.preferredHeight: visible ? 8 : 0
color: Color.transparent
NDivider {
anchors.centerIn: parent
width: parent.width - Style.marginL
visible: parent.visible
}
}
Rectangle {
id: addToFavoriteEntry
visible: root.trayItem !== null && root.widgetSection !== "" && root.widgetIndex >= 0
Layout.preferredWidth: parent.width
Layout.preferredHeight: visible ? 28 : 0
color: Color.transparent
readonly property bool isFavorite: {
if (!root.trayItem || root.widgetSection === "" || root.widgetIndex < 0)
return false
const itemName = root.trayItem.tooltipTitle || root.trayItem.name || root.trayItem.id || ""
if (!itemName)
return false
var widgets = Settings.data.bar.widgets[root.widgetSection]
if (!widgets || root.widgetIndex >= widgets.length)
return false
var widgetSettings = widgets[root.widgetIndex]
if (!widgetSettings || widgetSettings.id !== "Tray")
return false
var favorites = widgetSettings.favorites || []
for (var i = 0; i < favorites.length; i++) {
if (favorites[i] === itemName)
return true
}
return false
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 0
anchors.bottomMargin: 0
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - (Style.marginM * 2)
color: addToFavoriteMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Qt.alpha(Color.mPrimary, 0.08)
radius: Style.radiusS
border.color: Qt.alpha(Color.mPrimary, addToFavoriteMouseArea.containsMouse ? 0.4 : 0.2)
border.width: Style.borderS
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
spacing: Style.marginS
NIcon {
icon: addToFavoriteEntry.isFavorite ? "star" : "star-off"
pointSize: Style.fontSizeS
applyUiScale: false
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NText {
Layout.fillWidth: true
color: Color.mPrimary
text: addToFavoriteEntry.isFavorite ? I18n.tr("settings.bar.tray.unpin-application") : I18n.tr("settings.bar.tray.pin-application")
pointSize: Style.fontSizeS
font.weight: Font.Medium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
MouseArea {
id: addToFavoriteMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (addToFavoriteEntry.isFavorite) {
root.removeFromFavorites()
} else {
root.addToFavorites()
}
root.close()
}
}
}
}
}
}
}
Keys.onEscapePressed: root.close()
}
function addToFavorites() {
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
Logger.w("TrayMenu", "Cannot add as favorite: missing tray item or widget info")
return
}
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""
if (!itemName) {
Logger.w("TrayMenu", "Cannot add as favorite: tray item has no name")
return
}
var widgets = Settings.data.bar.widgets[widgetSection]
if (!widgets || widgetIndex >= widgets.length) {
Logger.w("TrayMenu", "Cannot add as favorite: invalid widget index")
return
}
var widgetSettings = widgets[widgetIndex]
if (!widgetSettings || widgetSettings.id !== "Tray") {
Logger.w("TrayMenu", "Cannot add as favorite: widget is not a Tray widget")
return
}
var favorites = widgetSettings.favorites || []
var newFavorites = favorites.slice()
newFavorites.push(itemName)
var newSettings = Object.assign({}, widgetSettings)
newSettings.favorites = newFavorites
widgets[widgetIndex] = newSettings
Settings.data.bar.widgets[widgetSection] = widgets
Settings.saveImmediate()
if (root.screen) {
const panel = PanelService.getPanel("trayDropdownPanel", root.screen)
if (panel)
panel.close()
}
}
function removeFromFavorites() {
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
Logger.w("TrayMenu", "Cannot remove from favorites: missing tray item or widget info")
return
}
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""
if (!itemName) {
Logger.w("TrayMenu", "Cannot remove from favorites: tray item has no name")
return
}
var widgets = Settings.data.bar.widgets[widgetSection]
if (!widgets || widgetIndex >= widgets.length) {
Logger.w("TrayMenu", "Cannot remove from favorites: invalid widget index")
return
}
var widgetSettings = widgets[widgetIndex]
if (!widgetSettings || widgetSettings.id !== "Tray") {
Logger.w("TrayMenu", "Cannot remove from favorites: widget is not a Tray widget")
return
}
var favorites = widgetSettings.favorites || []
var newFavorites = []
for (var i = 0; i < favorites.length; i++) {
if (favorites[i] !== itemName) {
newFavorites.push(favorites[i])
}
}
var newSettings = Object.assign({}, widgetSettings)
newSettings.favorites = newFavorites
widgets[widgetIndex] = newSettings
Settings.data.bar.widgets[widgetSection] = widgets
Settings.saveImmediate()
}
}
-589
View File
@@ -1,589 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
preferredWidth: 420 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio
property string passwordSsid: ""
property string passwordInput: ""
property string expandedSsid: ""
onOpened: NetworkService.scan()
panelContent: Rectangle {
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// Header
NBox {
Layout.fillWidth: true
Layout.preferredHeight: headerRow.implicitHeight + Style.marginM * 2
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
pointSize: Style.fontSizeXXL
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: I18n.tr("wifi.panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
id: wifiSwitch
checked: Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("tooltips.refresh")
baseSize: Style.baseWidgetSize * 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
onClicked: NetworkService.scan()
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
}
// Error message
Rectangle {
visible: NetworkService.lastError.length > 0
Layout.fillWidth: true
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * 2)
color: Qt.alpha(Color.mError, 0.1)
radius: Style.radiusS
border.width: Style.borderS
border.color: Color.mError
RowLayout {
id: errorRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
NIcon {
icon: "warning"
pointSize: Style.fontSizeL
color: Color.mError
}
NText {
text: NetworkService.lastError
color: Color.mError
pointSize: Style.fontSizeS
wrapMode: Text.Wrap
Layout.fillWidth: true
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.6
onClicked: NetworkService.lastError = ""
}
}
}
// Main content area
NBox {
Layout.fillWidth: true
Layout.fillHeight: true
// WiFi disabled state
ColumnLayout {
visible: !Settings.data.network.wifiEnabled
anchors.fill: parent
anchors.margins: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "wifi-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("wifi.panel.disabled")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("wifi.panel.enable-message")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Scanning state
ColumnLayout {
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NBusyIndicator {
running: true
color: Color.mPrimary
size: Style.baseWidgetSize
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("wifi.panel.searching")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Networks list container
NScrollView {
visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0)
anchors.fill: parent
anchors.margins: Style.marginM
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
ColumnLayout {
width: parent.width
spacing: Style.marginM
// Network list
Repeater {
model: {
if (!Settings.data.network.wifiEnabled)
return []
const nets = Object.values(NetworkService.networks)
return nets.sort((a, b) => {
if (a.connected !== b.connected)
return b.connected - a.connected
return b.signal - a.signal
})
}
Rectangle {
Layout.fillWidth: true
implicitHeight: netColumn.implicitHeight + (Style.marginM * 2)
radius: Style.radiusM
// Add opacity for operations in progress
opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface
border.width: Style.borderS
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
// Smooth opacity animation
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
ColumnLayout {
id: netColumn
width: parent.width - (Style.marginM * 2)
x: Style.marginM
y: Style.marginM
spacing: Style.marginS
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: NetworkService.signalIcon(modelData.signal)
pointSize: Style.fontSizeXXL
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
NText {
text: modelData.ssid
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginXS
NText {
text: I18n.tr("system.signal-strength", {
"signal": modelData.signal
})
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.preferredWidth: Style.marginXXS
}
// Update the status badges area (around line 237)
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.mPrimary
radius: height * 0.5
width: connectedText.implicitWidth + (Style.marginS * 2)
height: connectedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: connectedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.connected")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: disconnectingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.disconnecting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: forgettingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.forgetting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.transparent
border.color: Color.mOutline
border.width: Style.borderS
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * 2)
height: savedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: savedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.saved")
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS
NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary
size: Style.baseWidgetSize * 0.5
}
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "trash"
tooltipText: I18n.tr("tooltips.forget-network")
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
}
NButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return I18n.tr("wifi.panel.connect")
if (!NetworkService.isSecured(modelData.security))
return I18n.tr("wifi.panel.connect")
return I18n.tr("wifi.panel.password")
}
outlined: !hovered
fontSize: Style.fontSizeXS
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid)
} else {
passwordSsid = modelData.ssid
passwordInput = ""
expandedSsid = ""
}
}
}
NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: I18n.tr("wifi.panel.disconnect")
outlined: !hovered
fontSize: Style.fontSizeXS
backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Password input
Rectangle {
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.marginS * 2
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.radiusS
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusXS
color: Color.mSurface
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Style.borderS
TextInput {
id: pwdInput
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginS
text: passwordInput
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS
color: Color.mOnSurface
echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●"
onTextChanged: passwordInput = text
onVisibleChanged: if (visible)
forceActiveFocus()
onAccepted: {
if (text && !NetworkService.connecting) {
NetworkService.connect(passwordSsid, text)
passwordSsid = ""
passwordInput = ""
}
}
NText {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("wifi.panel.enter-password")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
}
}
NButton {
text: I18n.tr("wifi.panel.connect")
fontSize: Style.fontSizeXXS
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true
onClicked: {
NetworkService.connect(passwordSsid, passwordInput)
passwordSsid = ""
passwordInput = ""
}
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
passwordSsid = ""
passwordInput = ""
}
}
}
}
// Forget network
Rectangle {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2
color: Color.mSurfaceVariant
radius: Style.radiusS
border.width: Style.borderS
border.color: Color.mOutline
RowLayout {
id: forgetRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
RowLayout {
NIcon {
icon: "trash"
pointSize: Style.fontSizeL
color: Color.mError
}
NText {
text: I18n.tr("wifi.panel.forget-network")
pointSize: Style.fontSizeS
color: Color.mError
Layout.fillWidth: true
}
}
NButton {
id: forgetButton
text: I18n.tr("wifi.panel.forget")
fontSize: Style.fontSizeXXS
backgroundColor: Color.mError
outlined: forgetButton.hovered ? false : true
onClicked: {
NetworkService.forget(modelData.ssid)
expandedSsid = ""
}
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = ""
}
}
}
}
}
}
}
}
// Empty state when no networks
ColumnLayout {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NIcon {
icon: "search"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("wifi.panel.no-networks")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: I18n.tr("wifi.panel.scan-again")
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.AudioSpectrum
import qs.Widgets.AudioSpectrum
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -2,7 +2,7 @@ import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -5,7 +5,7 @@ import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Modules.Bar.Extras
Item {
+1 -1
View File
@@ -3,7 +3,7 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Modules.AudioSpectrum
import qs.Widgets.AudioSpectrum
import qs.Commons
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
+1 -1
View File
@@ -4,7 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
+1 -10
View File
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
@@ -353,13 +353,4 @@ Rectangle {
}
}
}
// MouseArea {
// anchors.fill: parent
// acceptedButtons: Qt.RightButton
// onClicked: {
// var directPanel = PanelService.getPanel("directWidgetSettingsPanel")
// directPanel.openWidgetSettings(root.section, root.sectionWidgetIndex, root.widgetId, root.widgetSettings)
// }
// }
}
+21 -21
View File
@@ -346,28 +346,28 @@ Rectangle {
}
}
PanelWindow {
id: trayPanel
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: Color.transparent
screen: screen
// PanelWindow {
// id: trayPanel
// anchors.top: true
// anchors.left: true
// anchors.right: true
// anchors.bottom: true
// visible: false
// color: Color.transparent
// screen: screen
function open() {
visible = true
}
// function open() {
// visible = true
// }
function close() {
visible = false
}
// function close() {
// visible = false
// }
// Clicking outside of the rectangle to close
MouseArea {
anchors.fill: parent
onClicked: trayPanel.close()
}
}
// // Clicking outside of the rectangle to close
// MouseArea {
// anchors.fill: parent
// onClicked: trayPanel.close()
// }
// }
}
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras