Skip to content

Commit 718bc18

Browse files
committed
node: queue runtime dialogs
Keep one active runtime dialog and queue follow-up requests. Blocking callers wait on their queued request, while non-blocking messages are shown in order instead of overwriting the visible dialog. Add unit coverage for queued blocking prompts and non-blocking dialogs.
1 parent 56ed20b commit 718bc18

3 files changed

Lines changed: 161 additions & 30 deletions

File tree

qml/models/nodemodel.cpp

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,6 @@ bool NodeModel::showRuntimeDialog(const QString& message, const QString& caption
676676

677677
bool NodeModel::showRuntimeDialogOnGuiThread(const QString& message, const QString& caption, unsigned int style, bool question)
678678
{
679-
if (m_runtime_dialog_loop) {
680-
return false;
681-
}
682-
683679
if (!m_runtime_dialogs_enabled && !question) {
684680
if (style & CClientUIInterface::ICON_ERROR) {
685681
recordStartupErrorMessage(message);
@@ -690,49 +686,70 @@ bool NodeModel::showRuntimeDialogOnGuiThread(const QString& message, const QStri
690686
}
691687

692688
const bool blocking{(style & CClientUIInterface::MODAL) || question};
689+
auto request{std::make_shared<RuntimeDialogRequest>()};
690+
request->message = message;
691+
request->caption = caption;
692+
request->style = style;
693+
request->question = question;
694+
693695
QEventLoop loop;
694696
if (blocking) {
695-
m_runtime_dialog_loop = &loop;
697+
request->loop = &loop;
696698
}
697699

698-
m_runtime_dialog_title = RuntimeDialogTitle(caption, style);
699-
m_runtime_dialog_message = message;
700-
m_runtime_dialog_icon = RuntimeDialogIcon(style);
701-
m_runtime_dialog_primary_text = RuntimeDialogPrimaryText(style, question);
702-
m_runtime_dialog_secondary_text = RuntimeDialogSecondaryText(style, question);
703-
m_runtime_dialog_question = question || !m_runtime_dialog_secondary_text.isEmpty();
704-
m_runtime_dialog_answer = false;
705-
m_runtime_dialog_answered = false;
706-
m_runtime_dialog_visible = true;
707-
Q_EMIT runtimeDialogChanged();
700+
if (m_runtime_dialog_active) {
701+
m_runtime_dialog_queue.push_back(request);
702+
} else {
703+
showRuntimeDialogRequest(request);
704+
}
708705

709706
if (!blocking) {
710707
return false;
711708
}
712709

713-
if (!m_runtime_dialog_answered) {
710+
if (!request->answered) {
714711
loop.exec();
715712
}
716-
m_runtime_dialog_loop = nullptr;
713+
request->loop = nullptr;
717714

718-
const bool result{m_runtime_dialog_answer};
719-
m_runtime_dialog_visible = false;
715+
return request->answer;
716+
}
717+
718+
void NodeModel::showRuntimeDialogRequest(const std::shared_ptr<RuntimeDialogRequest>& request)
719+
{
720+
m_runtime_dialog_active = request;
721+
m_runtime_dialog_title = RuntimeDialogTitle(request->caption, request->style);
722+
m_runtime_dialog_message = request->message;
723+
m_runtime_dialog_icon = RuntimeDialogIcon(request->style);
724+
m_runtime_dialog_primary_text = RuntimeDialogPrimaryText(request->style, request->question);
725+
m_runtime_dialog_secondary_text = RuntimeDialogSecondaryText(request->style, request->question);
726+
m_runtime_dialog_question = request->question || !m_runtime_dialog_secondary_text.isEmpty();
727+
m_runtime_dialog_visible = true;
720728
Q_EMIT runtimeDialogChanged();
721-
return result;
722729
}
723730

724731
void NodeModel::answerRuntimeDialog(bool accepted)
725732
{
726-
m_runtime_dialog_answer = accepted;
727-
m_runtime_dialog_answered = true;
728-
if (m_runtime_dialog_loop) {
729-
m_runtime_dialog_loop->quit();
733+
if (!m_runtime_dialog_active) {
730734
return;
731735
}
732-
if (m_runtime_dialog_visible) {
733-
m_runtime_dialog_visible = false;
734-
Q_EMIT runtimeDialogChanged();
736+
737+
auto answered_dialog{std::move(m_runtime_dialog_active)};
738+
answered_dialog->answer = accepted;
739+
answered_dialog->answered = true;
740+
if (answered_dialog->loop) {
741+
answered_dialog->loop->quit();
742+
}
743+
744+
if (!m_runtime_dialog_queue.empty()) {
745+
auto next_dialog{m_runtime_dialog_queue.front()};
746+
m_runtime_dialog_queue.pop_front();
747+
showRuntimeDialogRequest(next_dialog);
748+
return;
735749
}
750+
751+
m_runtime_dialog_visible = false;
752+
Q_EMIT runtimeDialogChanged();
736753
}
737754

738755
void NodeModel::ConnectToBannedListChangedSignal()

qml/models/nodemodel.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <interfaces/node.h>
1010
#include <clientversion.h>
1111

12+
#include <deque>
1213
#include <memory>
1314

1415
#include <QObject>
@@ -166,6 +167,16 @@ public Q_SLOTS:
166167
double max_usage_mb{0.0};
167168
};
168169

170+
struct RuntimeDialogRequest {
171+
QString message;
172+
QString caption;
173+
unsigned int style{0};
174+
bool question{false};
175+
bool answer{false};
176+
bool answered{false};
177+
QEventLoop* loop{nullptr};
178+
};
179+
169180
// Properties that are exposed to QML.
170181
int m_block_tip_height{0};
171182
int m_num_peers{0};
@@ -196,14 +207,13 @@ public Q_SLOTS:
196207
bool m_runtime_dialogs_enabled{false};
197208
bool m_runtime_dialog_visible{false};
198209
bool m_runtime_dialog_question{false};
199-
bool m_runtime_dialog_answer{false};
200-
bool m_runtime_dialog_answered{false};
201210
QString m_runtime_dialog_title;
202211
QString m_runtime_dialog_message;
203212
QString m_runtime_dialog_icon;
204213
QString m_runtime_dialog_primary_text;
205214
QString m_runtime_dialog_secondary_text;
206-
QEventLoop* m_runtime_dialog_loop{nullptr};
215+
std::shared_ptr<RuntimeDialogRequest> m_runtime_dialog_active;
216+
std::deque<std::shared_ptr<RuntimeDialogRequest>> m_runtime_dialog_queue;
207217

208218
int m_shutdown_polling_timer_id{0};
209219

@@ -240,6 +250,7 @@ public Q_SLOTS:
240250
void setHeaderSyncState(int height, int64_t block_time, bool presync);
241251
bool showRuntimeDialog(const QString& message, const QString& caption, unsigned int style, bool question);
242252
bool showRuntimeDialogOnGuiThread(const QString& message, const QString& caption, unsigned int style, bool question);
253+
void showRuntimeDialogRequest(const std::shared_ptr<RuntimeDialogRequest>& request);
243254
void requestMempoolInfoRefresh();
244255
void fetchMempoolInfo();
245256
void applyMempoolInfo(const MempoolInfo& info);

test/test_nodemodel.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ private Q_SLOTS:
148148
void startupWarningsAreShownOnceAndDoNotBecomeCurrentWarnings();
149149
void runtimeMessageHandlerOpensAfterInitialization();
150150
void runtimeQuestionHandlerBlocksForAnswerAndReturnsResult();
151+
void runtimeBlockingDialogsAreQueued();
152+
void runtimeNonBlockingDialogsAreQueued();
151153
void initializeFailureShowsStartupWarningsWithoutMakingThemCurrentWarnings();
152154
void initializeFailureUsesNodeErrorMessages();
153155
void runawayExceptionSetsFatalStartupError();
@@ -739,6 +741,107 @@ void NodeModelTests::runtimeQuestionHandlerBlocksForAnswerAndReturnsResult()
739741
QVERIFY(!model.runtimeDialogVisible());
740742
}
741743

744+
void NodeModelTests::runtimeBlockingDialogsAreQueued()
745+
{
746+
NiceMock<MockNode> node;
747+
MempoolState mempool;
748+
interfaces::Node::QuestionFn question_fn;
749+
750+
InstallDefaultHandlers(node);
751+
InstallMempoolGetters(node, mempool);
752+
ON_CALL(node, handleQuestion(testing::_))
753+
.WillByDefault(Invoke([&](interfaces::Node::QuestionFn fn) {
754+
question_fn = std::move(fn);
755+
return MakeNoopHandler();
756+
}));
757+
758+
NodeModel model{node};
759+
WaitForInitialMempoolRefresh(mempool);
760+
QVERIFY(question_fn);
761+
model.initializeResult(true, {});
762+
763+
QStringList prompts;
764+
bool first_result{false};
765+
bool second_result{true};
766+
767+
QObject::connect(&model, &NodeModel::runtimeDialogChanged, &model, [&] {
768+
if (!model.runtimeDialogVisible()) return;
769+
770+
prompts.push_back(model.runtimeDialogMessage());
771+
if (model.runtimeDialogMessage() == QStringLiteral("Translated first?")) {
772+
QTimer::singleShot(0, &model, [&model] {
773+
model.answerRuntimeDialog(true);
774+
});
775+
second_result = question_fn(
776+
bilingual_str{"Second?", "Translated second?"},
777+
"Non interactive",
778+
"Second caption",
779+
CClientUIInterface::ICON_WARNING | CClientUIInterface::BTN_YES | CClientUIInterface::BTN_NO | CClientUIInterface::MODAL);
780+
} else if (model.runtimeDialogMessage() == QStringLiteral("Translated second?")) {
781+
QTimer::singleShot(0, &model, [&model] {
782+
model.answerRuntimeDialog(false);
783+
});
784+
}
785+
});
786+
787+
first_result = question_fn(
788+
bilingual_str{"First?", "Translated first?"},
789+
"Non interactive",
790+
"First caption",
791+
CClientUIInterface::ICON_WARNING | CClientUIInterface::BTN_YES | CClientUIInterface::BTN_NO | CClientUIInterface::MODAL);
792+
793+
QCOMPARE(prompts, QStringList({QStringLiteral("Translated first?"), QStringLiteral("Translated second?")}));
794+
QVERIFY(first_result);
795+
QVERIFY(!second_result);
796+
QVERIFY(!model.runtimeDialogVisible());
797+
}
798+
799+
void NodeModelTests::runtimeNonBlockingDialogsAreQueued()
800+
{
801+
NiceMock<MockNode> node;
802+
MempoolState mempool;
803+
interfaces::Node::MessageBoxFn message_box_fn;
804+
805+
InstallDefaultHandlers(node);
806+
InstallMempoolGetters(node, mempool);
807+
ON_CALL(node, handleMessageBox(testing::_))
808+
.WillByDefault(Invoke([&](interfaces::Node::MessageBoxFn fn) {
809+
message_box_fn = std::move(fn);
810+
return MakeNoopHandler();
811+
}));
812+
813+
NodeModel model{node};
814+
WaitForInitialMempoolRefresh(mempool);
815+
QVERIFY(message_box_fn);
816+
model.initializeResult(true, {});
817+
818+
QSignalSpy runtime_dialog_spy{&model, &NodeModel::runtimeDialogChanged};
819+
QVERIFY(!message_box_fn(
820+
bilingual_str{"First", "Translated first"},
821+
"",
822+
CClientUIInterface::ICON_INFORMATION));
823+
QCOMPARE(runtime_dialog_spy.count(), 1);
824+
QVERIFY(model.runtimeDialogVisible());
825+
QCOMPARE(model.runtimeDialogMessage(), QStringLiteral("Translated first"));
826+
827+
QVERIFY(!message_box_fn(
828+
bilingual_str{"Second", "Translated second"},
829+
"",
830+
CClientUIInterface::ICON_WARNING));
831+
QCOMPARE(runtime_dialog_spy.count(), 1);
832+
QVERIFY(model.runtimeDialogVisible());
833+
QCOMPARE(model.runtimeDialogMessage(), QStringLiteral("Translated first"));
834+
835+
model.answerRuntimeDialog(true);
836+
QCOMPARE(runtime_dialog_spy.count(), 2);
837+
QVERIFY(model.runtimeDialogVisible());
838+
QCOMPARE(model.runtimeDialogMessage(), QStringLiteral("Translated second"));
839+
840+
model.answerRuntimeDialog(true);
841+
QCOMPARE(runtime_dialog_spy.count(), 3);
842+
QVERIFY(!model.runtimeDialogVisible());
843+
}
844+
742845
void NodeModelTests::initializeFailureShowsStartupWarningsWithoutMakingThemCurrentWarnings()
743846
{
744847
NiceMock<MockNode> node;

0 commit comments

Comments
 (0)