From b3e76f9b9fd6325d41e0314ddfe90b2d8314e53e Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sun, 27 Oct 2024 16:11:09 -0400 Subject: [PATCH] Split backend into a lib and frontend into a thin exec. off static lib This allows for different frontends (i.e. console-based) to be added easily, and also advances towards unit testing being easier (though at this moment its still very impractical). --- CMakeLists.txt | 17 +- app/CMakeLists.txt | 178 +------------- app/cmake/Frontend.cmake | 0 app/{ => cmake}/res/app/128x128/CLIFp.png | Bin app/{ => cmake}/res/app/16x16/CLIFp.png | Bin app/{ => cmake}/res/app/256x256/CLIFp.png | Bin app/{ => cmake}/res/app/32x32/CLIFp.png | Bin app/{ => cmake}/res/app/48x48/CLIFp.png | Bin app/{ => cmake}/res/app/64x64/CLIFp.png | Bin app/cmake/res/app/CLIFp.ico | Bin 0 -> 113928 bytes app/{ => cmake}/res/resources.qrc | 0 app/{ => cmake}/res/tray/Exit.png | Bin app/console/CMakeLists.txt | 0 app/{ => console}/res/app/CLIFp.ico | Bin app/gui/CMakeLists.txt | 27 +++ app/gui/res/resources.qrc | 5 + app/gui/res/tray/Exit.png | Bin 0 -> 2734 bytes app/gui/src/frontend/gui.cpp | 223 ++++++++++++++++++ app/gui/src/frontend/gui.h | 83 +++++++ app/gui/src/main.cpp | 13 + app/src/controller.cpp | 116 --------- app/src/controller.h | 49 ---- app/src/frontend/statusrelay.cpp | 198 ---------------- app/src/frontend/statusrelay.h | 92 -------- app/src/main.cpp | 45 ---- app/src/utility.cpp | 88 ------- lib/CMakeLists.txt | 2 + lib/backend/CMakeLists.txt | 156 ++++++++++++ .../backend/include}/kernel/directive.h | 33 +-- lib/backend/include/kernel/driver.h | 51 ++++ lib/backend/include/kernel/errorcode.h | 9 + .../backend}/src/command/c-download.cpp | 0 {app => lib/backend}/src/command/c-download.h | 0 {app => lib/backend}/src/command/c-link.cpp | 0 {app => lib/backend}/src/command/c-link.h | 8 +- .../backend}/src/command/c-link_linux.cpp | 10 +- .../backend}/src/command/c-link_win.cpp | 0 {app => lib/backend}/src/command/c-play.cpp | 3 - {app => lib/backend}/src/command/c-play.h | 0 .../backend}/src/command/c-prepare.cpp | 0 {app => lib/backend}/src/command/c-prepare.h | 0 {app => lib/backend}/src/command/c-run.cpp | 0 {app => lib/backend}/src/command/c-run.h | 0 {app => lib/backend}/src/command/c-share.cpp | 0 {app => lib/backend}/src/command/c-share.h | 0 {app => lib/backend}/src/command/c-show.cpp | 0 {app => lib/backend}/src/command/c-show.h | 0 {app => lib/backend}/src/command/c-update.cpp | 0 {app => lib/backend}/src/command/c-update.h | 0 {app => lib/backend}/src/command/command.cpp | 3 - {app => lib/backend}/src/command/command.h | 0 .../backend}/src/command/title-command.cpp | 0 .../backend}/src/command/title-command.h | 0 {app => lib/backend}/src/kernel/buildinfo.h | 0 {app => lib/backend}/src/kernel/core.cpp | 6 +- {app => lib/backend}/src/kernel/core.h | 9 +- {app => lib/backend}/src/kernel/director.cpp | 5 +- {app => lib/backend}/src/kernel/director.h | 4 +- .../backend}/src/kernel/directorate.cpp | 0 {app => lib/backend}/src/kernel/directorate.h | 0 {app => lib/backend}/src/kernel/driver.cpp | 89 ++++--- .../backend/src/kernel/driver_p.h | 46 ++-- .../backend}/src/kernel/errorstatus.cpp | 0 {app => lib/backend}/src/kernel/errorstatus.h | 0 .../backend}/src/task/t-awaitdocker.cpp | 0 {app => lib/backend}/src/task/t-awaitdocker.h | 0 .../backend}/src/task/t-bideprocess.cpp | 0 {app => lib/backend}/src/task/t-bideprocess.h | 0 {app => lib/backend}/src/task/t-download.cpp | 0 {app => lib/backend}/src/task/t-download.h | 0 {app => lib/backend}/src/task/t-exec.cpp | 0 {app => lib/backend}/src/task/t-exec.h | 0 .../backend}/src/task/t-exec_linux.cpp | 0 {app => lib/backend}/src/task/t-exec_win.cpp | 0 {app => lib/backend}/src/task/t-extra.cpp | 0 {app => lib/backend}/src/task/t-extra.h | 0 {app => lib/backend}/src/task/t-extract.cpp | 0 {app => lib/backend}/src/task/t-extract.h | 0 {app => lib/backend}/src/task/t-generic.cpp | 0 {app => lib/backend}/src/task/t-generic.h | 0 {app => lib/backend}/src/task/t-message.cpp | 0 {app => lib/backend}/src/task/t-message.h | 0 {app => lib/backend}/src/task/t-mount.cpp | 0 {app => lib/backend}/src/task/t-mount.h | 0 {app => lib/backend}/src/task/t-sleep.cpp | 0 {app => lib/backend}/src/task/t-sleep.h | 0 {app => lib/backend}/src/task/task.cpp | 0 {app => lib/backend}/src/task/task.h | 1 - .../src/tools/blockingprocessmanager.cpp | 0 .../src/tools/blockingprocessmanager.h | 0 .../src/tools/deferredprocessmanager.cpp | 0 .../src/tools/deferredprocessmanager.h | 0 .../src/tools/mounter_game_server.cpp | 0 .../backend}/src/tools/mounter_game_server.h | 0 .../backend}/src/tools/mounter_qmp.cpp | 0 {app => lib/backend}/src/tools/mounter_qmp.h | 10 +- .../backend}/src/tools/mounter_router.cpp | 0 .../backend}/src/tools/mounter_router.h | 10 +- {app => lib/backend}/src/utility.h | 6 - lib/frontend_framework/CMakeLists.txt | 45 ++++ .../cmake/FrontendFramework.cmake | 21 ++ .../include/frontend/framework.h | 98 ++++++++ lib/frontend_framework/res/app/CLIFp.ico | Bin 0 -> 113928 bytes lib/frontend_framework/res/resources.qrc | 5 + .../src/frontend/framework.cpp | 163 +++++++++++++ .../src/frontend/framework_linux.cpp | 79 +++++++ 106 files changed, 1112 insertions(+), 894 deletions(-) create mode 100644 app/cmake/Frontend.cmake rename app/{ => cmake}/res/app/128x128/CLIFp.png (100%) rename app/{ => cmake}/res/app/16x16/CLIFp.png (100%) rename app/{ => cmake}/res/app/256x256/CLIFp.png (100%) rename app/{ => cmake}/res/app/32x32/CLIFp.png (100%) rename app/{ => cmake}/res/app/48x48/CLIFp.png (100%) rename app/{ => cmake}/res/app/64x64/CLIFp.png (100%) create mode 100644 app/cmake/res/app/CLIFp.ico rename app/{ => cmake}/res/resources.qrc (100%) rename app/{ => cmake}/res/tray/Exit.png (100%) create mode 100644 app/console/CMakeLists.txt rename app/{ => console}/res/app/CLIFp.ico (100%) create mode 100644 app/gui/CMakeLists.txt create mode 100644 app/gui/res/resources.qrc create mode 100644 app/gui/res/tray/Exit.png create mode 100644 app/gui/src/frontend/gui.cpp create mode 100644 app/gui/src/frontend/gui.h create mode 100644 app/gui/src/main.cpp delete mode 100644 app/src/controller.cpp delete mode 100644 app/src/controller.h delete mode 100644 app/src/frontend/statusrelay.cpp delete mode 100644 app/src/frontend/statusrelay.h delete mode 100644 app/src/main.cpp delete mode 100644 app/src/utility.cpp create mode 100644 lib/CMakeLists.txt create mode 100644 lib/backend/CMakeLists.txt rename {app/src => lib/backend/include}/kernel/directive.h (82%) create mode 100644 lib/backend/include/kernel/driver.h create mode 100644 lib/backend/include/kernel/errorcode.h rename {app => lib/backend}/src/command/c-download.cpp (100%) rename {app => lib/backend}/src/command/c-download.h (100%) rename {app => lib/backend}/src/command/c-link.cpp (100%) rename {app => lib/backend}/src/command/c-link.h (92%) rename {app => lib/backend}/src/command/c-link_linux.cpp (87%) rename {app => lib/backend}/src/command/c-link_win.cpp (100%) rename {app => lib/backend}/src/command/c-play.cpp (99%) rename {app => lib/backend}/src/command/c-play.h (100%) rename {app => lib/backend}/src/command/c-prepare.cpp (100%) rename {app => lib/backend}/src/command/c-prepare.h (100%) rename {app => lib/backend}/src/command/c-run.cpp (100%) rename {app => lib/backend}/src/command/c-run.h (100%) rename {app => lib/backend}/src/command/c-share.cpp (100%) rename {app => lib/backend}/src/command/c-share.h (100%) rename {app => lib/backend}/src/command/c-show.cpp (100%) rename {app => lib/backend}/src/command/c-show.h (100%) rename {app => lib/backend}/src/command/c-update.cpp (100%) rename {app => lib/backend}/src/command/c-update.h (100%) rename {app => lib/backend}/src/command/command.cpp (99%) rename {app => lib/backend}/src/command/command.h (100%) rename {app => lib/backend}/src/command/title-command.cpp (100%) rename {app => lib/backend}/src/command/title-command.h (100%) rename {app => lib/backend}/src/kernel/buildinfo.h (100%) rename {app => lib/backend}/src/kernel/core.cpp (99%) rename {app => lib/backend}/src/kernel/core.h (98%) rename {app => lib/backend}/src/kernel/director.cpp (98%) rename {app => lib/backend}/src/kernel/director.h (99%) rename {app => lib/backend}/src/kernel/directorate.cpp (100%) rename {app => lib/backend}/src/kernel/directorate.h (100%) rename {app => lib/backend}/src/kernel/driver.cpp (79%) rename app/src/kernel/driver.h => lib/backend/src/kernel/driver_p.h (87%) rename {app => lib/backend}/src/kernel/errorstatus.cpp (100%) rename {app => lib/backend}/src/kernel/errorstatus.h (100%) rename {app => lib/backend}/src/task/t-awaitdocker.cpp (100%) rename {app => lib/backend}/src/task/t-awaitdocker.h (100%) rename {app => lib/backend}/src/task/t-bideprocess.cpp (100%) rename {app => lib/backend}/src/task/t-bideprocess.h (100%) rename {app => lib/backend}/src/task/t-download.cpp (100%) rename {app => lib/backend}/src/task/t-download.h (100%) rename {app => lib/backend}/src/task/t-exec.cpp (100%) rename {app => lib/backend}/src/task/t-exec.h (100%) rename {app => lib/backend}/src/task/t-exec_linux.cpp (100%) rename {app => lib/backend}/src/task/t-exec_win.cpp (100%) rename {app => lib/backend}/src/task/t-extra.cpp (100%) rename {app => lib/backend}/src/task/t-extra.h (100%) rename {app => lib/backend}/src/task/t-extract.cpp (100%) rename {app => lib/backend}/src/task/t-extract.h (100%) rename {app => lib/backend}/src/task/t-generic.cpp (100%) rename {app => lib/backend}/src/task/t-generic.h (100%) rename {app => lib/backend}/src/task/t-message.cpp (100%) rename {app => lib/backend}/src/task/t-message.h (100%) rename {app => lib/backend}/src/task/t-mount.cpp (100%) rename {app => lib/backend}/src/task/t-mount.h (100%) rename {app => lib/backend}/src/task/t-sleep.cpp (100%) rename {app => lib/backend}/src/task/t-sleep.h (100%) rename {app => lib/backend}/src/task/task.cpp (100%) rename {app => lib/backend}/src/task/task.h (98%) rename {app => lib/backend}/src/tools/blockingprocessmanager.cpp (100%) rename {app => lib/backend}/src/tools/blockingprocessmanager.h (100%) rename {app => lib/backend}/src/tools/deferredprocessmanager.cpp (100%) rename {app => lib/backend}/src/tools/deferredprocessmanager.h (100%) rename {app => lib/backend}/src/tools/mounter_game_server.cpp (100%) rename {app => lib/backend}/src/tools/mounter_game_server.h (100%) rename {app => lib/backend}/src/tools/mounter_qmp.cpp (100%) rename {app => lib/backend}/src/tools/mounter_qmp.h (92%) rename {app => lib/backend}/src/tools/mounter_router.cpp (100%) rename {app => lib/backend}/src/tools/mounter_router.h (88%) rename {app => lib/backend}/src/utility.h (83%) create mode 100644 lib/frontend_framework/CMakeLists.txt create mode 100644 lib/frontend_framework/cmake/FrontendFramework.cmake create mode 100644 lib/frontend_framework/include/frontend/framework.h create mode 100644 lib/frontend_framework/res/app/CLIFp.ico create mode 100644 lib/frontend_framework/res/resources.qrc create mode 100644 lib/frontend_framework/src/frontend/framework.cpp create mode 100644 lib/frontend_framework/src/frontend/framework_linux.cpp 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 0000000000000000000000000000000000000000..43a609b08acec64586e732309600447fce51af7f GIT binary patch literal 113928 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 Pg2!bo4h|JixDWV0g$rPO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..490b084b7835592b5c2bd4350a8c9a4def91618f GIT binary patch literal 2734 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 +#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 0000000000000000000000000000000000000000..43a609b08acec64586e732309600447fce51af7f GIT binary patch literal 113928 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 Pg2!bo4h|JixDWV0g$rPO literal 0 HcmV?d00001 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; +}