From 0ce65c319a600fcbc93ba8fb10ec82ac3c5b77cb Mon Sep 17 00:00:00 2001 From: Hugues Delorme Date: Thu, 8 Aug 2024 19:02:54 +0200 Subject: [PATCH] App: save/restore application UI state Relates to GitHub #277 --- CMakeLists.txt | 2 + src/app/app_context.cpp | 6 +-- src/app/app_module.cpp | 13 +++++ src/app/app_module_properties.cpp | 3 ++ src/app/app_module_properties.h | 2 + src/app/app_ui_state.cpp | 49 ++++++++++++++++++ src/app/app_ui_state.h | 25 +++++++++ src/app/main.cpp | 1 + src/app/mainwindow.cpp | 22 +++++++- src/app/mainwindow.h | 1 + src/app/widget_main_control.cpp | 16 +++--- src/base/property_value_conversion.cpp | 5 ++ src/base/property_value_conversion.h | 5 +- src/base/settings.cpp | 2 +- tests/test_app.cpp | 16 ++++++ tests/test_app.h | 2 + tests/test_base.cpp | 72 ++++++++++++++++++++++++++ tests/test_base.h | 2 + 18 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 src/app/app_ui_state.cpp create mode 100644 src/app/app_ui_state.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fdb96b3f..c2603e87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -632,6 +632,7 @@ list( APPEND MayoConv_HeaderFiles ${PROJECT_SOURCE_DIR}/src/app/app_module_properties.h ${PROJECT_SOURCE_DIR}/src/app/app_module.h + ${PROJECT_SOURCE_DIR}/src/app/app_ui_state.h ${PROJECT_SOURCE_DIR}/src/app/library_info.h ${PROJECT_SOURCE_DIR}/src/app/recent_files.h ) @@ -646,6 +647,7 @@ list( APPEND MayoConv_SourceFiles ${PROJECT_SOURCE_DIR}/src/app/app_module_properties.cpp ${PROJECT_SOURCE_DIR}/src/app/app_module.cpp + ${PROJECT_SOURCE_DIR}/src/app/app_ui_state.cpp ${PROJECT_SOURCE_DIR}/src/app/library_info.cpp ${PROJECT_SOURCE_DIR}/src/app/recent_files.cpp ) diff --git a/src/app/app_context.cpp b/src/app/app_context.cpp index 8cecc24e..13d6ae10 100644 --- a/src/app/app_context.cpp +++ b/src/app/app_context.cpp @@ -24,9 +24,9 @@ AppContext::AppContext(MainWindow* wnd) assert(m_wnd->widgetPageDocuments() != nullptr); QObject::connect( - m_wnd->widgetPageDocuments(), &WidgetMainControl::currentDocumentIndexChanged, - this, &AppContext::onCurrentDocumentIndexChanged - ); + m_wnd->widgetPageDocuments(), &WidgetMainControl::currentDocumentIndexChanged, + this, &AppContext::onCurrentDocumentIndexChanged + ); } GuiApplication* AppContext::guiApp() const diff --git a/src/app/app_module.cpp b/src/app/app_module.cpp index abc58978..5caa3948 100644 --- a/src/app/app_module.cpp +++ b/src/app/app_module.cpp @@ -169,6 +169,12 @@ Settings::Variant AppModule::toVariant(const Property& prop) const varBlob.setByteArray(true); return varBlob; } + else if (isType(prop)) { + const QByteArray blob = AppUiState::toBlob(constRef(prop)); + Variant varBlob(blob.toStdString()); + varBlob.setByteArray(true); + return varBlob; + } else { return PropertyValueConversion::toVariant(prop); } @@ -184,6 +190,13 @@ bool AppModule::fromVariant(Property* prop, const Settings::Variant& variant) co ptr(prop)->setValue(recentFiles); return stream.status() == QDataStream::Ok; } + else if (isType(prop)) { + bool ok = false; + auto blob = QtCoreUtils::QByteArray_frowRawData(variant.toConstRefString()); + auto uiState = AppUiState::fromBlob(blob, &ok); + ptr(prop)->setValue(uiState); + return ok; + } else { return PropertyValueConversion::fromVariant(prop, variant); } diff --git a/src/app/app_module_properties.cpp b/src/app/app_module_properties.cpp index 87805449..ddb04cd9 100644 --- a/src/app/app_module_properties.cpp +++ b/src/app/app_module_properties.cpp @@ -50,9 +50,11 @@ AppModuleProperties::AppModuleProperties(Settings* settings) settings->addSetting(&this->actionOnDocumentFileChange, groupId_application); settings->addSetting(&this->linkWithDocumentSelector, groupId_application); settings->addSetting(&this->forceOpenGlFallbackWidget, groupId_application); + settings->addSetting(&this->appUiState, groupId_application); this->recentFiles.setUserVisible(false); this->lastOpenDir.setUserVisible(false); this->lastSelectedFormatFilter.setUserVisible(false); + this->appUiState.setUserVisible(false); // Meshing this->meshingQuality.mutableEnumeration().changeTrContext(AppModuleProperties::textIdContext()); @@ -88,6 +90,7 @@ AppModuleProperties::AppModuleProperties(Settings* settings) this->lastSelectedFormatFilter.setValue({}); this->actionOnDocumentFileChange.setValue(ActionOnDocumentFileChange::None); this->linkWithDocumentSelector.setValue(true); + this->appUiState.setValue({}); #ifndef MAYO_OS_MAC this->forceOpenGlFallbackWidget.setValue(false); #else diff --git a/src/app/app_module_properties.h b/src/app/app_module_properties.h index 73a71af3..b4df2395 100644 --- a/src/app/app_module_properties.h +++ b/src/app/app_module_properties.h @@ -6,6 +6,7 @@ #pragma once +#include "app_ui_state.h" #include "recent_files.h" #include "../base/io_format.h" @@ -60,6 +61,7 @@ class AppModuleProperties : public PropertyGroup { PropertyEnum actionOnDocumentFileChange{ this, textId("actionOnDocumentFileChange") }; PropertyBool linkWithDocumentSelector{ this, textId("linkWithDocumentSelector") }; PropertyBool forceOpenGlFallbackWidget{ this, textId("forceOpenGlFallbackWidget") }; + PropertyAppUiState appUiState{ this, textId("appUiState") }; // Meshing const Settings::GroupIndex groupId_meshing; enum class BRepMeshQuality { VeryCoarse, Coarse, Normal, Precise, VeryPrecise, UserDefined }; diff --git a/src/app/app_ui_state.cpp b/src/app/app_ui_state.cpp new file mode 100644 index 00000000..09c389ad --- /dev/null +++ b/src/app/app_ui_state.cpp @@ -0,0 +1,49 @@ +#include "app_ui_state.h" + +#include "../qtcommon/qtcore_utils.h" +#include +#include + +namespace Mayo { + +template<> const char PropertyAppUiState::TypeName[] = "Mayo::PropertyAppUiState"; + +QByteArray AppUiState::toBlob(const AppUiState& state) +{ + QByteArray blob; + QDataStream stream(&blob, QIODevice::WriteOnly); + stream << QtCoreUtils::QByteArray_frowRawData(std::string_view{PropertyAppUiState::TypeName}); + constexpr uint32_t version = 1; + stream << version; + stream << state.mainWindowGeometry; + stream << state.pageDocuments_isLeftSideBarVisible; + return blob; + +} + +AppUiState AppUiState::fromBlob(const QByteArray& blob, bool* ok) +{ + auto fnSetOk = [=](bool v) { + if (ok) + *ok = v; + }; + + fnSetOk(false); + AppUiState state; + QDataStream stream(blob); + QByteArray identifier; + stream >> identifier; + if (identifier == PropertyAppUiState::TypeName) { + uint32_t version = 0; + stream >> version; + if (version == 1) { + stream >> state.mainWindowGeometry; + stream >> state.pageDocuments_isLeftSideBarVisible; + fnSetOk(true); + } + } + + return state; +} + +} // namespace Mayo diff --git a/src/app/app_ui_state.h b/src/app/app_ui_state.h new file mode 100644 index 00000000..05aa040b --- /dev/null +++ b/src/app/app_ui_state.h @@ -0,0 +1,25 @@ +/**************************************************************************** +** Copyright (c) 2024, Fougue Ltd. +** All rights reserved. +** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt +****************************************************************************/ + +#pragma once + +#include "../base/property_builtins.h" +#include + +namespace Mayo { + +// Stores the UI state of the main application widgets +struct AppUiState { + QByteArray mainWindowGeometry; // Provided by QWidget::saveGeometry() + bool pageDocuments_isLeftSideBarVisible = true; + + // Serialization functions + static QByteArray toBlob(const AppUiState& state); + static AppUiState fromBlob(const QByteArray& blob, bool* ok = nullptr); +}; +using PropertyAppUiState = GenericProperty; + +} // namespace Mayo diff --git a/src/app/main.cpp b/src/app/main.cpp index 450f90af..69b9b603 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -438,6 +438,7 @@ static int runApp(QCoreApplication* qtApp) // Create MainWindow MainWindow mainWindow(guiApp); mainWindow.setWindowTitle(QCoreApplication::applicationName()); + appModule->settings()->loadProperty(&appModule->properties()->appUiState); mainWindow.show(); if (!args.listFilepathToOpen.empty()) { QTimer::singleShot(0, qtApp, [&]{ mainWindow.openDocumentsFromList(args.listFilepathToOpen); }); diff --git a/src/app/mainwindow.cpp b/src/app/mainwindow.cpp index 2d8e10f9..3b2164ff 100644 --- a/src/app/mainwindow.cpp +++ b/src/app/mainwindow.cpp @@ -73,6 +73,13 @@ MainWindow::~MainWindow() void MainWindow::showEvent(QShowEvent* event) { + const auto& uiState = AppModule::get()->properties()->appUiState.value(); + if (!uiState.mainWindowGeometry.isEmpty()) + this->restoreGeometry(uiState.mainWindowGeometry); + + if (this->widgetPageDocuments()) + this->widgetPageDocuments()->widgetLeftSideBar()->setVisible(uiState.pageDocuments_isLeftSideBarVisible); + QMainWindow::showEvent(event); #if defined(Q_OS_WIN) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) constexpr Qt::FindChildOption findMode = Qt::FindDirectChildrenOnly; @@ -84,6 +91,17 @@ void MainWindow::showEvent(QShowEvent* event) #endif } +void MainWindow::closeEvent(QCloseEvent* event) +{ + AppUiState uiState = AppModule::get()->properties()->appUiState; + uiState.mainWindowGeometry = this->saveGeometry(); + if (this->widgetPageDocuments()) + uiState.pageDocuments_isLeftSideBarVisible = this->widgetPageDocuments()->widgetLeftSideBar()->isVisible(); + + AppModule::get()->properties()->appUiState.setValue(uiState); + QMainWindow::closeEvent(event); +} + void MainWindow::addPage(IAppContext::Page page, IWidgetMainPage* pageWidget) { assert(m_mapWidgetPage.find(page) == m_mapWidgetPage.cend()); @@ -91,8 +109,8 @@ void MainWindow::addPage(IAppContext::Page page, IWidgetMainPage* pageWidget) m_mapWidgetPage.insert({ page, pageWidget }); m_ui->stack_Main->addWidget(pageWidget); QObject::connect( - pageWidget, &IWidgetMainPage::updateGlobalControlsActivationRequired, - this, &MainWindow::updateControlsActivation + pageWidget, &IWidgetMainPage::updateGlobalControlsActivationRequired, + this, &MainWindow::updateControlsActivation ); } diff --git a/src/app/mainwindow.h b/src/app/mainwindow.h index c82dd1db..c3a50da4 100644 --- a/src/app/mainwindow.h +++ b/src/app/mainwindow.h @@ -35,6 +35,7 @@ class MainWindow : public QMainWindow { protected: void showEvent(QShowEvent* event) override; + void closeEvent(QCloseEvent* event) override; private: void addPage(IAppContext::Page page, IWidgetMainPage* pageWidget); diff --git a/src/app/widget_main_control.cpp b/src/app/widget_main_control.cpp index ebf14519..fb3f5f54 100644 --- a/src/app/widget_main_control.cpp +++ b/src/app/widget_main_control.cpp @@ -66,21 +66,21 @@ WidgetMainControl::WidgetMainControl(GuiApplication* guiApp, QWidget* parent) // "Window" actions and navigation in documents QObject::connect( - m_ui->combo_GuiDocuments, qOverload(&QComboBox::currentIndexChanged), - this, &WidgetMainControl::onCurrentDocumentIndexChanged + m_ui->combo_GuiDocuments, qOverload(&QComboBox::currentIndexChanged), + this, &WidgetMainControl::onCurrentDocumentIndexChanged ); QObject::connect( - m_ui->widget_FileSystem, &WidgetFileSystem::locationActivated, - this, &WidgetMainControl::onWidgetFileSystemLocationActivated + m_ui->widget_FileSystem, &WidgetFileSystem::locationActivated, + this, &WidgetMainControl::onWidgetFileSystemLocationActivated ); // ... QObject::connect( - m_ui->combo_LeftContents, qOverload(&QComboBox::currentIndexChanged), - this, &WidgetMainControl::onLeftContentsPageChanged + m_ui->combo_LeftContents, qOverload(&QComboBox::currentIndexChanged), + this, &WidgetMainControl::onLeftContentsPageChanged ); QObject::connect( - m_ui->listView_OpenedDocuments, &QListView::clicked, - this, [=](const QModelIndex& index) { this->setCurrentDocumentIndex(index.row()); } + m_ui->listView_OpenedDocuments, &QListView::clicked, + this, [=](const QModelIndex& index) { this->setCurrentDocumentIndex(index.row()); } ); guiApp->application()->signalDocumentFilePathChanged.connectSlot([=](const DocumentPtr& doc, const FilePath& fp) { diff --git a/src/base/property_value_conversion.cpp b/src/base/property_value_conversion.cpp index 9f8f6e71..0a3131e7 100644 --- a/src/base/property_value_conversion.cpp +++ b/src/base/property_value_conversion.cpp @@ -289,6 +289,11 @@ static void assignBoolPtr(bool* value, bool on) *value = on; } +bool PropertyValueConversion::Variant::isValid() const +{ + return this->index() != 0; // not std::monostate +} + bool PropertyValueConversion::Variant::toBool(bool* ok) const { assignBoolPtr(ok, true); diff --git a/src/base/property_value_conversion.h b/src/base/property_value_conversion.h index 4ac1c847..b2a419e1 100644 --- a/src/base/property_value_conversion.h +++ b/src/base/property_value_conversion.h @@ -17,9 +17,9 @@ namespace Mayo { class PropertyValueConversion { public: // Variant type to be used when (de)serializing values - class Variant : public std::variant { + class Variant : public std::variant { public: - using BaseType = std::variant; + using BaseType = std::variant; Variant() = default; Variant(bool v) : BaseType(v) {} Variant(int v) : BaseType(v) {} @@ -28,6 +28,7 @@ class PropertyValueConversion { Variant(const char* str) : BaseType(std::string(str)) {} Variant(const std::string& str) : BaseType(str) {} + bool isValid() const; bool toBool(bool* ok = nullptr) const; int toInt(bool* ok = nullptr) const; double toDouble(bool* ok = nullptr) const; diff --git a/src/base/settings.cpp b/src/base/settings.cpp index 219cb4a2..3115f675 100644 --- a/src/base/settings.cpp +++ b/src/base/settings.cpp @@ -89,7 +89,7 @@ class Settings::Private { const std::string settingPath = std::string(sectionPath).append("/").append(propertyKey); if (source.contains(settingPath)) { const Settings::Variant value = source.value(settingPath); - const bool ok = m_propValueConverter->fromVariant(property, value); + const bool ok = m_propValueConverter->fromVariant(property, value); if (!ok) { // TODO Use other output stream(dedicated Messenger object?) std::cerr << fmt::format("Failed to load setting [path={}]", settingPath) << std::endl; diff --git a/tests/test_app.cpp b/tests/test_app.cpp index 7cce3d15..f919ba12 100644 --- a/tests/test_app.cpp +++ b/tests/test_app.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include namespace Mayo { @@ -262,6 +263,21 @@ void TestApp::RecentFiles_QPixmap_test() } } +void TestApp::AppUiState_test() +{ + QWidget widget; + AppUiState uiState; + uiState.mainWindowGeometry = widget.saveGeometry(); + uiState.pageDocuments_isLeftSideBarVisible = true; + QByteArray blobSave = AppUiState::toBlob(uiState); + + bool ok = false; + const AppUiState uiState_read = AppUiState::fromBlob(blobSave, &ok); + QVERIFY(ok); + QCOMPARE(uiState.mainWindowGeometry, uiState_read.mainWindowGeometry); + QCOMPARE(uiState.pageDocuments_isLeftSideBarVisible, uiState_read.pageDocuments_isLeftSideBarVisible); +} + void TestApp::StringConv_test() { const QString text = "test_éç²µ§_测试_Тест"; diff --git a/tests/test_app.h b/tests/test_app.h index 28dd1867..20263041 100644 --- a/tests/test_app.h +++ b/tests/test_app.h @@ -26,6 +26,8 @@ private slots: void RecentFiles_test(); void RecentFiles_QPixmap_test(); + void AppUiState_test(); + void StringConv_test(); void QtGuiUtils_test(); diff --git a/tests/test_base.cpp b/tests/test_base.cpp index d4ab9c61..a84cfc7e 100644 --- a/tests/test_base.cpp +++ b/tests/test_base.cpp @@ -29,6 +29,7 @@ #include "../src/base/property_builtins.h" #include "../src/base/property_enumeration.h" #include "../src/base/property_value_conversion.h" +#include "../src/base/settings.h" #include "../src/base/string_conv.h" #include "../src/base/task_manager.h" #include "../src/base/tkernel_utils.h" @@ -72,6 +73,7 @@ #include #include #include +#include // Needed for Q_FECTH() Q_DECLARE_METATYPE(Mayo::UnitSystem::TranslateResult) @@ -998,6 +1000,76 @@ void TestBase::TKernelUtils_colorFromHex_test_data() QTest::newRow("RGB(100,150,200)") << 100 << 150 << 200 << "#6496C8"; } +namespace { + +class TestProperties : public PropertyGroup { + MAYO_DECLARE_TEXT_ID_FUNCTIONS(Mayo::TestProperties) +public: + TestProperties(Settings* settings) + : PropertyGroup(settings), + groupId_main(settings->addGroup(textId("main"))) + { + settings->addSetting(&this->someInt, groupId_main); + settings->addResetFunction(groupId_main, [&]{ + this->someInt.setValue(-1); + }); + } + + const Settings::GroupIndex groupId_main; + PropertyInt someInt{ this, textId("someInt") }; +}; + +class TestSettingsStorage : public Settings::Storage { +public: + bool contains(std::string_view key) const override + { + return m_mapValue.find(key) != m_mapValue.cend(); + } + + Settings::Variant value(std::string_view key) const override + { + auto it = m_mapValue.find(key); + return it != m_mapValue.cend() ? it->second : Settings::Variant{}; + } + + void setValue(std::string_view key, const Settings::Variant& value) override + { + m_mapValue.insert_or_assign(key, value); + } + + void sync() override + { + } + +private: + std::unordered_map m_mapValue; +}; + +} // namespace + +void TestBase::Settings_test() +{ + Settings settings; + { + auto settingsStorage = std::make_unique(); + settingsStorage->setValue("main/someInt", Settings::Variant{5}); + + Settings::Variant bytesVar("abcde_12345"); + bytesVar.setByteArray(true); + settingsStorage->setValue("main/someTestData", bytesVar); + + settings.setStorage(std::move(settingsStorage)); + } + + TestProperties props(&settings); + + settings.resetAll(); + QCOMPARE(props.someInt.value(), -1); + + settings.load(); + QCOMPARE(props.someInt.value(), 5); +} + void TestBase::UnitSystem_test() { QFETCH(UnitSystem::TranslateResult, trResultActual); diff --git a/tests/test_base.h b/tests/test_base.h index d90a9f3f..9b45f33a 100644 --- a/tests/test_base.h +++ b/tests/test_base.h @@ -69,6 +69,8 @@ private slots: void TKernelUtils_colorFromHex_test(); void TKernelUtils_colorFromHex_test_data(); + void Settings_test(); + void UnitSystem_test(); void UnitSystem_test_data();