Merge pull request #906 from atdma/main
controlcenter: many setting panes and minor features
This commit is contained in:
commit
bdcd13222f
91 changed files with 11040 additions and 1085 deletions
32
components/ConnectionHeader.qml
Normal file
32
components/ConnectionHeader.qml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property string icon
|
||||
required property string title
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
text: root.icon
|
||||
font.pointSize: Appearance.font.size.extraLarge * 3
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
60
components/ConnectionInfoSection.qml
Normal file
60
components/ConnectionInfoSection.qml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var deviceDetails
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("IP Address")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.deviceDetails?.ipAddress || qsTr("Not available")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Subnet Mask")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.deviceDetails?.subnet || qsTr("Not available")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Gateway")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.deviceDetails?.gateway || qsTr("Not available")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("DNS Servers")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
wrapMode: Text.Wrap
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
27
components/PropertyRow.qml
Normal file
27
components/PropertyRow.qml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
required property string value
|
||||
property bool showTopMargin: false
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0
|
||||
text: root.label
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.value
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
35
components/SectionContainer.qml
Normal file
35
components/SectionContainer.qml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
property real contentSpacing: Appearance.spacing.larger
|
||||
property bool alignTop: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.transparency.enabled
|
||||
? Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
: Colours.palette.m3surfaceContainerHigh
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: root.alignTop ? parent.top : undefined
|
||||
anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: root.contentSpacing
|
||||
}
|
||||
}
|
||||
|
||||
28
components/SectionHeader.qml
Normal file
28
components/SectionHeader.qml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property string title
|
||||
property string description: ""
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.description !== ""
|
||||
text: root.description
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ MouseArea {
|
|||
id: root
|
||||
|
||||
property bool disabled
|
||||
property bool showHoverBackground: true
|
||||
property color color: Colours.palette.m3onSurface
|
||||
property real radius: parent?.radius ?? 0
|
||||
property alias rect: hoverLayer
|
||||
|
|
@ -75,7 +76,7 @@ MouseArea {
|
|||
|
||||
anchors.fill: parent
|
||||
|
||||
color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0)
|
||||
color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0)
|
||||
radius: root.radius
|
||||
|
||||
StyledRect {
|
||||
|
|
|
|||
135
components/controls/CollapsibleSection.qml
Normal file
135
components/controls/CollapsibleSection.qml
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import ".."
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property string title
|
||||
property string description: ""
|
||||
property bool expanded: false
|
||||
property bool showBackground: false
|
||||
property bool nested: false
|
||||
|
||||
signal toggleRequested
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
id: sectionHeaderItem
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48)
|
||||
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "expand_more"
|
||||
rotation: root.expanded ? 180 : 0
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
color: Colours.palette.m3onSurface
|
||||
radius: Appearance.rounding.normal
|
||||
showHoverBackground: false
|
||||
function onClicked(): void {
|
||||
root.toggleRequested();
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: backgroundRect
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.transparency.enabled
|
||||
? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2)
|
||||
: (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer)
|
||||
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
|
||||
visible: root.showBackground
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
y: Appearance.spacing.small
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
anchors.bottomMargin: Appearance.spacing.small
|
||||
spacing: Appearance.spacing.small
|
||||
opacity: root.expanded ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: descriptionText
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0
|
||||
Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0
|
||||
visible: root.description !== ""
|
||||
text: root.description
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9,19 +9,69 @@ import QtQuick.Layouts
|
|||
RowLayout {
|
||||
id: root
|
||||
|
||||
property int value
|
||||
property real value
|
||||
property real max: Infinity
|
||||
property real min: -Infinity
|
||||
property real step: 1
|
||||
property alias repeatRate: timer.interval
|
||||
|
||||
signal valueModified(value: int)
|
||||
signal valueModified(value: real)
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
property bool isEditing: false
|
||||
property string displayText: root.value.toString()
|
||||
|
||||
onValueChanged: {
|
||||
if (!root.isEditing) {
|
||||
root.displayText = root.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: textField
|
||||
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
text: root.value
|
||||
onAccepted: root.valueModified(text)
|
||||
text: root.isEditing ? text : root.displayText
|
||||
validator: DoubleValidator {
|
||||
bottom: root.min
|
||||
top: root.max
|
||||
decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0
|
||||
}
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
root.isEditing = true;
|
||||
} else {
|
||||
root.isEditing = false;
|
||||
root.displayText = root.value.toString();
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
const numValue = parseFloat(text);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
|
||||
root.value = clampedValue;
|
||||
root.displayText = clampedValue.toString();
|
||||
root.valueModified(clampedValue);
|
||||
} else {
|
||||
text = root.displayText;
|
||||
}
|
||||
root.isEditing = false;
|
||||
}
|
||||
onEditingFinished: {
|
||||
if (text !== root.displayText) {
|
||||
const numValue = parseFloat(text);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
|
||||
root.value = clampedValue;
|
||||
root.displayText = clampedValue.toString();
|
||||
root.valueModified(clampedValue);
|
||||
} else {
|
||||
text = root.displayText;
|
||||
}
|
||||
}
|
||||
root.isEditing = false;
|
||||
}
|
||||
|
||||
padding: Appearance.padding.small
|
||||
leftPadding: Appearance.padding.normal
|
||||
|
|
@ -50,7 +100,13 @@ RowLayout {
|
|||
onReleased: timer.stop()
|
||||
|
||||
function onClicked(): void {
|
||||
root.valueModified(Math.min(root.max, root.value + 1));
|
||||
let newValue = Math.min(root.max, root.value + root.step);
|
||||
// Round to avoid floating point precision errors
|
||||
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
root.value = newValue;
|
||||
root.displayText = newValue.toString();
|
||||
root.valueModified(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +135,13 @@ RowLayout {
|
|||
onReleased: timer.stop()
|
||||
|
||||
function onClicked(): void {
|
||||
root.valueModified(Math.max(root.min, root.value - 1));
|
||||
let newValue = Math.max(root.min, root.value - root.step);
|
||||
// Round to avoid floating point precision errors
|
||||
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
root.value = newValue;
|
||||
root.displayText = newValue.toString();
|
||||
root.valueModified(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Slider {
|
|||
orientation: Qt.Vertical
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StyledRect {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import ".."
|
|||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
|
@ -53,7 +54,7 @@ StyledRect {
|
|||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
|
@ -62,7 +63,8 @@ StyledRect {
|
|||
MaterialIcon {
|
||||
id: iconLabel
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
|
||||
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
|
||||
fill: root.internalChecked ? 1 : 0
|
||||
|
||||
|
|
@ -74,7 +76,8 @@ StyledRect {
|
|||
StyledText {
|
||||
id: label
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575)
|
||||
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
components/controls/SpinBoxRow.qml
Normal file
53
components/controls/SpinBoxRow.qml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import ".."
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
required property real value
|
||||
required property real min
|
||||
required property real max
|
||||
property real step: 1
|
||||
property var onValueModified: function(value) {}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
min: root.min
|
||||
max: root.max
|
||||
step: root.step
|
||||
value: root.value
|
||||
onValueModified: value => {
|
||||
root.onValueModified(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
components/controls/StyledInputField.qml
Normal file
80
components/controls/StyledInputField.qml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property var validator: null
|
||||
property bool readOnly: false
|
||||
property int horizontalAlignment: TextInput.AlignHCenter
|
||||
property int implicitWidth: 70
|
||||
property bool enabled: true
|
||||
|
||||
// Expose activeFocus through alias to avoid FINAL property override
|
||||
readonly property alias hasFocus: inputField.activeFocus
|
||||
|
||||
signal textEdited(string text)
|
||||
signal editingFinished()
|
||||
|
||||
implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
StyledRect {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
color: inputHover.containsMouse || inputField.activeFocus
|
||||
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
|
||||
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: inputField.activeFocus
|
||||
? Colours.palette.m3primary
|
||||
: Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
opacity: root.enabled ? 1 : 0.5
|
||||
|
||||
Behavior on color { CAnim {} }
|
||||
Behavior on border.color { CAnim {} }
|
||||
|
||||
MouseArea {
|
||||
id: inputHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: root.enabled
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: inputField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: root.horizontalAlignment
|
||||
validator: root.validator
|
||||
readOnly: root.readOnly
|
||||
enabled: root.enabled
|
||||
|
||||
Binding {
|
||||
target: inputField
|
||||
property: "text"
|
||||
value: root.text
|
||||
when: !inputField.activeFocus
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
root.text = text;
|
||||
root.textEdited(text);
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
root.editingFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19,14 +19,51 @@ ScrollBar {
|
|||
shouldBeActive = flickable.moving;
|
||||
}
|
||||
|
||||
property bool _updatingFromFlickable: false
|
||||
property bool _updatingFromUser: false
|
||||
|
||||
// Sync nonAnimPosition with Qt's automatic position binding
|
||||
onPositionChanged: {
|
||||
if (position === nonAnimPosition)
|
||||
if (_updatingFromUser) {
|
||||
_updatingFromUser = false;
|
||||
return;
|
||||
}
|
||||
if (position === nonAnimPosition) {
|
||||
animating = false;
|
||||
else if (!animating)
|
||||
return;
|
||||
}
|
||||
if (!animating && !_updatingFromFlickable && !fullMouse.pressed) {
|
||||
nonAnimPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
position: nonAnimPosition
|
||||
// Sync nonAnimPosition with flickable when not animating
|
||||
Connections {
|
||||
target: flickable
|
||||
function onContentYChanged() {
|
||||
if (!animating && !fullMouse.pressed) {
|
||||
_updatingFromFlickable = true;
|
||||
const contentHeight = flickable.contentHeight;
|
||||
const height = flickable.height;
|
||||
if (contentHeight > height) {
|
||||
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
|
||||
} else {
|
||||
nonAnimPosition = 0;
|
||||
}
|
||||
_updatingFromFlickable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (flickable) {
|
||||
const contentHeight = flickable.contentHeight;
|
||||
const height = flickable.height;
|
||||
if (contentHeight > height) {
|
||||
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
|
||||
}
|
||||
}
|
||||
}
|
||||
implicitWidth: Appearance.padding.small
|
||||
|
||||
contentItem: StyledRect {
|
||||
|
|
@ -86,17 +123,62 @@ ScrollBar {
|
|||
|
||||
onPressed: event => {
|
||||
root.animating = true;
|
||||
root.nonAnimPosition = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
|
||||
root._updatingFromUser = true;
|
||||
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
|
||||
root.nonAnimPosition = newPos;
|
||||
// Update flickable position
|
||||
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
|
||||
if (root.flickable) {
|
||||
const contentHeight = root.flickable.contentHeight;
|
||||
const height = root.flickable.height;
|
||||
if (contentHeight > height) {
|
||||
const maxContentY = contentHeight - height;
|
||||
const maxPos = 1 - root.size;
|
||||
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
|
||||
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: event => root.nonAnimPosition = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2))
|
||||
onPositionChanged: event => {
|
||||
root._updatingFromUser = true;
|
||||
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
|
||||
root.nonAnimPosition = newPos;
|
||||
// Update flickable position
|
||||
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
|
||||
if (root.flickable) {
|
||||
const contentHeight = root.flickable.contentHeight;
|
||||
const height = root.flickable.height;
|
||||
if (contentHeight > height) {
|
||||
const maxContentY = contentHeight - height;
|
||||
const maxPos = 1 - root.size;
|
||||
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
|
||||
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
root.animating = true;
|
||||
root._updatingFromUser = true;
|
||||
let newPos = root.nonAnimPosition;
|
||||
if (event.angleDelta.y > 0)
|
||||
root.nonAnimPosition = Math.max(0, root.nonAnimPosition - 0.1);
|
||||
newPos = Math.max(0, root.nonAnimPosition - 0.1);
|
||||
else if (event.angleDelta.y < 0)
|
||||
root.nonAnimPosition = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
|
||||
newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
|
||||
root.nonAnimPosition = newPos;
|
||||
// Update flickable position
|
||||
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
|
||||
if (root.flickable) {
|
||||
const contentHeight = root.flickable.contentHeight;
|
||||
const height = root.flickable.height;
|
||||
if (contentHeight > height) {
|
||||
const maxContentY = contentHeight - height;
|
||||
const maxPos = 1 - root.size;
|
||||
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
|
||||
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Slider {
|
|||
|
||||
implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6
|
||||
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
color: Colours.palette.m3surfaceContainerHighest
|
||||
radius: Appearance.rounding.full
|
||||
topLeftRadius: root.implicitHeight / 15
|
||||
bottomLeftRadius: root.implicitHeight / 15
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ TextField {
|
|||
placeholderTextColor: Colours.palette.m3outline
|
||||
font.family: Appearance.font.family.sans
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
renderType: TextField.NativeRendering
|
||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
|
||||
cursorVisible: !readOnly
|
||||
|
||||
background: null
|
||||
|
|
|
|||
49
components/controls/SwitchRow.qml
Normal file
49
components/controls/SwitchRow.qml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import ".."
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
required property bool checked
|
||||
property bool enabled: true
|
||||
property var onToggled: function(checked) {}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.checked
|
||||
enabled: root.enabled
|
||||
onToggled: {
|
||||
root.onToggled(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
127
components/controls/ToggleButton.qml
Normal file
127
components/controls/ToggleButton.qml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property bool toggled
|
||||
property string icon
|
||||
property string label
|
||||
property string accent: "Secondary"
|
||||
property real iconSize: Appearance.font.size.large
|
||||
property real horizontalPadding: Appearance.padding.large
|
||||
property real verticalPadding: Appearance.padding.normal
|
||||
property string tooltip: ""
|
||||
|
||||
property bool hovered: false
|
||||
signal clicked
|
||||
|
||||
Component.onCompleted: {
|
||||
hovered = toggleStateLayer.containsMouse;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: toggleStateLayer
|
||||
function onContainsMouseChanged() {
|
||||
const newHovered = toggleStateLayer.containsMouse;
|
||||
if (hovered !== newHovered) {
|
||||
hovered = newHovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0)
|
||||
implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2
|
||||
|
||||
radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`]
|
||||
|
||||
StateLayer {
|
||||
id: toggleStateLayer
|
||||
|
||||
color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
|
||||
|
||||
function onClicked(): void {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: toggleBtnInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
id: toggleBtnIcon
|
||||
|
||||
visible: !!text
|
||||
fill: root.toggled ? 1 : 0
|
||||
text: root.icon
|
||||
color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
|
||||
font.pointSize: root.iconSize
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
asynchronous: true
|
||||
active: !!root.label
|
||||
visible: active
|
||||
|
||||
sourceComponent: StyledText {
|
||||
text: root.label
|
||||
color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip - positioned absolutely, doesn't affect layout
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: root.tooltip !== ""
|
||||
asynchronous: true
|
||||
z: 10000
|
||||
width: 0
|
||||
height: 0
|
||||
sourceComponent: Component {
|
||||
Tooltip {
|
||||
target: root
|
||||
text: root.tooltip
|
||||
}
|
||||
}
|
||||
// Completely remove from layout
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: false
|
||||
Layout.preferredWidth: 0
|
||||
Layout.preferredHeight: 0
|
||||
Layout.maximumWidth: 0
|
||||
Layout.maximumHeight: 0
|
||||
Layout.minimumWidth: 0
|
||||
Layout.minimumHeight: 0
|
||||
}
|
||||
}
|
||||
|
||||
29
components/controls/ToggleRow.qml
Normal file
29
components/controls/ToggleRow.qml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
|
||||
cLayer: 2
|
||||
}
|
||||
}
|
||||
|
||||
172
components/controls/Tooltip.qml
Normal file
172
components/controls/Tooltip.qml
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import ".."
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
required property Item target
|
||||
required property string text
|
||||
property int delay: 500
|
||||
property int timeout: 0
|
||||
|
||||
property bool tooltipVisible: false
|
||||
property Timer showTimer: Timer {
|
||||
interval: root.delay
|
||||
onTriggered: root.tooltipVisible = true
|
||||
}
|
||||
property Timer hideTimer: Timer {
|
||||
interval: root.timeout
|
||||
onTriggered: root.tooltipVisible = false
|
||||
}
|
||||
|
||||
// Popup properties - doesn't affect layout
|
||||
parent: {
|
||||
let p = target;
|
||||
// Walk up to find the root Item (usually has anchors.fill: parent)
|
||||
while (p && p.parent) {
|
||||
const parentItem = p.parent;
|
||||
// Check if this looks like a root pane Item
|
||||
if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) {
|
||||
return parentItem;
|
||||
}
|
||||
p = parentItem;
|
||||
}
|
||||
// Fallback
|
||||
return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target;
|
||||
}
|
||||
|
||||
visible: tooltipVisible
|
||||
modal: false
|
||||
closePolicy: Popup.NoAutoClose
|
||||
padding: 0
|
||||
margins: 0
|
||||
background: Item {}
|
||||
|
||||
// Update position when target moves or tooltip becomes visible
|
||||
onTooltipVisibleChanged: {
|
||||
if (tooltipVisible) {
|
||||
Qt.callLater(updatePosition);
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: root.target
|
||||
function onXChanged() { if (root.tooltipVisible) root.updatePosition(); }
|
||||
function onYChanged() { if (root.tooltipVisible) root.updatePosition(); }
|
||||
function onWidthChanged() { if (root.tooltipVisible) root.updatePosition(); }
|
||||
function onHeightChanged() { if (root.tooltipVisible) root.updatePosition(); }
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (!target || !parent) return;
|
||||
|
||||
// Wait for tooltipRect to have its size calculated
|
||||
Qt.callLater(() => {
|
||||
if (!target || !parent || !tooltipRect) return;
|
||||
|
||||
// Get target position in parent's coordinate system
|
||||
const targetPos = target.mapToItem(parent, 0, 0);
|
||||
const targetCenterX = targetPos.x + target.width / 2;
|
||||
|
||||
// Get tooltip size (use width/height if available, otherwise implicit)
|
||||
const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth;
|
||||
const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight;
|
||||
|
||||
// Center tooltip horizontally on target
|
||||
let newX = targetCenterX - tooltipWidth / 2;
|
||||
|
||||
// Position tooltip above target
|
||||
let newY = targetPos.y - tooltipHeight - Appearance.spacing.small;
|
||||
|
||||
// Keep within bounds
|
||||
const padding = Appearance.padding.normal;
|
||||
if (newX < padding) {
|
||||
newX = padding;
|
||||
} else if (newX + tooltipWidth > (parent.width - padding)) {
|
||||
newX = parent.width - tooltipWidth - padding;
|
||||
}
|
||||
|
||||
// Update popup position
|
||||
x = newX;
|
||||
y = newY;
|
||||
});
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor hover state
|
||||
Connections {
|
||||
target: root.target
|
||||
function onHoveredChanged() {
|
||||
if (target.hovered) {
|
||||
showTimer.start();
|
||||
if (timeout > 0) {
|
||||
hideTimer.stop();
|
||||
hideTimer.start();
|
||||
}
|
||||
} else {
|
||||
showTimer.stop();
|
||||
hideTimer.stop();
|
||||
tooltipVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: StyledRect {
|
||||
id: tooltipRect
|
||||
|
||||
implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
color: Colours.palette.m3surfaceContainerHighest
|
||||
radius: Appearance.rounding.small
|
||||
antialiasing: true
|
||||
|
||||
// Add elevation for depth
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
level: 3
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tooltipText
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: root.text
|
||||
color: Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (tooltipVisible) {
|
||||
updatePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,6 +4,7 @@ import qs.utils
|
|||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
|
@ -26,22 +27,420 @@ Singleton {
|
|||
property alias services: adapter.services
|
||||
property alias paths: adapter.paths
|
||||
|
||||
// Public save function - call this to persist config changes
|
||||
function save(): void {
|
||||
saveTimer.restart();
|
||||
recentlySaved = true;
|
||||
recentSaveCooldown.restart();
|
||||
}
|
||||
|
||||
property bool recentlySaved: false
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
timer.restart();
|
||||
try {
|
||||
// Parse current config to preserve structure and comments if possible
|
||||
let config = {};
|
||||
try {
|
||||
config = JSON.parse(fileView.text());
|
||||
} catch (e) {
|
||||
// If parsing fails, start with empty object
|
||||
config = {};
|
||||
}
|
||||
|
||||
// Update config with current values
|
||||
config = serializeConfig();
|
||||
|
||||
// Save to file with pretty printing
|
||||
fileView.setText(JSON.stringify(config, null, 2));
|
||||
} catch (e) {
|
||||
Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: recentSaveCooldown
|
||||
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
recentlySaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to serialize the config object
|
||||
function serializeConfig(): var {
|
||||
return {
|
||||
appearance: serializeAppearance(),
|
||||
general: serializeGeneral(),
|
||||
background: serializeBackground(),
|
||||
bar: serializeBar(),
|
||||
border: serializeBorder(),
|
||||
dashboard: serializeDashboard(),
|
||||
controlCenter: serializeControlCenter(),
|
||||
launcher: serializeLauncher(),
|
||||
notifs: serializeNotifs(),
|
||||
osd: serializeOsd(),
|
||||
session: serializeSession(),
|
||||
winfo: serializeWinfo(),
|
||||
lock: serializeLock(),
|
||||
utilities: serializeUtilities(),
|
||||
sidebar: serializeSidebar(),
|
||||
services: serializeServices(),
|
||||
paths: serializePaths()
|
||||
};
|
||||
}
|
||||
|
||||
function serializeAppearance(): var {
|
||||
return {
|
||||
rounding: { scale: appearance.rounding.scale },
|
||||
spacing: { scale: appearance.spacing.scale },
|
||||
padding: { scale: appearance.padding.scale },
|
||||
font: {
|
||||
family: {
|
||||
sans: appearance.font.family.sans,
|
||||
mono: appearance.font.family.mono,
|
||||
material: appearance.font.family.material,
|
||||
clock: appearance.font.family.clock
|
||||
},
|
||||
size: { scale: appearance.font.size.scale }
|
||||
},
|
||||
anim: {
|
||||
durations: { scale: appearance.anim.durations.scale }
|
||||
},
|
||||
transparency: {
|
||||
enabled: appearance.transparency.enabled,
|
||||
base: appearance.transparency.base,
|
||||
layers: appearance.transparency.layers
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeGeneral(): var {
|
||||
return {
|
||||
apps: {
|
||||
terminal: general.apps.terminal,
|
||||
audio: general.apps.audio,
|
||||
playback: general.apps.playback,
|
||||
explorer: general.apps.explorer
|
||||
},
|
||||
idle: {
|
||||
lockBeforeSleep: general.idle.lockBeforeSleep,
|
||||
inhibitWhenAudio: general.idle.inhibitWhenAudio,
|
||||
timeouts: general.idle.timeouts
|
||||
},
|
||||
battery: {
|
||||
warnLevels: general.battery.warnLevels,
|
||||
criticalLevel: general.battery.criticalLevel
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeBackground(): var {
|
||||
return {
|
||||
enabled: background.enabled,
|
||||
desktopClock: {
|
||||
enabled: background.desktopClock.enabled
|
||||
},
|
||||
visualiser: {
|
||||
enabled: background.visualiser.enabled,
|
||||
autoHide: background.visualiser.autoHide,
|
||||
blur: background.visualiser.blur,
|
||||
rounding: background.visualiser.rounding,
|
||||
spacing: background.visualiser.spacing
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeBar(): var {
|
||||
return {
|
||||
persistent: bar.persistent,
|
||||
showOnHover: bar.showOnHover,
|
||||
dragThreshold: bar.dragThreshold,
|
||||
scrollActions: {
|
||||
workspaces: bar.scrollActions.workspaces,
|
||||
volume: bar.scrollActions.volume,
|
||||
brightness: bar.scrollActions.brightness
|
||||
},
|
||||
popouts: {
|
||||
activeWindow: bar.popouts.activeWindow,
|
||||
tray: bar.popouts.tray,
|
||||
statusIcons: bar.popouts.statusIcons
|
||||
},
|
||||
workspaces: {
|
||||
shown: bar.workspaces.shown,
|
||||
activeIndicator: bar.workspaces.activeIndicator,
|
||||
occupiedBg: bar.workspaces.occupiedBg,
|
||||
showWindows: bar.workspaces.showWindows,
|
||||
showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces,
|
||||
activeTrail: bar.workspaces.activeTrail,
|
||||
perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces,
|
||||
label: bar.workspaces.label,
|
||||
occupiedLabel: bar.workspaces.occupiedLabel,
|
||||
activeLabel: bar.workspaces.activeLabel,
|
||||
capitalisation: bar.workspaces.capitalisation,
|
||||
specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons
|
||||
},
|
||||
tray: {
|
||||
background: bar.tray.background,
|
||||
recolour: bar.tray.recolour,
|
||||
compact: bar.tray.compact,
|
||||
iconSubs: bar.tray.iconSubs
|
||||
},
|
||||
status: {
|
||||
showAudio: bar.status.showAudio,
|
||||
showMicrophone: bar.status.showMicrophone,
|
||||
showKbLayout: bar.status.showKbLayout,
|
||||
showNetwork: bar.status.showNetwork,
|
||||
showBluetooth: bar.status.showBluetooth,
|
||||
showBattery: bar.status.showBattery,
|
||||
showLockStatus: bar.status.showLockStatus
|
||||
},
|
||||
clock: {
|
||||
showIcon: bar.clock.showIcon
|
||||
},
|
||||
sizes: {
|
||||
innerWidth: bar.sizes.innerWidth,
|
||||
windowPreviewSize: bar.sizes.windowPreviewSize,
|
||||
trayMenuWidth: bar.sizes.trayMenuWidth,
|
||||
batteryWidth: bar.sizes.batteryWidth,
|
||||
networkWidth: bar.sizes.networkWidth
|
||||
},
|
||||
entries: bar.entries
|
||||
};
|
||||
}
|
||||
|
||||
function serializeBorder(): var {
|
||||
return {
|
||||
thickness: border.thickness,
|
||||
rounding: border.rounding
|
||||
};
|
||||
}
|
||||
|
||||
function serializeDashboard(): var {
|
||||
return {
|
||||
enabled: dashboard.enabled,
|
||||
showOnHover: dashboard.showOnHover,
|
||||
mediaUpdateInterval: dashboard.mediaUpdateInterval,
|
||||
dragThreshold: dashboard.dragThreshold,
|
||||
sizes: {
|
||||
tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight,
|
||||
tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing,
|
||||
infoWidth: dashboard.sizes.infoWidth,
|
||||
infoIconSize: dashboard.sizes.infoIconSize,
|
||||
dateTimeWidth: dashboard.sizes.dateTimeWidth,
|
||||
mediaWidth: dashboard.sizes.mediaWidth,
|
||||
mediaProgressSweep: dashboard.sizes.mediaProgressSweep,
|
||||
mediaProgressThickness: dashboard.sizes.mediaProgressThickness,
|
||||
resourceProgessThickness: dashboard.sizes.resourceProgessThickness,
|
||||
weatherWidth: dashboard.sizes.weatherWidth,
|
||||
mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize,
|
||||
mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize,
|
||||
resourceSize: dashboard.sizes.resourceSize
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeControlCenter(): var {
|
||||
return {
|
||||
sizes: {
|
||||
heightMult: controlCenter.sizes.heightMult,
|
||||
ratio: controlCenter.sizes.ratio
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeLauncher(): var {
|
||||
return {
|
||||
enabled: launcher.enabled,
|
||||
showOnHover: launcher.showOnHover,
|
||||
maxShown: launcher.maxShown,
|
||||
maxWallpapers: launcher.maxWallpapers,
|
||||
specialPrefix: launcher.specialPrefix,
|
||||
actionPrefix: launcher.actionPrefix,
|
||||
enableDangerousActions: launcher.enableDangerousActions,
|
||||
dragThreshold: launcher.dragThreshold,
|
||||
vimKeybinds: launcher.vimKeybinds,
|
||||
hiddenApps: launcher.hiddenApps,
|
||||
useFuzzy: {
|
||||
apps: launcher.useFuzzy.apps,
|
||||
actions: launcher.useFuzzy.actions,
|
||||
schemes: launcher.useFuzzy.schemes,
|
||||
variants: launcher.useFuzzy.variants,
|
||||
wallpapers: launcher.useFuzzy.wallpapers
|
||||
},
|
||||
sizes: {
|
||||
itemWidth: launcher.sizes.itemWidth,
|
||||
itemHeight: launcher.sizes.itemHeight,
|
||||
wallpaperWidth: launcher.sizes.wallpaperWidth,
|
||||
wallpaperHeight: launcher.sizes.wallpaperHeight
|
||||
},
|
||||
actions: launcher.actions
|
||||
};
|
||||
}
|
||||
|
||||
function serializeNotifs(): var {
|
||||
return {
|
||||
expire: notifs.expire,
|
||||
defaultExpireTimeout: notifs.defaultExpireTimeout,
|
||||
clearThreshold: notifs.clearThreshold,
|
||||
expandThreshold: notifs.expandThreshold,
|
||||
actionOnClick: notifs.actionOnClick,
|
||||
groupPreviewNum: notifs.groupPreviewNum,
|
||||
sizes: {
|
||||
width: notifs.sizes.width,
|
||||
image: notifs.sizes.image,
|
||||
badge: notifs.sizes.badge
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeOsd(): var {
|
||||
return {
|
||||
enabled: osd.enabled,
|
||||
hideDelay: osd.hideDelay,
|
||||
enableBrightness: osd.enableBrightness,
|
||||
enableMicrophone: osd.enableMicrophone,
|
||||
sizes: {
|
||||
sliderWidth: osd.sizes.sliderWidth,
|
||||
sliderHeight: osd.sizes.sliderHeight
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeSession(): var {
|
||||
return {
|
||||
enabled: session.enabled,
|
||||
dragThreshold: session.dragThreshold,
|
||||
vimKeybinds: session.vimKeybinds,
|
||||
commands: {
|
||||
logout: session.commands.logout,
|
||||
shutdown: session.commands.shutdown,
|
||||
hibernate: session.commands.hibernate,
|
||||
reboot: session.commands.reboot
|
||||
},
|
||||
sizes: {
|
||||
button: session.sizes.button
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeWinfo(): var {
|
||||
return {
|
||||
sizes: {
|
||||
heightMult: winfo.sizes.heightMult,
|
||||
detailsWidth: winfo.sizes.detailsWidth
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeLock(): var {
|
||||
return {
|
||||
recolourLogo: lock.recolourLogo,
|
||||
enableFprint: lock.enableFprint,
|
||||
maxFprintTries: lock.maxFprintTries,
|
||||
sizes: {
|
||||
heightMult: lock.sizes.heightMult,
|
||||
ratio: lock.sizes.ratio,
|
||||
centerWidth: lock.sizes.centerWidth
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeUtilities(): var {
|
||||
return {
|
||||
enabled: utilities.enabled,
|
||||
maxToasts: utilities.maxToasts,
|
||||
sizes: {
|
||||
width: utilities.sizes.width,
|
||||
toastWidth: utilities.sizes.toastWidth
|
||||
},
|
||||
toasts: {
|
||||
configLoaded: utilities.toasts.configLoaded,
|
||||
chargingChanged: utilities.toasts.chargingChanged,
|
||||
gameModeChanged: utilities.toasts.gameModeChanged,
|
||||
dndChanged: utilities.toasts.dndChanged,
|
||||
audioOutputChanged: utilities.toasts.audioOutputChanged,
|
||||
audioInputChanged: utilities.toasts.audioInputChanged,
|
||||
capsLockChanged: utilities.toasts.capsLockChanged,
|
||||
numLockChanged: utilities.toasts.numLockChanged,
|
||||
kbLayoutChanged: utilities.toasts.kbLayoutChanged,
|
||||
vpnChanged: utilities.toasts.vpnChanged,
|
||||
nowPlaying: utilities.toasts.nowPlaying
|
||||
},
|
||||
vpn: {
|
||||
enabled: utilities.vpn.enabled,
|
||||
provider: utilities.vpn.provider
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeSidebar(): var {
|
||||
return {
|
||||
enabled: sidebar.enabled,
|
||||
dragThreshold: sidebar.dragThreshold,
|
||||
sizes: {
|
||||
width: sidebar.sizes.width
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeServices(): var {
|
||||
return {
|
||||
weatherLocation: services.weatherLocation,
|
||||
useFahrenheit: services.useFahrenheit,
|
||||
useTwelveHourClock: services.useTwelveHourClock,
|
||||
gpuType: services.gpuType,
|
||||
visualiserBars: services.visualiserBars,
|
||||
audioIncrement: services.audioIncrement,
|
||||
maxVolume: services.maxVolume,
|
||||
smartScheme: services.smartScheme,
|
||||
defaultPlayer: services.defaultPlayer,
|
||||
playerAliases: services.playerAliases
|
||||
};
|
||||
}
|
||||
|
||||
function serializePaths(): var {
|
||||
return {
|
||||
wallpaperDir: paths.wallpaperDir,
|
||||
sessionGif: paths.sessionGif,
|
||||
mediaGif: paths.mediaGif
|
||||
};
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: fileView
|
||||
|
||||
path: `${Paths.config}/shell.json`
|
||||
watchChanges: true
|
||||
onFileChanged: {
|
||||
timer.restart();
|
||||
reload();
|
||||
// Prevent reload loop - don't reload if we just saved
|
||||
if (!recentlySaved) {
|
||||
timer.restart();
|
||||
reload();
|
||||
} else {
|
||||
// Self-initiated save - reload without toast
|
||||
reload();
|
||||
}
|
||||
}
|
||||
onLoaded: {
|
||||
try {
|
||||
JSON.parse(text());
|
||||
if (adapter.utilities.toasts.configLoaded)
|
||||
Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(timer.elapsedMs()), "rule_settings");
|
||||
const elapsed = timer.elapsedMs();
|
||||
// Only show toast for external changes (not our own saves) and when elapsed time is meaningful
|
||||
if (adapter.utilities.toasts.configLoaded && !recentlySaved && elapsed > 0) {
|
||||
Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings");
|
||||
} else if (adapter.utilities.toasts.configLoaded && recentlySaved && elapsed > 0) {
|
||||
Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings");
|
||||
}
|
||||
} catch (e) {
|
||||
Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,27 @@ import qs.components.effects
|
|||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
|
||||
ColouredIcon {
|
||||
source: SysInfo.osLogo
|
||||
implicitSize: Appearance.font.size.large * 1.2
|
||||
colour: Colours.palette.m3tertiary
|
||||
Item {
|
||||
id: root
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const visibilities = Visibilities.getForActive();
|
||||
visibilities.launcher = !visibilities.launcher;
|
||||
}
|
||||
}
|
||||
|
||||
ColouredIcon {
|
||||
anchors.centerIn: parent
|
||||
source: SysInfo.osLogo
|
||||
implicitSize: Appearance.font.size.large * 1.2
|
||||
colour: Colours.palette.m3tertiary
|
||||
}
|
||||
|
||||
implicitWidth: Appearance.font.size.large * 1.2
|
||||
implicitHeight: Appearance.font.size.large * 1.2
|
||||
}
|
||||
|
|
|
|||
44
modules/bar/components/Settings.qml
Normal file
44
modules/bar/components/Settings.qml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import qs.components
|
||||
import qs.modules.controlcenter
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
// Cursed workaround to make the height larger than the parent
|
||||
anchors.fill: undefined
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
WindowFactory.create(null, {
|
||||
active: "network"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
|
||||
text: "settings"
|
||||
color: Colours.palette.m3onSurface
|
||||
font.bold: true
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
44
modules/bar/components/SettingsIcon.qml
Normal file
44
modules/bar/components/SettingsIcon.qml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import qs.components
|
||||
import qs.modules.controlcenter
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
// Cursed workaround to make the height larger than the parent
|
||||
anchors.fill: undefined
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
WindowFactory.create(null, {
|
||||
active: "network"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
|
||||
text: "settings"
|
||||
color: Colours.palette.m3onSurface
|
||||
font.bold: true
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -147,7 +147,19 @@ StyledRect {
|
|||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
|
||||
text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet icon
|
||||
WrappedLoader {
|
||||
name: "ethernet"
|
||||
active: Config.bar.status.showNetwork && Nmcli.activeEthernet
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: "cable"
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ StyledRect {
|
|||
implicitWidth: Config.bar.sizes.innerWidth
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.tray.background ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
Column {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Quickshell.Services.Pipewire
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import "../../controlcenter/network"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
|
@ -104,43 +105,16 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
IconTextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: Config.general.apps.audio.length > 0
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
verticalPadding: Appearance.padding.small
|
||||
text: qsTr("Open settings")
|
||||
icon: "settings"
|
||||
|
||||
implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: expandBtn.implicitHeight + Appearance.padding.small
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.palette.m3primaryContainer
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.wrapper.hasCurrent = false;
|
||||
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.audio]);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: expandBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.padding.smaller
|
||||
text: qsTr("Open settings")
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "chevron_right"
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
onClicked: root.wrapper.detach("audio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Quickshell
|
|||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../controlcenter/network"
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
|
@ -20,7 +21,7 @@ ColumnLayout {
|
|||
StyledText {
|
||||
Layout.topMargin: Appearance.padding.normal
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Bluetooth %1").arg(BluetoothAdapterState.toString(Bluetooth.defaultAdapter?.state).toLowerCase())
|
||||
text: qsTr("Bluetooth")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
|
|
@ -164,40 +165,16 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: expandBtn.implicitHeight + Appearance.padding.small
|
||||
IconTextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
verticalPadding: Appearance.padding.small
|
||||
text: qsTr("Open settings")
|
||||
icon: "settings"
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.palette.m3primaryContainer
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.wrapper.detach("bluetooth");
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: expandBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.padding.smaller
|
||||
text: qsTr("Open panel")
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "chevron_right"
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
onClicked: root.wrapper.detach("bluetooth")
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,65 @@ Item {
|
|||
}
|
||||
|
||||
Popout {
|
||||
id: networkPopout
|
||||
name: "network"
|
||||
sourceComponent: Network {}
|
||||
sourceComponent: Network {
|
||||
wrapper: root.wrapper
|
||||
view: "wireless"
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "ethernet"
|
||||
sourceComponent: Network {
|
||||
wrapper: root.wrapper
|
||||
view: "ethernet"
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
id: passwordPopout
|
||||
name: "wirelesspassword"
|
||||
sourceComponent: WirelessPassword {
|
||||
id: passwordComponent
|
||||
wrapper: root.wrapper
|
||||
network: networkPopout.item?.passwordNetwork ?? null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
function onCurrentNameChanged() {
|
||||
// Update network immediately when password popout becomes active
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
// Set network immediately if available
|
||||
if (networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
if (passwordPopout.item) {
|
||||
passwordPopout.item.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}
|
||||
// Also try after a short delay in case networkPopout.item wasn't ready
|
||||
Qt.callLater(() => {
|
||||
if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
passwordPopout.item.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: networkPopout
|
||||
function onItemChanged() {
|
||||
// When network popout loads, update password popout if it's active
|
||||
if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) {
|
||||
Qt.callLater(() => {
|
||||
if (networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
passwordPopout.item.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
|
|
|
|||
|
|
@ -12,35 +12,48 @@ import QtQuick.Layouts
|
|||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
|
||||
property string connectingToSsid: ""
|
||||
property string view: "wireless" // "wireless" or "ethernet"
|
||||
property var passwordNetwork: null
|
||||
property bool showPasswordDialog: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
width: Config.bar.sizes.networkWidth
|
||||
|
||||
// Wireless section
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.padding.normal
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.padding.normal : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Wifi %1").arg(Network.wifiEnabled ? "enabled" : "disabled")
|
||||
text: qsTr("Wireless")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Toggle {
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
label: qsTr("Enabled")
|
||||
checked: Network.wifiEnabled
|
||||
toggle.onToggled: Network.enableWifi(checked)
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: Nmcli.enableWifi(checked)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.spacing.small : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("%1 networks available").arg(Network.networks.length)
|
||||
text: qsTr("%1 networks available").arg(Nmcli.networks.length)
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Repeater {
|
||||
visible: root.view === "wireless"
|
||||
model: ScriptModel {
|
||||
values: [...Network.networks].sort((a, b) => {
|
||||
values: [...Nmcli.networks].sort((a, b) => {
|
||||
if (a.active !== b.active)
|
||||
return b.active - a.active;
|
||||
return b.strength - a.strength;
|
||||
|
|
@ -50,10 +63,12 @@ ColumnLayout {
|
|||
RowLayout {
|
||||
id: networkItem
|
||||
|
||||
required property Network.AccessPoint modelData
|
||||
required property Nmcli.AccessPoint modelData
|
||||
readonly property bool isConnecting: root.connectingToSsid === modelData.ssid
|
||||
readonly property bool loading: networkItem.isConnecting
|
||||
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.small
|
||||
|
|
@ -96,10 +111,8 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
|
||||
implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0)
|
||||
|
|
@ -111,20 +124,32 @@ ColumnLayout {
|
|||
|
||||
StateLayer {
|
||||
color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
disabled: networkItem.loading || !Network.wifiEnabled
|
||||
disabled: networkItem.loading || !Nmcli.wifiEnabled
|
||||
|
||||
function onClicked(): void {
|
||||
if (networkItem.modelData.active) {
|
||||
Network.disconnectFromNetwork();
|
||||
Nmcli.disconnectFromNetwork();
|
||||
} else {
|
||||
root.connectingToSsid = networkItem.modelData.ssid;
|
||||
Network.connectToNetwork(networkItem.modelData.ssid, "");
|
||||
NetworkConnection.handleConnect(
|
||||
networkItem.modelData,
|
||||
null,
|
||||
(network) => {
|
||||
// Password is required - show password dialog
|
||||
root.passwordNetwork = network;
|
||||
root.showPasswordDialog = true;
|
||||
root.wrapper.currentName = "wirelesspassword";
|
||||
}
|
||||
);
|
||||
|
||||
// Clear connecting state if connection succeeds immediately (saved profile)
|
||||
// This is handled by the onActiveChanged connection below
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
id: wirelessConnectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
|
|
@ -142,7 +167,9 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.spacing.small : 0
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
|
|
@ -151,10 +178,10 @@ ColumnLayout {
|
|||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
disabled: Network.scanning || !Network.wifiEnabled
|
||||
disabled: Nmcli.scanning || !Nmcli.wifiEnabled
|
||||
|
||||
function onClicked(): void {
|
||||
Network.rescanWifi();
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,17 +190,19 @@ ColumnLayout {
|
|||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.small
|
||||
opacity: Network.scanning ? 0 : 1
|
||||
opacity: Nmcli.scanning ? 0 : 1
|
||||
|
||||
MaterialIcon {
|
||||
id: scanIcon
|
||||
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
|
||||
animate: true
|
||||
text: "wifi_find"
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575)
|
||||
text: qsTr("Rescan networks")
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
}
|
||||
|
|
@ -188,26 +217,160 @@ ColumnLayout {
|
|||
strokeWidth: Appearance.padding.small / 2
|
||||
bgColour: "transparent"
|
||||
implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2
|
||||
running: Network.scanning
|
||||
running: Nmcli.scanning
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet section
|
||||
StyledText {
|
||||
visible: root.view === "ethernet"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.padding.normal : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Ethernet")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.view === "ethernet"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.spacing.small : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length)
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Repeater {
|
||||
visible: root.view === "ethernet"
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.ethernetDevices].sort((a, b) => {
|
||||
if (a.connected !== b.connected)
|
||||
return b.connected - a.connected;
|
||||
return (a.interface || "").localeCompare(b.interface || "");
|
||||
}).slice(0, 8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: ethernetItem
|
||||
|
||||
required property var modelData
|
||||
readonly property bool loading: false
|
||||
|
||||
visible: root.view === "ethernet"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "cable"
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.spacing.small / 2
|
||||
Layout.rightMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
text: ethernetItem.modelData.interface || qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
font.weight: ethernetItem.modelData.connected ? 500 : 400
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0)
|
||||
|
||||
CircularIndicator {
|
||||
anchors.fill: parent
|
||||
running: ethernetItem.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
disabled: ethernetItem.loading
|
||||
|
||||
function onClicked(): void {
|
||||
if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) {
|
||||
Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {});
|
||||
} else {
|
||||
Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: ethernetItem.modelData.connected ? "link_off" : "link"
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
opacity: ethernetItem.loading ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset connecting state when network changes
|
||||
Connections {
|
||||
target: Network
|
||||
target: Nmcli
|
||||
|
||||
function onActiveChanged(): void {
|
||||
if (Network.active && root.connectingToSsid === Network.active.ssid) {
|
||||
if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) {
|
||||
root.connectingToSsid = "";
|
||||
// Close password dialog if we successfully connected
|
||||
if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) {
|
||||
root.showPasswordDialog = false;
|
||||
root.passwordNetwork = null;
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
root.wrapper.currentName = "network";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onScanningChanged(): void {
|
||||
if (!Network.scanning)
|
||||
if (!Nmcli.scanning)
|
||||
scanIcon.rotation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
function onCurrentNameChanged(): void {
|
||||
// Clear password network when leaving password dialog
|
||||
if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) {
|
||||
root.showPasswordDialog = false;
|
||||
root.passwordNetwork = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
|
|
|
|||
606
modules/bar/popouts/WirelessPassword.qml
Normal file
606
modules/bar/popouts/WirelessPassword.qml
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
property var network: null
|
||||
property bool isClosing: false
|
||||
|
||||
readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword"
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
function onCurrentNameChanged() {
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
// Update network when popout becomes active
|
||||
Qt.callLater(() => {
|
||||
// Try to get network from parent Content's networkPopout
|
||||
const content = root.parent?.parent?.parent;
|
||||
if (content) {
|
||||
const networkPopout = content.children.find(c => c.name === "network");
|
||||
if (networkPopout && networkPopout.item) {
|
||||
root.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}
|
||||
// Force focus to password container when popout becomes active
|
||||
// Use Timer for actual delay to ensure dialog is fully rendered
|
||||
focusTimer.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: focusTimer
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
root.forceActiveFocus();
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
visible: shouldBeVisible || isClosing
|
||||
enabled: shouldBeVisible && !isClosing
|
||||
focus: enabled
|
||||
|
||||
Component.onCompleted: {
|
||||
if (shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure dialog is fully rendered
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure dialog is fully rendered
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: closeDialog()
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
visible: root.shouldBeVisible || root.isClosing
|
||||
opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0
|
||||
scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: root.isClosing
|
||||
onFinished: {
|
||||
if (root.isClosing) {
|
||||
root.isClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: parent
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: parent
|
||||
property: "scale"
|
||||
to: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: root.closeDialog()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "lock"
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Enter password")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: networkNameText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
if (root.network) {
|
||||
const ssid = root.network.ssid;
|
||||
if (ssid && ssid.length > 0) {
|
||||
return qsTr("Network: %1").arg(ssid);
|
||||
}
|
||||
}
|
||||
return qsTr("Network: Unknown");
|
||||
}
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 50
|
||||
running: root.shouldBeVisible && (!root.network || !root.network.ssid)
|
||||
repeat: true
|
||||
property int attempts: 0
|
||||
onTriggered: {
|
||||
attempts++;
|
||||
// Keep trying to get network from Network component
|
||||
const content = root.parent?.parent?.parent;
|
||||
if (content) {
|
||||
const networkPopout = content.children.find(c => c.name === "network");
|
||||
if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
root.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}
|
||||
// Stop if we got it or after 20 attempts (1 second)
|
||||
if ((root.network && root.network.ssid) || attempts >= 20) {
|
||||
stop();
|
||||
attempts = 0;
|
||||
}
|
||||
}
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
attempts = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: connectButton.connecting || connectButton.hasError
|
||||
text: {
|
||||
if (connectButton.hasError) {
|
||||
return qsTr("Connection failed. Please check your password and try again.");
|
||||
}
|
||||
if (connectButton.connecting) {
|
||||
return qsTr("Connecting...");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: 400
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: passwordContainer
|
||||
objectName: "passwordContainer"
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
|
||||
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
|
||||
property string passwordBuffer: ""
|
||||
|
||||
Keys.onPressed: event => {
|
||||
// Ensure we have focus when receiving keyboard input
|
||||
if (!activeFocus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (connectButton.enabled) {
|
||||
connectButton.clicked();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
passwordBuffer = "";
|
||||
} else {
|
||||
passwordBuffer = passwordBuffer.slice(0, -1);
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.text && event.text.length > 0) {
|
||||
passwordBuffer += event.text;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged(): void {
|
||||
if (root.shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure focus works correctly
|
||||
passwordFocusTimer.start();
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: passwordFocusTimer
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure focus works correctly
|
||||
passwordFocusTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
|
||||
border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0)
|
||||
border.color: {
|
||||
if (connectButton.hasError) {
|
||||
return Colours.palette.m3error;
|
||||
}
|
||||
if (passwordContainer.activeFocus) {
|
||||
return Colours.palette.m3primary;
|
||||
}
|
||||
return root.shouldBeVisible ? Colours.palette.m3outline : "transparent";
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
hoverEnabled: false
|
||||
cursorShape: Qt.IBeamCursor
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
function onClicked(): void {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholder
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Password")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.family: Appearance.font.family.mono
|
||||
opacity: passwordContainer.passwordBuffer ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: charList
|
||||
|
||||
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: fullWidth
|
||||
implicitHeight: Appearance.font.size.normal
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: passwordContainer.passwordBuffer.split("")
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
id: ch
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: charList.implicitHeight
|
||||
|
||||
color: Colours.palette.m3onSurface
|
||||
radius: Appearance.rounding.small / 2
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: ch
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: ch
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
}
|
||||
}
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
id: cancelButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
id: connectButton
|
||||
|
||||
property bool connecting: false
|
||||
property bool hasError: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3primary
|
||||
inactiveOnColour: Colours.palette.m3onPrimary
|
||||
text: qsTr("Connect")
|
||||
enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
|
||||
|
||||
onClicked: {
|
||||
if (!root.network || connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = passwordContainer.passwordBuffer;
|
||||
if (!password || password.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous error
|
||||
hasError = false;
|
||||
|
||||
// Set connecting state
|
||||
connecting = true;
|
||||
enabled = false;
|
||||
text = qsTr("Connecting...");
|
||||
|
||||
// Connect to network
|
||||
NetworkConnection.connectWithPassword(root.network, password, result => {
|
||||
if (result && result.success)
|
||||
// Connection successful, monitor will handle the rest
|
||||
{} else if (result && result.needsPassword) {
|
||||
// Shouldn't happen since we provided password
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
} else {
|
||||
// Connection failed immediately - show error
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start monitoring connection
|
||||
connectionMonitor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectionStatus(): void {
|
||||
if (!root.shouldBeVisible || !connectButton.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're connected to the target network (case-insensitive SSID comparison)
|
||||
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
|
||||
if (isConnected) {
|
||||
// Successfully connected - give it a moment for network list to update
|
||||
// Use Timer for actual delay
|
||||
connectionSuccessTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for connection failures - if pending connection was cleared but we're not connected
|
||||
if (Nmcli.pendingConnection === null && connectButton.connecting) {
|
||||
// Wait a bit more before giving up (allow time for connection to establish)
|
||||
if (connectionMonitor.repeatCount > 10) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionMonitor
|
||||
interval: 1000
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
property int repeatCount: 0
|
||||
|
||||
onTriggered: {
|
||||
repeatCount++;
|
||||
root.checkConnectionStatus();
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
repeatCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionSuccessTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
// Double-check connection is still active
|
||||
if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) {
|
||||
const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
if (stillConnected) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
// Return to network popout on successful connection
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
root.wrapper.currentName = "network";
|
||||
}
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
root.checkConnectionStatus();
|
||||
}
|
||||
}
|
||||
function onConnectionFailed(ssid: string) {
|
||||
if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
if (isClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isClosing = true;
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
connectionMonitor.stop();
|
||||
|
||||
// Return to network popout
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
root.wrapper.currentName = "network";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +55,25 @@ Item {
|
|||
implicitWidth: nonAnimWidth
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
Keys.onEscapePressed: close()
|
||||
focus: hasCurrent
|
||||
Keys.onEscapePressed: {
|
||||
// Forward escape to password popout if active, otherwise close
|
||||
if (currentName === "wirelesspassword" && content.item) {
|
||||
const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword");
|
||||
if (passwordPopout && passwordPopout.item) {
|
||||
passwordPopout.item.closeDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
// Don't intercept keys when password popout is active - let it handle them
|
||||
if (currentName === "wirelesspassword") {
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: root.isDetached
|
||||
|
|
@ -70,6 +88,14 @@ Item {
|
|||
property: "WlrLayershell.keyboardFocus"
|
||||
value: WlrKeyboardFocus.OnDemand
|
||||
}
|
||||
|
||||
Binding {
|
||||
when: root.hasCurrent && root.currentName === "wirelesspassword"
|
||||
|
||||
target: QsWindow.window
|
||||
property: "WlrLayershell.keyboardFocus"
|
||||
value: WlrKeyboardFocus.OnDemand
|
||||
}
|
||||
|
||||
Comp {
|
||||
id: content
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ Item {
|
|||
anchors.fill: parent
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
// Prevent tab switching during initial opening animation to avoid blank pages
|
||||
if (!panes.initialOpeningComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.angleDelta.y < 0)
|
||||
root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1);
|
||||
else if (event.angleDelta.y > 0)
|
||||
|
|
@ -76,10 +81,13 @@ Item {
|
|||
|
||||
screen: root.screen
|
||||
session: root.session
|
||||
initialOpeningComplete: root.initialOpeningComplete
|
||||
}
|
||||
}
|
||||
|
||||
Panes {
|
||||
id: panes
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
|
|
@ -88,4 +96,6 @@ Item {
|
|||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool initialOpeningComplete: panes.initialOpeningComplete
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
|
@ -12,6 +13,7 @@ Item {
|
|||
|
||||
required property ShellScreen screen
|
||||
required property Session session
|
||||
required property bool initialOpeningComplete
|
||||
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
|
|
@ -30,57 +32,17 @@ Item {
|
|||
|
||||
PropertyChanges {
|
||||
layout.spacing: Appearance.spacing.small
|
||||
menuIcon.opacity: 0
|
||||
menuIconExpanded.opacity: 1
|
||||
menuIcon.rotation: 180
|
||||
menuIconExpanded.rotation: 0
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "spacing,opacity,rotation"
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: menuBtn
|
||||
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
implicitWidth: menuIcon.implicitWidth + menuIcon.anchors.leftMargin * 2
|
||||
implicitHeight: menuIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.navExpanded = !root.session.navExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: menuIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
|
||||
text: "menu"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: menuIconExpanded
|
||||
|
||||
anchors.fill: menuIcon
|
||||
text: "menu_open"
|
||||
font.pointSize: menuIcon.font.pointSize
|
||||
opacity: 0
|
||||
rotation: -180
|
||||
properties: "spacing"
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
asynchronous: true
|
||||
active: !root.session.floating
|
||||
visible: active
|
||||
|
|
@ -102,7 +64,6 @@ Item {
|
|||
function onClicked(): void {
|
||||
root.session.root.close();
|
||||
WindowFactory.create(null, {
|
||||
screen: root.screen,
|
||||
active: root.session.active,
|
||||
navExpanded: root.session.navExpanded
|
||||
});
|
||||
|
|
@ -156,20 +117,15 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
NavItem {
|
||||
Layout.topMargin: Appearance.spacing.large * 2
|
||||
icon: "network_manage"
|
||||
label: "network"
|
||||
}
|
||||
Repeater {
|
||||
model: PaneRegistry.count
|
||||
|
||||
NavItem {
|
||||
icon: "settings_bluetooth"
|
||||
label: "bluetooth"
|
||||
}
|
||||
|
||||
NavItem {
|
||||
icon: "tune"
|
||||
label: "audio"
|
||||
NavItem {
|
||||
required property int index
|
||||
Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0
|
||||
icon: PaneRegistry.getByIndex(index).icon
|
||||
label: PaneRegistry.getByIndex(index).label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +178,10 @@ Item {
|
|||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
// Prevent tab switching during initial opening animation to avoid blank pages
|
||||
if (!root.initialOpeningComplete) {
|
||||
return;
|
||||
}
|
||||
root.session.active = item.label;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
87
modules/controlcenter/PaneRegistry.qml
Normal file
87
modules/controlcenter/PaneRegistry.qml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property list<QtObject> panes: [
|
||||
QtObject {
|
||||
readonly property string id: "network"
|
||||
readonly property string label: "network"
|
||||
readonly property string icon: "router"
|
||||
readonly property string component: "network/NetworkingPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "bluetooth"
|
||||
readonly property string label: "bluetooth"
|
||||
readonly property string icon: "settings_bluetooth"
|
||||
readonly property string component: "bluetooth/BtPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "audio"
|
||||
readonly property string label: "audio"
|
||||
readonly property string icon: "volume_up"
|
||||
readonly property string component: "audio/AudioPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "appearance"
|
||||
readonly property string label: "appearance"
|
||||
readonly property string icon: "palette"
|
||||
readonly property string component: "appearance/AppearancePane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "taskbar"
|
||||
readonly property string label: "taskbar"
|
||||
readonly property string icon: "task_alt"
|
||||
readonly property string component: "taskbar/TaskbarPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "launcher"
|
||||
readonly property string label: "launcher"
|
||||
readonly property string icon: "apps"
|
||||
readonly property string component: "launcher/LauncherPane.qml"
|
||||
}
|
||||
]
|
||||
|
||||
readonly property int count: panes.length
|
||||
|
||||
readonly property var labels: {
|
||||
const result = [];
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
result.push(panes[i].label);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getByIndex(index: int): QtObject {
|
||||
if (index >= 0 && index < panes.length) {
|
||||
return panes[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIndexByLabel(label: string): int {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].label === label) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getByLabel(label: string): QtObject {
|
||||
const index = getIndexByLabel(label);
|
||||
return getByIndex(index);
|
||||
}
|
||||
|
||||
function getById(id: string): QtObject {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].id === id) {
|
||||
return panes[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "bluetooth"
|
||||
import "network"
|
||||
import "audio"
|
||||
import "appearance"
|
||||
import "taskbar"
|
||||
import "launcher"
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
|
@ -13,74 +19,156 @@ ClippingRectangle {
|
|||
|
||||
required property Session session
|
||||
|
||||
readonly property bool initialOpeningComplete: layout.initialOpeningComplete
|
||||
|
||||
color: "transparent"
|
||||
clip: true
|
||||
focus: false
|
||||
activeFocusOnTab: false
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onPressed: function(mouse) {
|
||||
root.focus = true;
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
|
||||
function onActiveIndexChanged(): void {
|
||||
root.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
spacing: 0
|
||||
y: -root.session.activeIndex * root.height
|
||||
clip: true
|
||||
|
||||
Pane {
|
||||
index: 0
|
||||
sourceComponent: Item {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Work in progress")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
}
|
||||
property bool animationComplete: true
|
||||
property bool initialOpeningComplete: false
|
||||
|
||||
Timer {
|
||||
id: animationDelayTimer
|
||||
interval: Appearance.anim.durations.normal
|
||||
onTriggered: {
|
||||
layout.animationComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
index: 1
|
||||
sourceComponent: BtPane {
|
||||
session: root.session
|
||||
Timer {
|
||||
id: initialOpeningTimer
|
||||
interval: Appearance.anim.durations.large
|
||||
running: true
|
||||
onTriggered: {
|
||||
layout.initialOpeningComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
index: 2
|
||||
sourceComponent: Item {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Work in progress")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
}
|
||||
Repeater {
|
||||
model: PaneRegistry.count
|
||||
|
||||
Pane {
|
||||
required property int index
|
||||
paneIndex: index
|
||||
componentPath: PaneRegistry.getByIndex(index).component
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
function onActiveIndexChanged(): void {
|
||||
layout.animationComplete = false;
|
||||
animationDelayTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pane: Item {
|
||||
id: pane
|
||||
|
||||
required property int index
|
||||
property alias sourceComponent: loader.sourceComponent
|
||||
required property int paneIndex
|
||||
required property string componentPath
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: root.height
|
||||
|
||||
property bool hasBeenLoaded: false
|
||||
|
||||
function updateActive(): void {
|
||||
const diff = Math.abs(root.session.activeIndex - pane.paneIndex);
|
||||
const isActivePane = diff === 0;
|
||||
let shouldBeActive = false;
|
||||
|
||||
if (!layout.initialOpeningComplete) {
|
||||
shouldBeActive = isActivePane;
|
||||
} else {
|
||||
if (diff <= 1) {
|
||||
shouldBeActive = true;
|
||||
} else if (pane.hasBeenLoaded) {
|
||||
shouldBeActive = true;
|
||||
} else {
|
||||
shouldBeActive = layout.animationComplete;
|
||||
}
|
||||
}
|
||||
|
||||
loader.active = shouldBeActive;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
clip: false
|
||||
asynchronous: true
|
||||
active: {
|
||||
if (root.session.activeIndex === pane.index)
|
||||
return true;
|
||||
|
||||
const ly = -layout.y;
|
||||
const ty = pane.index * root.height;
|
||||
return ly + root.height > ty && ly < ty + root.height;
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
pane.updateActive();
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && !pane.hasBeenLoaded) {
|
||||
pane.hasBeenLoaded = true;
|
||||
}
|
||||
|
||||
if (active && !item) {
|
||||
loader.setSource(pane.componentPath, {
|
||||
"session": root.session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onItemChanged: {
|
||||
if (item) {
|
||||
pane.hasBeenLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
function onActiveIndexChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: layout
|
||||
function onInitialOpeningCompleteChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
function onAnimationCompleteChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import "./state"
|
||||
import qs.modules.controlcenter
|
||||
|
||||
QtObject {
|
||||
readonly property list<string> panes: ["network", "bluetooth", "audio"]
|
||||
readonly property list<string> panes: PaneRegistry.labels
|
||||
|
||||
required property var root
|
||||
property bool floating: false
|
||||
property string active: panes[0]
|
||||
property string active: "network"
|
||||
property int activeIndex: 0
|
||||
property bool navExpanded: false
|
||||
|
||||
readonly property Bt bt: Bt {}
|
||||
readonly property BluetoothState bt: BluetoothState {}
|
||||
readonly property NetworkState network: NetworkState {}
|
||||
readonly property EthernetState ethernet: EthernetState {}
|
||||
readonly property LauncherState launcher: LauncherState {}
|
||||
|
||||
onActiveChanged: activeIndex = panes.indexOf(active)
|
||||
onActiveIndexChanged: active = panes[activeIndex]
|
||||
|
||||
component Bt: QtObject {
|
||||
property BluetoothDevice active
|
||||
property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter
|
||||
property bool editingAdapterName
|
||||
property bool fabMenuOpen
|
||||
property bool editingDeviceName
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ Singleton {
|
|||
destroy();
|
||||
}
|
||||
|
||||
minimumSize.width: 1000
|
||||
minimumSize.height: 600
|
||||
|
||||
implicitWidth: cc.implicitWidth
|
||||
implicitHeight: cc.implicitHeight
|
||||
|
||||
minimumSize.width: implicitWidth
|
||||
minimumSize.height: implicitHeight
|
||||
maximumSize.width: implicitWidth
|
||||
maximumSize.height: implicitHeight
|
||||
|
||||
title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1))
|
||||
|
||||
ControlCenter {
|
||||
|
|
|
|||
244
modules/controlcenter/appearance/AppearancePane.qml
Normal file
244
modules/controlcenter/appearance/AppearancePane.qml
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "./sections"
|
||||
import "../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia.Models
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1
|
||||
property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded"
|
||||
property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF"
|
||||
property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik"
|
||||
property real fontSizeScale: Config.appearance.font.size.scale ?? 1
|
||||
property real paddingScale: Config.appearance.padding.scale ?? 1
|
||||
property real roundingScale: Config.appearance.rounding.scale ?? 1
|
||||
property real spacingScale: Config.appearance.spacing.scale ?? 1
|
||||
property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false
|
||||
property real transparencyBase: Config.appearance.transparency.base ?? 0.85
|
||||
property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4
|
||||
property real borderRounding: Config.border.rounding ?? 1
|
||||
property real borderThickness: Config.border.thickness ?? 1
|
||||
|
||||
property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false
|
||||
property bool backgroundEnabled: Config.background.enabled ?? true
|
||||
property bool visualiserEnabled: Config.background.visualiser.enabled ?? false
|
||||
property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true
|
||||
property real visualiserRounding: Config.background.visualiser.rounding ?? 1
|
||||
property real visualiserSpacing: Config.background.visualiser.spacing ?? 1
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function saveConfig() {
|
||||
Config.appearance.anim.durations.scale = root.animDurationsScale;
|
||||
|
||||
Config.appearance.font.family.material = root.fontFamilyMaterial;
|
||||
Config.appearance.font.family.mono = root.fontFamilyMono;
|
||||
Config.appearance.font.family.sans = root.fontFamilySans;
|
||||
Config.appearance.font.size.scale = root.fontSizeScale;
|
||||
|
||||
Config.appearance.padding.scale = root.paddingScale;
|
||||
Config.appearance.rounding.scale = root.roundingScale;
|
||||
Config.appearance.spacing.scale = root.spacingScale;
|
||||
|
||||
Config.appearance.transparency.enabled = root.transparencyEnabled;
|
||||
Config.appearance.transparency.base = root.transparencyBase;
|
||||
Config.appearance.transparency.layers = root.transparencyLayers;
|
||||
|
||||
Config.background.desktopClock.enabled = root.desktopClockEnabled;
|
||||
Config.background.enabled = root.backgroundEnabled;
|
||||
|
||||
Config.background.visualiser.enabled = root.visualiserEnabled;
|
||||
Config.background.visualiser.autoHide = root.visualiserAutoHide;
|
||||
Config.background.visualiser.rounding = root.visualiserRounding;
|
||||
Config.background.visualiser.spacing = root.visualiserSpacing;
|
||||
|
||||
Config.border.rounding = root.borderRounding;
|
||||
Config.border.thickness = root.borderThickness;
|
||||
|
||||
Config.save();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appearanceRightContentComponent
|
||||
|
||||
Item {
|
||||
id: rightAppearanceFlickable
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Appearance.spacing.normal
|
||||
text: qsTr("Wallpaper")
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 600
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: wallpaperLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: -Appearance.padding.large * 2
|
||||
|
||||
asynchronous: true
|
||||
active: {
|
||||
const isActive = root.session.activeIndex === 3;
|
||||
const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
|
||||
const splitLayout = root.children[0];
|
||||
const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null;
|
||||
const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent);
|
||||
return shouldActivate;
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
console.error("[AppearancePane] Wallpaper loader error!");
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: WallpaperGrid {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
StyledFlickable {
|
||||
id: sidebarFlickable
|
||||
readonly property var rootPane: root
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: sidebarLayout.height
|
||||
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sidebarFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sidebarLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
readonly property var rootPane: sidebarFlickable.rootPane
|
||||
|
||||
readonly property bool allSectionsExpanded:
|
||||
themeModeSection.expanded &&
|
||||
colorVariantSection.expanded &&
|
||||
colorSchemeSection.expanded &&
|
||||
animationsSection.expanded &&
|
||||
fontsSection.expanded &&
|
||||
scalesSection.expanded &&
|
||||
transparencySection.expanded &&
|
||||
borderSection.expanded &&
|
||||
backgroundSection.expanded
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Appearance")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more"
|
||||
type: IconButton.Text
|
||||
label.animate: true
|
||||
onClicked: {
|
||||
const shouldExpand = !sidebarLayout.allSectionsExpanded;
|
||||
themeModeSection.expanded = shouldExpand;
|
||||
colorVariantSection.expanded = shouldExpand;
|
||||
colorSchemeSection.expanded = shouldExpand;
|
||||
animationsSection.expanded = shouldExpand;
|
||||
fontsSection.expanded = shouldExpand;
|
||||
scalesSection.expanded = shouldExpand;
|
||||
transparencySection.expanded = shouldExpand;
|
||||
borderSection.expanded = shouldExpand;
|
||||
backgroundSection.expanded = shouldExpand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeModeSection {
|
||||
id: themeModeSection
|
||||
}
|
||||
|
||||
ColorVariantSection {
|
||||
id: colorVariantSection
|
||||
}
|
||||
|
||||
ColorSchemeSection {
|
||||
id: colorSchemeSection
|
||||
}
|
||||
|
||||
AnimationsSection {
|
||||
id: animationsSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
FontsSection {
|
||||
id: fontsSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
ScalesSection {
|
||||
id: scalesSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
TransparencySection {
|
||||
id: transparencySection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
BorderSection {
|
||||
id: borderSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
BackgroundSection {
|
||||
id: backgroundSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: appearanceRightContentComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Animations")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Animation duration scale")
|
||||
value: rootPane.animDurationsScale
|
||||
from: 0.1
|
||||
to: 5.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.animDurationsScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
105
modules/controlcenter/appearance/sections/BackgroundSection.qml
Normal file
105
modules/controlcenter/appearance/sections/BackgroundSection.qml
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Background")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Desktop clock")
|
||||
checked: rootPane.desktopClockEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Background enabled")
|
||||
checked: rootPane.backgroundEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.backgroundEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Visualiser")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Visualiser enabled")
|
||||
checked: rootPane.visualiserEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.visualiserEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Visualiser auto hide")
|
||||
checked: rootPane.visualiserAutoHide
|
||||
onToggled: checked => {
|
||||
rootPane.visualiserAutoHide = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Visualiser rounding")
|
||||
value: rootPane.visualiserRounding
|
||||
from: 0
|
||||
to: 10
|
||||
stepSize: 1
|
||||
validator: IntValidator { bottom: 0; top: 10 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.visualiserRounding = Math.round(newValue);
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Visualiser spacing")
|
||||
value: rootPane.visualiserSpacing
|
||||
from: 0
|
||||
to: 2
|
||||
validator: DoubleValidator { bottom: 0; top: 2 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.visualiserSpacing = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
modules/controlcenter/appearance/sections/BorderSection.qml
Normal file
63
modules/controlcenter/appearance/sections/BorderSection.qml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Border")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Border rounding")
|
||||
value: rootPane.borderRounding
|
||||
from: 0.1
|
||||
to: 100
|
||||
decimals: 1
|
||||
suffix: "px"
|
||||
validator: DoubleValidator { bottom: 0.1; top: 100 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.borderRounding = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Border thickness")
|
||||
value: rootPane.borderThickness
|
||||
from: 0.1
|
||||
to: 100
|
||||
decimals: 1
|
||||
suffix: "px"
|
||||
validator: DoubleValidator { bottom: 0.1; top: 100 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.borderThickness = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
modules/controlcenter/appearance/sections/ColorSchemeSection.qml
Normal file
147
modules/controlcenter/appearance/sections/ColorSchemeSection.qml
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Color scheme")
|
||||
description: qsTr("Available color schemes")
|
||||
showBackground: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
Repeater {
|
||||
model: Schemes.list
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property string schemeKey: `${modelData.name} ${modelData.flavour}`
|
||||
readonly property bool isCurrent: schemeKey === Schemes.currentScheme
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const name = modelData.name;
|
||||
const flavour = modelData.flavour;
|
||||
const schemeKey = `${name} ${flavour}`;
|
||||
|
||||
Schemes.currentScheme = schemeKey;
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
reloadTimer.restart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reloadTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
Schemes.reload();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: schemeRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
id: preview
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
border.width: 1
|
||||
border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5)
|
||||
|
||||
color: `#${modelData.colours?.surface}`
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: iconPlaceholder.implicitWidth
|
||||
implicitHeight: iconPlaceholder.implicitWidth
|
||||
|
||||
MaterialIcon {
|
||||
id: iconPlaceholder
|
||||
visible: false
|
||||
text: "circle"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: parent.implicitWidth / 2
|
||||
clip: true
|
||||
|
||||
StyledRect {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: preview.implicitWidth
|
||||
color: `#${modelData.colours?.primary}`
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: modelData.flavour ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name ?? ""
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3outline
|
||||
|
||||
elide: Text.ElideRight
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Color variant")
|
||||
description: qsTr("Material theme variant")
|
||||
showBackground: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
Repeater {
|
||||
model: M3Variants.list
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: modelData.variant === Schemes.currentVariant ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const variant = modelData.variant;
|
||||
|
||||
Schemes.currentVariant = variant;
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
reloadTimer.restart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reloadTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
Schemes.reload();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: variantRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: modelData.icon
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.variant === Schemes.currentVariant ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name
|
||||
font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: modelData.variant === Schemes.currentVariant
|
||||
text: "check"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
286
modules/controlcenter/appearance/sections/FontsSection.qml
Normal file
286
modules/controlcenter/appearance/sections/FontsSection.qml
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Fonts")
|
||||
showBackground: true
|
||||
|
||||
CollapsibleSection {
|
||||
id: materialFontSection
|
||||
title: qsTr("Material font family")
|
||||
expanded: true
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
id: materialFontLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
asynchronous: true
|
||||
active: materialFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: materialFontList
|
||||
property alias contentHeight: materialFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: materialFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilyMaterial = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilyMaterialRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: monoFontSection
|
||||
title: qsTr("Monospace font family")
|
||||
expanded: false
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
asynchronous: true
|
||||
active: monoFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: monoFontList
|
||||
property alias contentHeight: monoFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: monoFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilyMono
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilyMono = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilyMonoRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: sansFontSection
|
||||
title: qsTr("Sans-serif font family")
|
||||
expanded: false
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
asynchronous: true
|
||||
active: sansFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: sansFontList
|
||||
property alias contentHeight: sansFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sansFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilySans
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilySans = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilySansRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Font size scale")
|
||||
value: rootPane.fontSizeScale
|
||||
from: 0.7
|
||||
to: 1.5
|
||||
decimals: 2
|
||||
suffix: "×"
|
||||
validator: DoubleValidator { bottom: 0.7; top: 1.5 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.fontSizeScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
84
modules/controlcenter/appearance/sections/ScalesSection.qml
Normal file
84
modules/controlcenter/appearance/sections/ScalesSection.qml
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Scales")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Padding scale")
|
||||
value: rootPane.paddingScale
|
||||
from: 0.5
|
||||
to: 2.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator { bottom: 0.5; top: 2.0 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.paddingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Rounding scale")
|
||||
value: rootPane.roundingScale
|
||||
from: 0.1
|
||||
to: 5.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.roundingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Spacing scale")
|
||||
value: rootPane.spacingScale
|
||||
from: 0.1
|
||||
to: 2.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator { bottom: 0.1; top: 2.0 }
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.spacingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Theme mode")
|
||||
description: qsTr("Light or dark theme")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Dark mode")
|
||||
checked: !Colours.currentLight
|
||||
onToggled: checked => {
|
||||
Colours.setMode(checked ? "dark" : "light");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Transparency")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Transparency enabled")
|
||||
checked: rootPane.transparencyEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.transparencyEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Transparency base")
|
||||
value: rootPane.transparencyBase * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.transparencyBase = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Transparency layers")
|
||||
value: rootPane.transparencyLayers * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
rootPane.transparencyLayers = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
467
modules/controlcenter/audio/AudioPane.qml
Normal file
467
modules/controlcenter/audio/AudioPane.qml
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
StyledFlickable {
|
||||
id: leftAudioFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: leftContent.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftAudioFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: leftContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Audio")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: outputDevicesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Output devices")
|
||||
expanded: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Devices (%1)").arg(Audio.sinks.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available output devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Audio.sinks
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setAudioSink(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: outputRowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: Audio.sink?.id === modelData.id ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.description || qsTr("Unknown")
|
||||
font.weight: Audio.sink?.id === modelData.id ? 500 : 400
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: inputDevicesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Input devices")
|
||||
expanded: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Devices (%1)").arg(Audio.sources.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available input devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Audio.sources
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setAudioSource(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: inputRowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: "mic"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: Audio.source?.id === modelData.id ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.description || qsTr("Unknown")
|
||||
font.weight: Audio.source?.id === modelData.id ? 500 : 400
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
StyledFlickable {
|
||||
id: rightAudioFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: contentLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: rightAudioFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "volume_up"
|
||||
title: qsTr("Audio Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Output volume")
|
||||
description: qsTr("Control the volume of your output device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Volume")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: outputVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
enabled: !Audio.muted
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onVolumeChanged() {
|
||||
if (!outputVolumeInput.hasFocus) {
|
||||
outputVolumeInput.text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: (text) => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setVolume(val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.muted ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (Audio.sink?.audio) {
|
||||
Audio.sink.audio.muted = !Audio.sink.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: muteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Audio.muted ? "volume_off" : "volume_up"
|
||||
color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: outputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.volume
|
||||
enabled: !Audio.muted
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setVolume(value);
|
||||
if (!outputVolumeInput.hasFocus) {
|
||||
outputVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Input volume")
|
||||
description: qsTr("Control the volume of your input device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Volume")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: inputVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
enabled: !Audio.sourceMuted
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onSourceVolumeChanged() {
|
||||
if (!inputVolumeInput.hasFocus) {
|
||||
inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: (text) => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setSourceVolume(val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.sourceMuted ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (Audio.source?.audio) {
|
||||
Audio.source.audio.muted = !Audio.source.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: muteInputIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "mic_off"
|
||||
color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: inputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.sourceVolume
|
||||
enabled: !Audio.sourceMuted
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setSourceVolume(value);
|
||||
if (!inputVolumeInput.hasFocus) {
|
||||
inputVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +1,73 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components.effects
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: Math.floor(parent.width * 0.4)
|
||||
Layout.minimumWidth: 420
|
||||
Layout.fillHeight: true
|
||||
|
||||
DeviceList {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
|
||||
|
||||
session: root.session
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
activeItem: session.bt.active
|
||||
paneIdGenerator: function(item) {
|
||||
return item ? (item.address || "") : "";
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
leftContent: Component {
|
||||
StyledFlickable {
|
||||
id: leftFlickable
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: deviceList.height
|
||||
|
||||
radius: rightBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
property BluetoothDevice pane: root.session.bt.active
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: pane ? details : settings
|
||||
|
||||
Behavior on pane {
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.8
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
}
|
||||
PropertyAction {}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftFlickable
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: rightBorder
|
||||
DeviceList {
|
||||
id: deviceList
|
||||
|
||||
leftThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settings
|
||||
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: details
|
||||
|
||||
Details {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
target: loader
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
rightDetailsComponent: Component {
|
||||
Details {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
|
|
@ -12,409 +13,430 @@ import Quickshell.Bluetooth
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
StyledFlickable {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property BluetoothDevice device: session.bt.active
|
||||
|
||||
StyledFlickable {
|
||||
anchors.fill: parent
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: detailsWrapper.height
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: layout.height
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
Item {
|
||||
id: detailsWrapper
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: details.implicitHeight
|
||||
|
||||
DeviceDetails {
|
||||
id: details
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
anchors.top: parent.top
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
text: Icons.getBluetoothIcon(root.device.icon)
|
||||
font.pointSize: Appearance.font.size.extraLarge * 3
|
||||
font.bold: true
|
||||
}
|
||||
session: root.session
|
||||
device: root.device
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
text: root.device?.name ?? ""
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Connection status")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Connection settings for this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceStatus
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Connected")
|
||||
checked: root.device?.connected ?? false
|
||||
toggle.onToggled: root.device.connected = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Paired")
|
||||
checked: root.device?.paired ?? false
|
||||
toggle.onToggled: {
|
||||
if (root.device.paired)
|
||||
root.device.forget();
|
||||
else
|
||||
root.device.pair();
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Blocked")
|
||||
checked: root.device?.blocked ?? false
|
||||
toggle.onToggled: root.device.blocked = checked
|
||||
}
|
||||
headerComponent: Component {
|
||||
SettingsHeader {
|
||||
icon: Icons.getBluetoothIcon(root.device?.icon ?? "")
|
||||
title: root.device?.name ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device properties")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Additional settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceProps
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: renameDevice
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.spacing.small
|
||||
|
||||
implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "editingDeviceName"
|
||||
when: root.session.bt.editingDeviceName
|
||||
|
||||
AnchorChanges {
|
||||
target: deviceNameEdit
|
||||
anchors.top: renameDevice.top
|
||||
}
|
||||
PropertyChanges {
|
||||
renameDevice.implicitHeight: deviceNameEdit.implicitHeight
|
||||
renameLabel.opacity: 0
|
||||
deviceNameEdit.padding: Appearance.padding.normal
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
Anim {
|
||||
properties: "implicitHeight,opacity,padding"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: renameLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Device name")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: deviceNameEdit
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.device?.name ?? ""
|
||||
readOnly: !root.session.bt.editingDeviceName
|
||||
onAccepted: {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
root.device.name = text;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
rightPadding: Appearance.padding.normal
|
||||
|
||||
background: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 2
|
||||
border.color: Colours.palette.m3primary
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Connection status")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
scale: root.session.bt.editingDeviceName ? 1 : 0.5
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
disabled: !root.session.bt.editingDeviceName
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
deviceNameEdit.text = Qt.binding(() => root.device?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: cancelEditIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "cancel"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;
|
||||
if (root.session.bt.editingDeviceName)
|
||||
deviceNameEdit.forceActiveFocus();
|
||||
else
|
||||
deviceNameEdit.accepted();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: editIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.editingDeviceName ? "check_circle" : "edit"
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Trusted")
|
||||
checked: root.device?.trusted ?? false
|
||||
toggle.onToggled: root.device.trusted = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Wake allowed")
|
||||
checked: root.device?.wakeAllowed ?? false
|
||||
toggle.onToggled: root.device.wakeAllowed = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device information")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Information about this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledRect {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
StyledText {
|
||||
text: qsTr("Connection settings for this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
StyledRect {
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceStatus
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: parent.height * 0.25
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
implicitWidth: height
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Connected")
|
||||
checked: root.device?.connected ?? false
|
||||
toggle.onToggled: root.device.connected = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Paired")
|
||||
checked: root.device?.paired ?? false
|
||||
toggle.onToggled: {
|
||||
if (root.device.paired)
|
||||
root.device.forget();
|
||||
else
|
||||
root.device.pair();
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Blocked")
|
||||
checked: root.device?.blocked ?? false
|
||||
toggle.onToggled: root.device.blocked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Dbus path")
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device properties")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Additional settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceProps
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: renameDevice
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.spacing.small
|
||||
|
||||
implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "editingDeviceName"
|
||||
when: root.session.bt.editingDeviceName
|
||||
|
||||
AnchorChanges {
|
||||
target: deviceNameEdit
|
||||
anchors.top: renameDevice.top
|
||||
}
|
||||
PropertyChanges {
|
||||
renameDevice.implicitHeight: deviceNameEdit.implicitHeight
|
||||
renameLabel.opacity: 0
|
||||
deviceNameEdit.padding: Appearance.padding.normal
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
Anim {
|
||||
properties: "implicitHeight,opacity,padding"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: renameLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Device name")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: deviceNameEdit
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.device?.name ?? ""
|
||||
readOnly: !root.session.bt.editingDeviceName
|
||||
onAccepted: {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
root.device.name = text;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
rightPadding: Appearance.padding.normal
|
||||
|
||||
background: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 2
|
||||
border.color: Colours.palette.m3primary
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
scale: root.session.bt.editingDeviceName ? 1 : 0.5
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
disabled: !root.session.bt.editingDeviceName
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
deviceNameEdit.text = Qt.binding(() => root.device?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: cancelEditIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "cancel"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;
|
||||
if (root.session.bt.editingDeviceName)
|
||||
deviceNameEdit.forceActiveFocus();
|
||||
else
|
||||
deviceNameEdit.accepted();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: editIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.editingDeviceName ? "check_circle" : "edit"
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Trusted")
|
||||
checked: root.device?.trusted ?? false
|
||||
toggle.onToggled: root.device.trusted = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Wake allowed")
|
||||
checked: root.device?.wakeAllowed ?? false
|
||||
toggle.onToggled: root.device.wakeAllowed = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: root.device?.dbusPath ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device information")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("MAC address")
|
||||
}
|
||||
StyledText {
|
||||
text: qsTr("Information about this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.address ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Bonded")
|
||||
}
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
StyledText {
|
||||
text: root.device?.bonded ? qsTr("Yes") : qsTr("No")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
ColumnLayout {
|
||||
id: deviceInfo
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("System name")
|
||||
}
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
StyledText {
|
||||
text: root.device?.deviceName ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledRect {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
|
||||
StyledRect {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: parent.height * 0.25
|
||||
|
||||
implicitWidth: height
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Dbus path")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.dbusPath ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("MAC address")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.address ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Bonded")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.bonded ? qsTr("Yes") : qsTr("No")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("System name")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.deviceName ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -562,11 +584,11 @@ Item {
|
|||
Item {
|
||||
id: fabRoot
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitWidth: 64
|
||||
implicitHeight: 64
|
||||
x: root.contentX + root.width - width
|
||||
y: root.contentY + root.height - height
|
||||
width: 64
|
||||
height: 64
|
||||
z: 10000
|
||||
|
||||
StyledRect {
|
||||
id: fabBg
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
|
|
@ -12,172 +13,142 @@ import Quickshell.Bluetooth
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property bool smallDiscoverable: width <= 540
|
||||
readonly property bool smallPairable: width <= 480
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length)
|
||||
description: qsTr("All available bluetooth devices")
|
||||
activeItem: session.bt.active
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
model: ScriptModel {
|
||||
id: deviceModel
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
icon: "power"
|
||||
accent: "Tertiary"
|
||||
|
||||
function onClicked(): void {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = !adapter.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discoverable ?? false
|
||||
icon: root.smallDiscoverable ? "group_search" : ""
|
||||
label: root.smallDiscoverable ? "" : qsTr("Discoverable")
|
||||
|
||||
function onClicked(): void {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discoverable = !adapter.discoverable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.pairable ?? false
|
||||
icon: "missing_controller"
|
||||
label: root.smallPairable ? "" : qsTr("Pairable")
|
||||
|
||||
function onClicked(): void {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.pairable = !adapter.pairable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.bt.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
|
||||
function onClicked(): void {
|
||||
if (root.session.bt.active)
|
||||
root.session.bt.active = null;
|
||||
else {
|
||||
root.session.bt.active = deviceModel.values[0] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired))
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length)
|
||||
text: qsTr("Bluetooth")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available bluetooth devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
icon: "power"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Toggle Bluetooth")
|
||||
|
||||
radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = !adapter.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discoverable ?? false
|
||||
icon: root.smallDiscoverable ? "group_search" : ""
|
||||
label: root.smallDiscoverable ? "" : qsTr("Discoverable")
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Make discoverable")
|
||||
|
||||
function onClicked(): void {
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discoverable = !adapter.discoverable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.pairable ?? false
|
||||
icon: "missing_controller"
|
||||
label: root.smallPairable ? "" : qsTr("Pairable")
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Make pairable")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.pairable = !adapter.pairable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discovering ?? false
|
||||
icon: "bluetooth_searching"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Scan for devices")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discovering = !adapter.discovering;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: scanIcon
|
||||
ToggleButton {
|
||||
toggled: !root.session.bt.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Bluetooth settings")
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "bluetooth_searching"
|
||||
color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
onClicked: {
|
||||
if (root.session.bt.active)
|
||||
root.session.bt.active = null;
|
||||
else {
|
||||
root.session.bt.active = root.model.values[0] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledListView {
|
||||
id: view
|
||||
|
||||
model: ScriptModel {
|
||||
id: deviceModel
|
||||
|
||||
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired))
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: view
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
id: device
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
|
||||
readonly property bool connected: modelData.state === BluetoothDeviceState.Connected
|
||||
readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting)
|
||||
readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.bt.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.active = device.modelData;
|
||||
if (device.modelData)
|
||||
root.session.bt.active = device.modelData;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,20 +165,20 @@ ColumnLayout {
|
|||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: device.connected ? Colours.palette.m3primaryContainer : device.modelData.bonded ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getBluetoothIcon(device.modelData.icon)
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "")
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: device.connected ? 1 : 0
|
||||
|
||||
|
|
@ -224,13 +195,13 @@ ColumnLayout {
|
|||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: device.modelData.name
|
||||
text: device.modelData ? device.modelData.name : qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: device.modelData.address + (device.connected ? qsTr(" (Connected)") : device.modelData.bonded ? qsTr(" (Paired)") : "")
|
||||
text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
elide: Text.ElideRight
|
||||
|
|
@ -256,7 +227,18 @@ ColumnLayout {
|
|||
disabled: device.loading
|
||||
|
||||
function onClicked(): void {
|
||||
device.modelData.connected = !device.modelData.connected;
|
||||
if (device.loading)
|
||||
return;
|
||||
|
||||
if (device.connected) {
|
||||
device.modelData.connected = false;
|
||||
} else {
|
||||
if (device.modelData.bonded) {
|
||||
device.modelData.connected = true;
|
||||
} else {
|
||||
device.modelData.pair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +247,7 @@ ColumnLayout {
|
|||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: device.modelData.connected ? "link_off" : "link"
|
||||
text: device.connected ? "link_off" : "link"
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
opacity: device.loading ? 0 : 1
|
||||
|
|
@ -279,78 +261,7 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
component ToggleButton: StyledRect {
|
||||
id: toggleBtn
|
||||
|
||||
required property bool toggled
|
||||
property string icon
|
||||
property string label
|
||||
property string accent: "Secondary"
|
||||
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0)
|
||||
implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2
|
||||
implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`]
|
||||
|
||||
StateLayer {
|
||||
id: toggleStateLayer
|
||||
|
||||
color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`]
|
||||
|
||||
function onClicked(): void {
|
||||
toggleBtn.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: toggleBtnInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
id: toggleBtnIcon
|
||||
|
||||
visible: !!text
|
||||
fill: toggleBtn.toggled ? 1 : 0
|
||||
text: toggleBtn.icon
|
||||
color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`]
|
||||
font.pointSize: Appearance.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
asynchronous: true
|
||||
active: !!toggleBtn.label
|
||||
visible: active
|
||||
|
||||
sourceComponent: StyledText {
|
||||
text: toggleBtn.label
|
||||
color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
onItemSelected: function(item) {
|
||||
session.bt.active = item;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
|
|
@ -17,18 +18,9 @@ ColumnLayout {
|
|||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "bluetooth"
|
||||
font.pointSize: Appearance.font.size.extraLarge * 3
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Bluetooth settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
SettingsHeader {
|
||||
icon: "bluetooth"
|
||||
title: qsTr("Bluetooth Settings")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
|
@ -284,8 +276,12 @@ ColumnLayout {
|
|||
|
||||
CustomSpinBox {
|
||||
min: 0
|
||||
value: root.session.bt.currentAdapter.discoverableTimeout
|
||||
onValueModified: value => root.session.bt.currentAdapter.discoverableTimeout = value
|
||||
value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0
|
||||
onValueModified: value => {
|
||||
if (root.session.bt.currentAdapter) {
|
||||
root.session.bt.currentAdapter.discoverableTimeout = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -332,7 +328,7 @@ ColumnLayout {
|
|||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Rename adapter (currently does not work)") // FIXME: remove disclaimer when fixed
|
||||
text: qsTr("Rename adapter (currently does not work)")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
|
@ -345,12 +341,10 @@ ColumnLayout {
|
|||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.session.bt.currentAdapter.name
|
||||
text: root.session.bt.currentAdapter?.name ?? ""
|
||||
readOnly: !root.session.bt.editingAdapterName
|
||||
onAccepted: {
|
||||
root.session.bt.editingAdapterName = false;
|
||||
// Doesn't work for now, will be added to QS later
|
||||
// root.session.bt.currentAdapter.name = text;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
|
|
@ -392,7 +386,7 @@ ColumnLayout {
|
|||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingAdapterName = false;
|
||||
adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter.name);
|
||||
adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
71
modules/controlcenter/components/DeviceDetails.qml
Normal file
71
modules/controlcenter/components/DeviceDetails.qml
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Session session
|
||||
property var device: null
|
||||
|
||||
property Component headerComponent: null
|
||||
property list<Component> sections: []
|
||||
|
||||
property Component topContent: null
|
||||
property Component bottomContent: null
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Loader {
|
||||
id: headerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.headerComponent
|
||||
visible: root.headerComponent !== null
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topContentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.topContent
|
||||
visible: root.topContent !== null
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.sections
|
||||
|
||||
Loader {
|
||||
required property Component modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: modelData
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: bottomContentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.bottomContent
|
||||
visible: root.bottomContent !== null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
modules/controlcenter/components/DeviceList.qml
Normal file
85
modules/controlcenter/components/DeviceList.qml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property Session session: null
|
||||
property var model: null
|
||||
property Component delegate: null
|
||||
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
property var activeItem: null
|
||||
property Component headerComponent: null
|
||||
property Component titleSuffix: null
|
||||
property bool showHeader: true
|
||||
|
||||
signal itemSelected(var item)
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
id: headerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.headerComponent
|
||||
visible: root.headerComponent !== null && root.showHeader
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.headerComponent ? 0 : 0
|
||||
spacing: Appearance.spacing.small
|
||||
visible: root.title !== "" || root.description !== ""
|
||||
|
||||
StyledText {
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: root.titleSuffix
|
||||
visible: root.titleSuffix !== null
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
property alias view: view
|
||||
|
||||
StyledText {
|
||||
visible: root.description !== ""
|
||||
Layout.fillWidth: true
|
||||
text: root.description
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledListView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentHeight
|
||||
|
||||
model: root.model
|
||||
delegate: root.delegate
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
clip: false
|
||||
}
|
||||
}
|
||||
|
||||
72
modules/controlcenter/components/PaneTransition.qml
Normal file
72
modules/controlcenter/components/PaneTransition.qml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
SequentialAnimation {
|
||||
id: root
|
||||
|
||||
required property Item target
|
||||
property list<PropertyAction> propertyActions
|
||||
|
||||
property real scaleFrom: 1.0
|
||||
property real scaleTo: 0.8
|
||||
property real opacityFrom: 1.0
|
||||
property real opacityTo: 0.0
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
from: root.opacityFrom
|
||||
to: root.opacityTo
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "scale"
|
||||
from: root.scaleFrom
|
||||
to: root.scaleTo
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
for (let i = 0; i < root.propertyActions.length; i++) {
|
||||
const action = root.propertyActions[i];
|
||||
if (action.target && action.property !== undefined) {
|
||||
action.target[action.property] = action.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
from: root.opacityTo
|
||||
to: root.opacityFrom
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "scale"
|
||||
from: root.scaleTo
|
||||
to: root.scaleFrom
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
modules/controlcenter/components/SettingsHeader.qml
Normal file
38
modules/controlcenter/components/SettingsHeader.qml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string icon
|
||||
required property string title
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: column.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.icon
|
||||
font.pointSize: Appearance.font.size.extraLarge * 3
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
181
modules/controlcenter/components/SliderInput.qml
Normal file
181
modules/controlcenter/components/SliderInput.qml
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property real from: 0
|
||||
property real to: 100
|
||||
property real stepSize: 0
|
||||
property var validator: null
|
||||
property string suffix: "" // Optional suffix text (e.g., "×", "px")
|
||||
property int decimals: 1 // Number of decimal places to show (default: 1)
|
||||
property var formatValueFunction: null // Optional custom format function
|
||||
property var parseValueFunction: null // Optional custom parse function
|
||||
|
||||
function formatValue(val: real): string {
|
||||
if (formatValueFunction) {
|
||||
return formatValueFunction(val);
|
||||
}
|
||||
// Default format function
|
||||
// Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property)
|
||||
if (validator && validator.bottom !== undefined && validator.decimals === undefined) {
|
||||
return Math.round(val).toString();
|
||||
}
|
||||
// For DoubleValidator or no validator, use the decimals property
|
||||
return val.toFixed(root.decimals);
|
||||
}
|
||||
|
||||
function parseValue(text: string): real {
|
||||
if (parseValueFunction) {
|
||||
return parseValueFunction(text);
|
||||
}
|
||||
// Default parse function
|
||||
if (validator && validator.bottom !== undefined) {
|
||||
// Check if it's an integer validator
|
||||
if (validator.top !== undefined && validator.top === Math.floor(validator.top)) {
|
||||
return parseInt(text);
|
||||
}
|
||||
}
|
||||
return parseFloat(text);
|
||||
}
|
||||
|
||||
signal valueModified(real newValue)
|
||||
|
||||
property bool _initialized: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Component.onCompleted: {
|
||||
// Set initialized flag after a brief delay to allow component to fully load
|
||||
Qt.callLater(() => {
|
||||
_initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: inputField
|
||||
Layout.preferredWidth: 70
|
||||
validator: root.validator
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize text without triggering valueModified signal
|
||||
text = root.formatValue(root.value);
|
||||
}
|
||||
|
||||
onTextEdited: (text) => {
|
||||
if (hasFocus) {
|
||||
const val = root.parseValue(text);
|
||||
if (!isNaN(val)) {
|
||||
// Validate against validator bounds if available
|
||||
let isValid = true;
|
||||
if (root.validator) {
|
||||
if (root.validator.bottom !== undefined && val < root.validator.bottom) {
|
||||
isValid = false;
|
||||
}
|
||||
if (root.validator.top !== undefined && val > root.validator.top) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
root.valueModified(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = root.parseValue(text);
|
||||
let isValid = true;
|
||||
if (root.validator) {
|
||||
if (root.validator.bottom !== undefined && val < root.validator.bottom) {
|
||||
isValid = false;
|
||||
}
|
||||
if (root.validator.top !== undefined && val > root.validator.top) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(val) || !isValid) {
|
||||
text = root.formatValue(root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.suffix !== ""
|
||||
text: root.suffix
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: slider
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
from: root.from
|
||||
to: root.to
|
||||
stepSize: root.stepSize
|
||||
|
||||
// Use Binding to allow slider to move freely during dragging
|
||||
Binding {
|
||||
target: slider
|
||||
property: "value"
|
||||
value: root.value
|
||||
when: !slider.pressed
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
// Update input field text in real-time as slider moves during dragging
|
||||
// Always update when slider value changes (during dragging or external updates)
|
||||
if (!inputField.hasFocus) {
|
||||
const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
|
||||
inputField.text = root.formatValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
onMoved: {
|
||||
const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
|
||||
root.valueModified(newValue);
|
||||
if (!inputField.hasFocus) {
|
||||
inputField.text = root.formatValue(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update input field when value changes externally (slider is already bound)
|
||||
onValueChanged: {
|
||||
// Only update if component is initialized to avoid issues during creation
|
||||
if (root._initialized && !inputField.hasFocus) {
|
||||
inputField.text = root.formatValue(root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
modules/controlcenter/components/SplitPaneLayout.qml
Normal file
112
modules/controlcenter/components/SplitPaneLayout.qml
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: 0
|
||||
|
||||
property Component leftContent: null
|
||||
property Component rightContent: null
|
||||
|
||||
property real leftWidthRatio: 0.4
|
||||
property int leftMinimumWidth: 420
|
||||
property var leftLoaderProperties: ({})
|
||||
property var rightLoaderProperties: ({})
|
||||
|
||||
property alias leftLoader: leftLoader
|
||||
property alias rightLoader: rightLoader
|
||||
|
||||
Item {
|
||||
id: leftPane
|
||||
|
||||
Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio)
|
||||
Layout.minimumWidth: root.leftMinimumWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
ClippingRectangle {
|
||||
id: leftClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
|
||||
radius: leftBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: leftLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: root.leftContent
|
||||
|
||||
Component.onCompleted: {
|
||||
for (const key in root.leftLoaderProperties) {
|
||||
leftLoader[key] = root.leftLoaderProperties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: leftBorder
|
||||
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightPane
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ClippingRectangle {
|
||||
id: rightClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
|
||||
radius: rightBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: root.rightContent
|
||||
|
||||
Component.onCompleted: {
|
||||
for (const key in root.rightLoaderProperties) {
|
||||
rightLoader[key] = root.rightLoaderProperties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: rightBorder
|
||||
|
||||
leftThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
modules/controlcenter/components/SplitPaneWithDetails.qml
Normal file
93
modules/controlcenter/components/SplitPaneWithDetails.qml
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Component leftContent
|
||||
required property Component rightDetailsComponent
|
||||
required property Component rightSettingsComponent
|
||||
|
||||
property var activeItem: null
|
||||
property var paneIdGenerator: function(item) { return item ? String(item) : ""; }
|
||||
|
||||
property Component overlayComponent: null
|
||||
|
||||
SplitPaneLayout {
|
||||
id: splitLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: root.leftContent
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var pane: root.activeItem
|
||||
property string paneId: root.paneIdGenerator(pane)
|
||||
property Component targetComponent: root.rightSettingsComponent
|
||||
property Component nextComponent: root.rightSettingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
return pane ? root.rightDetailsComponent : root.rightSettingsComponent;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
|
||||
clip: false
|
||||
asynchronous: true
|
||||
sourceComponent: rightPaneItem.targetComponent
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightPaneItem
|
||||
property: "targetComponent"
|
||||
value: rightPaneItem.nextComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onPaneChanged: {
|
||||
nextComponent = getComponentForPane();
|
||||
paneId = root.paneIdGenerator(pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overlayLoader
|
||||
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
sourceComponent: root.overlayComponent
|
||||
active: root.overlayComponent !== null
|
||||
}
|
||||
}
|
||||
|
||||
241
modules/controlcenter/components/WallpaperGrid.qml
Normal file
241
modules/controlcenter/components/WallpaperGrid.qml
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Caelestia.Models
|
||||
import QtQuick
|
||||
|
||||
GridView {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property int minCellWidth: 200 + Appearance.spacing.normal
|
||||
readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth))
|
||||
|
||||
cellWidth: width / columnsCount
|
||||
cellHeight: 140 + Appearance.spacing.normal
|
||||
|
||||
model: Wallpapers.list
|
||||
|
||||
clip: true
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: root.cellWidth
|
||||
height: root.cellHeight
|
||||
|
||||
readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent
|
||||
readonly property real itemMargin: Appearance.spacing.normal / 2
|
||||
readonly property real itemRadius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
radius: itemRadius
|
||||
|
||||
function onClicked(): void {
|
||||
Wallpapers.setWallpaper(modelData.path);
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
radius: itemRadius
|
||||
antialiasing: true
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
CachingImage {
|
||||
id: cachingImage
|
||||
|
||||
path: modelData.path
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
sourceSize: Qt.size(width, height)
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if CachingImage fails to load
|
||||
Image {
|
||||
id: fallbackImage
|
||||
|
||||
anchors.fill: parent
|
||||
source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
sourceSize: Qt.size(width, height)
|
||||
|
||||
opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fallbackTimer
|
||||
|
||||
property bool triggered: false
|
||||
interval: 800
|
||||
running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
|
||||
onTriggered: triggered = true
|
||||
}
|
||||
|
||||
// Gradient overlay for filename
|
||||
Rectangle {
|
||||
id: filenameOverlay
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
|
||||
radius: 0
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Qt.rgba(Colours.palette.m3surface.r,
|
||||
Colours.palette.m3surface.g,
|
||||
Colours.palette.m3surface.b, 0)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Qt.rgba(Colours.palette.m3surface.r,
|
||||
Colours.palette.m3surface.g,
|
||||
Colours.palette.m3surface.b, 0.7)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.6
|
||||
color: Qt.rgba(Colours.palette.m3surface.r,
|
||||
Colours.palette.m3surface.g,
|
||||
Colours.palette.m3surface.b, 0.9)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.rgba(Colours.palette.m3surface.r,
|
||||
Colours.palette.m3surface.g,
|
||||
Colours.palette.m3surface.b, 0.95)
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
color: "transparent"
|
||||
radius: itemRadius + border.width
|
||||
border.width: isCurrent ? 2 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Appearance.padding.small
|
||||
|
||||
visible: isCurrent
|
||||
text: "check_circle"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: filenameText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
|
||||
anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
|
||||
text: modelData.name
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.weight: 500
|
||||
color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
599
modules/controlcenter/launcher/LauncherPane.qml
Normal file
599
modules/controlcenter/launcher/LauncherPane.qml
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../../utils/scripts/fuzzysort.js" as Fuzzy
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property var selectedApp: root.session.launcher.active
|
||||
property bool hideFromLauncherChecked: false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onSelectedAppChanged: {
|
||||
root.session.launcher.active = root.selectedApp;
|
||||
updateToggleState();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.launcher
|
||||
function onActiveChanged() {
|
||||
root.selectedApp = root.session.launcher.active;
|
||||
updateToggleState();
|
||||
}
|
||||
}
|
||||
|
||||
function updateToggleState() {
|
||||
if (!root.selectedApp) {
|
||||
root.hideFromLauncherChecked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
||||
|
||||
if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) {
|
||||
root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId);
|
||||
} else {
|
||||
root.hideFromLauncherChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveHiddenApps(isHidden) {
|
||||
if (!root.selectedApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
||||
|
||||
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
||||
|
||||
if (isHidden) {
|
||||
if (!hiddenApps.includes(appId)) {
|
||||
hiddenApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = hiddenApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
hiddenApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Config.launcher.hiddenApps = hiddenApps;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
|
||||
AppDb {
|
||||
id: allAppsDb
|
||||
|
||||
path: `${Paths.state}/apps.sqlite`
|
||||
entries: DesktopEntries.applications.values
|
||||
}
|
||||
|
||||
property string searchText: ""
|
||||
|
||||
function filterApps(search: string): list<var> {
|
||||
if (!search || search.trim() === "") {
|
||||
const apps = [];
|
||||
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
||||
apps.push(allAppsDb.apps[i]);
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
if (!allAppsDb.apps || allAppsDb.apps.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preparedApps = [];
|
||||
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
||||
const app = allAppsDb.apps[i];
|
||||
const name = app.name || app.entry?.name || "";
|
||||
preparedApps.push({
|
||||
_item: app,
|
||||
name: Fuzzy.prepare(name)
|
||||
});
|
||||
}
|
||||
|
||||
const results = Fuzzy.go(search, preparedApps, {
|
||||
all: true,
|
||||
keys: ["name"],
|
||||
scoreFn: r => r[0].score
|
||||
});
|
||||
|
||||
return results
|
||||
.sort((a, b) => b._score - a._score)
|
||||
.map(r => r.obj._item);
|
||||
}
|
||||
|
||||
property list<var> filteredApps: []
|
||||
|
||||
function updateFilteredApps() {
|
||||
filteredApps = filterApps(searchText);
|
||||
}
|
||||
|
||||
onSearchTextChanged: {
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: allAppsDb
|
||||
function onAppsChanged() {
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
ColumnLayout {
|
||||
id: leftLauncherLayout
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Launcher")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.launcher.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Launcher settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.launcher.active) {
|
||||
root.session.launcher.active = null;
|
||||
} else {
|
||||
if (root.filteredApps.length > 0) {
|
||||
root.session.launcher.active = root.filteredApps[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("All applications available in the launcher")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.bottomMargin: Appearance.spacing.small
|
||||
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight)
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
|
||||
text: "search"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: searchField
|
||||
|
||||
anchors.left: searchIcon.right
|
||||
anchors.right: clearIcon.left
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
|
||||
topPadding: Appearance.padding.normal
|
||||
bottomPadding: Appearance.padding.normal
|
||||
|
||||
placeholderText: qsTr("Search applications...")
|
||||
|
||||
onTextChanged: {
|
||||
root.searchText = text;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: clearIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
width: searchField.text ? implicitWidth : implicitWidth / 2
|
||||
opacity: {
|
||||
if (!searchField.text)
|
||||
return 0;
|
||||
if (clearMouse.pressed)
|
||||
return 0.7;
|
||||
if (clearMouse.containsMouse)
|
||||
return 0.8;
|
||||
return 1;
|
||||
}
|
||||
|
||||
text: "close"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: searchField.text ? Qt.PointingHandCursor : undefined
|
||||
|
||||
onClicked: searchField.text = ""
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appsListLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
asynchronous: true
|
||||
active: true
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: appsListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: root.filteredApps
|
||||
spacing: Appearance.spacing.small / 2
|
||||
clip: true
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: parent
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
|
||||
readonly property bool isSelected: root.selectedApp === modelData
|
||||
|
||||
color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.launcher.active = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitSize: 32
|
||||
source: {
|
||||
const entry = modelData.entry;
|
||||
return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name || modelData.entry?.name || qsTr("Unknown")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: 40
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightLauncherPane
|
||||
|
||||
property var pane: root.session.launcher.active
|
||||
property string paneId: pane ? (pane.id || pane.entry?.id || "") : ""
|
||||
property Component targetComponent: settings
|
||||
property Component nextComponent: settings
|
||||
property var displayedApp: null
|
||||
|
||||
function getComponentForPane() {
|
||||
return pane ? appDetails : settings;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
displayedApp = pane;
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLauncherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: rightLauncherPane.targetComponent
|
||||
active: true
|
||||
|
||||
property var displayedApp: rightLauncherPane.displayedApp
|
||||
|
||||
onItemChanged: {
|
||||
if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) {
|
||||
rightLauncherPane.displayedApp = rightLauncherPane.pane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLauncherLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightLauncherPane
|
||||
property: "displayedApp"
|
||||
value: rightLauncherPane.pane
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherLoader
|
||||
property: "active"
|
||||
value: false
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherPane
|
||||
property: "targetComponent"
|
||||
value: rightLauncherPane.nextComponent
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherLoader
|
||||
property: "active"
|
||||
value: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onPaneChanged: {
|
||||
nextComponent = getComponentForPane();
|
||||
paneId = pane ? (pane.id || pane.entry?.id || "") : "";
|
||||
}
|
||||
|
||||
onDisplayedAppChanged: {
|
||||
if (displayedApp) {
|
||||
const appId = displayedApp.id || displayedApp.entry?.id;
|
||||
if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) {
|
||||
root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId);
|
||||
} else {
|
||||
root.hideFromLauncherChecked = false;
|
||||
}
|
||||
} else {
|
||||
root.hideFromLauncherChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settings
|
||||
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appDetails
|
||||
|
||||
ColumnLayout {
|
||||
id: appDetailsLayout
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
visible: displayedApp === null
|
||||
icon: "apps"
|
||||
title: qsTr("Launcher Applications")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
visible: displayedApp !== null
|
||||
implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth)
|
||||
implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
id: appIconImage
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitSize: Appearance.font.size.extraLarge * 3 * 2
|
||||
source: {
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (!app) return "image-missing";
|
||||
const entry = app.entry;
|
||||
if (entry && entry.icon) {
|
||||
return Quickshell.iconPath(entry.icon, "image-missing");
|
||||
}
|
||||
return "image-missing";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: appTitleText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : ""
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
|
||||
StyledFlickable {
|
||||
id: detailsFlickable
|
||||
anchors.fill: parent
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: debugLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: parent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: debugLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SwitchRow {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: appDetailsLayout.displayedApp !== null
|
||||
label: qsTr("Hide from launcher")
|
||||
checked: root.hideFromLauncherChecked
|
||||
enabled: appDetailsLayout.displayedApp !== null
|
||||
onToggled: checked => {
|
||||
root.hideFromLauncherChecked = checked;
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (app) {
|
||||
const appId = app.id || app.entry?.id;
|
||||
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
||||
if (checked) {
|
||||
if (!hiddenApps.includes(appId)) {
|
||||
hiddenApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = hiddenApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
hiddenApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
Config.launcher.hiddenApps = hiddenApps;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
218
modules/controlcenter/launcher/Settings.qml
Normal file
218
modules/controlcenter/launcher/Settings.qml
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "apps"
|
||||
title: qsTr("Launcher Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("General")
|
||||
description: qsTr("General launcher settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: Config.launcher.enabled
|
||||
toggle.onToggled: {
|
||||
Config.launcher.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: Config.launcher.showOnHover
|
||||
toggle.onToggled: {
|
||||
Config.launcher.showOnHover = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Vim keybinds")
|
||||
checked: Config.launcher.vimKeybinds
|
||||
toggle.onToggled: {
|
||||
Config.launcher.vimKeybinds = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Enable dangerous actions")
|
||||
checked: Config.launcher.enableDangerousActions
|
||||
toggle.onToggled: {
|
||||
Config.launcher.enableDangerousActions = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Display")
|
||||
description: qsTr("Display and appearance settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Max shown items")
|
||||
value: qsTr("%1").arg(Config.launcher.maxShown)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Max wallpapers")
|
||||
value: qsTr("%1").arg(Config.launcher.maxWallpapers)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Drag threshold")
|
||||
value: qsTr("%1 px").arg(Config.launcher.dragThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Prefixes")
|
||||
description: qsTr("Command prefix settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Special prefix")
|
||||
value: Config.launcher.specialPrefix || qsTr("None")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Action prefix")
|
||||
value: Config.launcher.actionPrefix || qsTr("None")
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Fuzzy search")
|
||||
description: qsTr("Fuzzy search settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Apps")
|
||||
checked: Config.launcher.useFuzzy.apps
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.apps = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Actions")
|
||||
checked: Config.launcher.useFuzzy.actions
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.actions = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Schemes")
|
||||
checked: Config.launcher.useFuzzy.schemes
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.schemes = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Variants")
|
||||
checked: Config.launcher.useFuzzy.variants
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.variants = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Wallpapers")
|
||||
checked: Config.launcher.useFuzzy.wallpapers
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.wallpapers = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Sizes")
|
||||
description: qsTr("Size settings for launcher items")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Item width")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Item height")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Wallpaper width")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Wallpaper height")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Hidden apps")
|
||||
description: qsTr("Applications hidden from launcher")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Total hidden")
|
||||
value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
modules/controlcenter/network/EthernetDetails.qml
Normal file
118
modules/controlcenter/network/EthernetDetails.qml
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var ethernetDevice: root.session.ethernet.active
|
||||
|
||||
device: ethernetDevice
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ethernetDevice && ethernetDevice.interface) {
|
||||
Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
|
||||
}
|
||||
}
|
||||
|
||||
onEthernetDeviceChanged: {
|
||||
if (ethernetDevice && ethernetDevice.interface) {
|
||||
Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
|
||||
} else {
|
||||
Nmcli.ethernetDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: "cable"
|
||||
title: root.ethernetDevice?.interface ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("Connection settings for this device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Connected")
|
||||
checked: root.ethernetDevice?.connected ?? false
|
||||
toggle.onToggled: {
|
||||
if (checked) {
|
||||
Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {});
|
||||
} else {
|
||||
if (root.ethernetDevice?.connection) {
|
||||
Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Device properties")
|
||||
description: qsTr("Additional information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Interface")
|
||||
value: root.ethernetDevice?.interface ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Connection")
|
||||
value: root.ethernetDevice?.connection || qsTr("Not connected")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("State")
|
||||
value: root.ethernetDevice?.state ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection information")
|
||||
description: qsTr("Network connection details")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ConnectionInfoSection {
|
||||
deviceDetails: Nmcli.ethernetDeviceDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
177
modules/controlcenter/network/EthernetList.qml
Normal file
177
modules/controlcenter/network/EthernetList.qml
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length)
|
||||
description: qsTr("All available ethernet devices")
|
||||
activeItem: session.ethernet.active
|
||||
|
||||
model: Nmcli.ethernetDevices
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.ethernet.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
if (root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
else {
|
||||
root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
id: ethernetItem
|
||||
|
||||
required property var modelData
|
||||
readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.ethernet.active = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "cable"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.connected ? 1 : 0
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.interface || qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected")
|
||||
color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.connected ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
if (modelData.connected && modelData.connection) {
|
||||
Nmcli.disconnectEthernet(modelData.connection, () => {});
|
||||
} else {
|
||||
Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: modelData.connected ? "link_off" : "link"
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: function(item) {
|
||||
session.ethernet.active = item;
|
||||
}
|
||||
}
|
||||
50
modules/controlcenter/network/EthernetPane.qml
Normal file
50
modules/controlcenter/network/EthernetPane.qml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.ethernet.active
|
||||
paneIdGenerator: function(item) {
|
||||
return item ? (item.interface || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
EthernetList {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
EthernetDetails {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
clip: true
|
||||
|
||||
EthernetSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
modules/controlcenter/network/EthernetSettings.qml
Normal file
76
modules/controlcenter/network/EthernetSettings.qml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "cable"
|
||||
title: qsTr("Ethernet settings")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Ethernet devices")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Available ethernet devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: ethernetInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Total devices")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1").arg(Nmcli.ethernetDevices.length)
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Connected devices")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
modules/controlcenter/network/NetworkSettings.qml
Normal file
98
modules/controlcenter/network/NetworkSettings.qml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "router"
|
||||
title: qsTr("Network Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Ethernet")
|
||||
description: qsTr("Ethernet device information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Total devices")
|
||||
value: qsTr("%1").arg(Nmcli.ethernetDevices.length)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Connected devices")
|
||||
value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Wireless")
|
||||
description: qsTr("WiFi network settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("WiFi enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: {
|
||||
Nmcli.enableWifi(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Current connection")
|
||||
description: qsTr("Active network connection information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Network")
|
||||
value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected"))
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Signal strength")
|
||||
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Security")
|
||||
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Frequency")
|
||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
305
modules/controlcenter/network/NetworkingPane.qml
Normal file
305
modules/controlcenter/network/NetworkingPane.qml
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
SplitPaneLayout {
|
||||
id: splitLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
StyledFlickable {
|
||||
id: leftFlickable
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: leftContent.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: leftContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Network")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.wifiEnabled
|
||||
icon: "wifi"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Toggle WiFi")
|
||||
|
||||
onClicked: {
|
||||
Nmcli.toggleWifi(null);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.scanning
|
||||
icon: "wifi_find"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Scan for networks")
|
||||
|
||||
onClicked: {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.ethernet.active && !root.session.network.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Network settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.ethernet.active || root.session.network.active) {
|
||||
root.session.ethernet.active = null;
|
||||
root.session.network.active = null;
|
||||
} else {
|
||||
if (Nmcli.ethernetDevices.length > 0) {
|
||||
root.session.ethernet.active = Nmcli.ethernetDevices[0];
|
||||
} else if (Nmcli.networks.length > 0) {
|
||||
root.session.network.active = Nmcli.networks[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: ethernetListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Ethernet")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
EthernetList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: wirelessListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Wireless")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
WirelessList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var ethernetPane: root.session.ethernet.active
|
||||
property var wirelessPane: root.session.network.active
|
||||
property var pane: ethernetPane || wirelessPane
|
||||
property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")
|
||||
property Component targetComponent: settingsComponent
|
||||
property Component nextComponent: settingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
if (ethernetPane) return ethernetDetailsComponent;
|
||||
if (wirelessPane) return wirelessDetailsComponent;
|
||||
return settingsComponent;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.ethernet
|
||||
function onActiveChanged() {
|
||||
// Clear wireless when ethernet is selected
|
||||
if (root.session.ethernet.active && root.session.network.active) {
|
||||
root.session.network.active = null;
|
||||
return; // Let the network.onActiveChanged handle the update
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
// paneId will automatically update via property binding
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.network
|
||||
function onActiveChanged() {
|
||||
// Clear ethernet when wireless is selected
|
||||
if (root.session.network.active && root.session.ethernet.active) {
|
||||
root.session.ethernet.active = null;
|
||||
return; // Let the ethernet.onActiveChanged handle the update
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
// paneId will automatically update via property binding
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: rightPaneItem.targetComponent
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightPaneItem
|
||||
property: "targetComponent"
|
||||
value: rightPaneItem.nextComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settingsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
NetworkSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: ethernetDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: ethernetFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: ethernetDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: ethernetFlickable
|
||||
}
|
||||
|
||||
EthernetDetails {
|
||||
id: ethernetDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: wirelessDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: wirelessFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: wirelessDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: wirelessFlickable
|
||||
}
|
||||
|
||||
WirelessDetails {
|
||||
id: wirelessDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WirelessPasswordDialog {
|
||||
anchors.fill: parent
|
||||
session: root.session
|
||||
z: 1000
|
||||
}
|
||||
}
|
||||
212
modules/controlcenter/network/WirelessDetails.qml
Normal file
212
modules/controlcenter/network/WirelessDetails.qml
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var network: root.session.network.active
|
||||
|
||||
device: network
|
||||
|
||||
Component.onCompleted: {
|
||||
updateDeviceDetails();
|
||||
checkSavedProfile();
|
||||
}
|
||||
|
||||
onNetworkChanged: {
|
||||
connectionUpdateTimer.stop();
|
||||
if (network && network.ssid) {
|
||||
connectionUpdateTimer.start();
|
||||
}
|
||||
updateDeviceDetails();
|
||||
checkSavedProfile();
|
||||
}
|
||||
|
||||
function checkSavedProfile(): void {
|
||||
if (network && network.ssid) {
|
||||
Nmcli.loadSavedConnections(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
updateDeviceDetails();
|
||||
}
|
||||
function onWirelessDeviceDetailsChanged() {
|
||||
if (network && network.ssid) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) {
|
||||
connectionUpdateTimer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionUpdateTimer
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: network && network.ssid
|
||||
onTriggered: {
|
||||
if (network) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive) {
|
||||
if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) {
|
||||
Nmcli.getWirelessDeviceDetails("", () => {
|
||||
});
|
||||
} else {
|
||||
connectionUpdateTimer.stop();
|
||||
}
|
||||
} else {
|
||||
if (Nmcli.wirelessDeviceDetails !== null) {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDeviceDetails(): void {
|
||||
if (network && network.ssid) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive) {
|
||||
Nmcli.getWirelessDeviceDetails("");
|
||||
} else {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
} else {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: root.network?.isSecure ? "lock" : "wifi"
|
||||
title: root.network?.ssid ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("Connection settings for this network")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Connected")
|
||||
checked: root.network?.active ?? false
|
||||
toggle.onToggled: {
|
||||
if (checked) {
|
||||
NetworkConnection.handleConnect(root.network, root.session, null);
|
||||
} else {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
visible: {
|
||||
if (!root.network || !root.network.ssid) {
|
||||
return false;
|
||||
}
|
||||
return Nmcli.hasSavedProfile(root.network.ssid);
|
||||
}
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Forget Network")
|
||||
|
||||
onClicked: {
|
||||
if (root.network && root.network.ssid) {
|
||||
if (root.network.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
}
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Network properties")
|
||||
description: qsTr("Additional information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("SSID")
|
||||
value: root.network?.ssid ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("BSSID")
|
||||
value: root.network?.bssid ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Signal strength")
|
||||
value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Frequency")
|
||||
value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Security")
|
||||
value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection information")
|
||||
description: qsTr("Network connection details")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ConnectionInfoSection {
|
||||
deviceDetails: Nmcli.wirelessDeviceDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
226
modules/controlcenter/network/WirelessList.qml
Normal file
226
modules/controlcenter/network/WirelessList.qml
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
title: qsTr("Networks (%1)").arg(Nmcli.networks.length)
|
||||
description: qsTr("All available WiFi networks")
|
||||
activeItem: session.network.active
|
||||
|
||||
titleSuffix: Component {
|
||||
StyledText {
|
||||
visible: Nmcli.scanning
|
||||
text: qsTr("Scanning...")
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.networks].sort((a, b) => {
|
||||
if (a.active !== b.active)
|
||||
return b.active - a.active;
|
||||
return b.strength - a.strength;
|
||||
})
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.wifiEnabled
|
||||
icon: "wifi"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
Nmcli.toggleWifi(null);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.scanning
|
||||
icon: "wifi_find"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.network.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
if (root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
else {
|
||||
root.session.network.active = root.view.model.get(0)?.modelData ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.network.active = modelData;
|
||||
if (modelData && modelData.ssid) {
|
||||
root.checkSavedProfileForNetwork(modelData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure)
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.active ? 1 : 0
|
||||
color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.ssid || qsTr("Unknown")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (modelData.active) return qsTr("Connected");
|
||||
if (modelData.isSecure && modelData.security && modelData.security.length > 0) {
|
||||
return modelData.security;
|
||||
}
|
||||
if (modelData.isSecure) return qsTr("Secured");
|
||||
return qsTr("Open");
|
||||
}
|
||||
color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.active ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (modelData.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
} else {
|
||||
NetworkConnection.handleConnect(modelData, root.session, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData.active ? "link_off" : "link"
|
||||
color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: function(item) {
|
||||
session.network.active = item;
|
||||
if (item && item.ssid) {
|
||||
checkSavedProfileForNetwork(item.ssid);
|
||||
}
|
||||
}
|
||||
|
||||
function checkSavedProfileForNetwork(ssid: string): void {
|
||||
if (ssid && ssid.length > 0) {
|
||||
Nmcli.loadSavedConnections(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
57
modules/controlcenter/network/WirelessPane.qml
Normal file
57
modules/controlcenter/network/WirelessPane.qml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.network.active
|
||||
paneIdGenerator: function(item) {
|
||||
return item ? (item.ssid || item.bssid || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
WirelessList {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
WirelessDetails {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
clip: true
|
||||
|
||||
WirelessSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
overlayComponent: Component {
|
||||
WirelessPasswordDialog {
|
||||
anchors.fill: parent
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
512
modules/controlcenter/network/WirelessPasswordDialog.qml
Normal file
512
modules/controlcenter/network/WirelessPasswordDialog.qml
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property var network: {
|
||||
if (session.network.pendingNetwork) {
|
||||
return session.network.pendingNetwork;
|
||||
}
|
||||
if (session.network.active) {
|
||||
return session.network.active;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
property bool isClosing: false
|
||||
visible: session.network.showPasswordDialog || isClosing
|
||||
enabled: session.network.showPasswordDialog && !isClosing
|
||||
focus: enabled
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: dialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surface
|
||||
opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
|
||||
scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: root.isClosing
|
||||
onFinished: {
|
||||
if (root.isClosing) {
|
||||
root.session.network.showPasswordDialog = false;
|
||||
root.isClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: dialog
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: dialog
|
||||
property: "scale"
|
||||
to: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: closeDialog()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "lock"
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Enter password")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: connectButton.connecting || connectButton.hasError
|
||||
text: {
|
||||
if (connectButton.hasError) {
|
||||
return qsTr("Connection failed. Please check your password and try again.");
|
||||
}
|
||||
if (connectButton.connecting) {
|
||||
return qsTr("Connecting...");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: 400
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
||||
}
|
||||
|
||||
Item {
|
||||
id: passwordContainer
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
|
||||
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (!activeFocus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (connectButton.enabled) {
|
||||
connectButton.clicked();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
passwordBuffer = "";
|
||||
} else {
|
||||
passwordBuffer = passwordBuffer.slice(0, -1);
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.text && event.text.length > 0) {
|
||||
passwordBuffer += event.text;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
property string passwordBuffer: ""
|
||||
|
||||
Connections {
|
||||
target: root.session.network
|
||||
function onShowPasswordDialogChanged(): void {
|
||||
if (root.session.network.showPasswordDialog) {
|
||||
Qt.callLater(() => {
|
||||
passwordContainer.forceActiveFocus();
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.hasError = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onVisibleChanged(): void {
|
||||
if (root.visible) {
|
||||
Qt.callLater(() => {
|
||||
passwordContainer.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
|
||||
border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0)
|
||||
border.color: {
|
||||
if (connectButton.hasError) {
|
||||
return Colours.palette.m3error;
|
||||
}
|
||||
if (passwordContainer.activeFocus) {
|
||||
return Colours.palette.m3primary;
|
||||
}
|
||||
return root.visible ? Colours.palette.m3outline : "transparent";
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
hoverEnabled: false
|
||||
cursorShape: Qt.IBeamCursor
|
||||
|
||||
function onClicked(): void {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholder
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Password")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.family: Appearance.font.family.mono
|
||||
opacity: passwordContainer.passwordBuffer ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: charList
|
||||
|
||||
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: fullWidth
|
||||
implicitHeight: Appearance.font.size.normal
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: passwordContainer.passwordBuffer.split("")
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
id: ch
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: charList.implicitHeight
|
||||
|
||||
color: Colours.palette.m3onSurface
|
||||
radius: Appearance.rounding.small / 2
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: ch
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: ch
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
}
|
||||
}
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
id: cancelButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
id: connectButton
|
||||
|
||||
property bool connecting: false
|
||||
property bool hasError: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3primary
|
||||
inactiveOnColour: Colours.palette.m3onPrimary
|
||||
text: qsTr("Connect")
|
||||
enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
|
||||
|
||||
onClicked: {
|
||||
if (!root.network || connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = passwordContainer.passwordBuffer;
|
||||
if (!password || password.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasError = false;
|
||||
connecting = true;
|
||||
enabled = false;
|
||||
text = qsTr("Connecting...");
|
||||
|
||||
NetworkConnection.connectWithPassword(root.network, password, result => {
|
||||
if (result && result.success) {
|
||||
} else if (result && result.needsPassword) {
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
} else {
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connectionMonitor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectionStatus(): void {
|
||||
if (!root.visible || !connectButton.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
|
||||
if (isConnected) {
|
||||
connectionSuccessTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Nmcli.pendingConnection === null && connectButton.connecting) {
|
||||
if (connectionMonitor.repeatCount > 10) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionMonitor
|
||||
interval: 1000
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
property int repeatCount: 0
|
||||
|
||||
onTriggered: {
|
||||
repeatCount++;
|
||||
checkConnectionStatus();
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
repeatCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionSuccessTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
if (root.visible && Nmcli.active && Nmcli.active.ssid) {
|
||||
const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
if (stillConnected) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
if (root.visible) {
|
||||
checkConnectionStatus();
|
||||
}
|
||||
}
|
||||
function onConnectionFailed(ssid: string) {
|
||||
if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
if (isClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isClosing = true;
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
connectionMonitor.stop();
|
||||
}
|
||||
}
|
||||
73
modules/controlcenter/network/WirelessSettings.qml
Normal file
73
modules/controlcenter/network/WirelessSettings.qml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "wifi"
|
||||
title: qsTr("Network settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("WiFi status")
|
||||
description: qsTr("General WiFi settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("WiFi enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: {
|
||||
Nmcli.enableWifi(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Network information")
|
||||
description: qsTr("Current network connection")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Connected network")
|
||||
value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Signal strength")
|
||||
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Security")
|
||||
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Frequency")
|
||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
13
modules/controlcenter/state/BluetoothState.qml
Normal file
13
modules/controlcenter/state/BluetoothState.qml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property BluetoothDevice active: null
|
||||
property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter
|
||||
property bool editingAdapterName: false
|
||||
property bool fabMenuOpen: false
|
||||
property bool editingDeviceName: false
|
||||
}
|
||||
|
||||
8
modules/controlcenter/state/EthernetState.qml
Normal file
8
modules/controlcenter/state/EthernetState.qml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
}
|
||||
|
||||
8
modules/controlcenter/state/LauncherState.qml
Normal file
8
modules/controlcenter/state/LauncherState.qml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
}
|
||||
|
||||
10
modules/controlcenter/state/NetworkState.qml
Normal file
10
modules/controlcenter/state/NetworkState.qml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
property bool showPasswordDialog: false
|
||||
property var pendingNetwork: null
|
||||
}
|
||||
|
||||
109
modules/controlcenter/taskbar/ConnectedButtonGroup.qml
Normal file
109
modules/controlcenter/taskbar/ConnectedButtonGroup.qml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
property var options: [] // Array of {label: string, propertyName: string, onToggled: function}
|
||||
property var rootItem: null // The root item that contains the properties we want to bind to
|
||||
property string title: "" // Optional title text
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: buttonRow
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: root.options
|
||||
|
||||
delegate: TextButton {
|
||||
id: button
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
text: modelData.label
|
||||
|
||||
property bool _checked: false
|
||||
|
||||
checked: _checked
|
||||
toggle: false
|
||||
type: TextButton.Tonal
|
||||
|
||||
// Create binding in Component.onCompleted
|
||||
Component.onCompleted: {
|
||||
if (root.rootItem && modelData.propertyName) {
|
||||
const propName = modelData.propertyName;
|
||||
const rootItem = root.rootItem;
|
||||
_checked = Qt.binding(function() {
|
||||
return rootItem[propName] ?? false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Match utilities Toggles radius styling
|
||||
// Each button has full rounding (not connected) since they have spacing
|
||||
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal
|
||||
|
||||
// Match utilities Toggles inactive color
|
||||
inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
||||
|
||||
// Adjust width similar to utilities toggles
|
||||
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
|
||||
|
||||
onClicked: {
|
||||
if (modelData.onToggled && root.rootItem && modelData.propertyName) {
|
||||
const currentValue = root.rootItem[modelData.propertyName] ?? false;
|
||||
modelData.onToggled(!currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
637
modules/controlcenter/taskbar/TaskbarPane.qml
Normal file
637
modules/controlcenter/taskbar/TaskbarPane.qml
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property bool clockShowIcon: Config.bar.clock.showIcon ?? true
|
||||
property bool persistent: Config.bar.persistent ?? true
|
||||
property bool showOnHover: Config.bar.showOnHover ?? true
|
||||
property int dragThreshold: Config.bar.dragThreshold ?? 20
|
||||
property bool showAudio: Config.bar.status.showAudio ?? true
|
||||
property bool showMicrophone: Config.bar.status.showMicrophone ?? true
|
||||
property bool showKbLayout: Config.bar.status.showKbLayout ?? false
|
||||
property bool showNetwork: Config.bar.status.showNetwork ?? true
|
||||
property bool showBluetooth: Config.bar.status.showBluetooth ?? true
|
||||
property bool showBattery: Config.bar.status.showBattery ?? true
|
||||
property bool showLockStatus: Config.bar.status.showLockStatus ?? true
|
||||
property bool trayBackground: Config.bar.tray.background ?? false
|
||||
property bool trayCompact: Config.bar.tray.compact ?? false
|
||||
property bool trayRecolour: Config.bar.tray.recolour ?? false
|
||||
property int workspacesShown: Config.bar.workspaces.shown ?? 5
|
||||
property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true
|
||||
property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false
|
||||
property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false
|
||||
property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true
|
||||
property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true
|
||||
property bool scrollVolume: Config.bar.scrollActions.volume ?? true
|
||||
property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true
|
||||
property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true
|
||||
property bool popoutTray: Config.bar.popouts.tray ?? true
|
||||
property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: {
|
||||
if (Config.bar.entries) {
|
||||
entriesModel.clear();
|
||||
for (let i = 0; i < Config.bar.entries.length; i++) {
|
||||
const entry = Config.bar.entries[i];
|
||||
entriesModel.append({
|
||||
id: entry.id,
|
||||
enabled: entry.enabled !== false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(entryIndex, entryEnabled) {
|
||||
Config.bar.clock.showIcon = root.clockShowIcon;
|
||||
Config.bar.persistent = root.persistent;
|
||||
Config.bar.showOnHover = root.showOnHover;
|
||||
Config.bar.dragThreshold = root.dragThreshold;
|
||||
Config.bar.status.showAudio = root.showAudio;
|
||||
Config.bar.status.showMicrophone = root.showMicrophone;
|
||||
Config.bar.status.showKbLayout = root.showKbLayout;
|
||||
Config.bar.status.showNetwork = root.showNetwork;
|
||||
Config.bar.status.showBluetooth = root.showBluetooth;
|
||||
Config.bar.status.showBattery = root.showBattery;
|
||||
Config.bar.status.showLockStatus = root.showLockStatus;
|
||||
Config.bar.tray.background = root.trayBackground;
|
||||
Config.bar.tray.compact = root.trayCompact;
|
||||
Config.bar.tray.recolour = root.trayRecolour;
|
||||
Config.bar.workspaces.shown = root.workspacesShown;
|
||||
Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator;
|
||||
Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg;
|
||||
Config.bar.workspaces.showWindows = root.workspacesShowWindows;
|
||||
Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor;
|
||||
Config.bar.scrollActions.workspaces = root.scrollWorkspaces;
|
||||
Config.bar.scrollActions.volume = root.scrollVolume;
|
||||
Config.bar.scrollActions.brightness = root.scrollBrightness;
|
||||
Config.bar.popouts.activeWindow = root.popoutActiveWindow;
|
||||
Config.bar.popouts.tray = root.popoutTray;
|
||||
Config.bar.popouts.statusIcons = root.popoutStatusIcons;
|
||||
|
||||
const entries = [];
|
||||
for (let i = 0; i < entriesModel.count; i++) {
|
||||
const entry = entriesModel.get(i);
|
||||
let enabled = entry.enabled;
|
||||
if (entryIndex !== undefined && i === entryIndex) {
|
||||
enabled = entryEnabled;
|
||||
}
|
||||
entries.push({
|
||||
id: entry.id,
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
Config.bar.entries = entries;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: entriesModel
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: taskbarClippingRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
radius: taskbarBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: taskbarLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: taskbarContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: taskbarBorder
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: taskbarContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: sidebarFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: sidebarLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sidebarFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sidebarLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Taskbar")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Status Icons")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Speakers"),
|
||||
propertyName: "showAudio",
|
||||
onToggled: function(checked) {
|
||||
root.showAudio = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Microphone"),
|
||||
propertyName: "showMicrophone",
|
||||
onToggled: function(checked) {
|
||||
root.showMicrophone = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Keyboard"),
|
||||
propertyName: "showKbLayout",
|
||||
onToggled: function(checked) {
|
||||
root.showKbLayout = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Network"),
|
||||
propertyName: "showNetwork",
|
||||
onToggled: function(checked) {
|
||||
root.showNetwork = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Bluetooth"),
|
||||
propertyName: "showBluetooth",
|
||||
onToggled: function(checked) {
|
||||
root.showBluetooth = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Battery"),
|
||||
propertyName: "showBattery",
|
||||
onToggled: function(checked) {
|
||||
root.showBattery = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Capslock"),
|
||||
propertyName: "showLockStatus",
|
||||
onToggled: function(checked) {
|
||||
root.showLockStatus = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: mainRowLayout
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
id: leftColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Workspaces")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesShownRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Shown")
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
min: 1
|
||||
max: 20
|
||||
value: root.workspacesShown
|
||||
onValueModified: value => {
|
||||
root.workspacesShown = value;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesActiveIndicatorRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Active indicator")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesActiveIndicator
|
||||
onToggled: {
|
||||
root.workspacesActiveIndicator = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesOccupiedBgRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Occupied background")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesOccupiedBg
|
||||
onToggled: {
|
||||
root.workspacesOccupiedBg = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesShowWindowsRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Show windows")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesShowWindows
|
||||
onToggled: {
|
||||
root.workspacesShowWindows = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesPerMonitorRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Per monitor workspaces")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesPerMonitor
|
||||
onToggled: {
|
||||
root.workspacesPerMonitor = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Scroll Actions")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Workspaces"),
|
||||
propertyName: "scrollWorkspaces",
|
||||
onToggled: function(checked) {
|
||||
root.scrollWorkspaces = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Volume"),
|
||||
propertyName: "scrollVolume",
|
||||
onToggled: function(checked) {
|
||||
root.scrollVolume = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Brightness"),
|
||||
propertyName: "scrollBrightness",
|
||||
onToggled: function(checked) {
|
||||
root.scrollBrightness = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: middleColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Clock")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show clock icon")
|
||||
checked: root.clockShowIcon
|
||||
onToggled: checked => {
|
||||
root.clockShowIcon = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Bar Behavior")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Persistent")
|
||||
checked: root.persistent
|
||||
onToggled: checked => {
|
||||
root.persistent = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: root.showOnHover
|
||||
onToggled: checked => {
|
||||
root.showOnHover = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Drag threshold")
|
||||
value: root.dragThreshold
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "px"
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.dragThreshold = Math.round(newValue);
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: rightColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Popouts")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Active window")
|
||||
checked: root.popoutActiveWindow
|
||||
onToggled: checked => {
|
||||
root.popoutActiveWindow = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Tray")
|
||||
checked: root.popoutTray
|
||||
onToggled: checked => {
|
||||
root.popoutTray = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Status icons")
|
||||
checked: root.popoutStatusIcons
|
||||
onToggled: checked => {
|
||||
root.popoutStatusIcons = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Tray Settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Background"),
|
||||
propertyName: "trayBackground",
|
||||
onToggled: function(checked) {
|
||||
root.trayBackground = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Compact"),
|
||||
propertyName: "trayCompact",
|
||||
onToggled: function(checked) {
|
||||
root.trayCompact = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Recolour"),
|
||||
propertyName: "trayRecolour",
|
||||
onToggled: function(checked) {
|
||||
root.trayRecolour = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ Item {
|
|||
id: row
|
||||
|
||||
Pane {
|
||||
index: 0
|
||||
sourceComponent: Dash {
|
||||
visibilities: root.visibilities
|
||||
state: root.state
|
||||
|
|
@ -95,12 +96,14 @@ Item {
|
|||
}
|
||||
|
||||
Pane {
|
||||
index: 1
|
||||
sourceComponent: Media {
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
index: 2
|
||||
sourceComponent: Performance {}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,12 +129,14 @@ Item {
|
|||
}
|
||||
|
||||
component Pane: Loader {
|
||||
required property int index
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Component.onCompleted: active = Qt.binding(() => {
|
||||
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
|
||||
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
|
||||
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
|
||||
const current = view.currentIndex;
|
||||
// Activate current pane and adjacent panes for smooth scrolling
|
||||
return Math.abs(index - current) <= 1;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ Item {
|
|||
|
||||
visibilities: root.visibilities
|
||||
sidebar: sidebar
|
||||
popouts: popouts
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Item {
|
|||
StyledRect {
|
||||
id: searchWrapper
|
||||
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
anchors.left: parent.left
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ Item {
|
|||
CachingImage {
|
||||
path: root.modelData.path
|
||||
smooth: !root.PathView.view.moving
|
||||
cache: true
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Item {
|
|||
|
||||
required property var props
|
||||
required property var visibilities
|
||||
required property Item popouts
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
|
@ -28,6 +29,7 @@ Item {
|
|||
|
||||
Toggles {
|
||||
visibilities: root.visibilities
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Item {
|
|||
|
||||
required property var visibilities
|
||||
required property Item sidebar
|
||||
required property Item popouts
|
||||
|
||||
readonly property PersistentProperties props: PersistentProperties {
|
||||
property bool recordingListExpanded: false
|
||||
|
|
@ -89,6 +90,7 @@ Item {
|
|||
implicitWidth: root.implicitWidth - Appearance.padding.large * 2
|
||||
props: root.props
|
||||
visibilities: root.visibilities
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ StyledRect {
|
|||
id: root
|
||||
|
||||
required property var visibilities
|
||||
required property Item popouts
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
|
|
@ -67,9 +68,7 @@ StyledRect {
|
|||
toggle: false
|
||||
onClicked: {
|
||||
root.visibilities.utilities = false;
|
||||
WindowFactory.create(null, {
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
});
|
||||
root.popouts.detach("network");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +91,7 @@ StyledRect {
|
|||
visible: VPN.enabled
|
||||
onClicked: VPN.toggle()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,170 +3,203 @@ pragma Singleton
|
|||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
// Trigger ethernet device detection after initialization
|
||||
Qt.callLater(() => {
|
||||
getEthernetDevices();
|
||||
});
|
||||
// Load saved connections on startup
|
||||
Nmcli.loadSavedConnections(() => {
|
||||
root.savedConnections = Nmcli.savedConnections;
|
||||
root.savedConnectionSsids = Nmcli.savedConnectionSsids;
|
||||
});
|
||||
// Get initial WiFi status
|
||||
Nmcli.getWifiStatus((enabled) => {
|
||||
root.wifiEnabled = enabled;
|
||||
});
|
||||
// Sync networks from Nmcli on startup
|
||||
Qt.callLater(() => {
|
||||
syncNetworksFromNmcli();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
readonly property list<AccessPoint> networks: []
|
||||
readonly property AccessPoint active: networks.find(n => n.active) ?? null
|
||||
property bool wifiEnabled: true
|
||||
readonly property bool scanning: rescanProc.running
|
||||
readonly property bool scanning: Nmcli.scanning
|
||||
|
||||
property list<var> ethernetDevices: []
|
||||
readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null
|
||||
property int ethernetDeviceCount: 0
|
||||
property bool ethernetProcessRunning: false
|
||||
property var ethernetDeviceDetails: null
|
||||
property var wirelessDeviceDetails: null
|
||||
|
||||
function enableWifi(enabled: bool): void {
|
||||
const cmd = enabled ? "on" : "off";
|
||||
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
|
||||
Nmcli.enableWifi(enabled, (result) => {
|
||||
if (result.success) {
|
||||
root.getWifiStatus();
|
||||
Nmcli.getNetworks(() => {
|
||||
syncNetworksFromNmcli();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWifi(): void {
|
||||
const cmd = wifiEnabled ? "off" : "on";
|
||||
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
|
||||
Nmcli.toggleWifi((result) => {
|
||||
if (result.success) {
|
||||
root.getWifiStatus();
|
||||
Nmcli.getNetworks(() => {
|
||||
syncNetworksFromNmcli();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function rescanWifi(): void {
|
||||
rescanProc.running = true;
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
|
||||
function connectToNetwork(ssid: string, password: string): void {
|
||||
// TODO: Implement password
|
||||
connectProc.exec(["nmcli", "conn", "up", ssid]);
|
||||
property var pendingConnection: null
|
||||
signal connectionFailed(string ssid)
|
||||
|
||||
function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void {
|
||||
// Set up pending connection tracking if callback provided
|
||||
if (callback) {
|
||||
const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0;
|
||||
root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback };
|
||||
}
|
||||
|
||||
Nmcli.connectToNetwork(ssid, password, bssid, (result) => {
|
||||
if (result && result.success) {
|
||||
// Connection successful
|
||||
if (callback) callback(result);
|
||||
root.pendingConnection = null;
|
||||
} else if (result && result.needsPassword) {
|
||||
// Password needed - callback will handle showing dialog
|
||||
if (callback) callback(result);
|
||||
} else {
|
||||
// Connection failed
|
||||
if (result && result.error) {
|
||||
root.connectionFailed(ssid);
|
||||
}
|
||||
if (callback) callback(result);
|
||||
root.pendingConnection = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void {
|
||||
// Set up pending connection tracking
|
||||
const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0;
|
||||
root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback };
|
||||
|
||||
Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, (result) => {
|
||||
if (result && result.success) {
|
||||
// Connection successful
|
||||
if (callback) callback(result);
|
||||
root.pendingConnection = null;
|
||||
} else if (result && result.needsPassword) {
|
||||
// Password needed - callback will handle showing dialog
|
||||
if (callback) callback(result);
|
||||
} else {
|
||||
// Connection failed
|
||||
if (result && result.error) {
|
||||
root.connectionFailed(ssid);
|
||||
}
|
||||
if (callback) callback(result);
|
||||
root.pendingConnection = null;
|
||||
}
|
||||
}, bssid);
|
||||
}
|
||||
|
||||
function disconnectFromNetwork(): void {
|
||||
if (active) {
|
||||
disconnectProc.exec(["nmcli", "connection", "down", active.ssid]);
|
||||
// Try to disconnect - use connection name if available, otherwise use device
|
||||
Nmcli.disconnectFromNetwork();
|
||||
// Refresh network list after disconnection
|
||||
Qt.callLater(() => {
|
||||
Nmcli.getNetworks(() => {
|
||||
syncNetworksFromNmcli();
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function forgetNetwork(ssid: string): void {
|
||||
// Delete the connection profile for this network
|
||||
// This will remove the saved password and connection settings
|
||||
Nmcli.forgetNetwork(ssid, (result) => {
|
||||
if (result.success) {
|
||||
// Refresh network list after deletion
|
||||
Qt.callLater(() => {
|
||||
Nmcli.getNetworks(() => {
|
||||
syncNetworksFromNmcli();
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
property list<string> savedConnections: []
|
||||
property list<string> savedConnectionSsids: []
|
||||
|
||||
// Sync saved connections from Nmcli when they're updated
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onSavedConnectionsChanged() {
|
||||
root.savedConnections = Nmcli.savedConnections;
|
||||
}
|
||||
function onSavedConnectionSsidsChanged() {
|
||||
root.savedConnectionSsids = Nmcli.savedConnectionSsids;
|
||||
}
|
||||
}
|
||||
|
||||
function getWifiStatus(): void {
|
||||
wifiStatusProc.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["nmcli", "m"]
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
function syncNetworksFromNmcli(): void {
|
||||
const rNetworks = root.networks;
|
||||
const nNetworks = Nmcli.networks;
|
||||
|
||||
// Build a map of existing networks by key
|
||||
const existingMap = new Map();
|
||||
for (const rn of rNetworks) {
|
||||
const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`;
|
||||
existingMap.set(key, rn);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wifiStatusProc
|
||||
|
||||
running: true
|
||||
command: ["nmcli", "radio", "wifi"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.wifiEnabled = text.trim() === "enabled";
|
||||
|
||||
// Build a map of new networks by key
|
||||
const newMap = new Map();
|
||||
for (const nn of nNetworks) {
|
||||
const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`;
|
||||
newMap.set(key, nn);
|
||||
}
|
||||
|
||||
// Remove networks that no longer exist
|
||||
for (const [key, network] of existingMap) {
|
||||
if (!newMap.has(key)) {
|
||||
const index = rNetworks.indexOf(network);
|
||||
if (index >= 0) {
|
||||
rNetworks.splice(index, 1);
|
||||
network.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: enableWifiProc
|
||||
|
||||
onExited: {
|
||||
root.getWifiStatus();
|
||||
getNetworks.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: rescanProc
|
||||
|
||||
command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
|
||||
onExited: {
|
||||
getNetworks.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: console.warn("Network connection error:", text)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: disconnectProc
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getNetworks
|
||||
|
||||
running: true
|
||||
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
|
||||
const rep = new RegExp("\\\\:", "g");
|
||||
const rep2 = new RegExp(PLACEHOLDER, "g");
|
||||
|
||||
const allNetworks = text.trim().split("\n").map(n => {
|
||||
const net = n.replace(rep, PLACEHOLDER).split(":");
|
||||
return {
|
||||
active: net[0] === "yes",
|
||||
strength: parseInt(net[1]),
|
||||
frequency: parseInt(net[2]),
|
||||
ssid: net[3]?.replace(rep2, ":") ?? "",
|
||||
bssid: net[4]?.replace(rep2, ":") ?? "",
|
||||
security: net[5] ?? ""
|
||||
};
|
||||
}).filter(n => n.ssid && n.ssid.length > 0);
|
||||
|
||||
// Group networks by SSID and prioritize connected ones
|
||||
const networkMap = new Map();
|
||||
for (const network of allNetworks) {
|
||||
const existing = networkMap.get(network.ssid);
|
||||
if (!existing) {
|
||||
networkMap.set(network.ssid, network);
|
||||
} else {
|
||||
// Prioritize active/connected networks
|
||||
if (network.active && !existing.active) {
|
||||
networkMap.set(network.ssid, network);
|
||||
} else if (!network.active && !existing.active) {
|
||||
// If both are inactive, keep the one with better signal
|
||||
if (network.strength > existing.strength) {
|
||||
networkMap.set(network.ssid, network);
|
||||
}
|
||||
}
|
||||
// If existing is active and new is not, keep existing
|
||||
}
|
||||
}
|
||||
|
||||
const networks = Array.from(networkMap.values());
|
||||
|
||||
const rNetworks = root.networks;
|
||||
|
||||
const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
|
||||
for (const network of destroyed)
|
||||
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
|
||||
|
||||
for (const network of networks) {
|
||||
const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid);
|
||||
if (match) {
|
||||
match.lastIpcObject = network;
|
||||
} else {
|
||||
rNetworks.push(apComp.createObject(root, {
|
||||
lastIpcObject: network
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update networks from Nmcli
|
||||
for (const [key, nNetwork] of newMap) {
|
||||
const existing = existingMap.get(key);
|
||||
if (existing) {
|
||||
// Update existing network's lastIpcObject
|
||||
existing.lastIpcObject = nNetwork.lastIpcObject;
|
||||
} else {
|
||||
// Create new AccessPoint from Nmcli's data
|
||||
rNetworks.push(apComp.createObject(root, {
|
||||
lastIpcObject: nNetwork.lastIpcObject
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +217,100 @@ Singleton {
|
|||
|
||||
Component {
|
||||
id: apComp
|
||||
|
||||
AccessPoint {}
|
||||
}
|
||||
|
||||
function hasSavedProfile(ssid: string): bool {
|
||||
// Use Nmcli's hasSavedProfile which has the same logic
|
||||
return Nmcli.hasSavedProfile(ssid);
|
||||
}
|
||||
|
||||
function getWifiStatus(): void {
|
||||
Nmcli.getWifiStatus((enabled) => {
|
||||
root.wifiEnabled = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function getEthernetDevices(): void {
|
||||
root.ethernetProcessRunning = true;
|
||||
Nmcli.getEthernetInterfaces((interfaces) => {
|
||||
root.ethernetDevices = Nmcli.ethernetDevices;
|
||||
root.ethernetDeviceCount = Nmcli.ethernetDevices.length;
|
||||
root.ethernetProcessRunning = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function connectEthernet(connectionName: string, interfaceName: string): void {
|
||||
Nmcli.connectEthernet(connectionName, interfaceName, (result) => {
|
||||
if (result.success) {
|
||||
getEthernetDevices();
|
||||
// Refresh device details after connection
|
||||
Qt.callLater(() => {
|
||||
const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; });
|
||||
if (activeDevice && activeDevice.interface) {
|
||||
updateEthernetDeviceDetails(activeDevice.interface);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectEthernet(connectionName: string): void {
|
||||
Nmcli.disconnectEthernet(connectionName, (result) => {
|
||||
if (result.success) {
|
||||
getEthernetDevices();
|
||||
// Clear device details after disconnection
|
||||
Qt.callLater(() => {
|
||||
root.ethernetDeviceDetails = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateEthernetDeviceDetails(interfaceName: string): void {
|
||||
Nmcli.getEthernetDeviceDetails(interfaceName, (details) => {
|
||||
root.ethernetDeviceDetails = details;
|
||||
});
|
||||
}
|
||||
|
||||
function updateWirelessDeviceDetails(): void {
|
||||
// Find the wireless interface by looking for wifi devices
|
||||
// Pass empty string to let Nmcli find the active interface automatically
|
||||
Nmcli.getWirelessDeviceDetails("", (details) => {
|
||||
root.wirelessDeviceDetails = details;
|
||||
});
|
||||
}
|
||||
|
||||
function cidrToSubnetMask(cidr: string): string {
|
||||
// Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0")
|
||||
const cidrNum = parseInt(cidr);
|
||||
if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const mask = (0xffffffff << (32 - cidrNum)) >>> 0;
|
||||
const octets = [
|
||||
(mask >>> 24) & 0xff,
|
||||
(mask >>> 16) & 0xff,
|
||||
(mask >>> 8) & 0xff,
|
||||
mask & 0xff
|
||||
];
|
||||
|
||||
return octets.join(".");
|
||||
}
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["nmcli", "m"]
|
||||
stdout: SplitParser {
|
||||
onRead: {
|
||||
Nmcli.getNetworks(() => {
|
||||
syncNetworksFromNmcli();
|
||||
});
|
||||
getEthernetDevices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
1352
services/Nmcli.qml
Normal file
1352
services/Nmcli.qml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -21,7 +21,7 @@ Singleton {
|
|||
const name = providerName;
|
||||
const iface = interfaceName;
|
||||
const defaults = getBuiltinDefaults(name, iface);
|
||||
|
||||
|
||||
if (isCustomProvider) {
|
||||
const custom = providerInput;
|
||||
return {
|
||||
|
|
@ -31,7 +31,7 @@ Singleton {
|
|||
displayName: custom.displayName || defaults.displayName
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ Singleton {
|
|||
displayName: "Tailscale"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return builtins[name] || {
|
||||
connectCmd: [name, "up"],
|
||||
disconnectCmd: [name, "down"],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pragma Singleton
|
|||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
|
@ -115,16 +116,28 @@ Singleton {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
function getNetworkIcon(strength: int): string {
|
||||
if (strength >= 80)
|
||||
return "signal_wifi_4_bar";
|
||||
if (strength >= 60)
|
||||
return "network_wifi_3_bar";
|
||||
if (strength >= 40)
|
||||
return "network_wifi_2_bar";
|
||||
if (strength >= 20)
|
||||
return "network_wifi_1_bar";
|
||||
return "signal_wifi_0_bar";
|
||||
function getNetworkIcon(strength: int, isSecure = false): string {
|
||||
if (isSecure) {
|
||||
if (strength >= 80)
|
||||
return "network_wifi_locked";
|
||||
if (strength >= 60)
|
||||
return "network_wifi_3_bar_locked";
|
||||
if (strength >= 40)
|
||||
return "network_wifi_2_bar_locked";
|
||||
if (strength >= 20)
|
||||
return "network_wifi_1_bar_locked";
|
||||
return "signal_wifi_0_bar";
|
||||
} else {
|
||||
if (strength >= 80)
|
||||
return "network_wifi";
|
||||
if (strength >= 60)
|
||||
return "network_wifi_3_bar";
|
||||
if (strength >= 40)
|
||||
return "network_wifi_2_bar";
|
||||
if (strength >= 20)
|
||||
return "network_wifi_1_bar";
|
||||
return "signal_wifi_0_bar";
|
||||
}
|
||||
}
|
||||
|
||||
function getBluetoothIcon(icon: string): string {
|
||||
|
|
@ -194,13 +207,13 @@ Singleton {
|
|||
|
||||
function getSpecialWsIcon(name: string): string {
|
||||
name = name.toLowerCase().slice("special:".length);
|
||||
|
||||
|
||||
for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) {
|
||||
if (iconConfig.name === name) {
|
||||
return iconConfig.icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (name === "special")
|
||||
return "star";
|
||||
if (name === "communication")
|
||||
|
|
|
|||
122
utils/NetworkConnection.qml
Normal file
122
utils/NetworkConnection.qml
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.services
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* NetworkConnection
|
||||
*
|
||||
* Centralized utility for network connection logic. Provides a single source of truth
|
||||
* for connecting to wireless networks, eliminating code duplication across
|
||||
* controlcenter components and bar popouts.
|
||||
*
|
||||
* Usage:
|
||||
* ```qml
|
||||
* import qs.utils
|
||||
*
|
||||
* // With Session object (controlcenter)
|
||||
* NetworkConnection.handleConnect(network, session);
|
||||
*
|
||||
* // Without Session object (bar popouts) - provide password dialog callback
|
||||
* NetworkConnection.handleConnect(network, null, (network) => {
|
||||
* // Show password dialog
|
||||
* root.passwordNetwork = network;
|
||||
* root.showPasswordDialog = true;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* Handle network connection with automatic disconnection if needed.
|
||||
* If there's an active network different from the target, disconnects first,
|
||||
* then connects to the target network.
|
||||
*
|
||||
* @param network The network object to connect to (must have ssid property)
|
||||
* @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork)
|
||||
* @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts)
|
||||
*/
|
||||
function handleConnect(network, session, onPasswordNeeded): void {
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Nmcli.active && Nmcli.active.ssid !== network.ssid) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
Qt.callLater(() => {
|
||||
root.connectToNetwork(network, session, onPasswordNeeded);
|
||||
});
|
||||
} else {
|
||||
root.connectToNetwork(network, session, onPasswordNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a wireless network.
|
||||
* Handles both secured and open networks, checks for saved profiles,
|
||||
* and shows password dialog if needed.
|
||||
*
|
||||
* @param network The network object to connect to (must have ssid, isSecure, bssid properties)
|
||||
* @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork)
|
||||
* @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts)
|
||||
*/
|
||||
function connectToNetwork(network, session, onPasswordNeeded): void {
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (network.isSecure) {
|
||||
const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid);
|
||||
|
||||
if (hasSavedProfile) {
|
||||
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
|
||||
} else {
|
||||
// Use password check with callback
|
||||
Nmcli.connectToNetworkWithPasswordCheck(
|
||||
network.ssid,
|
||||
network.isSecure,
|
||||
(result) => {
|
||||
if (result.needsPassword) {
|
||||
// Clear pending connection if exists
|
||||
if (Nmcli.pendingConnection) {
|
||||
Nmcli.connectionCheckTimer.stop();
|
||||
Nmcli.immediateCheckTimer.stop();
|
||||
Nmcli.immediateCheckTimer.checkCount = 0;
|
||||
Nmcli.pendingConnection = null;
|
||||
}
|
||||
|
||||
// Handle password dialog - use session if available, otherwise use callback
|
||||
if (session && session.network) {
|
||||
session.network.showPasswordDialog = true;
|
||||
session.network.pendingNetwork = network;
|
||||
} else if (onPasswordNeeded) {
|
||||
onPasswordNeeded(network);
|
||||
}
|
||||
}
|
||||
},
|
||||
network.bssid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a wireless network with a provided password.
|
||||
* Used by password dialogs when the user has already entered a password.
|
||||
*
|
||||
* @param network The network object to connect to (must have ssid, bssid properties)
|
||||
* @param password The password to use for connection
|
||||
* @param onResult Optional callback function(result) called with connection result
|
||||
*/
|
||||
function connectWithPassword(network, password, onResult): void {
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
Nmcli.connectToNetwork(network.ssid, password || "", network.bssid || "", onResult || null);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue