11"""ANC (Noise Cancelling) control tab.
22
3- Each action opens a short-lived RFCOMM connection (connect, send, close). The
4- blocking work runs on a background thread so the UI never freezes; results come
5- back via Qt signals .
3+ Top level chooses Off / Transparency / Noise Cancelling; when noise cancelling
4+ is on, the strength row (Adaptive / Low / Mid / High) selects the level. Each
5+ action opens a short-lived RFCOMM connection on a background thread .
66"""
77
88from __future__ import annotations
1111
1212from PySide6 .QtCore import QObject , Signal
1313from PySide6 .QtWidgets import (
14- QGridLayout ,
14+ QFrame ,
15+ QHBoxLayout ,
1516 QLabel ,
1617 QPushButton ,
1718 QVBoxLayout ,
2021
2122from ..bluetooth .protocol import ANCMode , NothingController
2223
23- _MODES : list [tuple [str , ANCMode | None ]] = [
24- ("Off" , ANCMode .OFF ),
25- ("Transparency" , ANCMode .TRANSPARENCY ),
24+ _LEVELS : list [tuple [str , ANCMode ]] = [
25+ ("Adaptive" , ANCMode .ADAPTIVE ),
2626 ("Low" , ANCMode .LOW ),
2727 ("Mid" , ANCMode .MID ),
2828 ("High" , ANCMode .HIGH ),
29- ("Adaptive" , ANCMode .ADAPTIVE ),
3029]
30+ _LEVEL_SET = {ANCMode .ADAPTIVE , ANCMode .LOW , ANCMode .MID , ANCMode .HIGH }
3131
32- _MODE_LABEL = {
32+ _LABELS = {
3333 ANCMode .OFF : "Off" ,
3434 ANCMode .TRANSPARENCY : "Transparency" ,
35+ ANCMode .ADAPTIVE : "Adaptive" ,
3536 ANCMode .LOW : "Low" ,
3637 ANCMode .MID : "Mid" ,
3738 ANCMode .HIGH : "High" ,
38- ANCMode .ADAPTIVE : "Adaptive" ,
3939}
4040
4141
4242class _AncWorker (QObject ):
43- """Runs RFCOMM ANC operations off the UI thread."""
44-
4543 statusRead = Signal (object )
4644 actionDone = Signal (object )
4745 failed = Signal (str )
@@ -52,30 +50,24 @@ def __init__(self) -> None:
5250 self .channel = 15
5351
5452 def query (self ) -> None :
55- addr = self .address
56- if addr :
57- self ._spawn (self ._query , addr , self .channel )
53+ if self .address :
54+ threading .Thread (target = self ._query , daemon = True ).start ()
5855
5956 def set_mode (self , mode : ANCMode ) -> None :
60- addr = self .address
61- if addr :
62- self ._spawn (self ._set , addr , self .channel , mode )
63-
64- @staticmethod
65- def _spawn (fn , * args ) -> None :
66- threading .Thread (target = fn , args = args , daemon = True ).start ()
57+ if self .address :
58+ threading .Thread (target = self ._set , args = (mode ,), daemon = True ).start ()
6759
68- def _query (self , addr : str , channel : int ) -> None :
60+ def _query (self ) -> None :
6961 try :
70- with NothingController (addr , channel = channel ) as ctrl :
62+ with NothingController (self . address , channel = self . channel ) as ctrl :
7163 mode = ctrl .query_anc ()
7264 self .statusRead .emit (mode )
7365 except Exception as exc :
7466 self .failed .emit (str (exc ))
7567
76- def _set (self , addr : str , channel : int , mode : ANCMode ) -> None :
68+ def _set (self , mode : ANCMode ) -> None :
7769 try :
78- with NothingController (addr , channel = channel ) as ctrl :
70+ with NothingController (self . address , channel = self . channel ) as ctrl :
7971 ctrl .set_anc (mode )
8072 self .actionDone .emit (mode )
8173 except Exception as exc :
@@ -88,56 +80,87 @@ def __init__(self, mock: bool = False) -> None:
8880 self ._mock = mock
8981 self ._address : str | None = None
9082 self ._active : ANCMode | None = None
91- self ._buttons : dict [ ANCMode , QPushButton ] = {}
83+ self ._level = ANCMode . HIGH
9284
9385 self ._worker = _AncWorker ()
9486 self ._worker .statusRead .connect (self ._on_status )
95- self ._worker .actionDone .connect (self ._on_action_done )
87+ self ._worker .actionDone .connect (self ._on_status )
9688 self ._worker .failed .connect (self ._on_failed )
9789
9890 self ._build ()
9991
10092 def _build (self ) -> None :
101- v = QVBoxLayout (self )
102- v .setContentsMargins (0 , 16 , 0 , 0 )
103- v .setSpacing (12 )
104-
105- title = QLabel ("Noise Cancelling" )
106- title .setObjectName ("deviceName" )
107- v .addWidget (title )
93+ root = QVBoxLayout (self )
94+ root .setContentsMargins (0 , 16 , 0 , 0 )
95+ root .setSpacing (14 )
96+
97+ mode_card = QFrame ()
98+ mode_card .setObjectName ("card" )
99+ mv = QVBoxLayout (mode_card )
100+ mv .setContentsMargins (18 , 18 , 18 , 18 )
101+ mv .setSpacing (12 )
102+
103+ mtitle = QLabel ("MODE" )
104+ mtitle .setObjectName ("sectionTitle" )
105+ mv .addWidget (mtitle )
106+
107+ mode_row = QHBoxLayout ()
108+ mode_row .setSpacing (8 )
109+ self ._off_btn = self ._chip ("Off" , lambda : self .apply (ANCMode .OFF ))
110+ self ._transparency_btn = self ._chip (
111+ "Transparency" , lambda : self .apply (ANCMode .TRANSPARENCY )
112+ )
113+ self ._anc_btn = self ._chip ("Noise Cancelling" , self ._enable_anc )
114+ mode_row .addWidget (self ._off_btn )
115+ mode_row .addWidget (self ._transparency_btn )
116+ mode_row .addWidget (self ._anc_btn )
117+ mv .addLayout (mode_row )
118+ root .addWidget (mode_card )
119+
120+ level_card = QFrame ()
121+ level_card .setObjectName ("card" )
122+ lv = QVBoxLayout (level_card )
123+ lv .setContentsMargins (18 , 18 , 18 , 18 )
124+ lv .setSpacing (12 )
125+
126+ ltitle = QLabel ("STRENGTH" )
127+ ltitle .setObjectName ("sectionTitle" )
128+ lv .addWidget (ltitle )
129+
130+ level_row = QHBoxLayout ()
131+ level_row .setSpacing (8 )
132+ self ._level_buttons : dict [ANCMode , QPushButton ] = {}
133+ for label , mode in _LEVELS :
134+ btn = self ._chip (label , lambda _checked = False , m = mode : self .apply (m ))
135+ self ._level_buttons [mode ] = btn
136+ level_row .addWidget (btn )
137+ lv .addLayout (level_row )
138+ root .addWidget (level_card )
108139
109140 self ._hint = QLabel ("No connected Nothing device." )
110141 self ._hint .setObjectName ("deviceMeta" )
111142 self ._hint .setWordWrap (True )
112- v .addWidget (self ._hint )
113-
114- grid = QGridLayout ()
115- grid .setSpacing (8 )
116- for i , (label , mode ) in enumerate (_MODES ):
117- btn = QPushButton (label )
118- btn .setObjectName ("secondary" )
119- if mode is None :
120- btn .setEnabled (False )
121- btn .setToolTip ("Command not reverse-engineered yet" )
122- else :
123- btn .clicked .connect (lambda _checked = False , m = mode : self ._choose (m ))
124- self ._buttons [mode ] = btn
125- grid .addWidget (btn , i // 2 , i % 2 )
126- v .addLayout (grid )
127- v .addStretch (1 )
143+ root .addWidget (self ._hint )
144+ root .addStretch (1 )
145+
146+ self ._set_enabled (False )
128147
129- self ._set_buttons_enabled (False )
148+ def _chip (self , label : str , on_click ) -> QPushButton :
149+ btn = QPushButton (label )
150+ btn .setObjectName ("chip" )
151+ btn .setCheckable (True )
152+ btn .clicked .connect (lambda _checked = False : on_click ())
153+ return btn
130154
131155 def set_device (self , address : str | None , channel : int = 15 ) -> None :
132156 changed = address != self ._address
133157 self ._address = address
134158 self ._worker .address = address
135159 self ._worker .channel = channel
136-
137- self ._set_buttons_enabled (address is not None )
160+ self ._set_enabled (address is not None )
138161 if address is None :
139162 self ._active = None
140- self ._highlight ()
163+ self ._render ()
141164 self ._hint .setText ("No connected Nothing device." )
142165 elif changed :
143166 if self ._mock :
@@ -146,36 +169,43 @@ def set_device(self, address: str | None, channel: int = 15) -> None:
146169 self ._hint .setText ("Reading current mode ..." )
147170 self ._worker .query ()
148171
149- def _choose (self , mode : ANCMode ) -> None :
172+ def apply (self , mode : ANCMode ) -> None :
173+ if mode in _LEVEL_SET :
174+ self ._level = mode
175+ self ._active = mode
176+ self ._render ()
150177 if self ._mock :
151- self ._on_action_done ( mode )
178+ self ._hint . setText ( f"Mock: { _LABELS [ mode ] } " )
152179 return
153- self ._hint .setText (f"Setting { _MODE_LABEL [mode ]} ..." )
180+ self ._hint .setText (f"Setting { _LABELS [mode ]} ..." )
154181 self ._worker .set_mode (mode )
155182
183+ def _enable_anc (self ) -> None :
184+ self .apply (self ._level )
185+
156186 def _on_status (self , mode : object ) -> None :
157- self ._active = mode if isinstance (mode , ANCMode ) else None
158- self ._highlight ()
159- if self ._active is not None :
160- self ._hint .setText (f"Current mode: { _MODE_LABEL [self ._active ]} " )
187+ if isinstance (mode , ANCMode ):
188+ self ._active = mode
189+ if mode in _LEVEL_SET :
190+ self ._level = mode
191+ self ._hint .setText (f"Current mode: { _LABELS [mode ]} " )
161192 else :
162193 self ._hint .setText ("Current mode unknown." )
163-
164- def _on_action_done (self , mode : object ) -> None :
165- self ._active = mode if isinstance (mode , ANCMode ) else None
166- self ._highlight ()
167- if self ._active is not None :
168- self ._hint .setText (f"Current mode: { _MODE_LABEL [self ._active ]} " )
194+ self ._render ()
169195
170196 def _on_failed (self , message : str ) -> None :
171197 self ._hint .setText (f"Error: { message } " )
172198
173- def _set_buttons_enabled (self , enabled : bool ) -> None :
174- for btn in self ._buttons .values ():
199+ def _set_enabled (self , enabled : bool ) -> None :
200+ for btn in (self ._off_btn , self ._transparency_btn , self ._anc_btn ):
201+ btn .setEnabled (enabled )
202+ for btn in self ._level_buttons .values ():
175203 btn .setEnabled (enabled )
176204
177- def _highlight (self ) -> None :
178- for mode , btn in self ._buttons .items ():
179- btn .setObjectName ("" if mode == self ._active else "secondary" )
180- btn .style ().unpolish (btn )
181- btn .style ().polish (btn )
205+ def _render (self ) -> None :
206+ anc_on = self ._active in _LEVEL_SET
207+ self ._off_btn .setChecked (self ._active == ANCMode .OFF )
208+ self ._transparency_btn .setChecked (self ._active == ANCMode .TRANSPARENCY )
209+ self ._anc_btn .setChecked (anc_on )
210+ for mode , btn in self ._level_buttons .items ():
211+ btn .setChecked (self ._active == mode )
0 commit comments