diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d4b781..ac1470d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -13,7 +13,7 @@ project(CLIFp
# Get helper scripts
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake)
-fetch_ob_cmake("v0.3.7.1")
+fetch_ob_cmake("19d33b5bb1752b50767f78ca3e17796868354ac3")
# Initialize project according to standard rules
include(OB/Project)
@@ -101,9 +101,18 @@ ob_fetch_quazip(
include(OB/FetchMagicEnum)
ob_fetch_magicenum("v0.9.6")
+# Bring in frontend framework module
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/lib/frontend_framework/cmake")
+
# Process Targets
-set(APP_TARGET_NAME ${PROJECT_NAMESPACE_LC}_${PROJECT_NAMESPACE_LC})
-set(APP_ALIAS_NAME ${PROJECT_NAMESPACE})
+set(BACKEND_TARGET_NAME ${PROJECT_NAMESPACE_LC}_backend)
+set(BACKEND_ALIAS_NAME Backend)
+string(TOLOWER "${BACKEND_ALIAS_NAME}" BACKEND_ALIAS_NAME_LC)
+set(FRONTEND_FRAMEWORK_TARGET_NAME ${PROJECT_NAMESPACE_LC}_frontend_framework)
+set(FRONTEND_FRAMEWORK_ALIAS_NAME FrontendFramework)
+add_subdirectory(lib)
+set(APP_GUI_TARGET_NAME ${PROJECT_NAMESPACE_LC}_${PROJECT_NAMESPACE_LC})
+set(APP_GUI_ALIAS_NAME ${PROJECT_NAMESPACE})
add_subdirectory(app)
#--------------------Package Config-----------------------
@@ -112,7 +121,7 @@ ob_standard_project_package_config(
COMPATIBILITY "SameMinorVersion"
CONFIG STANDARD
TARGET_CONFIGS
- TARGET "${PROJECT_NAMESPACE}::${APP_ALIAS_NAME}" COMPONENT "${APP_ALIAS_NAME}" DEFAULT
+ TARGET "${PROJECT_NAMESPACE}::${APP_GUI_ALIAS_NAME}" COMPONENT "${APP_GUI_ALIAS_NAME}" DEFAULT
)
#================= Install ==========================
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 902de0c..fdf5e92 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -1,176 +1,2 @@
-#================= Common Build =========================
-
-# Pre-configure target
-set(CLIFP_SOURCE
- kernel/buildinfo.h
- kernel/core.h
- kernel/core.cpp
- kernel/directive.h
- kernel/director.h
- kernel/director.cpp
- kernel/directorate.h
- kernel/directorate.cpp
- kernel/driver.h
- kernel/driver.cpp
- kernel/errorstatus.h
- kernel/errorstatus.cpp
- command/command.h
- command/command.cpp
- command/c-download.h
- command/c-download.cpp
- command/c-link.h
- command/c-link.cpp
- command/c-play.h
- command/c-play.cpp
- command/c-prepare.h
- command/c-prepare.cpp
- command/c-run.h
- command/c-run.cpp
- command/c-share.h
- command/c-share.cpp
- command/c-show.cpp
- command/c-show.h
- command/c-update.h
- command/c-update.cpp
- command/title-command.h
- command/title-command.cpp
- task/task.h
- task/task.cpp
- task/t-download.h
- task/t-download.cpp
- task/t-exec.h
- task/t-exec.cpp
- task/t-extra.h
- task/t-extra.cpp
- task/t-extract.h
- task/t-extract.cpp
- task/t-generic.h
- task/t-generic.cpp
- task/t-message.h
- task/t-message.cpp
- task/t-mount.h
- task/t-mount.cpp
- task/t-sleep.h
- task/t-sleep.cpp
- tools/blockingprocessmanager.h
- tools/blockingprocessmanager.cpp
- tools/deferredprocessmanager.h
- tools/deferredprocessmanager.cpp
- tools/mounter_game_server.h
- tools/mounter_game_server.cpp
- tools/mounter_qmp.h
- tools/mounter_qmp.cpp
- tools/mounter_router.h
- tools/mounter_router.cpp
- frontend/statusrelay.h
- frontend/statusrelay.cpp
- controller.h
- controller.cpp
- utility.h
- utility.cpp
- main.cpp
-)
-
-set(CLIFP_LINKS
- PRIVATE
- Qt6::Core
- Qt6::Gui
- Qt6::Widgets
- Qt6::Sql
- Qt6::Network
- Qx::Core
- Qx::Io
- Qx::Network
- Qx::Widgets
- Fp::Fp
- QuaZip::QuaZip
- magic_enum::magic_enum
- QI-QMP::Qmpi
-)
-
-if(CMAKE_SYSTEM_NAME STREQUAL Windows)
- list(APPEND CLIFP_SOURCE
- command/c-link_win.cpp
- task/t-exec_win.cpp
- task/t-bideprocess.h
- task/t-bideprocess.cpp
- )
-
- list(APPEND CLIFP_LINKS
- PRIVATE
- Qx::Windows
- )
-endif()
-
-if(CMAKE_SYSTEM_NAME STREQUAL Linux)
- list(APPEND CLIFP_SOURCE
- command/c-link_linux.cpp
- task/t-awaitdocker.h
- task/t-awaitdocker.cpp
- task/t-exec_linux.cpp
- )
- list(APPEND CLIFP_LINKS
- PRIVATE
- Qx::Linux
- )
-endif()
-
-# Add via ob standard executable
-include(OB/Executable)
-ob_add_standard_executable(${APP_TARGET_NAME}
- NAMESPACE "${PROJECT_NAMESPACE}"
- ALIAS "${APP_ALIAS_NAME}"
- SOURCE ${CLIFP_SOURCE}
- RESOURCE "resources.qrc"
- LINKS ${CLIFP_LINKS}
- CONFIG STANDARD
- WIN32
-)
-
-## Forward select project variables to C++ code
-include(OB/CppVars)
-ob_add_cpp_vars(${APP_TARGET_NAME}
- NAME "project_vars"
- PREFIX "PROJECT_"
- VARS
- VERSION_STR "\"${PROJECT_VERSION_VERBOSE}\""
- SHORT_NAME "\"${APP_ALIAS_NAME}\""
- TARGET_FP_VER_PFX_STR "\"${TARGET_FP_VERSION_PREFIX}\""
- APP_NAME "\"${PROJECT_FORMAL_NAME}\""
-)
-
-## Add build info
-if(BUILD_SHARED_LIBS)
- set(link_str "Shared")
-else()
- set(link_str "Static")
-endif()
-
-ob_add_cpp_vars(${APP_TARGET_NAME}
- NAME "_buildinfo"
- PREFIX "BUILDINFO_"
- VARS
- SYSTEM "\"${CMAKE_SYSTEM_NAME}\""
- LINKAGE "\"${link_str}\""
- COMPILER "u\"${CMAKE_CXX_COMPILER_ID}\"_s"
- COMPILER_VER_STR "u\"${CMAKE_CXX_COMPILER_VERSION}\"_s"
-)
-
-## Add exe details on Windows
-if(CMAKE_SYSTEM_NAME STREQUAL Windows)
- # Set target exe details
- include(OB/WinExecutableDetails)
- ob_set_win_executable_details(${APP_TARGET_NAME}
- ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/app/CLIFp.ico"
- FILE_VER ${PROJECT_VERSION}
- PRODUCT_VER ${TARGET_FP_VERSION_PREFIX}
- COMPANY_NAME "oblivioncth"
- FILE_DESC "CLI for Flashpoint Archive"
- INTERNAL_NAME "CLIFp"
- COPYRIGHT "Open Source @ 2022 oblivioncth"
- TRADEMARKS_ONE "All Rights Reserved"
- TRADEMARKS_TWO "GNU AGPL V3"
- ORIG_FILENAME "CLIFp.exe"
- PRODUCT_NAME "${PROJECT_FORMAL_NAME}"
- )
-endif()
+add_subdirectory(gui)
+add_subdirectory(console)
diff --git a/app/cmake/Frontend.cmake b/app/cmake/Frontend.cmake
new file mode 100644
index 0000000..e69de29
diff --git a/app/res/app/128x128/CLIFp.png b/app/cmake/res/app/128x128/CLIFp.png
similarity index 100%
rename from app/res/app/128x128/CLIFp.png
rename to app/cmake/res/app/128x128/CLIFp.png
diff --git a/app/res/app/16x16/CLIFp.png b/app/cmake/res/app/16x16/CLIFp.png
similarity index 100%
rename from app/res/app/16x16/CLIFp.png
rename to app/cmake/res/app/16x16/CLIFp.png
diff --git a/app/res/app/256x256/CLIFp.png b/app/cmake/res/app/256x256/CLIFp.png
similarity index 100%
rename from app/res/app/256x256/CLIFp.png
rename to app/cmake/res/app/256x256/CLIFp.png
diff --git a/app/res/app/32x32/CLIFp.png b/app/cmake/res/app/32x32/CLIFp.png
similarity index 100%
rename from app/res/app/32x32/CLIFp.png
rename to app/cmake/res/app/32x32/CLIFp.png
diff --git a/app/res/app/48x48/CLIFp.png b/app/cmake/res/app/48x48/CLIFp.png
similarity index 100%
rename from app/res/app/48x48/CLIFp.png
rename to app/cmake/res/app/48x48/CLIFp.png
diff --git a/app/res/app/64x64/CLIFp.png b/app/cmake/res/app/64x64/CLIFp.png
similarity index 100%
rename from app/res/app/64x64/CLIFp.png
rename to app/cmake/res/app/64x64/CLIFp.png
diff --git a/app/cmake/res/app/CLIFp.ico b/app/cmake/res/app/CLIFp.ico
new file mode 100644
index 0000000..43a609b
Binary files /dev/null and b/app/cmake/res/app/CLIFp.ico differ
diff --git a/app/res/resources.qrc b/app/cmake/res/resources.qrc
similarity index 100%
rename from app/res/resources.qrc
rename to app/cmake/res/resources.qrc
diff --git a/app/res/tray/Exit.png b/app/cmake/res/tray/Exit.png
similarity index 100%
rename from app/res/tray/Exit.png
rename to app/cmake/res/tray/Exit.png
diff --git a/app/console/CMakeLists.txt b/app/console/CMakeLists.txt
new file mode 100644
index 0000000..e69de29
diff --git a/app/res/app/CLIFp.ico b/app/console/res/app/CLIFp.ico
similarity index 100%
rename from app/res/app/CLIFp.ico
rename to app/console/res/app/CLIFp.ico
diff --git a/app/gui/CMakeLists.txt b/app/gui/CMakeLists.txt
new file mode 100644
index 0000000..d420beb
--- /dev/null
+++ b/app/gui/CMakeLists.txt
@@ -0,0 +1,27 @@
+#================= Common Build =========================
+
+# Add via ob standard executable
+include(OB/Executable)
+ob_add_standard_executable(${APP_GUI_TARGET_NAME}
+ NAMESPACE "${PROJECT_NAMESPACE}"
+ ALIAS "${APP_GUI_ALIAS_NAME}"
+ SOURCE
+ frontend/gui.h
+ frontend/gui.cpp
+ main.cpp
+ RESOURCE "resources.qrc"
+ LINKS
+ PRIVATE
+ CLIFp::FrontendFramework
+ ${Qt}::Widgets
+ Qx::Widgets
+ magic_enum::magic_enum
+ CONFIG STANDARD
+ WIN32
+)
+
+# Add exe details on Windows
+if(CMAKE_SYSTEM_NAME STREQUAL Windows)
+ include(FrontendFramework)
+ set_clip_exe_details(${APP_GUI_TARGET_NAME} ${APP_GUI_ALIAS_NAME})
+endif()
diff --git a/app/gui/res/resources.qrc b/app/gui/res/resources.qrc
new file mode 100644
index 0000000..199cad7
--- /dev/null
+++ b/app/gui/res/resources.qrc
@@ -0,0 +1,5 @@
+
+
+ tray/Exit.png
+
+
diff --git a/app/gui/res/tray/Exit.png b/app/gui/res/tray/Exit.png
new file mode 100644
index 0000000..490b084
Binary files /dev/null and b/app/gui/res/tray/Exit.png differ
diff --git a/app/gui/src/frontend/gui.cpp b/app/gui/src/frontend/gui.cpp
new file mode 100644
index 0000000..c641f4a
--- /dev/null
+++ b/app/gui/src/frontend/gui.cpp
@@ -0,0 +1,223 @@
+// Unit Include
+#include "gui.h"
+
+// Qt Includes
+#include
+#include
+#include
+#include
+#include
+
+// Qx Includes
+#include
+
+// Magic enum
+#include "magic_enum_utility.hpp"
+
+//===============================================================================================================
+// FrontendGui
+//===============================================================================================================
+
+//-Constructor-------------------------------------------------------------------------------------------------------
+//Public:
+FrontendGui::FrontendGui(QApplication* app) :
+ FrontendFramework(app),
+ mSystemClipboard(QGuiApplication::clipboard())
+{
+ app->setQuitOnLastWindowClosed(false);
+#ifdef __linux__
+ // Set application icon
+ app->setWindowIcon(appIconFromResources());
+#endif
+ setupTrayIcon();
+ setupProgressDialog();
+}
+
+//-Class Functions--------------------------------------------------------------------------------------------------------
+//Private:
+QMessageBox::StandardButtons FrontendGui::choicesToButtons(DBlockingError::Choices cs)
+{
+ QMessageBox::StandardButtons bs;
+
+ magic_enum::enum_for_each([&] (auto val) {
+ constexpr DBlockingError::Choice c = val;
+ if(cs.testFlag(c))
+ bs.setFlag(smChoiceButtonMap.toRight(c));
+ });
+
+ return bs;
+}
+
+template
+ requires Qx::any_of
+QMessageBox* FrontendGui::prepareMessageBox(const MessageT& dMsg)
+{
+ QMessageBox* msg = new QMessageBox();
+ msg->setIcon(QMessageBox::Information);
+ msg->setWindowTitle(QCoreApplication::applicationName()); // This should be the default, but hey being explicit never hurt
+ msg->setText(dMsg.text);
+
+ if(dMsg.selectable)
+ msg->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ return msg;
+}
+
+bool FrontendGui::windowsAreOpen()
+{
+ // Based on Qt's own check here: https://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/kernel/qwindow.cpp?h=5.15.2#n2710
+ // and here: https://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/kernel/qguiapplication.cpp?h=5.15.2#n3629
+ QWindowList topWindows = QApplication::topLevelWindows();
+ for(const QWindow* window : qAsConst(topWindows))
+ {
+ if (window->isVisible() && !window->transientParent() && window->type() != Qt::ToolTip)
+ return true;
+ }
+ return false;
+}
+
+QIcon& FrontendGui::trayExitIconFromResources() { static QIcon ico(u":/frontend/tray/Exit.png"_s); return ico; }
+
+//-Instance Functions------------------------------------------------------------------------------------------------------
+//Private:
+void FrontendGui::handleMessage(const DMessage& d)
+{
+ auto mb = prepareMessageBox(d);
+ mb->setAttribute(Qt::WA_DeleteOnClose);
+ mb->show();
+}
+
+void FrontendGui::handleError(const DError& d) { Qx::postError(d.error); }
+
+void FrontendGui::handleProcedureStart(const DProcedureStart& d)
+{
+ // Set label
+ mProgressDialog.setLabelText(d.label);
+
+ // Show right away
+ mProgressDialog.setValue(0);
+}
+
+void FrontendGui::handleProcedureStop(const DProcedureStop& d)
+{
+ Q_UNUSED(d);
+ /* Always reset the dialog regardless of whether it is visible or not as it may not be currently visible,
+ * but queued to be visible on the next event loop iteration and therefore still needs to be hidden
+ * immediately after.
+ */
+ mProgressDialog.reset();
+}
+
+void FrontendGui::handleProcedureProgress(const DProcedureProgress& d) { mProgressDialog.setValue(d.current); }
+void FrontendGui::handleProcedureScale(const DProcedureScale& d) { mProgressDialog.setMaximum(d.max); }
+void FrontendGui::handleClipboardUpdate(const DClipboardUpdate& d) { mSystemClipboard->setText(d.text); }
+void FrontendGui::handleStatusUpdate(const DStatusUpdate& d) { mStatusHeading = d.heading; mStatusMessage = d.message; }
+
+// Sync directive handlers
+void FrontendGui::handleBlockingMessage(const DBlockingMessage& d)
+{
+ auto mb = prepareMessageBox(d);
+ mb->exec();
+ delete mb;
+}
+
+void FrontendGui::handleBlockingError(const DBlockingError& d)
+{
+ Q_ASSERT(d.response);
+ auto btns = choicesToButtons(d.choices);
+ auto def = smChoiceButtonMap.toRight(d.defaultChoice);
+ int rawRes = Qx::postBlockingError(d.error, btns, def);
+ *d.response = smChoiceButtonMap.toLeft(static_cast(rawRes));
+}
+
+void FrontendGui::handleSaveFilename(const DSaveFilename& d)
+{
+ Q_ASSERT(d.response);
+ *d.response = QFileDialog::getSaveFileName(nullptr, d.caption, d.dir, d.filter, d.selectedFilter);
+}
+
+void FrontendGui::handleExistingDir(const DExistingDir& d)
+{
+ Q_ASSERT(d.response);
+ *d.response = QFileDialog::getExistingDirectory(nullptr, d.caption, d.startingDir);
+}
+
+void FrontendGui::handleItemSelection(const DItemSelection& d)
+{
+ Q_ASSERT(d.response);
+ *d.response = QInputDialog::getItem(nullptr, d.caption, d.label, d.items, 0, false);
+}
+
+void FrontendGui::handleYesOrNo(const DYesOrNo& d)
+{
+ Q_ASSERT(d.response);
+ *d.response = QMessageBox::question(nullptr, QString(), d.question) == QMessageBox::Yes;
+}
+
+bool FrontendGui::aboutToExit()
+{
+ // Quit once no windows remain
+ if(windowsAreOpen())
+ {
+ connect(qApp, &QGuiApplication::lastWindowClosed, this, &FrontendGui::exit);
+ return false;
+ }
+ else
+ return true;
+}
+
+void FrontendGui::setupTrayIcon()
+{
+ // Set Icon
+ mTrayIcon.setIcon(appIconFromResources());
+
+ // Set ToolTip Action
+ mTrayIcon.setToolTip(SYS_TRAY_STATUS);
+ connect(&mTrayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason){
+ if(reason != QSystemTrayIcon::Context)
+ mTrayIcon.showMessage(mStatusHeading, mStatusMessage);
+ });
+
+ // Set Context Menu
+ QAction* quit = new QAction(&mTrayIconContextMenu);
+ quit->setIcon(trayExitIconFromResources());
+ quit->setText(u"Quit"_s);
+ connect(quit, &QAction::triggered, this, [this]{
+ // Notify driver to quit if it still exists
+ shutdownDriver();
+
+ // Close all top-level windows
+ qApp->closeAllWindows();
+ });
+ mTrayIconContextMenu.addAction(quit);
+ mTrayIcon.setContextMenu(&mTrayIconContextMenu);
+
+ // Display Icon
+ mTrayIcon.show();
+}
+
+void FrontendGui::setupProgressDialog()
+{
+ // Initialize dialog
+ mProgressDialog.setCancelButtonText(u"Cancel"_s);
+ mProgressDialog.setWindowModality(Qt::NonModal);
+ mProgressDialog.setMinimumDuration(0);
+ mProgressDialog.setAutoClose(true);
+ mProgressDialog.setAutoReset(false);
+ mProgressDialog.reset(); // Stops the auto-show timer that is started by QProgressDialog's ctor
+ connect(&mProgressDialog, &QProgressDialog::canceled, this, [this]{
+ /* A bit of a bodge. Pressing the Cancel button on a progress dialog
+ * doesn't count as closing it (it doesn't fire a close event) by the strict definition of
+ * QWidget, so here when the progress bar is closed we manually check to see if it was
+ * the last window if the application is ready to exit.
+ *
+ * Normally the progress bar should never still be open by that point, but this is here as
+ * a fail-safe as otherwise the application would deadlock when the progress bar is closed
+ * via the Cancel button.
+ */
+ if(readyToExit() && !windowsAreOpen())
+ exit();
+ else
+ cancelDriverTask();
+ });
+}
diff --git a/app/gui/src/frontend/gui.h b/app/gui/src/frontend/gui.h
new file mode 100644
index 0000000..dffca64
--- /dev/null
+++ b/app/gui/src/frontend/gui.h
@@ -0,0 +1,83 @@
+#ifndef GUI_H
+#define GUI_H
+
+// Qt Includes
+#include
+#include
+#include
+#include
+
+// Qx Includes
+#include
+#include
+
+// Project Includes
+#include "frontend/framework.h"
+
+class FrontendGui final : public FrontendFramework
+{
+//-Class Variables--------------------------------------------------------------------------------------------------------
+private:
+ static inline const Qx::Bimap smChoiceButtonMap{
+ {DBlockingError::Choice::Ok, QMessageBox::Ok},
+ {DBlockingError::Choice::Yes, QMessageBox::Yes},
+ {DBlockingError::Choice::No, QMessageBox::No},
+ };
+
+ static inline const QString SYS_TRAY_STATUS = u"CLIFp is running"_s;
+
+//-Instance Variables----------------------------------------------------------------------------------------------
+private:
+ QProgressDialog mProgressDialog;
+ QString mStatusHeading;
+ QString mStatusMessage;
+
+ QSystemTrayIcon mTrayIcon;
+ QMenu mTrayIconContextMenu;
+ QClipboard* mSystemClipboard;
+
+//-Constructor-------------------------------------------------------------------------------------------------------
+public:
+ explicit FrontendGui(QApplication* app);
+
+//-Class Functions--------------------------------------------------------------------------------------------------------
+//Private:
+static QMessageBox::StandardButtons choicesToButtons(DBlockingError::Choices cs);
+
+template
+ requires Qx::any_of
+static QMessageBox* prepareMessageBox(const MessageT& dMsg);
+
+static bool windowsAreOpen();
+
+static QIcon& trayExitIconFromResources();
+
+//-Instance Functions------------------------------------------------------------------------------------------------------
+private:
+ // Async directive handlers
+ void handleMessage(const DMessage& d) override;
+ void handleError(const DError& d) override;
+ void handleProcedureStart(const DProcedureStart& d) override;
+ void handleProcedureStop(const DProcedureStop& d) override;
+ void handleProcedureProgress(const DProcedureProgress& d) override;
+ void handleProcedureScale(const DProcedureScale& d) override;
+ void handleClipboardUpdate(const DClipboardUpdate& d) override;
+ void handleStatusUpdate(const DStatusUpdate& d) override;
+
+ // Sync directive handlers
+ void handleBlockingMessage(const DBlockingMessage& d) override;
+ void handleBlockingError(const DBlockingError& d) override;
+ void handleSaveFilename(const DSaveFilename& d) override;
+ void handleExistingDir(const DExistingDir& d) override;
+ void handleItemSelection(const DItemSelection& d) override;
+ void handleYesOrNo(const DYesOrNo& d) override;
+
+ // Control
+ bool aboutToExit() override;
+
+ // Derived
+ void setupTrayIcon();
+ void setupProgressDialog();
+};
+
+#endif // GUI_H
diff --git a/app/gui/src/main.cpp b/app/gui/src/main.cpp
new file mode 100644
index 0000000..ec510f0
--- /dev/null
+++ b/app/gui/src/main.cpp
@@ -0,0 +1,13 @@
+// Qt Includes
+#include
+
+// Project Includes
+#include "frontend/gui.h"
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ FrontendGui frontend(&app);
+ return frontend.exec();
+}
+
diff --git a/app/src/controller.cpp b/app/src/controller.cpp
deleted file mode 100644
index 4c41f81..0000000
--- a/app/src/controller.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-// Unit Include
-#include "controller.h"
-
-// Qt Include
-#include
-#include
-
-// Project Includes
-#include "kernel/driver.h"
-
-//===============================================================================================================
-// CONTROLLER
-//===============================================================================================================
-
-//-Constructor-------------------------------------------------------------
-Controller::Controller(QObject* parent) :
- QObject(parent),
- mStatusRelay(this),
- mExitCode(0),
- mReadyToExit(false)
-{
- // Create driver
- Driver* driver = new Driver(QApplication::arguments());
- driver->moveToThread(&mWorkerThread);
-
- // Connect driver - Operation
- connect(&mWorkerThread, &QThread::started, driver, &Driver::drive); // Thread start causes driver start
- connect(driver, &Driver::finished, this, &Controller::driverFinishedHandler); // Result handling
- connect(driver, &Driver::finished, driver, &QObject::deleteLater); // Have driver clean up itself
- connect(driver, &Driver::finished, &mWorkerThread, &QThread::quit); // Have driver finish cause thread finish
- connect(&mWorkerThread, &QThread::finished, this, &Controller::finisher); // Finish execution when thread quits
-
- // Connect driver - Directives
- connect(driver, &Driver::asyncDirectiveAccounced, &mStatusRelay, &StatusRelay::asyncDirectiveHandler);
- connect(driver, &Driver::syncDirectiveAccounced, &mStatusRelay, &StatusRelay::syncDirectiveHandler, Qt::BlockingQueuedConnection);
-
- // Connect driver - Task Cancellation
- connect(&mStatusRelay, &StatusRelay::longTaskCanceled, driver, &Driver::cancelActiveLongTask);
- connect(&mStatusRelay, &StatusRelay::longTaskCanceled, this, &Controller::longTaskCanceledHandler);
-
- // Connect quit handler
- connect(&mStatusRelay, &StatusRelay::quitRequested, this, &Controller::quitRequestHandler);
- connect(this, &Controller::quit, driver, &Driver::quitNow);
-}
-
-//-Destructor-------------------------------------------------------------
-Controller::~Controller()
-{
- // Just to be safe, but never should be the case
- if(mWorkerThread.isRunning())
- {
- mWorkerThread.quit();
- mWorkerThread.wait();
- }
-}
-
-//-Instance Functions-------------------------------------------------------------
-//Private:
-bool Controller::windowsAreOpen()
-{
- // Based on Qt's own check here: https://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/kernel/qwindow.cpp?h=5.15.2#n2710
- // and here: https://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/kernel/qguiapplication.cpp?h=5.15.2#n3629
- QWindowList topWindows = QApplication::topLevelWindows();
- for(const QWindow* window : qAsConst(topWindows))
- {
- if (window->isVisible() && !window->transientParent() && window->type() != Qt::ToolTip)
- return true;
- }
- return false;
-}
-
-
-//Public:
-void Controller::run() { mWorkerThread.start(); }
-
-//-Slots--------------------------------------------------------------------------------
-//Private:
-void Controller::driverFinishedHandler(ErrorCode code) { mExitCode = code; }
-
-void Controller::quitRequestHandler()
-{
- // Notify driver to quit if it still exists
- emit quit();
-
- // Close all top-level windows
- qApp->closeAllWindows();
-}
-
-void Controller::longTaskCanceledHandler()
-{
- /* A bit of a bodge. Pressing the Cancel button on a progress dialog
- * doesn't count as closing it (it doesn't fire a close event) by the strict definition of
- * QWidget, so here when the progress bar is closed we manually check to see if it was
- * the last window if the application is ready to exit.
- *
- * Normally the progress bar should never still be open by that point, but this is here as
- * a fail-safe as otherwise the application would deadlock when the progress bar is closed
- * via the Cancel button.
- */
- if(mReadyToExit && !windowsAreOpen())
- exit();
-}
-
-void Controller::finisher()
-{
- // Quit once no windows remain
- if(windowsAreOpen())
- {
- mReadyToExit = true;
- connect(qApp, &QGuiApplication::lastWindowClosed, this, &Controller::exit);
- }
- else
- exit();
-}
-
-void Controller::exit() { QApplication::exit(mExitCode); }
diff --git a/app/src/controller.h b/app/src/controller.h
deleted file mode 100644
index 86c3bd8..0000000
--- a/app/src/controller.h
+++ /dev/null
@@ -1,49 +0,0 @@
-#ifndef CONTROLLER_H
-#define CONTROLLER_H
-
-// Qt Includes
-#include
-#include
-
-// Project Includes
-#include "frontend/statusrelay.h"
-#include "kernel/director.h"
-
-class Controller : public QObject
-{
- Q_OBJECT
-//-Instance Variables------------------------------------------------------------------------------------------------------------
-private:
- QThread mWorkerThread;
- StatusRelay mStatusRelay;
- ErrorCode mExitCode;
- bool mReadyToExit;
-
-//-Constructor-------------------------------------------------------------------------------------------------
-public:
- explicit Controller(QObject* parent = nullptr);
-
-//-Destructor----------------------------------------------------------------------------------------------------------
-public:
- ~Controller();
-
-//-Instance Functions------------------------------------------------------------------------------------------------------
-private:
- bool windowsAreOpen();
-
-public:
- void run();
-
-//-Signals & Slots------------------------------------------------------------------------------------------------------------
-private slots:
- void driverFinishedHandler(ErrorCode code);
- void quitRequestHandler();
- void longTaskCanceledHandler();
- void finisher();
- void exit();
-
-signals:
- void quit();
-};
-
-#endif // CONTROLLER_H
diff --git a/app/src/frontend/statusrelay.cpp b/app/src/frontend/statusrelay.cpp
deleted file mode 100644
index 1f1d1ed..0000000
--- a/app/src/frontend/statusrelay.cpp
+++ /dev/null
@@ -1,198 +0,0 @@
-// Unit Include
-#include "statusrelay.h"
-
-// Qt Includes
-#include
-#include
-#include
-#include
-
-// Qx Includes
-#include
-#include
-
-// Magic enum
-#include "magic_enum_utility.hpp"
-
-//===============================================================================================================
-// STATUS RELAY
-//===============================================================================================================
-
-//-Constructor--------------------------------------------------------------------
-StatusRelay::StatusRelay(QObject* parent) :
- QObject(parent),
- mSystemClipboard(QGuiApplication::clipboard())
-{
- setupTrayIcon();
- setupProgressDialog();
-}
-
-//-Class Functions----------------------------------------------------------------------------------------------------
-//Private:
-QMessageBox::StandardButtons StatusRelay::choicesToButtons(DBlockingError::Choices cs)
-{
- QMessageBox::StandardButtons bs;
-
- magic_enum::enum_for_each([&] (auto val) {
- constexpr DBlockingError::Choice c = val;
- if(cs.testFlag(c))
- bs.setFlag(smChoiceButtonMap.toRight(c));
- });
-
- return bs;
-}
-
-template
- requires Qx::any_of
-QMessageBox* StatusRelay::prepareMessageBox(const MessageT& dMsg)
-{
- QMessageBox* msg = new QMessageBox();
- msg->setIcon(QMessageBox::Information);
- msg->setWindowTitle(QApplication::applicationName()); // This should be the default, but hey being explicit never hurt
- msg->setText(dMsg.text);
-
- if(dMsg.selectable)
- msg->setTextInteractionFlags(Qt::TextSelectableByMouse);
-
- return msg;
-}
-
-//-Instance Functions--------------------------------------------------------------------------------------------------
-//Private:
-void StatusRelay::setupTrayIcon()
-{
- // Set Icon
- mTrayIcon.setIcon(QIcon(u":/app/CLIFp.ico"_s));
-
- // Set ToolTip Action
- mTrayIcon.setToolTip(SYS_TRAY_STATUS);
- connect(&mTrayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason){
- if(reason != QSystemTrayIcon::Context)
- mTrayIcon.showMessage(mStatusHeading, mStatusMessage);
- });
-
- // Set Context Menu
- QAction* quit = new QAction(&mTrayIconContextMenu);
- quit->setIcon(QIcon(u":/tray/Exit.png"_s));
- quit->setText(u"Quit"_s);
- connect(quit, &QAction::triggered, this, &StatusRelay::quitRequested);
- mTrayIconContextMenu.addAction(quit);
- mTrayIcon.setContextMenu(&mTrayIconContextMenu);
-
- // Display Icon
- mTrayIcon.show();
-}
-
-void StatusRelay::setupProgressDialog()
-{
- // Initialize dialog
- mLongTaskProgressDialog.setCancelButtonText(u"Cancel"_s);
- mLongTaskProgressDialog.setWindowModality(Qt::NonModal);
- mLongTaskProgressDialog.setMinimumDuration(0);
- mLongTaskProgressDialog.setAutoClose(true);
- mLongTaskProgressDialog.setAutoReset(false);
- mLongTaskProgressDialog.reset(); // Stops the auto-show timer that is started by QProgressDialog's ctor
- connect(&mLongTaskProgressDialog, &QProgressDialog::canceled, this, &StatusRelay::longTaskCanceled);
-}
-
-void StatusRelay::handleMessage(const DMessage& d)
-{
- auto mb = prepareMessageBox(d);
- mb->setAttribute(Qt::WA_DeleteOnClose);
- mb->show();
-}
-
-void StatusRelay::handleError(const DError& d) { Qx::postError(d.error); }
-
-void StatusRelay::handleProcedureStart(const DProcedureStart& d)
-{
- // Set label
- mLongTaskProgressDialog.setLabelText(d.label);
-
- // Show right away
- mLongTaskProgressDialog.setValue(0);
-}
-
-void StatusRelay::handleProcedureStop(const DProcedureStop& d)
-{
- Q_UNUSED(d);
- /* Always reset the dialog regardless of whether it is visible or not as it may not be currently visible,
- * but queued to be visible on the next event loop iteration and therefore still needs to be hidden
- * immediately after.
- */
- mLongTaskProgressDialog.reset();
-}
-
-void StatusRelay::handleProcedureProgress(const DProcedureProgress& d) { mLongTaskProgressDialog.setValue(d.current); }
-void StatusRelay::handleProcedureScale(const DProcedureScale& d) { mLongTaskProgressDialog.setMaximum(d.max); }
-void StatusRelay::handleClipboardUpdate(const DClipboardUpdate& d) { mSystemClipboard->setText(d.text); }
-void StatusRelay::handleStatusUpdate(const DStatusUpdate& d) { mStatusHeading = d.heading; mStatusMessage = d.message; }
-
-// Sync directive handlers
-void StatusRelay::handleBlockingMessage(const DBlockingMessage& d)
-{
- auto mb = prepareMessageBox(d);
- mb->exec();
- delete mb;
-}
-
-void StatusRelay::handleBlockingError(const DBlockingError& d)
-{
- Q_ASSERT(d.response);
- auto btns = choicesToButtons(d.choices);
- auto def = smChoiceButtonMap.toRight(d.defaultChoice);
- int rawRes = Qx::postBlockingError(d.error, btns, def);
- *d.response = smChoiceButtonMap.toLeft(static_cast(rawRes));
-}
-
-void StatusRelay::handleSaveFilename(const DSaveFilename& d)
-{
- Q_ASSERT(d.response);
- *d.response = QFileDialog::getSaveFileName(nullptr, d.caption, d.dir, d.filter, d.selectedFilter);
-}
-
-void StatusRelay::handleExistingDir(const DExistingDir& d)
-{
- Q_ASSERT(d.response);
- *d.response = QFileDialog::getExistingDirectory(nullptr, d.caption, d.startingDir);
-}
-
-void StatusRelay::handleItemSelection(const DItemSelection& d)
-{
- Q_ASSERT(d.response);
- *d.response = QInputDialog::getItem(nullptr, d.caption, d.label, d.items, 0, false);
-}
-
-void StatusRelay::handleYesOrNo(const DYesOrNo& d)
-{
- Q_ASSERT(d.response);
- *d.response = QMessageBox::question(nullptr, QString(), d.question) == QMessageBox::Yes;
-}
-
-//-Signals & Slots-------------------------------------------------------------
-//Public Slots:
-void StatusRelay::asyncDirectiveHandler(const AsyncDirective& aDirective)
-{
- std::visit(qxFuncAggregate{
- [this](DMessage d){ handleMessage(d); },
- [this](DError d) { handleError(d); },
- [this](DProcedureStart d) { handleProcedureStart(d); },
- [this](DProcedureStop d) { handleProcedureStop(d); },
- [this](DProcedureProgress d) { handleProcedureProgress(d); },
- [this](DProcedureScale d) { handleProcedureScale(d); },
- [this](DClipboardUpdate d) { handleClipboardUpdate(d); },
- [this](DStatusUpdate d) { handleStatusUpdate(d); },
- }, aDirective);
-}
-
-void StatusRelay::syncDirectiveHandler(const SyncDirective& sDirective)
-{
- std::visit(qxFuncAggregate{
- [this](DBlockingMessage d){ handleBlockingMessage(d); },
- [this](DBlockingError d){ handleBlockingError(d); },
- [this](DSaveFilename d) { handleSaveFilename(d); },
- [this](DExistingDir d) { handleExistingDir(d); },
- [this](DItemSelection d) { handleItemSelection(d); },
- [this](DYesOrNo d) { handleYesOrNo(d); }
- }, sDirective);
-}
diff --git a/app/src/frontend/statusrelay.h b/app/src/frontend/statusrelay.h
deleted file mode 100644
index 242ecfb..0000000
--- a/app/src/frontend/statusrelay.h
+++ /dev/null
@@ -1,92 +0,0 @@
-#ifndef STATUSRELAY_H
-#define STATUSRELAY_H
-
-// Qt Includes
-#include
-#include
-#include
-#include
-#include
-#include
-
-// Qx Includes
-#include
-#include
-#include
-
-// Project Includes
-#include "kernel/directive.h"
-
-class StatusRelay : public QObject
-{
- Q_OBJECT
-//-Class Variables------------------------------------------------------------------------------------------------------
-private:
- // System Messages
- static inline const QString SYS_TRAY_STATUS = u"CLIFp is running"_s;
-
-//-Class Variables--------------------------------------------------------------------------------------------------------
-private:
- Qx::Bimap smChoiceButtonMap{
- {DBlockingError::Choice::Ok, QMessageBox::Ok},
- {DBlockingError::Choice::Yes, QMessageBox::Yes},
- {DBlockingError::Choice::No, QMessageBox::No},
- };
-
-//-Instance Variables------------------------------------------------------------------------------------------------------
-public:
- QProgressDialog mLongTaskProgressDialog;
- QString mStatusHeading;
- QString mStatusMessage;
-
- QSystemTrayIcon mTrayIcon;
- QMenu mTrayIconContextMenu;
- QClipboard* mSystemClipboard;
-
-//-Constructor----------------------------------------------------------------------------------------------------------
-public:
- explicit StatusRelay(QObject* parent = nullptr);
-
-//-Instance Functions--------------------------------------------------------------------------------------------------
-private:
- QMessageBox::StandardButtons choicesToButtons(DBlockingError::Choices cs);
-
- template
- requires Qx::any_of
- QMessageBox* prepareMessageBox(const MessageT& dMsg);
-
-//-Instance Functions--------------------------------------------------------------------------------------------------
-private:
- void setupTrayIcon();
- void setupProgressDialog();
-
- // Async directive handlers
- void handleMessage(const DMessage& d);
- void handleError(const DError& d);
- void handleProcedureStart(const DProcedureStart& d);
- void handleProcedureStop(const DProcedureStop& d);
- void handleProcedureProgress(const DProcedureProgress& d);
- void handleProcedureScale(const DProcedureScale& d);
- void handleClipboardUpdate(const DClipboardUpdate& d);
- void handleStatusUpdate(const DStatusUpdate& d);
-
- // Sync directive handlers
- void handleBlockingMessage(const DBlockingMessage& d);
- void handleBlockingError(const DBlockingError& d);
- void handleSaveFilename(const DSaveFilename& d);
- void handleExistingDir(const DExistingDir& d);
- void handleItemSelection(const DItemSelection& d);
- void handleYesOrNo(const DYesOrNo& d);
-
-//-Signals & Slots------------------------------------------------------------------------------------------------------
-public slots:
- // Directive handlers
- void asyncDirectiveHandler(const AsyncDirective& aDirective);
- void syncDirectiveHandler(const SyncDirective& sDirective);
-
-signals:
- void longTaskCanceled();
- void quitRequested();
-};
-
-#endif // STATUSRELAY_H
diff --git a/app/src/main.cpp b/app/src/main.cpp
deleted file mode 100644
index 6e7e8d6..0000000
--- a/app/src/main.cpp
+++ /dev/null
@@ -1,45 +0,0 @@
-// Qt Includes
-#include
-
-// Project Includes
-#include "kernel/directive.h"
-#include "controller.h"
-#ifdef __linux__
- #include "utility.h"
-#endif
-#include "project_vars.h"
-
-int main(int argc, char *argv[])
-{
- //-Basic Application Setup-------------------------------------------------------------
-
- // QApplication Object
- QApplication app(argc, argv);
- app.setQuitOnLastWindowClosed(false);
-
- // Set application name
- app.setApplicationName(PROJECT_APP_NAME);
- app.setApplicationVersion(PROJECT_VERSION_STR);
-
-#ifdef __linux__
- // Set application icon
- app.setWindowIcon(Utility::appIconFromResources());
-#endif
-
- // Register metatypes
- qRegisterMetaType();
- qRegisterMetaType();
-
- // Create application controller
- Controller appController(&app);
-
- // Start driver
- appController.run();
-
- // Start event loop
- return app.exec();
-}
-
-
-
-
diff --git a/app/src/utility.cpp b/app/src/utility.cpp
deleted file mode 100644
index cc5cdf4..0000000
--- a/app/src/utility.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-// Unit Include
-#include "utility.h"
-
-// Qt Includes
-#include
-#include
-
-// Project Includes
-#include "project_vars.h"
-
-//-Macros----------------------------------------
-#define APP_ICON_RES_BASE_PATH u":/app/"_s
-
-using namespace Qt::Literals::StringLiterals;
-
-namespace Utility
-{
-
-namespace
-{
- const QString dimStr(int w, int h)
- {
- static const QString dimTemplate = u"%1x%2"_s;
- return dimTemplate.arg(w).arg(h);
- }
-
- const QString appIconResourceFullPath(int w, int h)
- {
- return APP_ICON_RES_BASE_PATH + dimStr(w, h) + '/' + PROJECT_SHORT_NAME u".png"_s;
- }
-
- const QList& availableAppIconSizes()
- {
- static QList sizes;
- if(sizes.isEmpty())
- {
- QDirIterator itr(APP_ICON_RES_BASE_PATH, QDir::Dirs | QDir::NoDotAndDotDot);
- while(itr.hasNext())
- {
- itr.next();
- QStringList dims = itr.fileName().split('x');
- int w = dims.first().toInt();
- int h = dims.last().toInt();
- sizes << QSize(w, h);
- }
- }
- return sizes;
- }
-}
-
-//-Functions----------------------------------------------------
-const QIcon& appIconFromResources()
-{
- static QIcon appIcon;
- if(appIcon.isNull())
- {
- const QList& sizes = availableAppIconSizes();
- for(const QSize& size : sizes)
- appIcon.addFile(appIconResourceFullPath(size.width(), size.height()), size);
- }
- return appIcon;
-}
-
-bool installAppIconForUser()
-{
- static const QDir iconDestBaseDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/icons/hicolor"_s);
-
- const QList& sizes = availableAppIconSizes();
- for(const QSize& size : sizes)
- {
- QString resSpecificSubPath = dimStr(size.width(), size.height()) + u"/apps"_s;
-
- // Ensure path exists
- iconDestBaseDir.mkpath(u"./"_s + resSpecificSubPath);
-
- // Determine paths
- QString fullSrcPath = appIconResourceFullPath(size.width(), size.height());
- QString fullDestPath = iconDestBaseDir.absolutePath() + '/' + resSpecificSubPath + '/' + PROJECT_SHORT_NAME + u".png"_s;
-
- // Remove exiting file if it exists (icon could need to be updated), then copy the new icon
- if((QFile::exists(fullDestPath) && !QFile::remove(fullDestPath) ) || !QFile::copy(fullSrcPath, fullDestPath))
- return false;
- }
-
- return true;
-}
-
-}
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
new file mode 100644
index 0000000..17de2f9
--- /dev/null
+++ b/lib/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(backend)
+add_subdirectory(frontend_framework)
diff --git a/lib/backend/CMakeLists.txt b/lib/backend/CMakeLists.txt
new file mode 100644
index 0000000..4b64881
--- /dev/null
+++ b/lib/backend/CMakeLists.txt
@@ -0,0 +1,156 @@
+#================= Common Build =========================
+
+# Pre-configure target
+set(BACKEND_API
+ kernel/directive.h
+ kernel/driver.h
+ kernel/errorcode.h
+)
+
+set(BACKEND_IMPLEMENTATION
+ kernel/buildinfo.h
+ kernel/core.h
+ kernel/core.cpp
+ kernel/director.h
+ kernel/director.cpp
+ kernel/directorate.h
+ kernel/directorate.cpp
+ kernel/driver_p.h
+ kernel/driver.cpp
+ kernel/errorstatus.h
+ kernel/errorstatus.cpp
+ command/command.h
+ command/command.cpp
+ command/c-download.h
+ command/c-download.cpp
+ command/c-link.h
+ command/c-link.cpp
+ command/c-play.h
+ command/c-play.cpp
+ command/c-prepare.h
+ command/c-prepare.cpp
+ command/c-run.h
+ command/c-run.cpp
+ command/c-share.h
+ command/c-share.cpp
+ command/c-show.cpp
+ command/c-show.h
+ command/c-update.h
+ command/c-update.cpp
+ command/title-command.h
+ command/title-command.cpp
+ task/task.h
+ task/task.cpp
+ task/t-download.h
+ task/t-download.cpp
+ task/t-exec.h
+ task/t-exec.cpp
+ task/t-extra.h
+ task/t-extra.cpp
+ task/t-extract.h
+ task/t-extract.cpp
+ task/t-generic.h
+ task/t-generic.cpp
+ task/t-message.h
+ task/t-message.cpp
+ task/t-mount.h
+ task/t-mount.cpp
+ task/t-sleep.h
+ task/t-sleep.cpp
+ tools/blockingprocessmanager.h
+ tools/blockingprocessmanager.cpp
+ tools/deferredprocessmanager.h
+ tools/deferredprocessmanager.cpp
+ tools/mounter_game_server.h
+ tools/mounter_game_server.cpp
+ tools/mounter_qmp.h
+ tools/mounter_qmp.cpp
+ tools/mounter_router.h
+ tools/mounter_router.cpp
+ utility.h
+)
+
+set(BACKEND_LINKS
+ PRIVATE
+ Qt6::Sql
+ Qt6::Network
+ Qx::Io
+ Qx::Network
+ Fp::Fp
+ QuaZip::QuaZip
+ magic_enum::magic_enum
+ QI-QMP::Qmpi
+ PUBLIC
+ Qt6::Core
+ Qx::Core
+)
+
+if(CMAKE_SYSTEM_NAME STREQUAL Windows)
+ list(APPEND BACKEND_IMPLEMENTATION
+ command/c-link_win.cpp
+ task/t-exec_win.cpp
+ task/t-bideprocess.h
+ task/t-bideprocess.cpp
+ )
+
+ list(APPEND BACKEND_LINKS
+ PRIVATE
+ Qx::Windows
+ )
+endif()
+
+if(CMAKE_SYSTEM_NAME STREQUAL Linux)
+ list(APPEND BACKEND_IMPLEMENTATION
+ command/c-link_linux.cpp
+ task/t-awaitdocker.h
+ task/t-awaitdocker.cpp
+ task/t-exec_linux.cpp
+ )
+ list(APPEND BACKEND_LINKS
+ PRIVATE
+ Qx::Linux
+ )
+endif()
+
+# Add via ob standard library
+include(OB/Library)
+ob_add_standard_library(${BACKEND_TARGET_NAME}
+ NAMESPACE "${PROJECT_NAMESPACE}"
+ ALIAS "${BACKEND_ALIAS_NAME}"
+ EXPORT_HEADER
+ PATH "${PROJECT_NAMESPACE_LC}_${BACKEND_ALIAS_NAME_LC}_export.h"
+ HEADERS_API
+ FILES ${BACKEND_API}
+ IMPLEMENTATION
+ ${BACKEND_IMPLEMENTATION}
+ LINKS
+ ${BACKEND_LINKS}
+)
+
+## Forward select project variables to C++ code
+include(OB/CppVars)
+ob_add_cpp_vars(${BACKEND_TARGET_NAME}
+ NAME "_backend_project_vars"
+ PREFIX "PROJECT_"
+ VARS
+ VERSION_STR "\"${PROJECT_VERSION_VERBOSE}\""
+ SHORT_NAME "\"${PROJECT_NAME}\""
+ TARGET_FP_VER_PFX_STR "\"${TARGET_FP_VERSION_PREFIX}\""
+)
+
+## Add build info
+if(BUILD_SHARED_LIBS)
+ set(link_str "Shared")
+else()
+ set(link_str "Static")
+endif()
+
+ob_add_cpp_vars(${BACKEND_TARGET_NAME}
+ NAME "_buildinfo"
+ PREFIX "BUILDINFO_"
+ VARS
+ SYSTEM "\"${CMAKE_SYSTEM_NAME}\""
+ LINKAGE "\"${link_str}\""
+ COMPILER "u\"${CMAKE_CXX_COMPILER_ID}\"_s"
+ COMPILER_VER_STR "u\"${CMAKE_CXX_COMPILER_VERSION}\"_s"
+)
diff --git a/app/src/kernel/directive.h b/lib/backend/include/kernel/directive.h
similarity index 82%
rename from app/src/kernel/directive.h
rename to lib/backend/include/kernel/directive.h
index 74668e8..eaf8079 100644
--- a/app/src/kernel/directive.h
+++ b/lib/backend/include/kernel/directive.h
@@ -1,12 +1,17 @@
#ifndef DIRECTIVE_H
#define DIRECTIVE_H
+// Shared Library Support
+#include "clifp_backend_export.h"
+
// Qt Includes
#include
// Qx Includes
#include
+// Shared-lib
+
/* TODO:
*
* In this file there are some structs with redundant members where one could easily conceive
@@ -34,40 +39,40 @@
*/
//-Non-blocking Directives-----------------------------------------------------------------
-struct DMessage
+CLIFP_BACKEND_EXPORT struct DMessage
{
QString text;
bool selectable = false;
};
-struct DError
+CLIFP_BACKEND_EXPORT struct DError
{
Qx::Error error;
};
-struct DProcedureStart
+CLIFP_BACKEND_EXPORT struct DProcedureStart
{
QString label;
};
-struct DProcedureStop {};
+CLIFP_BACKEND_EXPORT struct DProcedureStop {};
-struct DProcedureProgress
+CLIFP_BACKEND_EXPORT struct DProcedureProgress
{
quint64 current;
};
-struct DProcedureScale
+CLIFP_BACKEND_EXPORT struct DProcedureScale
{
quint64 max;
};
-struct DClipboardUpdate
+CLIFP_BACKEND_EXPORT struct DClipboardUpdate
{
QString text;
};
-struct DStatusUpdate
+CLIFP_BACKEND_EXPORT struct DStatusUpdate
{
QString heading;
QString message;
@@ -88,13 +93,13 @@ template
concept AsyncDirectiveT = requires(AsyncDirective ad, T t) { ad = t; };
//-Blocking Directives---------------------------------------------------------------------
-struct DBlockingMessage
+CLIFP_BACKEND_EXPORT struct DBlockingMessage
{
QString text;
bool selectable = false;
};
-struct DBlockingError
+CLIFP_BACKEND_EXPORT struct DBlockingError
{
enum class Choice {Ok, Yes, No};
Q_DECLARE_FLAGS(Choices, Choice);
@@ -106,7 +111,7 @@ struct DBlockingError
};
Q_DECLARE_OPERATORS_FOR_FLAGS(DBlockingError::Choices);
-struct DSaveFilename
+CLIFP_BACKEND_EXPORT struct DSaveFilename
{
QString caption;
QString dir;
@@ -115,7 +120,7 @@ struct DSaveFilename
QString* response = nullptr;
};
-struct DExistingDir
+CLIFP_BACKEND_EXPORT struct DExistingDir
{
QString caption;
QString startingDir;
@@ -123,7 +128,7 @@ struct DExistingDir
// TODO: Make sure to use QFileDialog::ShowDirsOnly on receiving end
};
-struct DItemSelection
+CLIFP_BACKEND_EXPORT struct DItemSelection
{
QString caption;
QString label;
@@ -131,7 +136,7 @@ struct DItemSelection
QString* response = nullptr;
};
-struct DYesOrNo
+CLIFP_BACKEND_EXPORT struct DYesOrNo
{
QString question;
bool* response = nullptr;
diff --git a/lib/backend/include/kernel/driver.h b/lib/backend/include/kernel/driver.h
new file mode 100644
index 0000000..1e731b8
--- /dev/null
+++ b/lib/backend/include/kernel/driver.h
@@ -0,0 +1,51 @@
+#ifndef DRIVER_H
+#define DRIVER_H
+
+// Shared Library Support
+#include "clifp_backend_export.h"
+
+// Qt Includes
+#include
+
+// Project Includes
+#include "kernel/errorcode.h"
+#include "kernel/directive.h"
+
+class DriverPrivate;
+
+CLIFP_BACKEND_EXPORT class Driver : public QObject
+{
+ Q_OBJECT;
+ Q_DECLARE_PRIVATE(Driver);
+
+//-Instance Variables------------------------------------------------------------------------------------------------------------
+private:
+ std::unique_ptr d_ptr;
+
+//-Constructor-------------------------------------------------------------------------------------------------
+public:
+ Driver(QStringList arguments);
+
+//-Destructor-------------------------------------------------------------------------------------------------
+public:
+ ~Driver(); // Required for d_ptr destructor to compile
+
+//-Signals & Slots------------------------------------------------------------------------------------------------------------
+public slots:
+ // Worker main
+ void drive();
+
+ // Termination
+ void cancelActiveLongTask();
+ void quitNow();
+
+signals:
+ // Worker status
+ void finished(ErrorCode errorCode);
+
+ // Director forwarders
+ void asyncDirectiveAccounced(const AsyncDirective& aDirective);
+ void syncDirectiveAccounced(const SyncDirective& sDirective);
+};
+
+#endif // DRIVER_H
diff --git a/lib/backend/include/kernel/errorcode.h b/lib/backend/include/kernel/errorcode.h
new file mode 100644
index 0000000..92721cf
--- /dev/null
+++ b/lib/backend/include/kernel/errorcode.h
@@ -0,0 +1,9 @@
+#ifndef ERRORCODE_H
+#define ERRORCODE_H
+
+#include
+
+// General Aliases
+using ErrorCode = quint32;
+
+#endif // ERRORCODE_H
diff --git a/app/src/command/c-download.cpp b/lib/backend/src/command/c-download.cpp
similarity index 100%
rename from app/src/command/c-download.cpp
rename to lib/backend/src/command/c-download.cpp
diff --git a/app/src/command/c-download.h b/lib/backend/src/command/c-download.h
similarity index 100%
rename from app/src/command/c-download.h
rename to lib/backend/src/command/c-download.h
diff --git a/app/src/command/c-link.cpp b/lib/backend/src/command/c-link.cpp
similarity index 100%
rename from app/src/command/c-link.cpp
rename to lib/backend/src/command/c-link.cpp
diff --git a/app/src/command/c-link.h b/lib/backend/src/command/c-link.h
similarity index 92%
rename from app/src/command/c-link.h
rename to lib/backend/src/command/c-link.h
index 0f5e338..14e6c11 100644
--- a/app/src/command/c-link.h
+++ b/lib/backend/src/command/c-link.h
@@ -10,7 +10,7 @@
class QX_ERROR_TYPE(CLinkError, "CLinkError", 1213)
{
friend class CLink;
- //-Class Enums-------------------------------------------------------------
+//-Class Enums-------------------------------------------------------------
public:
enum Type
{
@@ -19,7 +19,7 @@ class QX_ERROR_TYPE(CLinkError, "CLinkError", 1213)
IconInstallFailed = 2
};
- //-Class Variables-------------------------------------------------------------
+//-Class Variables-------------------------------------------------------------
private:
static inline const QHash ERR_STRINGS{
{NoError, u""_s},
@@ -27,7 +27,7 @@ class QX_ERROR_TYPE(CLinkError, "CLinkError", 1213)
{IconInstallFailed, u"Failed to install icons required for the shortcut."_s}
};
- //-Instance Variables-------------------------------------------------------------
+//-Instance Variables-------------------------------------------------------------
private:
Type mType;
QString mSpecific;
@@ -36,7 +36,7 @@ class QX_ERROR_TYPE(CLinkError, "CLinkError", 1213)
private:
CLinkError(Type t = NoError, const QString& s = {});
- //-Instance Functions-------------------------------------------------------------
+//-Instance Functions-------------------------------------------------------------
public:
bool isValid() const;
Type type() const;
diff --git a/app/src/command/c-link_linux.cpp b/lib/backend/src/command/c-link_linux.cpp
similarity index 87%
rename from app/src/command/c-link_linux.cpp
rename to lib/backend/src/command/c-link_linux.cpp
index 14e671e..a7e25df 100644
--- a/app/src/command/c-link_linux.cpp
+++ b/lib/backend/src/command/c-link_linux.cpp
@@ -6,7 +6,7 @@
// Project Includes
#include "c-play.h"
-#include "project_vars.h"
+#include "_backend_project_vars.h"
#include "utility.h"
//===============================================================================================================
@@ -17,14 +17,6 @@
//Private:
Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id)
{
- // Add/update CLIFp icon set
- if(!Utility::installAppIconForUser())
- {
- CLinkError err(CLinkError::IconInstallFailed);
- postDirective(err);
- return err;
- }
-
// Setup desktop entry
Qx::ApplicationDesktopEntry ade;
ade.setName(name);
diff --git a/app/src/command/c-link_win.cpp b/lib/backend/src/command/c-link_win.cpp
similarity index 100%
rename from app/src/command/c-link_win.cpp
rename to lib/backend/src/command/c-link_win.cpp
diff --git a/app/src/command/c-play.cpp b/lib/backend/src/command/c-play.cpp
similarity index 99%
rename from app/src/command/c-play.cpp
rename to lib/backend/src/command/c-play.cpp
index 30e9eef..77b74be 100644
--- a/app/src/command/c-play.cpp
+++ b/lib/backend/src/command/c-play.cpp
@@ -1,9 +1,6 @@
// Unit Include
#include "c-play.h"
-// Qt Includes
-#include
-
// Qx Includes
#include
diff --git a/app/src/command/c-play.h b/lib/backend/src/command/c-play.h
similarity index 100%
rename from app/src/command/c-play.h
rename to lib/backend/src/command/c-play.h
diff --git a/app/src/command/c-prepare.cpp b/lib/backend/src/command/c-prepare.cpp
similarity index 100%
rename from app/src/command/c-prepare.cpp
rename to lib/backend/src/command/c-prepare.cpp
diff --git a/app/src/command/c-prepare.h b/lib/backend/src/command/c-prepare.h
similarity index 100%
rename from app/src/command/c-prepare.h
rename to lib/backend/src/command/c-prepare.h
diff --git a/app/src/command/c-run.cpp b/lib/backend/src/command/c-run.cpp
similarity index 100%
rename from app/src/command/c-run.cpp
rename to lib/backend/src/command/c-run.cpp
diff --git a/app/src/command/c-run.h b/lib/backend/src/command/c-run.h
similarity index 100%
rename from app/src/command/c-run.h
rename to lib/backend/src/command/c-run.h
diff --git a/app/src/command/c-share.cpp b/lib/backend/src/command/c-share.cpp
similarity index 100%
rename from app/src/command/c-share.cpp
rename to lib/backend/src/command/c-share.cpp
diff --git a/app/src/command/c-share.h b/lib/backend/src/command/c-share.h
similarity index 100%
rename from app/src/command/c-share.h
rename to lib/backend/src/command/c-share.h
diff --git a/app/src/command/c-show.cpp b/lib/backend/src/command/c-show.cpp
similarity index 100%
rename from app/src/command/c-show.cpp
rename to lib/backend/src/command/c-show.cpp
diff --git a/app/src/command/c-show.h b/lib/backend/src/command/c-show.h
similarity index 100%
rename from app/src/command/c-show.h
rename to lib/backend/src/command/c-show.h
diff --git a/app/src/command/c-update.cpp b/lib/backend/src/command/c-update.cpp
similarity index 100%
rename from app/src/command/c-update.cpp
rename to lib/backend/src/command/c-update.cpp
diff --git a/app/src/command/c-update.h b/lib/backend/src/command/c-update.h
similarity index 100%
rename from app/src/command/c-update.h
rename to lib/backend/src/command/c-update.h
diff --git a/app/src/command/command.cpp b/lib/backend/src/command/command.cpp
similarity index 99%
rename from app/src/command/command.cpp
rename to lib/backend/src/command/command.cpp
index 754e562..225e515 100644
--- a/app/src/command/command.cpp
+++ b/lib/backend/src/command/command.cpp
@@ -1,9 +1,6 @@
// Unit Includes
#include "command.h"
-// Qt Includes
-#include
-
// Qx Includes
#include
diff --git a/app/src/command/command.h b/lib/backend/src/command/command.h
similarity index 100%
rename from app/src/command/command.h
rename to lib/backend/src/command/command.h
diff --git a/app/src/command/title-command.cpp b/lib/backend/src/command/title-command.cpp
similarity index 100%
rename from app/src/command/title-command.cpp
rename to lib/backend/src/command/title-command.cpp
diff --git a/app/src/command/title-command.h b/lib/backend/src/command/title-command.h
similarity index 100%
rename from app/src/command/title-command.h
rename to lib/backend/src/command/title-command.h
diff --git a/app/src/kernel/buildinfo.h b/lib/backend/src/kernel/buildinfo.h
similarity index 100%
rename from app/src/kernel/buildinfo.h
rename to lib/backend/src/kernel/buildinfo.h
diff --git a/app/src/kernel/core.cpp b/lib/backend/src/kernel/core.cpp
similarity index 99%
rename from app/src/kernel/core.cpp
rename to lib/backend/src/kernel/core.cpp
index 2ca012b..5e81735 100644
--- a/app/src/kernel/core.cpp
+++ b/lib/backend/src/kernel/core.cpp
@@ -1,9 +1,6 @@
// Unit Include
#include "core.h"
-// Qt Includes
-#include
-
// Qx Includes
#include
#include
@@ -58,8 +55,7 @@ QString CoreError::deriveSecondary() const { return mSpecific; }
//-Constructor-------------------------------------------------------------
//Public:
-Core::Core(QObject* parent) :
- QObject(parent),
+Core::Core() :
Directorate(&mDirector),
mServicesMode(ServicesMode::Standalone),
mStatusHeading(u"Initializing"_s),
diff --git a/app/src/kernel/core.h b/lib/backend/src/kernel/core.h
similarity index 98%
rename from app/src/kernel/core.h
rename to lib/backend/src/kernel/core.h
index a3819e0..5620493 100644
--- a/app/src/kernel/core.h
+++ b/lib/backend/src/kernel/core.h
@@ -7,15 +7,10 @@
// Qt Includes
#include
#include
-#include
-#include
#include
-#include
-#include
// Qx Includes
#include
-#include
// libfp Includes
#include
@@ -24,7 +19,7 @@
#include "kernel/buildinfo.h"
#include "kernel/directorate.h"
#include "task/task.h"
-#include "project_vars.h"
+#include "_backend_project_vars.h"
class QX_ERROR_TYPE(CoreError, "CoreError", 1200)
{
@@ -224,7 +219,7 @@ class Core : public QObject, public Directorate
//-Constructor----------------------------------------------------------------------------------------------------------
public:
- explicit Core(QObject* parent);
+ explicit Core();
//-Instance Functions------------------------------------------------------------------------------------------------------
private:
diff --git a/app/src/kernel/director.cpp b/lib/backend/src/kernel/director.cpp
similarity index 98%
rename from app/src/kernel/director.cpp
rename to lib/backend/src/kernel/director.cpp
index 8291e2d..eb0038d 100644
--- a/app/src/kernel/director.cpp
+++ b/lib/backend/src/kernel/director.cpp
@@ -5,9 +5,9 @@
#include
// Project Includes
-#include "project_vars.h"
#include "utility.h"
#include "task/task.h"
+#include "_backend_project_vars.h"
//===============================================================================================================
// DirectorError
@@ -167,8 +167,7 @@ void Director::logEvent(const QString& src, const QString& event)
log([&](){ return mLogger.recordGeneralEvent(src, event); });
}
-
-// TODO: Have task have a toString function/operator instead of "members()"
+// TODO: Have task have a toString function/operator instead of "members()" (or make members() private and have toString() use that)
void Director::logTask(const QString& src, const Task* task) { logEvent(src, LOG_EVENT_TASK_ENQ.arg(task->name(), task->members().join(u", "_s))); }
ErrorCode Director::logFinish(const QString& src, const Qx::Error& errorState)
diff --git a/app/src/kernel/director.h b/lib/backend/src/kernel/director.h
similarity index 99%
rename from app/src/kernel/director.h
rename to lib/backend/src/kernel/director.h
index b16e6de..f1be958 100644
--- a/app/src/kernel/director.h
+++ b/lib/backend/src/kernel/director.h
@@ -11,9 +11,7 @@
// Project Includes
#include "kernel/directive.h"
-
-// General Aliases
-using ErrorCode = quint32;
+#include "kernel/errorcode.h"
class Task;
diff --git a/app/src/kernel/directorate.cpp b/lib/backend/src/kernel/directorate.cpp
similarity index 100%
rename from app/src/kernel/directorate.cpp
rename to lib/backend/src/kernel/directorate.cpp
diff --git a/app/src/kernel/directorate.h b/lib/backend/src/kernel/directorate.h
similarity index 100%
rename from app/src/kernel/directorate.h
rename to lib/backend/src/kernel/directorate.h
diff --git a/app/src/kernel/driver.cpp b/lib/backend/src/kernel/driver.cpp
similarity index 79%
rename from app/src/kernel/driver.cpp
rename to lib/backend/src/kernel/driver.cpp
index 243ac46..3940cf1 100644
--- a/app/src/kernel/driver.cpp
+++ b/lib/backend/src/kernel/driver.cpp
@@ -1,14 +1,16 @@
// Unit Include
-#include "driver.h"
+#include "kernel/driver.h"
+#include "driver_p.h"
// Qt Includes
-#include
+#include
// Qx Includes
#include
#include
// Project Includes
+#include "kernel/core.h"
#include "command/command.h"
#include "command/c-update.h"
#include "task/t-exec.h"
@@ -38,11 +40,13 @@ QString DriverError::derivePrimary() const { return ERR_STRINGS.value(mType); }
QString DriverError::deriveSecondary() const { return mSpecific; }
//===============================================================================================================
-// Driver
+// DriverPrivate
//===============================================================================================================
//-Constructor--------------------------------------------------------------------
-Driver::Driver(QStringList arguments) :
+//Public:
+DriverPrivate::DriverPrivate(Driver* q, QStringList arguments) :
+ q_ptr(q),
mArguments(arguments),
mCore(nullptr),
mErrorStatus(),
@@ -53,23 +57,24 @@ Driver::Driver(QStringList arguments) :
//-Instance Functions-------------------------------------------------------------
//Private:
-QString Driver::name() const { return NAME; }
+QString DriverPrivate::name() const { return NAME; }
-void Driver::init()
+void DriverPrivate::init()
{
+ Q_Q(Driver);
// Create core, attach director to self
- mCore = new Core(this);
+ mCore = std::make_unique();
Director* dtor = mCore->director();
setDirector(mCore->director());
//-Setup Core & Director---------------------------
- connect(mCore, &Core::abort, this, [this](CoreError err){
+ QObject::connect(mCore.get(), &Core::abort, q, [this](CoreError err){
logEvent(LOG_EVENT_CORE_ABORT);
mErrorStatus = err;
quit();
});
- connect(dtor, &Director::announceAsyncDirective, this, &Driver::asyncDirectiveAccounced);
- connect(dtor, &Director::announceSyncDirective, this, &Driver::syncDirectiveAccounced);
+ QObject::connect(dtor, &Director::announceAsyncDirective, q, &Driver::asyncDirectiveAccounced);
+ QObject::connect(dtor, &Director::announceSyncDirective, q, &Driver::syncDirectiveAccounced);
//-Setup deferred process manager------
/* NOTE: It looks like the manager should just be a stack member of TExec that is constructed
@@ -85,8 +90,10 @@ void Driver::init()
TExec::installDeferredProcessManager(dpm);
}
-void Driver::startNextTask()
+void DriverPrivate::startNextTask()
{
+ Q_Q(Driver);
+
// Ensure tasks exist
if(!mCore->hasTasks())
qFatal("Called with no tasks remaining.");
@@ -103,33 +110,31 @@ void Driver::startNextTask()
// Only execute task after an error/quit if it is a Shutdown task
bool isShutdown = mCurrentTask->stage() == Task::Stage::Shutdown;
- if(mErrorStatus.isSet() && !isShutdown)
- {
- logEvent(LOG_EVENT_TASK_SKIP_ERROR);
+ bool erroring = mErrorStatus.isSet();
+ bool qutting = mQuitRequested;
+ bool skip = (erroring || qutting) && !isShutdown;
- // Queue up finished handler directly (executes on next event loop cycle) since task was skipped
- // Can't connect directly because newer connect syntax doesn't support default args
- QTimer::singleShot(0, this, [this](){ completeTaskHandler(); });
- }
- else if(mQuitRequested && !isShutdown)
+ if(skip)
{
- logEvent(LOG_EVENT_TASK_SKIP_QUIT);
+ logEvent(erroring ? LOG_EVENT_TASK_SKIP_ERROR : LOG_EVENT_TASK_SKIP_QUIT);
// Queue up finished handler directly (executes on next event loop cycle) since task was skipped
// Can't connect directly because newer connect syntax doesn't support default args
- QTimer::singleShot(0, this, [this](){ completeTaskHandler(); });
+ QTimer::singleShot(0, q, [this](){ completeTaskHandler(); });
}
else
{
// QueuedConnection, allow event processing between tasks
- connect(mCurrentTask, &Task::complete, this, &Driver::completeTaskHandler, Qt::QueuedConnection);
+ QObject::connect(mCurrentTask, &Task::complete, q, [this](const Qx::Error& e){
+ completeTaskHandler(e);
+ }, Qt::QueuedConnection);
// Perform task
mCurrentTask->perform();
}
}
-void Driver::cleanup()
+void DriverPrivate::cleanup()
{
logEvent(LOG_EVENT_CLEANUP_START);
@@ -140,8 +145,10 @@ void Driver::cleanup()
logEvent(LOG_EVENT_CLEANUP_FINISH);
}
-void Driver::finish()
+void DriverPrivate::finish()
{
+ Q_Q(Driver);
+
logEvent(LOG_EVENT_FINISH);
// Clear update cache
@@ -153,10 +160,10 @@ void Driver::finish()
logEvent(LOG_EVENT_CLEARED_UPDATE_CACHE);
}
- emit finished(logFinish(mErrorStatus.value()));
+ emit q->finished(logFinish(mErrorStatus.value()));
}
-void Driver::quit()
+void DriverPrivate::quit()
{
mQuitRequested = true;
@@ -166,7 +173,7 @@ void Driver::quit()
}
// Helper functions
-std::unique_ptr Driver::findFlashpointInstall()
+std::unique_ptr DriverPrivate::findFlashpointInstall()
{
QDir currentDir(CLIFP_DIR_PATH);
std::unique_ptr fpInstall;
@@ -201,7 +208,7 @@ std::unique_ptr Driver::findFlashpointInstall()
//-Slots--------------------------------------------------------------------------------
//Private:
-void Driver::completeTaskHandler(Qx::Error e)
+void DriverPrivate::completeTaskHandler(const Qx::Error& e)
{
// Handle errors
if(e.isValid())
@@ -226,7 +233,7 @@ void Driver::completeTaskHandler(Qx::Error e)
}
//Public:
-void Driver::drive()
+void DriverPrivate::drive()
{
// Initialize
init();
@@ -293,7 +300,7 @@ void Driver::drive()
//-Catch early core errors-------------------------------------------------------------------
QThread::msleep(100);
- QApplication::processEvents();
+ QCoreApplication::processEvents();
if(mErrorStatus.isSet())
{
finish();
@@ -320,13 +327,13 @@ void Driver::drive()
finish();
}
-void Driver::cancelActiveLongTask()
+void DriverPrivate::cancelActiveLongTask()
{
if(mCurrentTask)
mCurrentTask->stop();
}
-void Driver::quitNow()
+void DriverPrivate::quitNow()
{
// Handle quit state
if(mQuitRequested)
@@ -338,3 +345,21 @@ void Driver::quitNow()
logEvent(LOG_EVENT_QUIT_REQUEST);
quit();
}
+
+//===============================================================================================================
+// Driver
+//===============================================================================================================
+
+//-Constructor--------------------------------------------------------------------
+//Public:
+Driver::Driver(QStringList arguments) : d_ptr(std::make_unique(this, arguments)) {}
+
+//-Destructor--------------------------------------------------------------------
+//Public:
+Driver::~Driver() = default;
+
+//-Slots--------------------------------------------------------------------------------
+//Public:
+void Driver::drive() { Q_D(Driver); d->drive(); }
+void Driver::cancelActiveLongTask() { Q_D(Driver); d->cancelActiveLongTask(); }
+void Driver::quitNow() { Q_D(Driver); d->quitNow(); }
diff --git a/app/src/kernel/driver.h b/lib/backend/src/kernel/driver_p.h
similarity index 87%
rename from app/src/kernel/driver.h
rename to lib/backend/src/kernel/driver_p.h
index ec968c2..5a5cb86 100644
--- a/app/src/kernel/driver.h
+++ b/lib/backend/src/kernel/driver_p.h
@@ -1,5 +1,5 @@
-#ifndef DRIVER_H
-#define DRIVER_H
+#ifndef DRIVER_P_H
+#define DRIVER_P_H
// Qt Includes
#include
@@ -11,11 +11,16 @@
// Project Includes
#include "kernel/errorstatus.h"
#include "kernel/directorate.h"
-#include "kernel/core.h"
+
+//TODO: Use PIMPL so that this is the only class exposed in the lib
+
+class Driver;
+class Core;
+namespace Fp { class Install; }
class QX_ERROR_TYPE(DriverError, "DriverError", 1202)
{
- friend class Driver;
+ friend class DriverPrivate;
//-Class Enums-------------------------------------------------------------
public:
enum Type
@@ -55,9 +60,9 @@ class QX_ERROR_TYPE(DriverError, "DriverError", 1202)
QString deriveSecondary() const override;
};
-class Driver : public QObject, public Directorate
+class DriverPrivate : public Directorate
{
- Q_OBJECT
+ Q_DECLARE_PUBLIC(Driver);
//-Class Variables------------------------------------------------------------------------------------------------------
private:
// Error Messages
@@ -90,10 +95,13 @@ class Driver : public QObject, public Directorate
//-Instance Variables------------------------------------------------------------------------------------------------------------
private:
+ Driver* const q_ptr;
QStringList mArguments;
-
- Core* mCore; // Must not be spawned during construction but after object is moved to thread and operated (since it uses signals/slots)
+ std::unique_ptr mCore; // Must not be spawned during construction but after object is moved to thread and operated (since it uses signals/slots)
/*
+ * This note might be outdated, now that we're using PIMPL, as parenting the members to the public class might cause race conditions for signal disconnection
+ * purposes.
+ *
* NOTE: The pointer members here could be on stack if they are assigned as children to this Driver instance in its initialization list, but at the moment using
* pointers instead for simplicity. If set as a child of this instance, they will be moved with the instance automatically when the instance is moved to a separate
* thread. Otherwise (and without using pointers and init during drive()), the connections they have will be invoked on the main thread since they are spawned when
@@ -114,7 +122,7 @@ class Driver : public QObject, public Directorate
//-Constructor-------------------------------------------------------------------------------------------------
public:
- Driver(QStringList arguments);
+ DriverPrivate(Driver* q, QStringList arguments);
//-Instance Functions------------------------------------------------------------------------------------------------------------
private:
@@ -125,6 +133,7 @@ class Driver : public QObject, public Directorate
// Process
void startNextTask();
+ void completeTaskHandler(const Qx::Error& e = {});
void cleanup();
void finish();
@@ -133,25 +142,10 @@ class Driver : public QObject, public Directorate
// Helper
std::unique_ptr findFlashpointInstall();
-//-Signals & Slots------------------------------------------------------------------------------------------------------------
-private slots:
- void completeTaskHandler(Qx::Error e = {});
-
-public slots:
- // Worker main
+public:
void drive();
-
- // Termination
void cancelActiveLongTask();
void quitNow();
-
-signals:
- // Worker status
- void finished(ErrorCode errorCode);
-
- // Director forwarders
- void asyncDirectiveAccounced(const AsyncDirective& aDirective);
- void syncDirectiveAccounced(const SyncDirective& sDirective);
};
-#endif // DRIVER_H
+#endif // DRIVER_P_H
diff --git a/app/src/kernel/errorstatus.cpp b/lib/backend/src/kernel/errorstatus.cpp
similarity index 100%
rename from app/src/kernel/errorstatus.cpp
rename to lib/backend/src/kernel/errorstatus.cpp
diff --git a/app/src/kernel/errorstatus.h b/lib/backend/src/kernel/errorstatus.h
similarity index 100%
rename from app/src/kernel/errorstatus.h
rename to lib/backend/src/kernel/errorstatus.h
diff --git a/app/src/task/t-awaitdocker.cpp b/lib/backend/src/task/t-awaitdocker.cpp
similarity index 100%
rename from app/src/task/t-awaitdocker.cpp
rename to lib/backend/src/task/t-awaitdocker.cpp
diff --git a/app/src/task/t-awaitdocker.h b/lib/backend/src/task/t-awaitdocker.h
similarity index 100%
rename from app/src/task/t-awaitdocker.h
rename to lib/backend/src/task/t-awaitdocker.h
diff --git a/app/src/task/t-bideprocess.cpp b/lib/backend/src/task/t-bideprocess.cpp
similarity index 100%
rename from app/src/task/t-bideprocess.cpp
rename to lib/backend/src/task/t-bideprocess.cpp
diff --git a/app/src/task/t-bideprocess.h b/lib/backend/src/task/t-bideprocess.h
similarity index 100%
rename from app/src/task/t-bideprocess.h
rename to lib/backend/src/task/t-bideprocess.h
diff --git a/app/src/task/t-download.cpp b/lib/backend/src/task/t-download.cpp
similarity index 100%
rename from app/src/task/t-download.cpp
rename to lib/backend/src/task/t-download.cpp
diff --git a/app/src/task/t-download.h b/lib/backend/src/task/t-download.h
similarity index 100%
rename from app/src/task/t-download.h
rename to lib/backend/src/task/t-download.h
diff --git a/app/src/task/t-exec.cpp b/lib/backend/src/task/t-exec.cpp
similarity index 100%
rename from app/src/task/t-exec.cpp
rename to lib/backend/src/task/t-exec.cpp
diff --git a/app/src/task/t-exec.h b/lib/backend/src/task/t-exec.h
similarity index 100%
rename from app/src/task/t-exec.h
rename to lib/backend/src/task/t-exec.h
diff --git a/app/src/task/t-exec_linux.cpp b/lib/backend/src/task/t-exec_linux.cpp
similarity index 100%
rename from app/src/task/t-exec_linux.cpp
rename to lib/backend/src/task/t-exec_linux.cpp
diff --git a/app/src/task/t-exec_win.cpp b/lib/backend/src/task/t-exec_win.cpp
similarity index 100%
rename from app/src/task/t-exec_win.cpp
rename to lib/backend/src/task/t-exec_win.cpp
diff --git a/app/src/task/t-extra.cpp b/lib/backend/src/task/t-extra.cpp
similarity index 100%
rename from app/src/task/t-extra.cpp
rename to lib/backend/src/task/t-extra.cpp
diff --git a/app/src/task/t-extra.h b/lib/backend/src/task/t-extra.h
similarity index 100%
rename from app/src/task/t-extra.h
rename to lib/backend/src/task/t-extra.h
diff --git a/app/src/task/t-extract.cpp b/lib/backend/src/task/t-extract.cpp
similarity index 100%
rename from app/src/task/t-extract.cpp
rename to lib/backend/src/task/t-extract.cpp
diff --git a/app/src/task/t-extract.h b/lib/backend/src/task/t-extract.h
similarity index 100%
rename from app/src/task/t-extract.h
rename to lib/backend/src/task/t-extract.h
diff --git a/app/src/task/t-generic.cpp b/lib/backend/src/task/t-generic.cpp
similarity index 100%
rename from app/src/task/t-generic.cpp
rename to lib/backend/src/task/t-generic.cpp
diff --git a/app/src/task/t-generic.h b/lib/backend/src/task/t-generic.h
similarity index 100%
rename from app/src/task/t-generic.h
rename to lib/backend/src/task/t-generic.h
diff --git a/app/src/task/t-message.cpp b/lib/backend/src/task/t-message.cpp
similarity index 100%
rename from app/src/task/t-message.cpp
rename to lib/backend/src/task/t-message.cpp
diff --git a/app/src/task/t-message.h b/lib/backend/src/task/t-message.h
similarity index 100%
rename from app/src/task/t-message.h
rename to lib/backend/src/task/t-message.h
diff --git a/app/src/task/t-mount.cpp b/lib/backend/src/task/t-mount.cpp
similarity index 100%
rename from app/src/task/t-mount.cpp
rename to lib/backend/src/task/t-mount.cpp
diff --git a/app/src/task/t-mount.h b/lib/backend/src/task/t-mount.h
similarity index 100%
rename from app/src/task/t-mount.h
rename to lib/backend/src/task/t-mount.h
diff --git a/app/src/task/t-sleep.cpp b/lib/backend/src/task/t-sleep.cpp
similarity index 100%
rename from app/src/task/t-sleep.cpp
rename to lib/backend/src/task/t-sleep.cpp
diff --git a/app/src/task/t-sleep.h b/lib/backend/src/task/t-sleep.h
similarity index 100%
rename from app/src/task/t-sleep.h
rename to lib/backend/src/task/t-sleep.h
diff --git a/app/src/task/task.cpp b/lib/backend/src/task/task.cpp
similarity index 100%
rename from app/src/task/task.cpp
rename to lib/backend/src/task/task.cpp
diff --git a/app/src/task/task.h b/lib/backend/src/task/task.h
similarity index 98%
rename from app/src/task/task.h
rename to lib/backend/src/task/task.h
index 9887822..32a7bfd 100644
--- a/app/src/task/task.h
+++ b/lib/backend/src/task/task.h
@@ -3,7 +3,6 @@
// Qt Includes
#include
-#include
// Qx Includes
#include
diff --git a/app/src/tools/blockingprocessmanager.cpp b/lib/backend/src/tools/blockingprocessmanager.cpp
similarity index 100%
rename from app/src/tools/blockingprocessmanager.cpp
rename to lib/backend/src/tools/blockingprocessmanager.cpp
diff --git a/app/src/tools/blockingprocessmanager.h b/lib/backend/src/tools/blockingprocessmanager.h
similarity index 100%
rename from app/src/tools/blockingprocessmanager.h
rename to lib/backend/src/tools/blockingprocessmanager.h
diff --git a/app/src/tools/deferredprocessmanager.cpp b/lib/backend/src/tools/deferredprocessmanager.cpp
similarity index 100%
rename from app/src/tools/deferredprocessmanager.cpp
rename to lib/backend/src/tools/deferredprocessmanager.cpp
diff --git a/app/src/tools/deferredprocessmanager.h b/lib/backend/src/tools/deferredprocessmanager.h
similarity index 100%
rename from app/src/tools/deferredprocessmanager.h
rename to lib/backend/src/tools/deferredprocessmanager.h
diff --git a/app/src/tools/mounter_game_server.cpp b/lib/backend/src/tools/mounter_game_server.cpp
similarity index 100%
rename from app/src/tools/mounter_game_server.cpp
rename to lib/backend/src/tools/mounter_game_server.cpp
diff --git a/app/src/tools/mounter_game_server.h b/lib/backend/src/tools/mounter_game_server.h
similarity index 100%
rename from app/src/tools/mounter_game_server.h
rename to lib/backend/src/tools/mounter_game_server.h
diff --git a/app/src/tools/mounter_qmp.cpp b/lib/backend/src/tools/mounter_qmp.cpp
similarity index 100%
rename from app/src/tools/mounter_qmp.cpp
rename to lib/backend/src/tools/mounter_qmp.cpp
diff --git a/app/src/tools/mounter_qmp.h b/lib/backend/src/tools/mounter_qmp.h
similarity index 92%
rename from app/src/tools/mounter_qmp.h
rename to lib/backend/src/tools/mounter_qmp.h
index 69c067d..0d7a963 100644
--- a/app/src/tools/mounter_qmp.h
+++ b/lib/backend/src/tools/mounter_qmp.h
@@ -18,7 +18,7 @@
class QX_ERROR_TYPE(MounterQmpError, "MounterQmpError", 1233)
{
friend class MounterQmp;
- //-Class Enums-------------------------------------------------------------
+//-Class Enums-------------------------------------------------------------
public:
enum Type
{
@@ -28,7 +28,7 @@ class QX_ERROR_TYPE(MounterQmpError, "MounterQmpError", 1233)
QemuCommand
};
- //-Class Variables-------------------------------------------------------------
+//-Class Variables-------------------------------------------------------------
private:
static inline const QHash ERR_STRINGS{
{NoError, u""_s},
@@ -37,16 +37,16 @@ class QX_ERROR_TYPE(MounterQmpError, "MounterQmpError", 1233)
{QemuCommand, u"QMPI command error."_s},
};
- //-Instance Variables-------------------------------------------------------------
+//-Instance Variables-------------------------------------------------------------
private:
Type mType;
QString mSpecific;
- //-Constructor-------------------------------------------------------------
+//-Constructor-------------------------------------------------------------
private:
MounterQmpError(Type t = NoError, const QString& s = {});
- //-Instance Functions-------------------------------------------------------------
+//-Instance Functions-------------------------------------------------------------
public:
bool isValid() const;
Type type() const;
diff --git a/app/src/tools/mounter_router.cpp b/lib/backend/src/tools/mounter_router.cpp
similarity index 100%
rename from app/src/tools/mounter_router.cpp
rename to lib/backend/src/tools/mounter_router.cpp
diff --git a/app/src/tools/mounter_router.h b/lib/backend/src/tools/mounter_router.h
similarity index 88%
rename from app/src/tools/mounter_router.h
rename to lib/backend/src/tools/mounter_router.h
index a406777..ba41167 100644
--- a/app/src/tools/mounter_router.h
+++ b/lib/backend/src/tools/mounter_router.h
@@ -17,7 +17,7 @@
class QX_ERROR_TYPE(MounterRouterError, "MounterRouterError", 1234)
{
friend class MounterRouter;
- //-Class Enums-------------------------------------------------------------
+//-Class Enums-------------------------------------------------------------
public:
enum Type
{
@@ -25,23 +25,23 @@ class QX_ERROR_TYPE(MounterRouterError, "MounterRouterError", 1234)
Failed = 1
};
- //-Class Variables-------------------------------------------------------------
+//-Class Variables-------------------------------------------------------------
private:
static inline const QHash ERR_STRINGS{
{NoError, u""_s},
{Failed, u"Failed to mount data pack via router."_s},
};
- //-Instance Variables-------------------------------------------------------------
+//-Instance Variables-------------------------------------------------------------
private:
Type mType;
QString mSpecific;
- //-Constructor-------------------------------------------------------------
+//-Constructor-------------------------------------------------------------
private:
MounterRouterError(Type t = NoError, const QString& s = {});
- //-Instance Functions-------------------------------------------------------------
+//-Instance Functions-------------------------------------------------------------
public:
bool isValid() const;
Type type() const;
diff --git a/app/src/utility.h b/lib/backend/src/utility.h
similarity index 83%
rename from app/src/utility.h
rename to lib/backend/src/utility.h
index 3ae94cb..3362f22 100644
--- a/app/src/utility.h
+++ b/lib/backend/src/utility.h
@@ -19,10 +19,4 @@
#define CLIFP_CANONICAL_APP_FILNAME u"clifp"_s
#endif
-namespace Utility
-{
-//-Functions------------------------------------
-const QIcon& appIconFromResources();
-bool installAppIconForUser();
-}
#endif // UTILITY_H
diff --git a/lib/frontend_framework/CMakeLists.txt b/lib/frontend_framework/CMakeLists.txt
new file mode 100644
index 0000000..0cf8e38
--- /dev/null
+++ b/lib/frontend_framework/CMakeLists.txt
@@ -0,0 +1,45 @@
+# Add via ob standard object library
+include(OB/Library)
+
+# Originally was gonna do this with an OBJECT library, for a lightly faster compilation,
+# but because this includes a header it causes some wack with MOC and redefinitions,
+# as MOC compiles the header file twice: here, and by consumers
+
+ob_add_standard_library(${FRONTEND_FRAMEWORK_TARGET_NAME}
+ NAMESPACE "${PROJECT_NAMESPACE}"
+ ALIAS "${FRONTEND_FRAMEWORK_ALIAS_NAME}"
+ TYPE "STATIC"
+ HEADERS_API
+ FILES
+ frontend/framework.h
+ IMPLEMENTATION
+ frontend/framework.cpp
+ frontend/framework_linux.cpp
+ RESOURCE "resources.qrc"
+ LINKS
+ PUBLIC
+ CLIFp::Backend
+ ${Qt}::Gui # Needed for QIcon, silly, but technically not an issue
+)
+
+# Include ICO hash in the following on Linux
+if(CMAKE_SYSTEM_NAME STREQUAL Linux)
+ include(FrontendFramework)
+ app_ico_hash(ico_hash)
+ set(COND_ICO_HASH "APP_ICO_HASH" "\"${ico_hash}\"")
+else()
+ set(COND_ICO_HASH "")
+endif()
+
+
+## Forward select project variables to C++ code
+include(OB/CppVars)
+ob_add_cpp_vars(${FRONTEND_FRAMEWORK_TARGET_NAME}
+ NAME "_frontend_project_vars"
+ PREFIX "PROJECT_"
+ VARS
+ VERSION_STR "\"${PROJECT_VERSION_VERBOSE}\""
+ APP_NAME "\"${PROJECT_FORMAL_NAME}\""
+ SHORT_NAME "\"${PROJECT_NAME}\""
+ ${COND_ICO_HASH}
+)
diff --git a/lib/frontend_framework/cmake/FrontendFramework.cmake b/lib/frontend_framework/cmake/FrontendFramework.cmake
new file mode 100644
index 0000000..744c687
--- /dev/null
+++ b/lib/frontend_framework/cmake/FrontendFramework.cmake
@@ -0,0 +1,21 @@
+function(set_clip_exe_details tgt name)
+ include(OB/WinExecutableDetails)
+ ob_set_win_executable_details(${tgt}
+ ICON "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../res/app/CLIFp.ico"
+ FILE_VER ${PROJECT_VERSION}
+ PRODUCT_VER ${TARGET_FP_VERSION_PREFIX}
+ COMPANY_NAME "oblivioncth"
+ FILE_DESC "CLI for Flashpoint Archive"
+ INTERNAL_NAME "${name}"
+ COPYRIGHT "Open Source @ 2024 oblivioncth"
+ TRADEMARKS_ONE "All Rights Reserved"
+ TRADEMARKS_TWO "GNU AGPL V3"
+ ORIG_FILENAME "${name}.exe"
+ PRODUCT_NAME "${PROJECT_FORMAL_NAME}"
+ )
+endfunction()
+
+function(app_ico_hash return)
+ file(SHA1 "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../res/app/CLIFp.ico" hash)
+ set(${return} ${hash} PARENT_SCOPE)
+endfunction()
diff --git a/lib/frontend_framework/include/frontend/framework.h b/lib/frontend_framework/include/frontend/framework.h
new file mode 100644
index 0000000..bcc785e
--- /dev/null
+++ b/lib/frontend_framework/include/frontend/framework.h
@@ -0,0 +1,98 @@
+#ifndef FRAMEWORK_H
+#define FRAMEWORK_H
+
+// Qt Includes
+#include
+#include
+#include
+
+// Project Includes
+#include "kernel/errorcode.h"
+#include "kernel/directive.h"
+
+class QGuiApplication;
+class Driver;
+
+class SafeDriver
+{
+ /* This whole class exists to allow Framework to control Driver without using signals, safely.
+ * Call it overkill, but it makes Framework itself cleaner overall.
+ *
+ * Specifically, this ensure that the Driver pointer is valid and only allows using it to invoke
+ * methods in drivers thread, which prevents accidental direct method use in Framework's thread.
+ */
+private:
+ QPointer mDriver;
+
+public:
+ SafeDriver(Driver* driver = nullptr);
+
+ // This signature needs to be tweaked if we ever need the version that can pass method arguments added in Qt 6.7
+ template
+ bool invokeMethod(Functor&& function, FunctorReturnType* ret = nullptr);
+ SafeDriver& operator=(Driver* driver);
+};
+
+class FrontendFramework : public QObject
+{
+ Q_OBJECT
+//-Instance Variables----------------------------------------------------------------------------------------------
+private:
+ QThread mWorkerThread;
+ QGuiApplication* mApp;
+ SafeDriver mDriver;
+ std::optional mExitCode;
+
+//-Constructor-------------------------------------------------------------------------------------------------------
+public:
+ explicit FrontendFramework(QGuiApplication* app); // Can be QCoreApplication* if dependency on QGui for QIcon et. al. is ever broken
+
+//-Destructor----------------------------------------------------------------------------------------------------------
+public:
+ virtual ~FrontendFramework();
+
+//-Class Functions---------------------------------------------------------------------------------------------------------
+protected:
+#ifdef __linux__
+ static bool updateUserIcons();
+#endif
+ static const QIcon& appIconFromResources();
+
+//-Instance Functions------------------------------------------------------------------------------------------------------
+protected:
+ // Async directive handlers
+ virtual void handleMessage(const DMessage& d) = 0;
+ virtual void handleError(const DError& d) = 0;
+ virtual void handleProcedureStart(const DProcedureStart& d) = 0;
+ virtual void handleProcedureStop(const DProcedureStop& d) = 0;
+ virtual void handleProcedureProgress(const DProcedureProgress& d) = 0;
+ virtual void handleProcedureScale(const DProcedureScale& d) = 0;
+ virtual void handleClipboardUpdate(const DClipboardUpdate& d) = 0;
+ virtual void handleStatusUpdate(const DStatusUpdate& d) = 0;
+
+ // Sync directive handlers
+ virtual void handleBlockingMessage(const DBlockingMessage& d) = 0;
+ virtual void handleBlockingError(const DBlockingError& d) = 0;
+ virtual void handleSaveFilename(const DSaveFilename& d) = 0;
+ virtual void handleExistingDir(const DExistingDir& d) = 0;
+ virtual void handleItemSelection(const DItemSelection& d) = 0;
+ virtual void handleYesOrNo(const DYesOrNo& d) = 0;
+
+ // Control
+ virtual bool aboutToExit();
+ void shutdownDriver();
+ void cancelDriverTask();
+ bool readyToExit() const;
+ void exit();
+
+public:
+ ErrorCode exec();
+
+//-Signals & Slots------------------------------------------------------------------------------------------------------------
+private slots:
+ void threadFinishHandler();
+ void asyncDirectiveHandler(const AsyncDirective& aDirective);
+ void syncDirectiveHandler(const SyncDirective& sDirective);
+};
+
+#endif // FRAMEWORK_H
diff --git a/lib/frontend_framework/res/app/CLIFp.ico b/lib/frontend_framework/res/app/CLIFp.ico
new file mode 100644
index 0000000..43a609b
Binary files /dev/null and b/lib/frontend_framework/res/app/CLIFp.ico differ
diff --git a/lib/frontend_framework/res/resources.qrc b/lib/frontend_framework/res/resources.qrc
new file mode 100644
index 0000000..73ca77a
--- /dev/null
+++ b/lib/frontend_framework/res/resources.qrc
@@ -0,0 +1,5 @@
+
+
+ app/CLIFp.ico
+
+
diff --git a/lib/frontend_framework/src/frontend/framework.cpp b/lib/frontend_framework/src/frontend/framework.cpp
new file mode 100644
index 0000000..002980a
--- /dev/null
+++ b/lib/frontend_framework/src/frontend/framework.cpp
@@ -0,0 +1,163 @@
+// Unit Include
+#include "frontend/framework.h"
+
+// Qt Includes
+#include
+#include
+
+// Qx Includes
+#include
+
+// Project Includes
+#include "kernel/driver.h"
+#include "_frontend_project_vars.h"
+
+//===============================================================================================================
+// SafeDriver
+//===============================================================================================================
+
+//-Constructor-------------------------------------------------------------
+//Public:
+SafeDriver::SafeDriver(Driver* driver) : mDriver(driver) {}
+
+//-Instance Functions------------------------------------------------------
+//Public:
+template
+bool SafeDriver::invokeMethod(Functor&& function, FunctorReturnType* ret)
+{
+ if(!mDriver)
+ {
+ qWarning("Tried using driver when it does not exist!");
+ return false;
+ }
+
+ return QMetaObject::invokeMethod(mDriver, function, Qt::QueuedConnection, ret);
+}
+
+SafeDriver& SafeDriver::operator=(Driver* driver) { mDriver = driver; return *this; }
+
+//===============================================================================================================
+// Framework
+//===============================================================================================================
+
+//-Constructor-------------------------------------------------------------
+//Public:
+FrontendFramework::FrontendFramework(QGuiApplication* app) :
+ mApp(app),
+ mExitCode(0)
+{
+ // Prepare app
+ mApp->setApplicationName(PROJECT_APP_NAME);
+ mApp->setApplicationVersion(PROJECT_VERSION_STR);
+
+ // Register metatypes
+ qRegisterMetaType();
+ qRegisterMetaType();
+
+ // Create driver
+ Driver* driver = new Driver(mApp->arguments());
+ driver->moveToThread(&mWorkerThread);
+
+ // Connect driver - Operation
+ connect(&mWorkerThread, &QThread::started, driver, &Driver::drive); // Thread start causes driver start
+ connect(driver, &Driver::finished, this, [this](ErrorCode ec){ mExitCode = ec; }); // Result handling
+ connect(driver, &Driver::finished, driver, &QObject::deleteLater); // Have driver clean up itself
+ connect(driver, &Driver::finished, &mWorkerThread, &QThread::quit); // Have driver finish cause thread finish
+ connect(&mWorkerThread, &QThread::finished, this, &FrontendFramework::threadFinishHandler); // Start execution finish when thread quits
+
+ // Connect driver - Directives
+ connect(driver, &Driver::asyncDirectiveAccounced, this, &FrontendFramework::asyncDirectiveHandler);
+ connect(driver, &Driver::syncDirectiveAccounced, this, &FrontendFramework::syncDirectiveHandler, Qt::BlockingQueuedConnection);
+
+ // Store driver for use later
+ mDriver = driver;
+
+#ifdef __linux__
+ if(!updateUserIcons())
+ qWarning("Failed to upate user app icons!");
+#endif
+}
+
+//-Destructor-------------------------------------------------------------
+//Public:
+FrontendFramework::~FrontendFramework()
+{
+ // Just to be safe, but never should be the case
+ if(mWorkerThread.isRunning())
+ {
+ mWorkerThread.quit();
+ mWorkerThread.wait();
+ }
+}
+
+//-Class Functions-----------------------------------------------------------------------------
+//Protected:
+const QIcon& FrontendFramework::appIconFromResources() { static QIcon ico(u":/frontend/app/CLIFp.ico"_s); return ico; }
+
+//-Instance Functions--------------------------------------------------------------------------
+//Protected:
+bool FrontendFramework::aboutToExit()
+{
+ /* Derivatives can override this to do things last minute, and return true,
+ * or they can return false in order to delay exit until they manually call exit()
+ */
+ return true;
+}
+
+void FrontendFramework::shutdownDriver() { mDriver.invokeMethod(&Driver::quitNow); }
+void FrontendFramework::cancelDriverTask() { mDriver.invokeMethod(&Driver::cancelActiveLongTask); }
+
+bool FrontendFramework::readyToExit() const { return mExitCode && mWorkerThread.isFinished(); }
+
+void FrontendFramework::exit()
+{
+ // Ignore if driver thread is not finished
+ if(!readyToExit())
+ return;
+
+ mApp->exit(*mExitCode);
+}
+
+//Public:
+ErrorCode FrontendFramework::exec()
+{
+ // Run thread
+ mWorkerThread.start();
+
+ // Loop until quit
+ return mApp->exec();
+}
+
+//-Signals & Slots------------------------------------------------------------------------------------------------------------
+//Private Slots:
+void FrontendFramework::threadFinishHandler()
+{
+ if(aboutToExit())
+ exit();
+}
+
+void FrontendFramework::asyncDirectiveHandler(const AsyncDirective& aDirective)
+{
+ std::visit(qxFuncAggregate{
+ [this](DMessage d){ handleMessage(d); },
+ [this](DError d) { handleError(d); },
+ [this](DProcedureStart d) { handleProcedureStart(d); },
+ [this](DProcedureStop d) { handleProcedureStop(d); },
+ [this](DProcedureProgress d) { handleProcedureProgress(d); },
+ [this](DProcedureScale d) { handleProcedureScale(d); },
+ [this](DClipboardUpdate d) { handleClipboardUpdate(d); },
+ [this](DStatusUpdate d) { handleStatusUpdate(d); },
+ }, aDirective);
+}
+
+void FrontendFramework::syncDirectiveHandler(const SyncDirective& sDirective)
+{
+ std::visit(qxFuncAggregate{
+ [this](DBlockingMessage d){ handleBlockingMessage(d); },
+ [this](DBlockingError d){ handleBlockingError(d); },
+ [this](DSaveFilename d) { handleSaveFilename(d); },
+ [this](DExistingDir d) { handleExistingDir(d); },
+ [this](DItemSelection d) { handleItemSelection(d); },
+ [this](DYesOrNo d) { handleYesOrNo(d); }
+ }, sDirective);
+}
diff --git a/lib/frontend_framework/src/frontend/framework_linux.cpp b/lib/frontend_framework/src/frontend/framework_linux.cpp
new file mode 100644
index 0000000..2b888a4
--- /dev/null
+++ b/lib/frontend_framework/src/frontend/framework_linux.cpp
@@ -0,0 +1,79 @@
+// Unit Include
+#include "frontend/framework.h"
+
+// Qt Includes
+#include
+#include
+#include
+#include
+#include
+
+// Project Includes
+#include "_frontend_project_vars.h"
+
+namespace
+{
+
+const QString dimStr(int w, int h)
+{
+ static const QString dimTemplate = u"%1x%2"_s;
+ return dimTemplate.arg(w).arg(h);
+}
+
+}
+
+//===============================================================================================================
+// Framework
+//===============================================================================================================
+
+//-Class Functions-----------------------------------------------------------------------------
+//Protected:
+bool FrontendFramework::updateUserIcons()
+{
+ static const QString HASH_KEY = u"CLIFP_ICO_HASH"_s;
+ static const QDir ICON_DEST_BASE_DIR(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/icons/hicolor"_s);
+
+ bool checkHash = true;
+ QImageWriter imgWriter;
+
+ const QIcon& appIcon = appIconFromResources();
+ for(const QSize& size : appIcon.availableSizes())
+ {
+ QString resSpecificSubPath = dimStr(size.width(), size.height()) + u"/apps"_s;
+
+ // Ensure path exists
+ ICON_DEST_BASE_DIR.mkpath(u"./"_s + resSpecificSubPath);
+
+ // Determine dest
+ QFile destFile(ICON_DEST_BASE_DIR.absolutePath() + '/' + resSpecificSubPath + '/' + PROJECT_SHORT_NAME + u".png"_s);
+
+ if(destFile.exists())
+ {
+ if(checkHash)
+ {
+ // Check if file seems up-to-date, if so, assume all are
+ QImageReader imgReader(&destFile);
+ if(imgReader.text(HASH_KEY) == PROJECT_APP_ICO_HASH)
+ return true;
+
+ // If not, assume all need to be replaced
+ checkHash = false;
+ }
+
+ // Remove old file
+ if(!destFile.remove())
+ return false;
+ }
+
+ // Synthesize specifc size image
+ QImage img = appIcon.pixmap(size).toImage();
+
+ // Write image
+ imgWriter.setDevice(&destFile);
+ imgWriter.setText(HASH_KEY, PROJECT_APP_ICO_HASH); // I think we have to do this each time after changing the file
+ if(!imgWriter.write(img))
+ return false;
+ }
+
+ return true;
+}