commit 97b5778172572504c8dfbe173b18281e786270c0
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date: Fri, 25 Apr 2025 16:18:26 +0200
Initial commit
Diffstat:
22 files changed, 1107 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,8 @@
+qt-quick/build
+qt-quick/nissy
+qt-quick/run
+qt-widgets/Makefile
+qt-widgets/build
+qt-widgets/generated_files
+qt-widgets/nissyqt
+qt-widgets/.qmake*
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "nissy-core"]
+ path = nissy-core
+ url = https://git.tronto.net/nissy-core
diff --git a/README.md b/README.md
@@ -0,0 +1,9 @@
+# Experiments with QT framework and nissy
+
+This repository contains some experiments with [QT](https://www.qt.io)
+in an attempt to make a usable UI for [nissy](https://nissy.tronto.net).
+
+These experiments are not fully-featured and I may not develop further in
+this direction.
+
+If you want to try them out, the most usable one is in the `qt-quick` folder.
diff --git a/nissy-core b/nissy-core
@@ -0,0 +1 @@
+Subproject commit 24c2fd2ffa582ea83172184b1da2816555340a01
diff --git a/qt-quick/CMakeLists.txt b/qt-quick/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.16)
+
+project(nissyqt VERSION 0.1 LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_STANDARD 20)
+
+find_package(Qt6 REQUIRED COMPONENTS Quick Concurrent)
+
+qt_standard_project_setup(REQUIRES 6.5)
+
+qt_add_executable(appnissyqt
+ main.cpp
+ adapter.cpp adapter.h
+ build/nissy.h build/nissy.cpp build/nissy.o
+)
+
+qt_add_qml_module(appnissyqt
+ URI nissyqt
+ VERSION 1.0
+ QML_FILES Main.qml
+)
+
+target_link_libraries(appnissyqt
+ PRIVATE Qt6::Quick
+)
diff --git a/qt-quick/Main.qml b/qt-quick/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-quick/Makefile b/qt-quick/Makefile
@@ -0,0 +1,30 @@
+all: nissyqt
+
+build:
+ mkdir -p build
+
+build/nissy.h:
+ cp ../nissy-core/cpp/nissy.h build/
+
+build/nissy.cpp:
+ cp ../nissy-core/cpp/nissy.cpp build/
+
+../nissy-core/config.mk:
+ cd ../nissy-core && ./configure.sh
+
+build/nissy.o: build ../nissy-core/config.mk
+ cd ../nissy-core && make nissy.o
+ cp ../nissy-core/nissy.o build/
+
+nissyqt: build/nissy.o build/nissy.h build/nissy.cpp
+ cmake . -B build
+ cd build && make
+ cp build/appnissyqt ./run
+
+run:
+ QT_LOGGING_RULES="*.debug=true; qt.*.debug=false" ./run
+
+clean:
+ rm -rf run build
+
+.PHONY: all nissyqt run clean
diff --git a/qt-quick/README.md b/qt-quick/README.md
@@ -0,0 +1,28 @@
+# QT-quick implementation of a nissy UI
+
+This is a UI for nissy written using
+[QT Quick](https://doc.qt.io/qt-6/qtquick-index.html).
+
+It has been tested only on Linux with QT 6.9.
+Building requires CMake.
+
+To build this project, run
+
+```
+make
+```
+
+To run it you can use
+
+```
+./run
+``
+
+or
+
+```
+make run
+```
+
+The latter command is going to build the project (if has not already
+been built) and run it with some debug options enabled.
diff --git a/qt-quick/adapter.cpp b/qt-quick/adapter.cpp
@@ -0,0 +1,163 @@
+#include "adapter.h"
+
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vector>
+#include <QDebug>
+#include <QtConcurrent/QtConcurrent>
+
+const std::string tablesdir = "../nissy-core/tables/";
+
+void logWrapper(const char *str, void *data)
+{
+ 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 {
+ "h48h0k4",
+ "h48h1k2",
+ "h48h2k2",
+ "h48h3k2",
+ "h48h7k2",
+ };
+
+ for (auto s : solverNames)
+ initSolver(s);
+
+ writeLog = [&](std::string str) {
+ emit appendLog(QString::fromStdString(str));
+ };
+
+ nissy::set_logger(&logWrapper, &writeLog);
+}
+
+void NissyAdapter::initSolver(const std::string& s) {
+ auto se = nissy::solver::get(s);
+ if (std::holds_alternative<nissy::error>(se)) {
+ qDebug("Error loading solver!");
+ return;
+ }
+ auto ss = std::get<nissy::solver>(se);
+ solvers.push_back(ss);
+}
+
+bool NissyAdapter::loadSolverData(nissy::solver& solver) {
+ if (solver.data_checked)
+ return true;
+
+ std::filesystem::path filePath(tablesdir + 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!"));
+ return false;
+ }
+ std::filesystem::create_directory(tablesdir);
+ std::ofstream ofs(filePath, std::ios::binary);
+ 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;
+}
+
+Q_INVOKABLE bool NissyAdapter::isValidScramble(QString qscr)
+{
+ nissy::cube c;
+ return c.move(qscr.toStdString()).ok();
+}
+
+Q_INVOKABLE void NissyAdapter::requestSolve(
+ QString scramble,
+ QString solver,
+ int minmoves,
+ int maxmoves,
+ int maxsolutions,
+ int optimal
+)
+{
+ nissy::cube c;
+ if (!c.move(scramble.toStdString()).ok()) {
+ emit solverError(QString("Unexpected error: invalid scramble"));
+ return;
+ }
+
+ nissy::solver *ss = nullptr;
+ for (auto& s : solvers)
+ if (s.name == solver)
+ ss = &s;
+ if (ss == nullptr) {
+ std::string msg = "Error: solver '" + solver.toStdString()
+ + "' not available";
+ emit solverError(QString::fromStdString(msg));
+ return;
+ }
+
+ SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves,
+ (unsigned)maxsolutions, (unsigned)optimal};
+ auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts);
+ return;
+}
+
+void NissyAdapter::startSolve(SolveOptions opts)
+{
+ loadSolverData(*opts.solver);
+
+ auto result = opts.solver->solve(opts.cube, nissy::nissflag::NORMAL,
+ opts.minmoves, opts.maxmoves, opts.maxsolutions, opts.optimal, 8);
+
+ if (!result.err.ok()) {
+ std::string msg = "Error computing solutions: " +
+ std::to_string(result.err.value);
+ emit solverError(QString::fromStdString(msg));
+ return;
+ }
+
+ auto& sols = result.solutions;
+ if (sols.size() == 0) {
+ emit solutionsReady("No solution found", "");
+ } else {
+ std::stringstream hs;
+ hs << "Found " << sols.size() << " solution"
+ << (sols.size() > 1 ? "s:" : ":");
+
+ std::stringstream ss;
+ for (auto s : sols) {
+ auto n = nissy::count_moves(s).value;
+ 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-quick/adapter.h b/qt-quick/adapter.h
@@ -0,0 +1,55 @@
+#ifndef ADAPTER_H
+#define ADAPTER_H
+
+#include "../nissy-core/cpp/nissy.h"
+
+#include <map>
+#include <string>
+#include <QObject>
+#include <QtQmlIntegration>
+
+struct SolveOptions {
+ nissy::cube cube;
+ nissy::solver *solver;
+ unsigned minmoves;
+ unsigned maxmoves;
+ unsigned maxsolutions;
+ unsigned optimal;
+};
+
+class NissyAdapter : public QObject {
+ Q_OBJECT
+ QML_SINGLETON
+ QML_ELEMENT
+
+public:
+ static constexpr int maxSolutionsHardLimit = 9999;
+
+ NissyAdapter();
+
+ Q_INVOKABLE bool isValidScramble(QString);
+ Q_INVOKABLE void requestSolve(
+ QString scramble,
+ QString solver,
+ int minmoves,
+ int maxmoves,
+ int maxsolutions,
+ int optimal
+ );
+
+signals:
+ void solutionsReady(QString, QString);
+ void solverError(QString);
+ void appendLog(QString);
+
+private:
+ std::vector<nissy::solver> solvers;
+ 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-quick/main.cpp b/qt-quick/main.cpp
@@ -0,0 +1,16 @@
+#include "adapter.h"
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+ QQmlApplicationEngine engine;
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
+ &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
+
+ engine.loadFromModule("nissyqt", "Main");
+
+ return app.exec();
+}
diff --git a/qt-widgets/README.md b/qt-widgets/README.md
@@ -0,0 +1,8 @@
+# QT-widgets implementation of a nissy UI
+
+This is a UI for nissy written using
+[QT Widgets](https://doc.qt.io/qt-6/qtwidgets-index.html)
+
+You can't actually build or run this as is, because it relies on
+an older version of the C++ bindings for nissy. I'll fix it
+at some point, maybe.
diff --git a/qt-widgets/adapter.cpp b/qt-widgets/adapter.cpp
@@ -0,0 +1,46 @@
+#include "adapter.h"
+
+#include <array>
+#include <filesystem>
+#include <fstream>
+#include <sstream>
+#include <QDebug>
+
+NissyAdapter::NissyAdapter()
+{
+ auto sid = nissy::solverinfo(defaultOptimalSolver);
+ auto [sz, dataid] = std::get<std::pair<size_t, std::string>>(sid);
+ const std::string path = "../nissy-core/tables/" + dataid;
+
+ std::filesystem::path filePath(path);
+ if (std::filesystem::file_size(filePath) != static_cast<uintmax_t>(sz))
+ qDebug("Error in file size!"); // TODO: better handle error, gentable
+
+ optimalSolverData.value.resize(sz);
+ std::ifstream ifs(path, std::ios::binary);
+ ifs.read(reinterpret_cast<char *>(optimalSolverData.value.data()), sz);
+ ifs.close();
+}
+
+NissyAdapter::~NissyAdapter() {}
+
+void NissyAdapter::solve(SolverConfiguration cfg)
+{
+ auto se = nissy::solve(cfg.cube, defaultOptimalSolver,
+ nissy::nissflag::NORMAL, cfg.minmoves, cfg.maxmoves,
+ cfg.maxsolutions, cfg.optimal, cfg.threads, optimalSolverData);
+ if (std::holds_alternative<nissy::error_t>(se)) {
+ auto code = std::get<nissy::error_t>(se).value;
+ emit solveDone(QString("Error " + code));
+ return;
+ }
+
+ auto [sols, stats] = std::get<nissy::solve_result_t>(se);
+ if (sols.size() == 0) {
+ emit solveDone(QString("No solution found"));
+ } else {
+ auto solstr = std::accumulate(
+ sols.begin(), sols.end(), std::string{});
+ emit solveDone(QString::fromStdString(solstr));
+ }
+}
diff --git a/qt-widgets/adapter.h b/qt-widgets/adapter.h
@@ -0,0 +1,35 @@
+#ifndef ADAPTER_H
+#define ADAPTER_H
+
+#include "../nissy-core/cpp/nissy.h"
+
+#include <string>
+#include <vector>
+#include <QObject>
+
+struct SolverConfiguration {
+ nissy::cube cube{nissy::cube::SOLVED};
+ unsigned minmoves{0};
+ unsigned maxmoves{20};
+ unsigned maxsolutions{1};
+ int optimal{-1};
+ int threads{8};
+};
+
+class NissyAdapter : public QObject {
+ Q_OBJECT
+
+public:
+ NissyAdapter();
+ ~NissyAdapter();
+ void solve(SolverConfiguration);
+
+signals:
+ void solveDone(QString);
+
+private:
+ static constexpr nissy::solver defaultOptimalSolver{"h48h3k2"};
+ nissy::solver_data_t optimalSolverData;
+};
+
+#endif
diff --git a/qt-widgets/main.cpp b/qt-widgets/main.cpp
@@ -0,0 +1,11 @@
+#include "nissywindow.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ NissyWindow w;
+ w.show();
+ return a.exec();
+}
diff --git a/qt-widgets/nissyqt.pro b/qt-widgets/nissyqt.pro
@@ -0,0 +1,18 @@
+# Source files
+SOURCES = main.cpp adapter.cpp nissywindow.cpp solvercfgwidget.cpp
+HEADERS = adapter.h nissywindow.h solvercfgwidget.h
+FORMS = nissywindow.ui solvercfgwidget.ui
+
+# Add nissy backend headers and code
+SOURCES += ../nissy-core/cpp/nissy.cpp
+HEADERS += ../nissy-core/cpp/nissy.h
+
+# Compiler configuration
+CONFIG += qt debug c++20
+LIBS += ../nissy-core/nissy.o
+QT += widgets concurrent
+
+# Destination folders for generated files
+MOC_DIR = generated_files
+UI_DIR = generated_files
+OBJECTS_DIR = build
diff --git a/qt-widgets/nissywindow.cpp b/qt-widgets/nissywindow.cpp
@@ -0,0 +1,35 @@
+#include "nissywindow.h"
+#include "./ui_nissywindow.h"
+#include "nissy/cpp/nissy.h"
+
+#include <QtConcurrent>
+
+NissyWindow::NissyWindow(QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::NissyWindow)
+{
+ ui->setupUi(this);
+
+ QObject::connect(ui->solverCfgWidget,
+ SIGNAL(solveRequest(const SolverConfiguration&)), this,
+ SLOT(startSolve(const SolverConfiguration&)));
+ QObject::connect(&adapter, SIGNAL(solveDone(QString)), this,
+ SLOT(showSolutions(QString)));
+}
+
+NissyWindow::~NissyWindow()
+{
+ delete ui;
+}
+
+void NissyWindow::startSolve(const SolverConfiguration& config)
+{
+ ui->solverCfgWidget->lockSubmit();
+ ui->solutionsLabel->setText("Loading solutions...");
+ auto _ = QtConcurrent::run(&NissyAdapter::solve, &adapter, config);
+}
+
+void NissyWindow::showSolutions(QString solutions) {
+ QString header = QString("Solution(s) found:\n");
+ ui->solutionsLabel->setText(header + solutions);
+ ui->solverCfgWidget->unlockSubmit();
+}
diff --git a/qt-widgets/nissywindow.h b/qt-widgets/nissywindow.h
@@ -0,0 +1,36 @@
+#ifndef NISSYWINDOW_H
+#define NISSYWINDOW_H
+
+#include "adapter.h"
+#include "solvercfgwidget.h"
+
+#include <QMainWindow>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QString>
+
+QT_BEGIN_NAMESPACE
+namespace Ui {
+class NissyWindow;
+}
+QT_END_NAMESPACE
+
+class NissyWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ NissyWindow(QWidget *parent = nullptr);
+ ~NissyWindow();
+
+private slots:
+ void showSolutions(QString);
+ void startSolve(const SolverConfiguration&);
+
+private:
+ Ui::NissyWindow *ui;
+ NissyAdapter adapter;
+};
+
+#endif
diff --git a/qt-widgets/nissywindow.ui b/qt-widgets/nissywindow.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+<class>NissyWindow</class>
+<widget class="QMainWindow" name="nissyWindow">
+
+<property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+</property>
+<property name="windowTitle">
+ <string>Nissy 3.0 - preview</string>
+</property>
+
+<widget class="QWidget" name="centralWidget">
+
+<layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="SolverCfgWidget" name="solverCfgWidget" />
+ </item>
+
+ <item>
+ <widget class="QLabel" name="solutionsLabel" />
+ </item>
+</layout>
+
+</widget>
+
+</widget>
+</ui>
diff --git a/qt-widgets/solvercfgwidget.cpp b/qt-widgets/solvercfgwidget.cpp
@@ -0,0 +1,90 @@
+#include "solvercfgwidget.h"
+#include "./ui_solvercfgwidget.h"
+
+enum class ScrambleState { empty, valid, invalid };
+
+ScrambleState getScrambleState(const std::string&);
+
+SolverCfgWidget::SolverCfgWidget(QWidget *parent)
+ : QWidget(parent), ui(new Ui::SolverCfgWidget)
+{
+ ui->setupUi(this);
+ onScrambleChanged(ui->scrambleEditor->text());
+
+ nmovesValidator = new QIntValidator(0, 20, this);
+ ui->minMovesEditor->setValidator(nmovesValidator);
+ ui->maxMovesEditor->setValidator(nmovesValidator);
+
+ QObject::connect(ui->scrambleEditor,
+ SIGNAL(textChanged(const QString&)), this,
+ SLOT(onScrambleChanged(const QString&)));
+ QObject::connect(ui->scrambleEditor, SIGNAL(returnPressed()),
+ this, SLOT(onScrambleSubmitted()));
+ QObject::connect(ui->solveButton, SIGNAL(clicked()),
+ this, SLOT(onScrambleSubmitted()));
+}
+
+SolverCfgWidget::~SolverCfgWidget()
+{
+ delete nmovesValidator;
+ delete ui;
+}
+
+void SolverCfgWidget::lockSubmit()
+{
+ submitLocked = true;
+ onScrambleChanged(ui->scrambleEditor->text());
+}
+
+void SolverCfgWidget::unlockSubmit()
+{
+ submitLocked = false;
+ onScrambleChanged(ui->scrambleEditor->text());
+}
+
+void SolverCfgWidget::onScrambleChanged(const QString& text)
+{
+ auto scrambleState = getScrambleState(text.toStdString());
+ switch (scrambleState) {
+ case ScrambleState::empty:
+ ui->solveButton->setEnabled(false);
+ ui->scrambleEditor->setStyleSheet("");
+ break;
+ case ScrambleState::valid:
+ ui->solveButton->setEnabled(!submitLocked);
+ ui->scrambleEditor->setStyleSheet("");
+ break;
+ case ScrambleState::invalid:
+ ui->solveButton->setEnabled(false);
+ ui->scrambleEditor->setStyleSheet("border : 2px solid red");
+ break;
+ }
+}
+
+void SolverCfgWidget::onScrambleSubmitted()
+{
+ std::string scramble = ui->scrambleEditor->text().toStdString();
+ auto state = getScrambleState(scramble);
+ if (state != ScrambleState::valid)
+ return;
+
+ SolverConfiguration sc {
+ .cube = std::get<nissy::cube_t>(nissy::applymoves(
+ nissy::cube::SOLVED, nissy::moves_t{scramble})),
+ .minmoves = ui->minMovesEditor->text().toUInt(),
+ .maxmoves = ui->maxMovesEditor->text().toUInt(),
+ };
+ emit solveRequest(sc);
+}
+
+ScrambleState getScrambleState(const std::string& s)
+{
+ if (std::all_of(s.begin(), s.end(),
+ [](std::string::value_type c){ return std::isspace(c); }))
+ return ScrambleState::empty;
+
+ auto c = nissy::applymoves(nissy::cube::SOLVED, nissy::moves_t{s});
+ return std::holds_alternative<nissy::error_t>(c) ?
+ ScrambleState::invalid : ScrambleState::valid;
+}
+
diff --git a/qt-widgets/solvercfgwidget.h b/qt-widgets/solvercfgwidget.h
@@ -0,0 +1,43 @@
+#ifndef SOLVERCFGWIDGET_H
+#define SOLVERCFGWIDGET_H
+
+#include "adapter.h"
+
+#include <QWidget>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QString>
+#include <QIntValidator>
+
+QT_BEGIN_NAMESPACE
+namespace Ui {
+class SolverCfgWidget;
+}
+QT_END_NAMESPACE
+
+class SolverCfgWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ SolverCfgWidget(QWidget *parent);
+ ~SolverCfgWidget();
+
+ void lockSubmit();
+ void unlockSubmit();
+
+signals:
+ void solveRequest(const SolverConfiguration&);
+
+private slots:
+ void onScrambleChanged(const QString&);
+ void onScrambleSubmitted();
+
+private:
+ bool submitLocked;
+ QIntValidator *nmovesValidator;
+
+ Ui::SolverCfgWidget *ui;
+};
+
+#endif
diff --git a/qt-widgets/solvercfgwidget.ui b/qt-widgets/solvercfgwidget.ui
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+<class>SolverCfgWidget</class>
+<widget class="SolverCfgWidget" name="solverCfgWidget">
+
+<layout class="QVBoxLayout">
+
+<item>
+<layout class="QHBoxLayout">
+ <item>
+ <widget class="QLineEdit" name="scrambleEditor">
+ <property name="placeholderText">
+ <string>Enter your scramble here...</string>
+ </property>
+ </widget>
+ </item>
+
+ <item>
+ <widget class="QPushButton" name="solveButton">
+ <property name="text">
+ <string>Solve!</string>
+ </property>
+ </widget>
+ </item>
+</layout>
+</item>
+
+<item>
+ <layout class="QHBoxLayout">
+ <item>
+ <widget class="QLabel">
+ <property name="text">
+ <string>Min moves</string>
+ </property>
+ </widget>
+ </item>
+
+ <item>
+ <widget class="QLineEdit" name="minMovesEditor">
+ <property name="maximumSize">
+ <size>
+ <width>25</width>
+ <height>20</height>
+ </size>
+ </property>
+
+ <property name="text">
+ <string>0</string>
+ </property>
+ </widget>
+ </item>
+
+ <item>
+ <widget class="QLabel">
+ <property name="text">
+ <string>Max moves</string>
+ </property>
+ </widget>
+ </item>>
+
+ <item>
+ <widget class="QLineEdit" name="maxMovesEditor">
+ <property name="maximumSize">
+ <size>
+ <width>25</width>
+ <height>20</height>
+ </size>
+ </property>
+
+ <property name="text">
+ <string>20</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+</item>
+</layout>
+
+
+</widget>
+</ui>