aboutsummaryrefslogtreecommitdiff
path: root/qt-quick
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano@tronto.net>2025-04-25 16:18:26 +0200
committerSebastiano Tronto <sebastiano@tronto.net>2025-04-25 16:19:02 +0200
commit97b5778172572504c8dfbe173b18281e786270c0 (patch)
treee775099577e9ef61465717ae453d71d8ff44559f /qt-quick
downloadqt-experiments-97b5778172572504c8dfbe173b18281e786270c0.tar.gz
qt-experiments-97b5778172572504c8dfbe173b18281e786270c0.zip
Initial commit
Diffstat (limited to 'qt-quick')
-rw-r--r--qt-quick/CMakeLists.txt26
-rw-r--r--qt-quick/Main.qml332
-rw-r--r--qt-quick/Makefile30
-rw-r--r--qt-quick/README.md28
-rw-r--r--qt-quick/adapter.cpp163
-rw-r--r--qt-quick/adapter.h55
-rw-r--r--qt-quick/main.cpp16
7 files changed, 650 insertions, 0 deletions
diff --git a/qt-quick/CMakeLists.txt b/qt-quick/CMakeLists.txt
new file mode 100644
index 0000000..8c7874e
--- /dev/null
+++ b/qt-quick/CMakeLists.txt
@@ -0,0 +1,26 @@
1cmake_minimum_required(VERSION 3.16)
2
3project(nissyqt VERSION 0.1 LANGUAGES CXX)
4
5set(CMAKE_CXX_STANDARD_REQUIRED ON)
6set(CMAKE_CXX_STANDARD 20)
7
8find_package(Qt6 REQUIRED COMPONENTS Quick Concurrent)
9
10qt_standard_project_setup(REQUIRES 6.5)
11
12qt_add_executable(appnissyqt
13 main.cpp
14 adapter.cpp adapter.h
15 build/nissy.h build/nissy.cpp build/nissy.o
16)
17
18qt_add_qml_module(appnissyqt
19 URI nissyqt
20 VERSION 1.0
21 QML_FILES Main.qml
22)
23
24target_link_libraries(appnissyqt
25 PRIVATE Qt6::Quick
26)
diff --git a/qt-quick/Main.qml b/qt-quick/Main.qml
new file mode 100644
index 0000000..a6e034a
--- /dev/null
+++ b/qt-quick/Main.qml
@@ -0,0 +1,332 @@
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5Window {
6 id: mainWindow
7
8 width: 800
9 height: 600
10 visible: true
11 title: "Nissy 3.0 - Preview"
12
13 SplitView {
14 id: splitView
15 anchors.fill: parent
16 orientation: Qt.Vertical
17
18 handle: Rectangle {
19 implicitHeight: 3
20 color: SplitHandle.hovered ? "black" : "#AAAAAA"
21 }
22
23 component MyScrollBar: ScrollBar {
24 orientation: Qt.Vertical
25 size: parent.height
26 policy: ScrollBar.AlwaysOn
27 anchors.right: parent.right
28 anchors.top: parent.top
29 anchors.bottom: parent.bottom
30 contentItem: Rectangle {
31 implicitWidth: 4
32 radius: implicitWidth/2
33 color: "black"
34 }
35 background: Rectangle {
36 implicitWidth: 4
37 radius: implicitWidth/2
38 color: "#AAAAAA"
39 }
40 }
41
42 ColumnLayout {
43 id: mainArea
44
45 property alias scramble: scrambleRow.scramble
46 property alias solver: solverCfg.solver
47 property alias minmoves: solverCfg.minmoves
48 property alias maxmoves: solverCfg.maxmoves
49 property alias maxsolutions: solverCfg.maxsolutions
50 property alias optimal: solverCfg.optimal
51 property alias sols: sols.text
52 property alias solsHeader: solsHeader.text
53
54 property bool solutionsLoading: false
55
56 anchors.top: parent.top
57 anchors.left: parent.left
58 anchors.right: parent.right
59 anchors.bottom: logView.top
60 anchors.margins: 6
61 spacing: 10
62
63 SplitView.minimumHeight: 180
64 SplitView.preferredHeight: 500
65
66 component Separator: Rectangle {
67 height: 1
68 Layout.fillWidth: true
69 color: "black"
70 }
71
72 component OptionalValue: RowLayout {
73 property alias currentValue: valueRect.value
74 property alias from: spinBox.from
75 property alias to: spinBox.to
76 property alias defaultValue: spinBox.value
77 property alias defaultEnabled: sw.checked
78 property alias label: sw.text
79 property int defaultSavedValue: 1
80 property int savedValue: defaultSavedValue
81
82 Switch {
83 id: sw
84
85 checked: true
86
87 onToggled: () => {
88 if (checked) {
89 currentValue = savedValue
90 } else {
91 savedValue = currentValue
92 currentValue = spinBox.to
93 }
94 }
95 }
96
97 Rectangle {
98 id: valueRect
99
100 property alias enabled: sw.checked
101 property alias value: spinBox.value
102
103 width: 65
104 height: 20
105
106 SpinBox {
107 id: spinBox
108
109 width: parent.width
110 editable: true
111 enabled: parent.enabled
112 }
113 }
114 }
115
116 ColumnLayout {
117 id: scrambleRow
118
119 property alias scramble: scrambleRowLayout.scramble
120
121 RowLayout {
122 id: scrambleRowLayout
123
124 property alias scramble: scrambleEditor.text
125
126 spacing: 6
127
128 TextField {
129 id: scrambleEditor
130
131 placeholderText: "Enter scramble here"
132 Layout.fillWidth: true
133 padding: 4
134
135 readonly property bool empty: text.trim().length == 0
136 readonly property bool valid: NissyAdapter.isValidScramble(text)
137
138 onAccepted: if (!empty && valid) submitScramble()
139 }
140
141 Button {
142 id: solveButton
143
144 enabled: !scrambleEditor.empty && scrambleEditor.valid &&
145 !mainArea.solutionsLoading
146 text: "Solve!"
147
148 onPressed: submitScramble()
149 }
150 }
151
152 Label {
153 id: invalidScrambleWarning
154 text: scrambleEditor.empty || scrambleEditor.valid ?
155 "" : "Invalid Scramble"
156 }
157 }
158
159 Separator {}
160
161 ColumnLayout {
162 id: solverCfg
163
164 property alias minmoves: minMaxRow.min
165 property alias maxmoves: minMaxRow.max
166 property alias maxsolutions: maxSols.currentValue
167 property alias optimal: optimal.currentValue
168 property alias solver: solverRow.solver
169
170 RowLayout {
171 id: solverRow
172
173 property alias solver: comboBox.currentValue
174
175 Label { text: "Solver" }
176 ComboBox {
177 id: comboBox
178
179 currentIndex: 3
180 textRole: "text"
181 valueRole: "name"
182 implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted
183
184 model: ListModel {
185 ListElement { text: "h48 h=0, k=4 (59 Mb)"; name: "h48h0k4" }
186 ListElement { text: "h48 h=1, k=2 (115 Mb)"; name: "h48h1k2" }
187 ListElement { text: "h48 h=2, k=2 (171 Mb)"; name: "h48h2k2" }
188 ListElement { text: "h48 h=3, k=2 (283 Mb)"; name: "h48h3k2" }
189 ListElement { text: "h48 h=7, k=2 (3.6 Gb)"; name: "h48h7k2" }
190 }
191 }
192 }
193
194 RowLayout {
195 id: minMaxRow
196
197 property alias min: slider.min
198 property alias max: slider.max
199
200 Rectangle {
201 width: 100
202 height: 20
203 Label { text: "Min moves: " + slider.min }
204 }
205 RangeSlider {
206 id: slider
207 from: 0
208 to: 20
209 first.value: from
210 second.value: to
211 stepSize: 1
212 snapMode: RangeSlider.SnapAlways
213
214 readonly property int min: Math.round(first.value)
215 readonly property int max: Math.round(second.value)
216 }
217 Rectangle {
218 width: 100
219 height: 20
220 Label { text: "Max moves: " + slider.max }
221 }
222 }
223
224 OptionalValue {
225 id: optimal
226
227 label: "Above optimal by at most"
228 from: 0
229 to: 20
230 defaultValue: 20
231 defaultEnabled: false
232 defaultSavedValue: 0
233 }
234
235 OptionalValue {
236 id: maxSols
237
238 label: "Limit number of solutions to"
239 from: 1
240 to: 999
241 defaultValue: 1
242 defaultEnabled: true
243 defaultSavedValue: 1
244 }
245 }
246
247 Separator {}
248
249 StackLayout {
250 Layout.maximumHeight: 30
251 currentIndex: mainArea.solutionsLoading ? 0 : 1
252
253 BusyIndicator { running: mainArea.solutionsLoading }
254 Label { id: solsHeader }
255 }
256
257 ScrollView {
258 Layout.fillHeight: true
259 Layout.fillWidth: true
260 Layout.bottomMargin: 10
261 ScrollBar.vertical: MyScrollBar {}
262
263 TextEdit {
264 id: sols
265 readOnly: true
266 font.family: "Monospace"
267 }
268 }
269 }
270
271 ScrollView {
272 id: logView
273
274 property alias text: logText.text
275
276 anchors.left: parent.left
277 anchors.right: parent.right
278 anchors.bottom: parent.bottom
279 anchors.margins: 6
280
281 SplitView.preferredHeight: 300
282
283 background: Rectangle {
284 color: "#404040"
285 radius: 4
286 }
287
288 ScrollBar.vertical: MyScrollBar {
289 id: scrollBar
290 position: 1.0 - size
291 }
292 Label {
293 id: logText
294
295 font.family: "Monospace"
296 color: "white"
297 }
298 }
299 }
300
301 function submitScramble() {
302 mainArea.solutionsLoading = true;
303 mainArea.solsHeader = ""
304 mainArea.sols = ""
305 logView.text = ""
306 NissyAdapter.requestSolve(
307 mainArea.scramble,
308 mainArea.solver,
309 mainArea.minmoves,
310 mainArea.maxmoves,
311 mainArea.maxsolutions,
312 mainArea.optimal
313 )
314 }
315
316 Connections {
317 target: NissyAdapter
318 function onSolutionsReady(header, sols) {
319 mainArea.solutionsLoading = false
320 mainArea.solsHeader = header
321 mainArea.sols = sols
322 }
323 function onSolverError(msg) {
324 mainArea.solutionsLoading = false
325 mainArea.solsHeader = msg
326 mainArea.sols = ""
327 }
328 function onAppendLog(msg) {
329 logView.text += msg
330 }
331 }
332}
diff --git a/qt-quick/Makefile b/qt-quick/Makefile
new file mode 100644
index 0000000..3132ab2
--- /dev/null
+++ b/qt-quick/Makefile
@@ -0,0 +1,30 @@
1all: nissyqt
2
3build:
4 mkdir -p build
5
6build/nissy.h:
7 cp ../nissy-core/cpp/nissy.h build/
8
9build/nissy.cpp:
10 cp ../nissy-core/cpp/nissy.cpp build/
11
12../nissy-core/config.mk:
13 cd ../nissy-core && ./configure.sh
14
15build/nissy.o: build ../nissy-core/config.mk
16 cd ../nissy-core && make nissy.o
17 cp ../nissy-core/nissy.o build/
18
19nissyqt: build/nissy.o build/nissy.h build/nissy.cpp
20 cmake . -B build
21 cd build && make
22 cp build/appnissyqt ./run
23
24run:
25 QT_LOGGING_RULES="*.debug=true; qt.*.debug=false" ./run
26
27clean:
28 rm -rf run build
29
30.PHONY: all nissyqt run clean
diff --git a/qt-quick/README.md b/qt-quick/README.md
new file mode 100644
index 0000000..54c4136
--- /dev/null
+++ b/qt-quick/README.md
@@ -0,0 +1,28 @@
1# QT-quick implementation of a nissy UI
2
3This is a UI for nissy written using
4[QT Quick](https://doc.qt.io/qt-6/qtquick-index.html).
5
6It has been tested only on Linux with QT 6.9.
7Building requires CMake.
8
9To build this project, run
10
11```
12make
13```
14
15To run it you can use
16
17```
18./run
19``
20
21or
22
23```
24make run
25```
26
27The latter command is going to build the project (if has not already
28been built) and run it with some debug options enabled.
diff --git a/qt-quick/adapter.cpp b/qt-quick/adapter.cpp
new file mode 100644
index 0000000..9594c56
--- /dev/null
+++ b/qt-quick/adapter.cpp
@@ -0,0 +1,163 @@
1#include "adapter.h"
2
3#include <fstream>
4#include <sstream>
5#include <string>
6#include <vector>
7#include <QDebug>
8#include <QtConcurrent/QtConcurrent>
9
10const std::string tablesdir = "../nissy-core/tables/";
11
12void logWrapper(const char *str, void *data)
13{
14 auto f = *reinterpret_cast<std::function<void(std::string)>*>(data);
15 f(std::string{str});
16}
17
18NissyAdapter::NissyAdapter()
19{
20 // TODO: this list must be kept in sync with UI code, it is a bit ugly
21 std::vector<std::string> solverNames {
22 "h48h0k4",
23 "h48h1k2",
24 "h48h2k2",
25 "h48h3k2",
26 "h48h7k2",
27 };
28
29 for (auto s : solverNames)
30 initSolver(s);
31
32 writeLog = [&](std::string str) {
33 emit appendLog(QString::fromStdString(str));
34 };
35
36 nissy::set_logger(&logWrapper, &writeLog);
37}
38
39void NissyAdapter::initSolver(const std::string& s) {
40 auto se = nissy::solver::get(s);
41 if (std::holds_alternative<nissy::error>(se)) {
42 qDebug("Error loading solver!");
43 return;
44 }
45 auto ss = std::get<nissy::solver>(se);
46 solvers.push_back(ss);
47}
48
49bool NissyAdapter::loadSolverData(nissy::solver& solver) {
50 if (solver.data_checked)
51 return true;
52
53 std::filesystem::path filePath(tablesdir + solver.id);
54 if (!std::filesystem::exists(filePath)) {
55 logLine("Data file for solver " + solver.name + " not found, "
56 "generating it...");
57 auto err = solver.generate_data();
58 if (!err.ok()) {
59 emit solverError(QString("Error generating data!"));
60 return false;
61 }
62 std::filesystem::create_directory(tablesdir);
63 std::ofstream ofs(filePath, std::ios::binary);
64 ofs.write(reinterpret_cast<char *>(solver.data.data()),
65 solver.size);
66 ofs.close();
67 logLine("Data generated succesfully");
68 } else {
69 logLine("Reading data for solver " + solver.name +
70 " from file");
71 std::ifstream ifs(filePath, std::ios::binary);
72 solver.read_data(ifs);
73 ifs.close();
74 logLine("Data loaded");
75 }
76
77 logLine("Checking data integrity "
78 "(this is done only once per solver per session)...");
79 if (!solver.check_data().ok()) {
80 emit solverError(QString("Error reading data!"));
81 return false;
82 }
83 logLine("Data checked");
84
85 return true;
86}
87
88Q_INVOKABLE bool NissyAdapter::isValidScramble(QString qscr)
89{
90 nissy::cube c;
91 return c.move(qscr.toStdString()).ok();
92}
93
94Q_INVOKABLE void NissyAdapter::requestSolve(
95 QString scramble,
96 QString solver,
97 int minmoves,
98 int maxmoves,
99 int maxsolutions,
100 int optimal
101)
102{
103 nissy::cube c;
104 if (!c.move(scramble.toStdString()).ok()) {
105 emit solverError(QString("Unexpected error: invalid scramble"));
106 return;
107 }
108
109 nissy::solver *ss = nullptr;
110 for (auto& s : solvers)
111 if (s.name == solver)
112 ss = &s;
113 if (ss == nullptr) {
114 std::string msg = "Error: solver '" + solver.toStdString()
115 + "' not available";
116 emit solverError(QString::fromStdString(msg));
117 return;
118 }
119
120 SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves,
121 (unsigned)maxsolutions, (unsigned)optimal};
122 auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts);
123 return;
124}
125
126void NissyAdapter::startSolve(SolveOptions opts)
127{
128 loadSolverData(*opts.solver);
129
130 auto result = opts.solver->solve(opts.cube, nissy::nissflag::NORMAL,
131 opts.minmoves, opts.maxmoves, opts.maxsolutions, opts.optimal, 8);
132
133 if (!result.err.ok()) {
134 std::string msg = "Error computing solutions: " +
135 std::to_string(result.err.value);
136 emit solverError(QString::fromStdString(msg));
137 return;
138 }
139
140 auto& sols = result.solutions;
141 if (sols.size() == 0) {
142 emit solutionsReady("No solution found", "");
143 } else {
144 std::stringstream hs;
145 hs << "Found " << sols.size() << " solution"
146 << (sols.size() > 1 ? "s:" : ":");
147
148 std::stringstream ss;
149 for (auto s : sols) {
150 auto n = nissy::count_moves(s).value;
151 ss << s << " (" << n << ")" << std::endl; // TODO: remove last newline
152 }
153 emit solutionsReady(QString::fromStdString(hs.str()),
154 QString::fromStdString(ss.str()));
155 }
156}
157
158void NissyAdapter::logLine(std::string str)
159{
160 std::stringstream ss;
161 ss << str << std::endl;
162 writeLog(ss.str());
163}
diff --git a/qt-quick/adapter.h b/qt-quick/adapter.h
new file mode 100644
index 0000000..bde618f
--- /dev/null
+++ b/qt-quick/adapter.h
@@ -0,0 +1,55 @@
1#ifndef ADAPTER_H
2#define ADAPTER_H
3
4#include "../nissy-core/cpp/nissy.h"
5
6#include <map>
7#include <string>
8#include <QObject>
9#include <QtQmlIntegration>
10
11struct SolveOptions {
12 nissy::cube cube;
13 nissy::solver *solver;
14 unsigned minmoves;
15 unsigned maxmoves;
16 unsigned maxsolutions;
17 unsigned optimal;
18};
19
20class NissyAdapter : public QObject {
21 Q_OBJECT
22 QML_SINGLETON
23 QML_ELEMENT
24
25public:
26 static constexpr int maxSolutionsHardLimit = 9999;
27
28 NissyAdapter();
29
30 Q_INVOKABLE bool isValidScramble(QString);
31 Q_INVOKABLE void requestSolve(
32 QString scramble,
33 QString solver,
34 int minmoves,
35 int maxmoves,
36 int maxsolutions,
37 int optimal
38 );
39
40signals:
41 void solutionsReady(QString, QString);
42 void solverError(QString);
43 void appendLog(QString);
44
45private:
46 std::vector<nissy::solver> solvers;
47 std::function<void(std::string)> writeLog;
48
49 void initSolver(const std::string&);
50 void startSolve(SolveOptions);
51 bool loadSolverData(nissy::solver&);
52 void logLine(std::string);
53};
54
55#endif
diff --git a/qt-quick/main.cpp b/qt-quick/main.cpp
new file mode 100644
index 0000000..a012642
--- /dev/null
+++ b/qt-quick/main.cpp
@@ -0,0 +1,16 @@
1#include "adapter.h"
2
3#include <QGuiApplication>
4#include <QQmlApplicationEngine>
5
6int main(int argc, char *argv[])
7{
8 QGuiApplication app(argc, argv);
9 QQmlApplicationEngine engine;
10 QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
11 &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
12
13 engine.loadFromModule("nissyqt", "Main");
14
15 return app.exec();
16}

Generated with cgit - Back to sebastiano.tronto.net