Skip to content

Commit 7bdf10d

Browse files
committed
Redesign theme; restructure ANC; add ANC to tray
- Refined monochrome theme: soft near-black surfaces and off-white text instead of harsh pure black/white, rounded shapes, styled menus, hover states; EQ curve recoloured to match. - ANC tab restructured: top mode (Off / Transparency / Noise Cancelling) with a separate strength row (Adaptive / Low / Mid / High); a public apply() method drives both the UI and the device. - Tray menu gains a Noise Cancelling submenu to set ANC directly. https://claude.ai/code/session_014KrpftzUgVFGFXVUU3DUTT
1 parent b6bc522 commit 7bdf10d

4 files changed

Lines changed: 202 additions & 174 deletions

File tree

src/nothing_ear/ui/anc_tab.py

Lines changed: 105 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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

88
from __future__ import annotations
@@ -11,7 +11,8 @@
1111

1212
from PySide6.QtCore import QObject, Signal
1313
from PySide6.QtWidgets import (
14-
QGridLayout,
14+
QFrame,
15+
QHBoxLayout,
1516
QLabel,
1617
QPushButton,
1718
QVBoxLayout,
@@ -20,28 +21,25 @@
2021

2122
from ..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

4242
class _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)

src/nothing_ear/ui/eq_curve.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,30 +91,30 @@ def _response(self, freq: float) -> float:
9191
def paintEvent(self, _event) -> None:
9292
p = QPainter(self)
9393
p.setRenderHint(QPainter.Antialiasing)
94-
p.fillRect(self.rect(), QColor("#0b0b0b"))
94+
p.fillRect(self.rect(), QColor("#141418"))
9595
r = self._rect()
9696

9797
label_font = QFont()
9898
label_font.setPointSize(8)
9999
p.setFont(label_font)
100100

101-
p.setPen(QPen(QColor("#1d1d1d"), 1))
101+
p.setPen(QPen(QColor("#232329"), 1))
102102
for freq in (100, 1000, 10000):
103103
x = self._x(freq, r)
104104
p.drawLine(int(x), int(r.top()), int(x), int(r.bottom()))
105105
for gain in (-6, 6):
106106
y = self._y(gain, r)
107107
p.drawLine(int(r.left()), int(y), int(r.right()), int(y))
108108

109-
p.setPen(QColor("#5a5a5a"))
109+
p.setPen(QColor("#6a6a72"))
110110
for freq, text in ((100, "100"), (1000, "1k"), (10000, "10k")):
111111
x = self._x(freq, r)
112112
p.drawText(int(x) - 10, int(r.bottom()) + 16, text)
113113
for gain in (-12, 0, 12):
114114
y = self._y(gain, r)
115115
p.drawText(2, int(y) + 4, f"{gain:+d}")
116116

117-
p.setPen(QPen(QColor("#3a3a3a"), 1))
117+
p.setPen(QPen(QColor("#34343c"), 1))
118118
y0 = self._y(0, r)
119119
p.drawLine(int(r.left()), int(y0), int(r.right()), int(y0))
120120

@@ -128,16 +128,16 @@ def paintEvent(self, _event) -> None:
128128
fill.lineTo(r.right(), y0)
129129
fill.lineTo(r.left(), y0)
130130
fill.closeSubpath()
131-
p.fillPath(fill, QColor(255, 255, 255, 14))
132-
p.setPen(QPen(QColor("#ffffff"), 2))
131+
p.fillPath(fill, QColor(236, 236, 238, 16))
132+
p.setPen(QPen(QColor("#ececee"), 2))
133133
p.drawPath(path)
134134

135135
for i, b in enumerate(self._bands):
136136
x = self._x(b.frequency, r)
137137
y = self._y(b.gain, r)
138138
selected = i == self._selected
139-
p.setPen(QPen(QColor("#ffffff"), 2))
140-
p.setBrush(QBrush(QColor("#ffffff") if selected else QColor("#0b0b0b")))
139+
p.setPen(QPen(QColor("#fafafa"), 2))
140+
p.setBrush(QBrush(QColor("#fafafa") if selected else QColor("#141418")))
141141
radius = 8 if selected else 5
142142
p.drawEllipse(QPointF(x, y), radius, radius)
143143

src/nothing_ear/ui/main_window.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from .. import __version__
2828
from ..bluetooth import BtDevice
29+
from ..bluetooth.protocol import ANCMode
2930
from ..config import Settings
3031
from .anc_tab import AncTab
3132
from .eq_tab import EqTab
@@ -101,10 +102,25 @@ def _setup_tray(self) -> None:
101102
menu = QMenu()
102103
open_action = QAction("Open", self)
103104
open_action.triggered.connect(self._show_window)
104-
quit_action = QAction("Quit", self)
105-
quit_action.triggered.connect(self._quit)
106105
menu.addAction(open_action)
106+
107+
anc_menu = menu.addMenu("Noise Cancelling")
108+
tray_modes = [
109+
("Off", ANCMode.OFF),
110+
("Transparency", ANCMode.TRANSPARENCY),
111+
("Adaptive", ANCMode.ADAPTIVE),
112+
("Low", ANCMode.LOW),
113+
("Mid", ANCMode.MID),
114+
("High", ANCMode.HIGH),
115+
]
116+
for label, mode in tray_modes:
117+
action = QAction(label, self)
118+
action.triggered.connect(lambda _checked=False, m=mode: self._anc_tab.apply(m))
119+
anc_menu.addAction(action)
120+
107121
menu.addSeparator()
122+
quit_action = QAction("Quit", self)
123+
quit_action.triggered.connect(self._quit)
108124
menu.addAction(quit_action)
109125

110126
tray.setContextMenu(menu)

0 commit comments

Comments
 (0)