mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
Refactor: Panels and Bar background are now drawn separately with Shapes.
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.AudioSpectrum
|
||||
import qs.Widgets.AudioSpectrum
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user