Skip to content

Commit d97e5bd

Browse files
emir-hasanbegovicclaude
authored andcommitted
Replace modal dialogs with in-window pages and make binding async
UI navigation - MainWindow hosts a QStackedWidget with Dashboard, Connections, and Pairing pages instead of stacking modal QDialogs. Each non-dashboard page has a Back button and tracks its return target so pairing returns to the page that launched it. - New ErrorBanner widget styled with Theme::error (matches dish-mac and dish-android colorError #E74C3C) replaces QMessageBox; sits below the stack so it appears at the bottom of every page. - Connections page disables Connect/Forget until something is selected, and shows an indeterminate progress bar while scanning. - Pairing page accepts a 4-digit PIN (matching dialog_pairing.xml), disables Pair until the input is complete, and shows a spinner during the async pair attempt with errors surfaced inline. - Spinners use setRetainSizeWhenHidden so showing/hiding them doesn't shift the rest of the layout. Async controller registration - WifiConnection::registerController no longer blocks the main thread with QThread::msleep. A QTimer polls lastControllerAck_ for up to 2 s and decodes the result byte from MSG_CONTROLLER_ACK. - New errorOccurred and registrationFailed signals surface ack codes (BACKEND_UNAVAIL, NO_SLOTS, ALREADY_EXISTS, PLUGIN_FAIL, timeout) as human-readable messages via the error banner. - ConnectionHub auto-unbinds when registrationFailed fires, so the local UI rolls back when the server rejects a controller add. - AppModel exposes a busy flag aggregated from all WifiConnections; the dashboard shows an indeterminate spinner while any registration is in flight. Theme + icon - QPushButton#primary:disabled now uses Theme::surfaceDim + muted text so disabled primary buttons read as inactive instead of identical to active ones. - QProgressBar + ::chunk styled with Theme::primary so indeterminate bars actually animate under the custom stylesheet. - Icon SVG bundled in the binary via resources/icons.qrc; main.cpp calls QGuiApplication::setWindowIcon for the in-binary case. Cleanup - Removed the unused virtual-controller slot, SlotInputType enum, and kVirtualSlotId constant. - Removed the telemetry footer plus TelemetrySnapshot / drainTelemetry / counter fields on GamepadInputProcessor; the test that exercised them is gone too. - SlotCard's Unbind button is enabled when bound (was incorrectly tied to "available" connections being non-empty). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ca5be37 commit d97e5bd

27 files changed

Lines changed: 632 additions & 338 deletions

CMakeLists.txt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,15 @@ set(DISH_UI_SOURCES
120120
src/UI/Theme.cpp
121121
src/UI/SlotCard.h
122122
src/UI/SlotCard.cpp
123-
src/UI/PairingDialog.h
124-
src/UI/PairingDialog.cpp
125-
src/UI/ConnectionsDialog.h
126-
src/UI/ConnectionsDialog.cpp
123+
src/UI/PairingPage.h
124+
src/UI/PairingPage.cpp
125+
src/UI/ConnectionsPage.h
126+
src/UI/ConnectionsPage.cpp
127+
src/UI/ErrorBanner.h
128+
src/UI/ErrorBanner.cpp
127129
src/UI/MainWindow.h
128130
src/UI/MainWindow.cpp
131+
resources/icons.qrc
129132
src/main.cpp)
130133

131134
add_executable(Dish ${DISH_UI_SOURCES})

resources/icons.qrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<RCC>
2+
<qresource prefix="/icons">
3+
<file alias="dish.svg">../packaging/dish.svg</file>
4+
</qresource>
5+
</RCC>

src/AppModel.cpp

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,9 @@ void AppModel::onWifiEvent(const net::ConnectionEvent& evt) {
8080

8181
void AppModel::rebuild() {
8282
QList<models::ControllerSlot> next;
83-
models::ControllerSlot virt;
84-
virt.id = QString::fromLatin1(models::kVirtualSlotId);
85-
virt.inputType = models::SlotInputType::Virtual;
86-
virt.name = QStringLiteral("Virtual Controller");
87-
next.append(virt);
8883
for (const auto& d : bridge_->devices()) {
8984
models::ControllerSlot s;
9085
s.id = d.id;
91-
s.inputType = models::SlotInputType::Physical;
9286
s.name = d.name;
9387
s.physicalDeviceId = d.id;
9488
next.append(s);
@@ -105,6 +99,15 @@ void AppModel::rebuild() {
10599
}
106100
state_.slotList = std::move(next);
107101

102+
bool busy = false;
103+
for (auto* conn : wifi_->connections()) {
104+
if (conn->isRegisteringController()) {
105+
busy = true;
106+
break;
107+
}
108+
}
109+
state_.busy = busy;
110+
108111
// Update the routing table to mirror the new slot/binding shape.
109112
QHash<QString, net::ConnectionHub::ReportSender> nextRouting;
110113
for (const auto& slot : state_.slotList) {

src/AppModel.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ struct MainUiState {
3333
QList<models::ControllerSlot> slotList;
3434
QList<models::ConnectionSummary> connections;
3535
std::optional<models::DiscoveredServer> pairingTarget;
36+
// True while any WiFi connection is registering a controller. Drives the
37+
// dashboard's indeterminate spinner.
38+
bool busy = false;
3639
};
3740

3841
// Top-level application state. Owns the network + input layers and stitches

src/Input/GamepadInputProcessor.cpp

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ void GamepadInputProcessor::publish(const DeviceId& id, const DeviceState& state
2727
if (auto it = deadzones_.find(id); it != deadzones_.end()) { dz = it->second; }
2828
filtered = applyDeadzones(state, dz);
2929
states_[id] = filtered;
30-
++telEvents_;
31-
++telSends_;
32-
++telTotalSent_;
3330
snapshot = sender_;
3431
}
3532
if (snapshot) {
@@ -60,14 +57,6 @@ void GamepadInputProcessor::remove(const DeviceId& id) {
6057
deadzones_.erase(id);
6158
}
6259

63-
GamepadInputProcessor::TelemetrySnapshot GamepadInputProcessor::drainTelemetry() {
64-
std::lock_guard<std::mutex> lock(mtx_);
65-
TelemetrySnapshot snap{telEvents_, telSends_, telTotalSent_};
66-
telEvents_ = 0;
67-
telSends_ = 0;
68-
return snap;
69-
}
70-
7160
std::int16_t scaleAxis(float v, float maxMagnitude) {
7261
const auto clamped = std::clamp(v, -1.0f, 1.0f);
7362
const auto scaled = static_cast<int>(clamped * maxMagnitude);

src/Input/GamepadInputProcessor.h

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,27 +72,17 @@ class GamepadInputProcessor {
7272
}
7373
};
7474

75-
struct TelemetrySnapshot {
76-
int events = 0;
77-
int sends = 0;
78-
std::uint64_t totalSent = 0;
79-
};
80-
8175
void setReportSender(ReportSender sender);
8276
void setDeadzones(const DeviceId& id, const Deadzones& dz);
8377
void publish(const DeviceId& id, const DeviceState& state);
8478
void zeroAndSendAll();
8579
void remove(const DeviceId& id);
86-
TelemetrySnapshot drainTelemetry();
8780

8881
private:
8982
std::mutex mtx_;
9083
std::unordered_map<DeviceId, DeviceState> states_;
9184
std::unordered_map<DeviceId, Deadzones> deadzones_;
9285
ReportSender sender_;
93-
int telEvents_ = 0;
94-
int telSends_ = 0;
95-
std::uint64_t telTotalSent_ = 0;
9686
};
9787

9888
// Pure helpers — easily testable.

src/Models/Models.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,14 @@ struct ConnectionSummary {
6969
std::optional<QString> boundSlotId;
7070
};
7171

72-
enum class SlotInputType { Virtual, Physical };
73-
7472
struct ControllerSlot {
7573
QString id;
76-
SlotInputType inputType = SlotInputType::Virtual;
7774
QString name;
7875
QString physicalDeviceId;
7976
std::optional<QString> boundConnectionId;
8077
std::optional<ConnectionSummary> boundStatus;
8178
};
8279

83-
inline constexpr const char* kVirtualSlotId = "virtual";
84-
8580
struct RememberedWifi {
8681
QString id;
8782
QString name;

src/Network/ConnectionHub.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace dish::net {
1212
ConnectionHub::ConnectionHub(WifiConnectionManager* wifi, ConnectionStore* store, QObject* parent)
1313
: QObject(parent), wifi_(wifi), store_(store) {
1414
QObject::connect(wifi_, &WifiConnectionManager::poolChanged, this, &ConnectionHub::rebuild);
15+
QObject::connect(wifi_, &WifiConnectionManager::slotRegistrationFailed, this,
16+
&ConnectionHub::unbind);
1517
rebuild();
1618
}
1719

src/Network/WifiConnection.cpp

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,31 @@
33

44
#include "WifiConnection.h"
55

6-
#include <QThread>
7-
86
namespace dish::net {
97

8+
namespace {
9+
10+
QString controllerAckErrorMessage(std::uint8_t result) {
11+
// Matches the satellite/src/core/types.h codes verbatim.
12+
switch (result) {
13+
case 0x01:
14+
return QStringLiteral(
15+
"Server has no virtual gamepad backend — controller cannot be created");
16+
case 0x02:
17+
return QStringLiteral("Server has no free controller slots");
18+
case 0x03:
19+
return QStringLiteral("Controller already added on the server");
20+
case 0x04:
21+
return QStringLiteral("Controller not found on the server");
22+
case 0x05:
23+
return QStringLiteral("Server failed to plug in the virtual controller");
24+
default:
25+
return QStringLiteral("Server rejected controller add (code %1)").arg(result);
26+
}
27+
}
28+
29+
} // namespace
30+
1031
WifiConnection::WifiConnection(QString id, models::DiscoveredServer server, QObject* parent)
1132
: QObject(parent), id_(std::move(id)), server_(std::move(server)) {}
1233

@@ -64,6 +85,8 @@ void WifiConnection::markDisconnected() {
6485
aliveTimer_->deleteLater();
6586
aliveTimer_ = nullptr;
6687
}
88+
if (ackPollTimer_ != nullptr) { ackPollTimer_->stop(); }
89+
controllerRegistering_ = false;
6790
if (existing) {
6891
existing->stopHeartbeat();
6992
existing->stopReceiveLoop();
@@ -96,20 +119,59 @@ void WifiConnection::detachSlot() {
96119
void WifiConnection::registerController(int type) {
97120
auto c = clientRef_.get();
98121
if (!c) { return; }
122+
pendingControllerType_ = type;
99123
c->resetControllerAck();
100124
c->controllerAdd(kDefaultCtrlIndex, kDefaultCaps);
101-
// Spin briefly waiting for the server's controller ACK; same shape as the
102-
// Mac client. This blocks the calling (main) thread for up to ~2s in the
103-
// worst case, but the satellite normally responds within a few ms.
104-
for (int i = 0; i < kAckWaitAttempts && c->lastControllerAck() == -1; ++i) {
105-
QThread::msleep(kAckWaitIntervalMs);
125+
ackPollCount_ = 0;
126+
controllerRegistering_ = true;
127+
if (ackPollTimer_ == nullptr) {
128+
ackPollTimer_ = new QTimer(this);
129+
ackPollTimer_->setInterval(kAckWaitIntervalMs);
130+
QObject::connect(ackPollTimer_, &QTimer::timeout, this, &WifiConnection::pollControllerAck);
106131
}
107-
if (c->lastControllerAck() != -1) {
108-
c->sendControllerType(kDefaultCtrlIndex, type);
132+
ackPollTimer_->start();
133+
emit changed();
134+
}
135+
136+
void WifiConnection::pollControllerAck() {
137+
auto c = clientRef_.get();
138+
if (!c) {
139+
const auto slotId = boundSlotId_.value_or(QString());
140+
finishRegistration();
141+
emit errorOccurred(QStringLiteral("Connection dropped before controller acknowledgement"));
142+
if (!slotId.isEmpty()) { emit registrationFailed(slotId); }
143+
return;
144+
}
145+
const auto ack = c->lastControllerAck();
146+
if (ack == -1) {
147+
if (++ackPollCount_ >= kAckWaitAttempts) {
148+
const auto slotId = boundSlotId_.value_or(QString());
149+
finishRegistration();
150+
emit errorOccurred(
151+
QStringLiteral("Server did not acknowledge controller add (timeout)"));
152+
if (!slotId.isEmpty()) { emit registrationFailed(slotId); }
153+
}
154+
return;
155+
}
156+
const std::uint8_t result = static_cast<std::uint8_t>(ack & 0xFF);
157+
if (result == 0x00 /* ACK_OK */) {
158+
c->sendControllerType(kDefaultCtrlIndex, pendingControllerType_);
109159
controllerAdded_ = true;
160+
finishRegistration();
161+
} else {
162+
const auto slotId = boundSlotId_.value_or(QString());
163+
finishRegistration();
164+
emit errorOccurred(controllerAckErrorMessage(result));
165+
if (!slotId.isEmpty()) { emit registrationFailed(slotId); }
110166
}
111167
}
112168

169+
void WifiConnection::finishRegistration() {
170+
if (ackPollTimer_ != nullptr) { ackPollTimer_->stop(); }
171+
controllerRegistering_ = false;
172+
emit changed();
173+
}
174+
113175
void WifiConnection::sendReport(std::uint16_t buttons, std::uint8_t lt, std::uint8_t rt,
114176
std::int16_t lx, std::int16_t ly, std::int16_t rx,
115177
std::int16_t ry) {

src/Network/WifiConnection.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,19 @@ class WifiConnection : public QObject {
6464
void attachSlot(const QString& slotId, int controllerType);
6565
void detachSlot();
6666

67+
bool isRegisteringController() const { return controllerRegistering_; }
68+
6769
// Hot path: called directly from the SDL gamepad thread.
6870
void sendReport(std::uint16_t buttons, std::uint8_t lt, std::uint8_t rt, std::int16_t lx,
6971
std::int16_t ly, std::int16_t rx, std::int16_t ry);
7072

7173
signals:
7274
void changed();
75+
void errorOccurred(const QString& message);
76+
// Emitted when the in-flight controller registration for `slotId` was
77+
// rejected or timed out. Listened to by ConnectionHub to roll back the
78+
// local binding so the UI reflects reality.
79+
void registrationFailed(const QString& slotId);
7380

7481
private:
7582
static constexpr int kDefaultCtrlIndex = 0;
@@ -78,6 +85,8 @@ class WifiConnection : public QObject {
7885
static constexpr int kAckWaitIntervalMs = 100;
7986

8087
void registerController(int type);
88+
void pollControllerAck();
89+
void finishRegistration();
8190

8291
QString id_;
8392
models::DiscoveredServer server_;
@@ -87,6 +96,9 @@ class WifiConnection : public QObject {
8796

8897
ClientRef clientRef_;
8998
QTimer* aliveTimer_ = nullptr;
99+
QTimer* ackPollTimer_ = nullptr;
100+
int ackPollCount_ = 0;
101+
bool controllerRegistering_ = false;
90102
std::function<void()> onDead_;
91103
bool controllerAdded_ = false;
92104
int pendingControllerType_ = 0;

0 commit comments

Comments
 (0)