[Linux] Use segment control for setting noise control mode (#92)

* [Linux] Enhance GUI with icons

* Improve visibility

* Smarter hiding of battery values

* Add simple opacity based ear detection indication

* Hide disconnected devices

* Add airpods 3 icon

* Support more devices

* Better icons

* Add documentation

* Add segmented control

* Support keyboard navigation

* Fix ear detection when primary pod changes

* Support up to 9 modes with the keyboard

* Redisign

* Use id

* Use correct images

* Remove duplicates

* Use correct image

* Remove more merge conflicts

* Remove unused code

* Remove unused code

* Make all text readbale
This commit is contained in:
Tim Gromeyer
2025-04-04 09:40:32 +02:00
committed by GitHub
parent fb3f948250
commit e0624ce084
5 changed files with 137 additions and 17 deletions

View File

@@ -27,6 +27,7 @@ qt_add_qml_module(applinux
QML_FILES
Main.qml
BatteryIndicator.qml
SegmentedControl.qml
)
# Add the resource file

View File

@@ -21,7 +21,7 @@ ApplicationWindow {
Column {
spacing: 5
opacity: airPodsTrayApp.isLeftPodInEar ? 1 : 0.5
opacity: airPodsTrayApp.leftPodInEar ? 1 : 0.5
visible: airPodsTrayApp.battery.leftPodAvailable
Image {
@@ -36,7 +36,6 @@ ApplicationWindow {
}
BatteryIndicator {
visible: airPodsTrayApp.leftPodAvailable
batteryLevel: airPodsTrayApp.battery.leftPodLevel
isCharging: airPodsTrayApp.battery.leftPodCharging
darkMode: true
@@ -46,7 +45,7 @@ ApplicationWindow {
Column {
spacing: 5
opacity: airPodsTrayApp.isRightPodInEar ? 1 : 0.5
opacity: airPodsTrayApp.rightPodInEar ? 1 : 0.5
visible: airPodsTrayApp.battery.rightPodAvailable
Image {
@@ -62,7 +61,6 @@ ApplicationWindow {
}
BatteryIndicator {
visible: airPodsTrayApp.rightPodAvailable
batteryLevel: airPodsTrayApp.battery.rightPodLevel
isCharging: airPodsTrayApp.battery.rightPodCharging
darkMode: true
@@ -87,7 +85,6 @@ ApplicationWindow {
}
BatteryIndicator {
visible: airPodsTrayApp.caseAvailable
batteryLevel: airPodsTrayApp.battery.caseLevel
isCharging: airPodsTrayApp.battery.caseCharging
darkMode: true
@@ -95,19 +92,21 @@ ApplicationWindow {
}
}
SegmentedControl {
id: noiseControlMode
// width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
model: ["Off", "Noise Cancellation", "Transparency", "Adaptive"]
currentIndex: airPodsTrayApp.noiseControlMode
onCurrentIndexChanged: airPodsTrayApp.noiseControlMode = currentIndex
}
Text {
id: earDetectionStatus
text: "Ear Detection Status: " + airPodsTrayApp.earDetectionStatus
color: "#ffffff"
}
ComboBox {
id: noiseControlMode
model: ["Off", "Noise Cancellation", "Transparency", "Adaptive"]
currentIndex: airPodsTrayApp.noiseControlMode
onCurrentIndexChanged: airPodsTrayApp.noiseControlMode = currentIndex
}
Switch {
id: caToggle
text: "Conversational Awareness"
@@ -116,6 +115,7 @@ ApplicationWindow {
}
Slider {
id: noiseLevelSlider
visible: airPodsTrayApp.adaptiveModeActive
from: 0
to: 100
@@ -125,8 +125,8 @@ ApplicationWindow {
property Timer debounceTimer: Timer {
interval: 500 // 500ms delay after last change
onTriggered: {
if (!parent.pressed) {
airPodsTrayApp.setAdaptiveNoiseLevel(parent.value)
if (!noiseLevelSlider.pressed) {
airPodsTrayApp.setAdaptiveNoiseLevel(noiseLevelSlider.value)
}
}
}

104
linux/SegmentedControl.qml Normal file
View File

@@ -0,0 +1,104 @@
pragma ComponentBehavior: Bound
import QtQuick 2.15
import QtQuick.Controls 2.15
Control {
id: root
// Properties
property var model: ["Option 1", "Option 2"] // Default model
property int currentIndex: 0
// Colors using system palette
readonly property color backgroundColor: palette.light
readonly property color selectedColor: palette.highlight
readonly property color textColor: palette.buttonText
readonly property color selectedTextColor: palette.highlightedText
// System palette
SystemPalette {
id: palette
}
// Internal properties
padding: 6
implicitHeight: 32
// Removed: implicitWidth: Math.max(200, model.length * 100)
// Set focus policy to enable keyboard navigation
focusPolicy: Qt.StrongFocus
activeFocusOnTab: true
// Styling
background: Rectangle {
radius: height / 2
color: root.backgroundColor
border.width: root.activeFocus ? 1 : 0
border.color: root.selectedColor
}
contentItem: Row {
spacing: root.padding
Repeater {
model: root.model
delegate: Button {
id: segmentButton
required property int index
required property string modelData
text: modelData
// Removed: width: (root.availableWidth - (root.model.length - 1) * root.padding) / root.model.length
height: root.availableHeight
focusPolicy: Qt.NoFocus // Let the root control handle focus
background: Rectangle {
radius: height / 2
color: root.currentIndex === segmentButton.index ? root.selectedColor : "transparent"
border.width: 0
Behavior on color {
ColorAnimation {
duration: 600
easing.type: Easing.OutQuad
}
}
}
onClicked: {
if (root.currentIndex !== index) {
root.currentIndex = index;
}
}
}
}
}
// Handle key events for navigation
Keys.onPressed: event => {
if (event.key === Qt.Key_Left) {
if (root.currentIndex > 0) {
root.currentIndex--;
event.accepted = true;
}
} else if (event.key === Qt.Key_Right) {
if (root.currentIndex < root.model.length - 1) {
root.currentIndex++;
event.accepted = true;
}
} else if (event.key === Qt.Key_Home) {
root.currentIndex = 0;
event.accepted = true;
} else if (event.key === Qt.Key_End) {
root.currentIndex = root.model.length - 1;
event.accepted = true;
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
const index = event.key - Qt.Key_1;
if (index <= root.model.length) {
root.currentIndex = index;
event.accepted = true;
}
}
}
}

View File

@@ -104,7 +104,12 @@ public:
// Set primary and secondary pods based on order
if (!podsInPacket.isEmpty())
{
primaryPod = podsInPacket[0]; // First pod is primary
Component newPrimaryPod = podsInPacket[0]; // First pod is primary
if (newPrimaryPod != primaryPod)
{
primaryPod = newPrimaryPod;
emit primaryChanged();
}
}
if (podsInPacket.size() >= 2)
{
@@ -166,6 +171,7 @@ public:
signals:
void batteryStatusChanged();
void primaryChanged();
private:
bool isStatus(Component component, BatteryStatus status) const

View File

@@ -25,8 +25,8 @@ class AirPodsTrayApp : public QObject {
Q_PROPERTY(bool oneOrMorePodsInCase READ oneOrMorePodsInCase NOTIFY earDetectionStatusChanged)
Q_PROPERTY(QString podIcon READ podIcon NOTIFY modelChanged)
Q_PROPERTY(QString caseIcon READ caseIcon NOTIFY modelChanged)
Q_PROPERTY(bool isLeftPodInEar READ isLeftPodInEar NOTIFY earDetectionStatusChanged)
Q_PROPERTY(bool isRightPodInEar READ isRightPodInEar NOTIFY earDetectionStatusChanged)
Q_PROPERTY(bool leftPodInEar READ isLeftPodInEar NOTIFY primaryChanged)
Q_PROPERTY(bool rightPodInEar READ isRightPodInEar NOTIFY primaryChanged)
public:
AirPodsTrayApp(bool debugMode)
@@ -55,6 +55,8 @@ public:
mediaController->initializeMprisInterface();
mediaController->followMediaChanges();
connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged);
// load conversational awareness state
setConversationalAwareness(loadConversationalAwarenessState());
@@ -476,6 +478,9 @@ private slots:
m_model = parseModelNumber(modelNumber);
emit modelChanged();
m_model = parseModelNumber(modelNumber);
emit modelChanged();
emit deviceNameChanged(m_deviceName);
@@ -598,10 +603,13 @@ private slots:
char secondary = data[7];
m_primaryInEar = primary == 0x00;
m_secoundaryInEar = secondary == 0x00;
m_primaryInEar = primary == 0x00;
m_secoundaryInEar = secondary == 0x00;
m_earDetectionStatus = QString("Primary: %1, Secondary: %2")
.arg(getEarStatus(primary), getEarStatus(secondary));
LOG_INFO("Ear detection status: " << m_earDetectionStatus);
emit earDetectionStatusChanged(m_earDetectionStatus);
emit primaryChanged();
}
// Battery Status
else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS))
@@ -854,6 +862,7 @@ signals:
void adaptiveNoiseLevelChanged(int level);
void deviceNameChanged(const QString &name);
void modelChanged();
void primaryChanged();
private:
QSystemTrayIcon *trayIcon;