qt-experiments

Experiments with QT framework and nissy
git clone https://git.tronto.net/qt-experiments
Download | Log | Files | Refs | Submodules | README

commit 97b5778172572504c8dfbe173b18281e786270c0
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Fri, 25 Apr 2025 16:18:26 +0200

Initial commit

Diffstat:
A.gitignore | 8++++++++
A.gitmodules | 3+++
AREADME.md | 9+++++++++
Anissy-core | 1+
Aqt-quick/CMakeLists.txt | 26++++++++++++++++++++++++++
Aqt-quick/Main.qml | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt-quick/Makefile | 30++++++++++++++++++++++++++++++
Aqt-quick/README.md | 28++++++++++++++++++++++++++++
Aqt-quick/adapter.cpp | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt-quick/adapter.h | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt-quick/main.cpp | 16++++++++++++++++
Aqt-widgets/README.md | 8++++++++
Aqt-widgets/adapter.cpp | 46++++++++++++++++++++++++++++++++++++++++++++++
Aqt-widgets/adapter.h | 35+++++++++++++++++++++++++++++++++++
Aqt-widgets/main.cpp | 11+++++++++++
Aqt-widgets/nissyqt.pro | 18++++++++++++++++++
Aqt-widgets/nissywindow.cpp | 35+++++++++++++++++++++++++++++++++++
Aqt-widgets/nissywindow.h | 36++++++++++++++++++++++++++++++++++++
Aqt-widgets/nissywindow.ui | 33+++++++++++++++++++++++++++++++++
Aqt-widgets/solvercfgwidget.cpp | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt-widgets/solvercfgwidget.h | 43+++++++++++++++++++++++++++++++++++++++++++
Aqt-widgets/solvercfgwidget.ui | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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>