commit f91e5c4c83a071ab720417d490fd44a8f4678bc1
parent c182f3f16e56e9533060ffa06b25391e239a28ee
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date: Thu, 17 Apr 2025 16:27:38 +0200
Improvements to QT UI
Diffstat:
6 files changed, 366 insertions(+), 271 deletions(-)
diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt
@@ -19,7 +19,7 @@ qt_add_executable(appnissyqt
qt_add_qml_module(appnissyqt
URI nissyqt
VERSION 1.0
- QML_FILES NissyMain.qml
+ QML_FILES Main.qml
)
target_link_libraries(appnissyqt
diff --git a/qt/Main.qml b/qt/Main.qml
@@ -0,0 +1,332 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Window {
+ id: mainWindow
+
+ width: 800
+ height: 600
+ visible: true
+ title: "Nissy 3.0 - Preview"
+
+ SplitView {
+ id: splitView
+ anchors.fill: parent
+ orientation: Qt.Vertical
+
+ handle: Rectangle {
+ implicitHeight: 3
+ color: SplitHandle.hovered ? "black" : "#AAAAAA"
+ }
+
+ component MyScrollBar: ScrollBar {
+ orientation: Qt.Vertical
+ size: parent.height
+ policy: ScrollBar.AlwaysOn
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ contentItem: Rectangle {
+ implicitWidth: 4
+ radius: implicitWidth/2
+ color: "black"
+ }
+ background: Rectangle {
+ implicitWidth: 4
+ radius: implicitWidth/2
+ color: "#AAAAAA"
+ }
+ }
+
+ ColumnLayout {
+ id: mainArea
+
+ property alias scramble: scrambleRow.scramble
+ property alias solver: solverCfg.solver
+ property alias minmoves: solverCfg.minmoves
+ property alias maxmoves: solverCfg.maxmoves
+ property alias maxsolutions: solverCfg.maxsolutions
+ property alias optimal: solverCfg.optimal
+ property alias sols: sols.text
+ property alias solsHeader: solsHeader.text
+
+ property bool solutionsLoading: false
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: logView.top
+ anchors.margins: 6
+ spacing: 10
+
+ SplitView.minimumHeight: 180
+ SplitView.preferredHeight: 500
+
+ component Separator: Rectangle {
+ height: 1
+ Layout.fillWidth: true
+ color: "black"
+ }
+
+ component OptionalValue: RowLayout {
+ property alias currentValue: valueRect.value
+ property alias from: spinBox.from
+ property alias to: spinBox.to
+ property alias defaultValue: spinBox.value
+ property alias defaultEnabled: sw.checked
+ property alias label: sw.text
+ property int defaultSavedValue: 1
+ property int savedValue: defaultSavedValue
+
+ Switch {
+ id: sw
+
+ checked: true
+
+ onToggled: () => {
+ if (checked) {
+ currentValue = savedValue
+ } else {
+ savedValue = currentValue
+ currentValue = spinBox.to
+ }
+ }
+ }
+
+ Rectangle {
+ id: valueRect
+
+ property alias enabled: sw.checked
+ property alias value: spinBox.value
+
+ width: 65
+ height: 20
+
+ SpinBox {
+ id: spinBox
+
+ width: parent.width
+ editable: true
+ enabled: parent.enabled
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: scrambleRow
+
+ property alias scramble: scrambleRowLayout.scramble
+
+ RowLayout {
+ id: scrambleRowLayout
+
+ property alias scramble: scrambleEditor.text
+
+ spacing: 6
+
+ TextField {
+ id: scrambleEditor
+
+ placeholderText: "Enter scramble here"
+ Layout.fillWidth: true
+ padding: 4
+
+ readonly property bool empty: text.trim().length == 0
+ readonly property bool valid: NissyAdapter.isValidScramble(text)
+
+ onAccepted: if (!empty && valid) submitScramble()
+ }
+
+ Button {
+ id: solveButton
+
+ enabled: !scrambleEditor.empty && scrambleEditor.valid &&
+ !mainArea.solutionsLoading
+ text: "Solve!"
+
+ onPressed: submitScramble()
+ }
+ }
+
+ Label {
+ id: invalidScrambleWarning
+ text: scrambleEditor.empty || scrambleEditor.valid ?
+ "" : "Invalid Scramble"
+ }
+ }
+
+ Separator {}
+
+ ColumnLayout {
+ id: solverCfg
+
+ property alias minmoves: minMaxRow.min
+ property alias maxmoves: minMaxRow.max
+ property alias maxsolutions: maxSols.currentValue
+ property alias optimal: optimal.currentValue
+ property alias solver: solverRow.solver
+
+ RowLayout {
+ id: solverRow
+
+ property alias solver: comboBox.currentValue
+
+ Label { text: "Solver" }
+ ComboBox {
+ id: comboBox
+
+ currentIndex: 3
+ textRole: "text"
+ valueRole: "name"
+ implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted
+
+ model: ListModel {
+ ListElement { text: "h48 h=0, k=4 (59 Mb)"; name: "h48h0k4" }
+ ListElement { text: "h48 h=1, k=2 (115 Mb)"; name: "h48h1k2" }
+ ListElement { text: "h48 h=2, k=2 (171 Mb)"; name: "h48h2k2" }
+ ListElement { text: "h48 h=3, k=2 (283 Mb)"; name: "h48h3k2" }
+ ListElement { text: "h48 h=7, k=2 (3.6 Gb)"; name: "h48h7k2" }
+ }
+ }
+ }
+
+ RowLayout {
+ id: minMaxRow
+
+ property alias min: slider.min
+ property alias max: slider.max
+
+ Rectangle {
+ width: 100
+ height: 20
+ Label { text: "Min moves: " + slider.min }
+ }
+ RangeSlider {
+ id: slider
+ from: 0
+ to: 20
+ first.value: from
+ second.value: to
+ stepSize: 1
+ snapMode: RangeSlider.SnapAlways
+
+ readonly property int min: Math.round(first.value)
+ readonly property int max: Math.round(second.value)
+ }
+ Rectangle {
+ width: 100
+ height: 20
+ Label { text: "Max moves: " + slider.max }
+ }
+ }
+
+ OptionalValue {
+ id: optimal
+
+ label: "Above optimal by at most"
+ from: 0
+ to: 20
+ defaultValue: 20
+ defaultEnabled: false
+ defaultSavedValue: 0
+ }
+
+ OptionalValue {
+ id: maxSols
+
+ label: "Limit number of solutions to"
+ from: 1
+ to: 999
+ defaultValue: 1
+ defaultEnabled: true
+ defaultSavedValue: 1
+ }
+ }
+
+ Separator {}
+
+ StackLayout {
+ Layout.maximumHeight: 30
+ currentIndex: mainArea.solutionsLoading ? 0 : 1
+
+ BusyIndicator { running: mainArea.solutionsLoading }
+ Label { id: solsHeader }
+ }
+
+ ScrollView {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.bottomMargin: 10
+ ScrollBar.vertical: MyScrollBar {}
+
+ TextEdit {
+ id: sols
+ readOnly: true
+ font.family: "Monospace"
+ }
+ }
+ }
+
+ ScrollView {
+ id: logView
+
+ property alias text: logText.text
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: 6
+
+ SplitView.preferredHeight: 300
+
+ background: Rectangle {
+ color: "#404040"
+ radius: 4
+ }
+
+ ScrollBar.vertical: MyScrollBar {
+ id: scrollBar
+ position: 1.0 - size
+ }
+ Label {
+ id: logText
+
+ font.family: "Monospace"
+ color: "white"
+ }
+ }
+ }
+
+ function submitScramble() {
+ mainArea.solutionsLoading = true;
+ mainArea.solsHeader = ""
+ mainArea.sols = ""
+ logView.text = ""
+ NissyAdapter.requestSolve(
+ mainArea.scramble,
+ mainArea.solver,
+ mainArea.minmoves,
+ mainArea.maxmoves,
+ mainArea.maxsolutions,
+ mainArea.optimal
+ )
+ }
+
+ Connections {
+ target: NissyAdapter
+ function onSolutionsReady(header, sols) {
+ mainArea.solutionsLoading = false
+ mainArea.solsHeader = header
+ mainArea.sols = sols
+ }
+ function onSolverError(msg) {
+ mainArea.solutionsLoading = false
+ mainArea.solsHeader = msg
+ mainArea.sols = ""
+ }
+ function onAppendLog(msg) {
+ logView.text += msg
+ }
+ }
+}
diff --git a/qt/NissyMain.qml b/qt/NissyMain.qml
@@ -1,261 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-Window {
- id: mainWindow
-
- width: 800
- height: 600
- visible: true
- title: "Nissy 3.0 - Preview"
-
- SplitView {
- id: splitView
- anchors.fill: parent
- orientation: Qt.Vertical
-
- handle: Rectangle {
- implicitHeight: 3
- color: SplitHandle.hovered ? "black" : "#AAAAAA"
- }
-
- component MyScrollBar: ScrollBar {
- orientation: Qt.Vertical
- size: parent.height
- policy: ScrollBar.AlwaysOn
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.bottom: parent.bottom
- contentItem: Rectangle {
- implicitWidth: 4
- radius: implicitWidth/2
- color: "black"
- }
- background: Rectangle {
- implicitWidth: 4
- radius: implicitWidth/2
- color: "#AAAAAA"
- }
- }
-
- ColumnLayout {
- id: mainArea
-
- property alias scramble: scrambleRow.scramble
- property alias minmoves: solverCfg.minmoves
- property alias maxmoves: solverCfg.maxmoves
- property alias maxsolutions: solverCfg.maxsolutions
- property alias sols: sols.text
- property alias solsHeader: solsHeader.text
-
- property bool solutionsLoading: false
-
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.bottom: logView.top
- anchors.margins: 6
- spacing: 10
-
- SplitView.minimumHeight: 180
- SplitView.preferredHeight: 300
-
- component Separator: Rectangle {
- height: 1
- Layout.fillWidth: true
- color: "black"
- }
-
- ColumnLayout {
- id: scrambleRow
-
- property alias scramble: scrambleRowLayout.scramble
-
- RowLayout {
- id: scrambleRowLayout
-
- property alias scramble: scrambleEditor.text
-
- spacing: 6
-
- TextField {
- id: scrambleEditor
-
- placeholderText: "Enter scramble here"
- Layout.fillWidth: true
- padding: 4
-
- readonly property bool empty: text.trim().length == 0
- readonly property bool valid: NissyAdapter.isValidScramble(text)
-
- onAccepted: if (!empty && valid) submitScramble()
- }
-
- Button {
- id: solveButton
-
- enabled: !scrambleEditor.empty && scrambleEditor.valid &&
- !mainArea.solutionsLoading
- text: "Solve!"
-
- onPressed: submitScramble()
- }
- }
-
- Label {
- id: invalidScrambleWarning
- text: scrambleEditor.empty || scrambleEditor.valid ?
- "" : "Invalid Scramble"
- }
- }
-
- Separator {}
-
- ColumnLayout {
- id: solverCfg
- property alias minmoves: minMaxRow.min
- property alias maxmoves: minMaxRow.max
- property alias maxsolutions: maxOptimal.maxsolutions
-
- RowLayout {
- id: minMaxRow
-
- property alias min: slider.min
- property alias max: slider.max
-
- Rectangle {
- width: 100
- height: 20
- Label { text: "Min moves: " + slider.min }
- }
- RangeSlider {
- id: slider
- from: 0
- to: 20
- first.value: from
- second.value: to
- stepSize: 1
- snapMode: RangeSlider.SnapAlways
-
- readonly property int min: Math.round(first.value)
- readonly property int max: Math.round(second.value)
- }
- Rectangle {
- width: 100
- height: 20
- Label { text: "Max moves: " + slider.max }
- }
- }
-
- RowLayout {
- id: maxOptimal
-
- property alias maxsolutions: maxSolsRect.maxsolutions
-
- Rectangle {
- width: 220
- height: 20
- Label { text: "Maximum number of solutions:" }
- }
- Rectangle {
- id: maxSolsRect
-
- property int maxsolutions: parseInt(textField.text)
-
- width: 35
- height: 20
-
- TextField {
- id: textField
-
- width: parent.width
- text: "1"
- validator: IntValidator{ bottom: 1; top: 999; }
-
- onAccepted: submitScramble()
- }
- }
- }
- }
-
- Separator {}
-
- Label { id: solsHeader }
-
- ScrollView {
- Layout.fillHeight: true
- Layout.fillWidth: true
- Layout.bottomMargin: 10
- ScrollBar.vertical: MyScrollBar {}
-
- TextEdit {
- id: sols
- readOnly: true
- font.family: "Monospace"
- }
- }
- }
-
- ScrollView {
- id: logView
-
- property alias text: logText.text
-
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.bottom: parent.bottom
- anchors.margins: 6
-
- SplitView.preferredHeight: 300
-
- background: Rectangle {
- color: "#404040"
- radius: 4
- }
-
- ScrollBar.vertical: MyScrollBar {
- id: scrollBar
- position: 1.0 - size
- }
- Label {
- id: logText
-
- font.family: "Monospace"
- color: "white"
- }
- }
- }
-
- function submitScramble() {
- mainArea.solutionsLoading = true;
- mainArea.solsHeader = "Loading solutions..."
- mainArea.sols = ""
- logView.text = ""
- NissyAdapter.requestSolve(
- mainArea.scramble,
- "h48h3k2",
- mainArea.minmoves,
- mainArea.maxmoves,
- mainArea.maxsolutions,
- -1
- )
- }
-
- Connections {
- target: NissyAdapter
- function onSolutionsReady(header, sols) {
- mainArea.solutionsLoading = false
- mainArea.solsHeader = header
- mainArea.sols = sols
- }
- function onSolverError(msg) {
- mainArea.solutionsLoading = false
- mainArea.solusHeader = msg
- mainArea.sols = ""
- }
- function onAppendLog(msg) {
- logView.text += msg
- }
- }
-}
diff --git a/qt/adapter.cpp b/qt/adapter.cpp
@@ -9,20 +9,25 @@
void logWrapper(const char *str, void *data)
{
- auto f = *reinterpret_cast<std::function<void(const char*)>*>(data);
- f(str);
+ auto f = *reinterpret_cast<std::function<void(std::string)>*>(data);
+ f(std::string{str});
}
NissyAdapter::NissyAdapter()
{
+ // TODO: this list must be kept in sync with UI code, it is a bit ugly
std::vector<std::string> solverNames {
- "h48h3k2"
+ "h48h0k4",
+ "h48h1k2",
+ "h48h2k2",
+ "h48h3k2",
+ "h48h7k2",
};
for (auto s : solverNames)
initSolver(s);
- writeLog = [&](const char *str) {
+ writeLog = [&](std::string str) {
emit appendLog(QString::fromStdString(str));
};
@@ -45,6 +50,8 @@ bool NissyAdapter::loadSolverData(nissy::solver& solver) {
std::filesystem::path filePath("./tables/" + solver.id);
if (!std::filesystem::exists(filePath)) {
+ logLine("Data file for solver " + solver.name + " not found, "
+ "generating it...");
auto err = solver.generate_data();
if (!err.ok()) {
emit solverError(QString("Error generating data!"));
@@ -55,16 +62,23 @@ bool NissyAdapter::loadSolverData(nissy::solver& solver) {
ofs.write(reinterpret_cast<char *>(solver.data.data()),
solver.size);
ofs.close();
+ logLine("Data generated succesfully");
} else {
+ logLine("Reading data for solver " + solver.name +
+ " from file");
std::ifstream ifs(filePath, std::ios::binary);
solver.read_data(ifs);
ifs.close();
+ logLine("Data loaded");
}
+ logLine("Checking data integrity "
+ "(this is done only once per solver per session)...");
if (!solver.check_data().ok()) {
emit solverError(QString("Error reading data!"));
return false;
}
+ logLine("Data checked");
return true;
}
@@ -102,7 +116,7 @@ Q_INVOKABLE void NissyAdapter::requestSolve(
}
SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves,
- (unsigned)maxsolutions, optimal};
+ (unsigned)maxsolutions, (unsigned)optimal};
auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts);
return;
}
@@ -132,9 +146,16 @@ void NissyAdapter::startSolve(SolveOptions opts)
std::stringstream ss;
for (auto s : sols) {
auto n = nissy::count_moves(s).value;
- ss << s << "(" << n << ")" << std::endl; // TODO: remove last newline
+ ss << s << " (" << n << ")" << std::endl; // TODO: remove last newline
}
emit solutionsReady(QString::fromStdString(hs.str()),
QString::fromStdString(ss.str()));
}
}
+
+void NissyAdapter::logLine(std::string str)
+{
+ std::stringstream ss;
+ ss << str << std::endl;
+ writeLog(ss.str());
+}
diff --git a/qt/adapter.h b/qt/adapter.h
@@ -14,7 +14,7 @@ struct SolveOptions {
unsigned minmoves;
unsigned maxmoves;
unsigned maxsolutions;
- int optimal;
+ unsigned optimal;
};
class NissyAdapter : public QObject {
@@ -23,6 +23,8 @@ class NissyAdapter : public QObject {
QML_ELEMENT
public:
+ static constexpr int maxSolutionsHardLimit = 9999;
+
NissyAdapter();
Q_INVOKABLE bool isValidScramble(QString);
@@ -42,11 +44,12 @@ signals:
private:
std::vector<nissy::solver> solvers;
- std::function<void(const char*)> writeLog;
+ std::function<void(std::string)> writeLog;
void initSolver(const std::string&);
void startSolve(SolveOptions);
bool loadSolverData(nissy::solver&);
+ void logLine(std::string);
};
#endif
diff --git a/qt/main.cpp b/qt/main.cpp
@@ -10,7 +10,7 @@ int main(int argc, char *argv[])
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
&app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
- engine.loadFromModule("nissyqt", "NissyMain");
+ engine.loadFromModule("nissyqt", "Main");
return app.exec();
}