diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 1bf492d20..67e5c3843 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -461,7 +461,7 @@ jobs: run: apk add --upgrade glibmm-dev gtkmm3-dev - name: Get Dependencies (Qt6) if: ${{ needs.what-to-make.outputs.make-qt == 'true' }} - run: apk add --upgrade qt6-qttools-dev qt6-qtsvg-dev + run: apk add --upgrade qt6-qtbase-dev qt6-qttools-dev qt6-qtsvg-dev xcb-util-cursor - name: Get Source uses: actions/checkout@v4 with: @@ -494,6 +494,7 @@ jobs: if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} env: TMPDIR: /private/tmp + QT_QPA_PLATFORM: offscreen run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure - name: Install run: cmake --build obj --config RelWithDebInfo --target install/strip @@ -765,6 +766,8 @@ jobs: run: cmake --build obj --config RelWithDebInfo - name: Test if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} + env: + QT_QPA_PLATFORM: offscreen run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure - name: Install run: cmake --build obj --config RelWithDebInfo --target install/strip @@ -814,7 +817,7 @@ jobs: run: dnf install -y glibmm2.68-devel gtkmm4.0-devel - name: Get Dependencies (Qt6) if: ${{ needs.what-to-make.outputs.make-qt == 'true' }} - run: dnf install -y qt6-qtbase-devel qt6-qtsvg-devel qt6-qttools-devel + run: dnf install -y qt6-qtbase-devel qt6-qtsvg-devel qt6-qttools-devel xcb-util-cursor - name: Get Source uses: actions/download-artifact@v4 with: @@ -845,6 +848,8 @@ jobs: run: cmake --build obj --config RelWithDebInfo - name: Test if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} + env: + QT_QPA_PLATFORM: offscreen run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure - name: Install run: cmake --build obj --config RelWithDebInfo --target install/strip @@ -1014,7 +1019,7 @@ jobs: run: sudo apt-get install -y --no-install-recommends libglibmm-2.4-dev libgtkmm-3.0-dev - name: Get Dependencies (Qt6) if: ${{ needs.what-to-make.outputs.make-qt == 'true' }} - run: sudo apt-get install -y --no-install-recommends qt6-svg-dev qt6-tools-dev + run: sudo apt-get install -y --no-install-recommends qt6-base-dev qt6-svg-dev qt6-tools-dev - name: Get Source uses: actions/checkout@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 94e370def..1143d71b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -409,6 +409,9 @@ if(ENABLE_QT) Network Svg LinguistTools) + if(ENABLE_TESTS) + list(APPEND QT_REQUIRED_MODULES Test) + endif() set(QT_OPTIONAL_MODULES DBus AxContainer @@ -429,7 +432,7 @@ if(ENABLE_QT) foreach(M ${QT_REQUIRED_MODULES}) find_package(Qt${Qt_VERSION_MAJOR}${M} ${QT_MINIMUM} QUIET) if(Qt${Qt_VERSION_MAJOR}${M}_FOUND) - if(NOT M STREQUAL "LinguistTools") + if(NOT M STREQUAL "LinguistTools" AND NOT M STREQUAL "Test") list(APPEND QT_TARGETS Qt${Qt_VERSION_MAJOR}::${M}) endif() else() @@ -811,11 +814,6 @@ if(RUN_CLANG_TIDY) endif() endif() -if(ENABLE_TESTS) - enable_testing() - add_subdirectory(tests) -endif() - function(tr_install_web DST_DIR) if(INSTALL_WEB) install( @@ -844,6 +842,11 @@ foreach(P cli daemon gtk mac qt utils) endif() endforeach() +if(ENABLE_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + if(ENABLE_DAEMON OR ENABLE_GTK OR ENABLE_QT) tr_install_web(${CMAKE_INSTALL_DATAROOTDIR}/${TR_NAME}) endif() diff --git a/qt/Application.cc b/qt/Application.cc index 939542970..d60e8dffa 100644 --- a/qt/Application.cc +++ b/qt/Application.cc @@ -128,14 +128,14 @@ QAccessibleInterface* accessibleFactory(QString const& className, QObject* objec } // namespace Application::Application( - std::unique_ptr prefs, + Prefs& prefs, bool minimized, QString const& config_dir, QStringList const& filenames, int& argc, char** argv) : QApplication{ argc, argv } - , prefs_(std::move(prefs)) + , prefs_{ prefs } { setApplicationName(ConfigName); loadTranslations(); @@ -162,9 +162,9 @@ Application::Application( QAccessible::installFactory(&accessibleFactory); #endif - session_ = std::make_unique(config_dir, *prefs_); - model_ = std::make_unique(*prefs_); - window_ = std::make_unique(*session_, *prefs_, *model_, minimized); + session_ = std::make_unique(config_dir, prefs_); + model_ = std::make_unique(prefs_); + window_ = std::make_unique(*session_, prefs_, *model_, minimized); watch_dir_ = std::make_unique(*model_); connect(this, &QCoreApplication::aboutToQuit, this, &Application::saveGeometry); @@ -172,7 +172,7 @@ Application::Application( connect(model_.get(), &TorrentModel::torrentsCompleted, this, &Application::onTorrentsCompleted); connect(model_.get(), &TorrentModel::torrentsEdited, this, &Application::onTorrentsEdited); connect(model_.get(), &TorrentModel::torrentsNeedInfo, this, &Application::onTorrentsNeedInfo); - connect(prefs_.get(), &Prefs::changed, this, &Application::refreshPref); + connect(&prefs_, &Prefs::changed, this, &Application::refreshPref); connect(session_.get(), &Session::sourceChanged, this, &Application::onSessionSourceChanged); connect(session_.get(), &Session::torrentsRemoved, model_.get(), &TorrentModel::removeTorrents); connect(session_.get(), &Session::torrentsUpdated, model_.get(), &TorrentModel::updateTorrents); @@ -295,7 +295,7 @@ QStringList Application::getNames(torrent_ids_t const& torrent_ids) const void Application::onTorrentsAdded(torrent_ids_t const& torrent_ids) const { - if (!prefs_->get(Prefs::SHOW_NOTIFICATION_ON_ADD)) + if (!prefs_.get(Prefs::SHOW_NOTIFICATION_ON_ADD)) { return; } @@ -308,19 +308,19 @@ void Application::onTorrentsAdded(torrent_ids_t const& torrent_ids) const void Application::onTorrentsCompleted(torrent_ids_t const& torrent_ids) const { - if (prefs_->get(Prefs::SHOW_NOTIFICATION_ON_COMPLETE)) + if (prefs_.get(Prefs::SHOW_NOTIFICATION_ON_COMPLETE)) { auto const title = tr("Torrent(s) Completed", nullptr, static_cast(std::size(torrent_ids))); auto const body = getNames(torrent_ids).join(QStringLiteral("\n")); notifyApp(title, body); } - if (prefs_->get(Prefs::COMPLETE_SOUND_ENABLED)) + if (prefs_.get(Prefs::COMPLETE_SOUND_ENABLED)) { #if defined(Q_OS_WIN) || defined(Q_OS_MAC) beep(); #else - auto args = prefs_->get(Prefs::COMPLETE_SOUND_COMMAND); + auto args = prefs_.get(Prefs::COMPLETE_SOUND_COMMAND); auto const command = args.takeFirst(); QProcess::execute(command, args); #endif @@ -346,13 +346,13 @@ void Application::notifyTorrentAdded(Torrent const* tor) const void Application::saveGeometry() const { - if (prefs_ != nullptr && window_ != nullptr) + if (window_ != nullptr) { auto const geometry = window_->geometry(); - prefs_->set(Prefs::MAIN_WINDOW_HEIGHT, std::max(100, geometry.height())); - prefs_->set(Prefs::MAIN_WINDOW_WIDTH, std::max(100, geometry.width())); - prefs_->set(Prefs::MAIN_WINDOW_X, geometry.x()); - prefs_->set(Prefs::MAIN_WINDOW_Y, geometry.y()); + prefs_.set(Prefs::MAIN_WINDOW_HEIGHT, std::max(100, geometry.height())); + prefs_.set(Prefs::MAIN_WINDOW_WIDTH, std::max(100, geometry.width())); + prefs_.set(Prefs::MAIN_WINDOW_X, geometry.x()); + prefs_.set(Prefs::MAIN_WINDOW_Y, geometry.y()); } } @@ -368,7 +368,7 @@ void Application::refreshPref(int key) const case Prefs::DIR_WATCH: case Prefs::DIR_WATCH_ENABLED: - watch_dir_->setPath(prefs_->get(Prefs::DIR_WATCH), prefs_->get(Prefs::DIR_WATCH_ENABLED)); + watch_dir_->setPath(prefs_.get(Prefs::DIR_WATCH), prefs_.get(Prefs::DIR_WATCH_ENABLED)); break; default: @@ -378,19 +378,19 @@ void Application::refreshPref(int key) const void Application::maybeUpdateBlocklist() const { - if (!prefs_->get(Prefs::BLOCKLIST_UPDATES_ENABLED)) + if (!prefs_.get(Prefs::BLOCKLIST_UPDATES_ENABLED)) { return; } - QDateTime const last_updated_at = prefs_->get(Prefs::BLOCKLIST_DATE); - QDateTime const next_update_at = last_updated_at.addDays(7); - QDateTime const now = QDateTime::currentDateTime(); + auto const last_updated_at = prefs_.get(Prefs::BLOCKLIST_DATE); + auto const next_update_at = last_updated_at.addDays(7); + auto const now = QDateTime::currentDateTime(); if (now < next_update_at) { session_->updateBlocklist(); - prefs_->set(Prefs::BLOCKLIST_DATE, now); + prefs_.set(Prefs::BLOCKLIST_DATE, now); } } @@ -426,8 +426,8 @@ void Application::refreshTorrents() void Application::addWatchdirTorrent(QString const& filename) const { auto add_data = AddData{ filename }; - auto const disposal = prefs_->get(Prefs::TRASH_ORIGINAL) ? AddData::FilenameDisposal::Delete : - AddData::FilenameDisposal::Rename; + auto const disposal = prefs_.get(Prefs::TRASH_ORIGINAL) ? AddData::FilenameDisposal::Delete : + AddData::FilenameDisposal::Rename; add_data.setFileDisposal(disposal); addTorrent(std::move(add_data)); } @@ -441,18 +441,18 @@ void Application::addTorrent(AddData addme) const // if there's not already a disposal action set, // then honor the `trash original` preference setting - if (!addme.fileDisposal() && prefs_->get(Prefs::TRASH_ORIGINAL)) + if (!addme.fileDisposal() && prefs_.get(Prefs::TRASH_ORIGINAL)) { addme.setFileDisposal(AddData::FilenameDisposal::Delete); } - if (!prefs_->get(Prefs::OPTIONS_PROMPT)) + if (!prefs_.get(Prefs::OPTIONS_PROMPT)) { session_->addTorrent(addme); } else { - auto* o = new OptionsDialog{ *session_, *prefs_, addme, window_.get() }; + auto* o = new OptionsDialog{ *session_, prefs_, addme, window_.get() }; o->show(); } diff --git a/qt/Application.h b/qt/Application.h index 73ad19e71..2e9fee163 100644 --- a/qt/Application.h +++ b/qt/Application.h @@ -36,13 +36,7 @@ class Application : public QApplication Q_OBJECT public: - Application( - std::unique_ptr prefs, - bool minimized, - QString const& config_dir, - QStringList const& filenames, - int& argc, - char** argv); + Application(Prefs& prefs, bool minimized, QString const& config_dir, QStringList const& filenames, int& argc, char** argv); Application(Application&&) = delete; Application(Application const&) = delete; Application& operator=(Application&&) = delete; @@ -107,7 +101,7 @@ private: std::unordered_set interned_strings_; - std::unique_ptr prefs_; + Prefs& prefs_; std::unique_ptr session_; std::unique_ptr model_; std::unique_ptr window_; diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index 1a6f81ef2..3cdfe061a 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -3,9 +3,9 @@ set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "Generated Files") # https://doc.qt.io/qt-6/macos.html#supported-versions set(CMAKE_OSX_DEPLOYMENT_TARGET 11) -add_executable(${TR_NAME}-qt WIN32) +add_library(${TR_NAME}-qt-lib STATIC) -target_sources(${TR_NAME}-qt +target_sources(${TR_NAME}-qt-lib PRIVATE AboutDialog.cc AboutDialog.h @@ -56,7 +56,6 @@ target_sources(${TR_NAME}-qt InteropObject.h LicenseDialog.cc LicenseDialog.h - main.cc MainWindow.cc MainWindow.h MakeDialog.cc @@ -107,6 +106,8 @@ target_sources(${TR_NAME}-qt TrackerModel.h TrackerModelFilter.cc TrackerModelFilter.h + TrQtInit.cc + TrQtInit.h Typedefs.h Utils.cc Utils.h @@ -121,7 +122,7 @@ tr_allow_compile_if( [=[[ENABLE_QT_DBUS_INTEROP]]=] DBusInteropHelper.cc) -target_sources(${TR_NAME}-qt +target_sources(${TR_NAME}-qt-lib PRIVATE AboutDialog.ui DetailsDialog.ui @@ -139,7 +140,7 @@ target_sources(${TR_NAME}-qt source_group(Ui REGULAR_EXPRESSION [[.*\.ui$]]) -target_sources(${TR_NAME}-qt +target_sources(${TR_NAME}-qt-lib PRIVATE application.qrc) @@ -196,35 +197,49 @@ if(TS_FILES) tr_qt_add_translation(QM_FILES ${TS_FILES}) endif() -target_sources(${TR_NAME}-qt +target_sources(${TR_NAME}-qt-lib PRIVATE ${QM_FILES}) if(ENABLE_QT_COM_INTEROP) - tr_target_idl_files(${TR_NAME}-qt + tr_target_idl_files(${TR_NAME}-qt-lib transmission-qt.idl) endif() -target_include_directories(${TR_NAME}-qt - PRIVATE +target_include_directories(${TR_NAME}-qt-lib + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(${TR_NAME}-qt - PRIVATE +target_link_libraries(${TR_NAME}-qt-lib + PUBLIC ${TR_NAME}-app ${TR_NAME} transmission::qt_impl) -target_compile_definitions(${TR_NAME}-qt +target_compile_definitions(${TR_NAME}-qt-lib PRIVATE "TRANSLATIONS_DIR=\"${CMAKE_INSTALL_FULL_DATADIR}/${TR_NAME}/translations\"" QT_NO_CAST_FROM_ASCII $<$:ENABLE_COM_INTEROP> $<$:ENABLE_DBUS_INTEROP>) -if(MSVC) - tr_append_target_property(${TR_NAME}-qt LINK_FLAGS "/ENTRY:mainCRTStartup") -endif() +set_target_properties( + ${TR_NAME}-qt-lib + PROPERTIES + AUTOMOC ON + AUTORCC ON + AUTOUIC ON) + +add_executable(${TR_NAME}-qt WIN32) + +target_sources(${TR_NAME}-qt + PRIVATE + main.cc + application.qrc) + +target_link_libraries(${TR_NAME}-qt + PRIVATE + ${TR_NAME}-qt-lib) set_target_properties( ${TR_NAME}-qt @@ -233,6 +248,10 @@ set_target_properties( AUTORCC ON AUTOUIC ON) +if(MSVC) + tr_append_target_property(${TR_NAME}-qt LINK_FLAGS "/ENTRY:mainCRTStartup") +endif() + tr_win32_app_info(${TR_NAME}-qt "Transmission Qt Client" "${TR_NAME}-qt" diff --git a/qt/Prefs.cc b/qt/Prefs.cc index 068064d29..d7a730514 100644 --- a/qt/Prefs.cc +++ b/qt/Prefs.cc @@ -3,20 +3,14 @@ // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. -#include #include #include +#include #include #include #include #include -#include -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#else -#include -#endif #include @@ -27,42 +21,108 @@ #include "CustomVariantType.h" #include "Filters.h" #include "Prefs.h" -#include "VariantHelpers.h" namespace api_compat = libtransmission::api_compat; -using libtransmission::serializer::to_value; -using libtransmission::serializer::to_variant; -using ::trqt::variant_helpers::dictAdd; -using ::trqt::variant_helpers::getValue; +namespace ser = libtransmission::serializer; using namespace std::string_view_literals; // --- namespace { - -void ensureSoundCommandIsAList(tr_variant* dict) +template +[[nodiscard]] QVariant qvarFromOptional(std::optional const& val) { - tr_quark const key = TR_KEY_torrent_complete_sound_command; - - if (tr_variant* list = nullptr; tr_variantDictFindList(dict, key, &list)) - { - return; - } - - tr_variantDictRemove(dict, key); - dictAdd( - dict, - key, - std::array{ - "canberra-gtk-play", - "-i", - "complete-download", - "-d", - "transmission torrent downloaded", - }); + return val ? QVariant::fromValue(*val) : QVariant{}; } +[[nodiscard]] QVariant qvarFromTVar(tr_variant const& var, int const qt_metatype) +{ + switch (qt_metatype) + { + case QMetaType::Int: + return qvarFromOptional(ser::to_value(var)); + + case CustomVariantType::EncryptionModeType: + return qvarFromOptional(ser::to_value(var)); + + case CustomVariantType::SortModeType: + return qvarFromOptional(ser::to_value(var)); + + case CustomVariantType::ShowModeType: + return qvarFromOptional(ser::to_value(var)); + + case QMetaType::QString: + return qvarFromOptional(ser::to_value(var)); + + case QMetaType::QStringList: + return qvarFromOptional(ser::to_value(var)); + + case QMetaType::Bool: + return qvarFromOptional(ser::to_value(var)); + + case QMetaType::Double: + return qvarFromOptional(ser::to_value(var)); + + case QMetaType::QDateTime: + return qvarFromOptional(ser::to_value(var)); + + default: + assert(false && "unhandled type"); + return {}; + } +} + +[[nodiscard]] tr_variant trvarFromQVar(QVariant const& var, int const qt_metatype) +{ + switch (qt_metatype) + { + case QMetaType::Int: + return ser::to_variant(var.value()); + + case CustomVariantType::EncryptionModeType: + return ser::to_variant(var.value()); + + case CustomVariantType::SortModeType: + return ser::to_variant(var.value()); + + case CustomVariantType::ShowModeType: + return ser::to_variant(var.value()); + + case QMetaType::QString: + return ser::to_variant(var.value()); + + case QMetaType::QStringList: + return ser::to_variant(var.value()); + + case QMetaType::Bool: + return ser::to_variant(var.value()); + + case QMetaType::Double: + return ser::to_variant(var.value()); + + case QMetaType::QDateTime: + return ser::to_variant(var.value()); + + default: + assert(false && "unhandled type"); + return {}; + } +} + +void ensureSoundCommandIsAList(tr_variant::Map& map) +{ + auto constexpr Key = TR_KEY_torrent_complete_sound_command; + auto constexpr DefaultVal = std::array{ "canberra-gtk-play", + "-i", + "complete-download", + "-d", + "transmission torrent downloaded" }; + if (map.find_if(Key) == nullptr) + { + map.insert_or_assign(Key, ser::to_variant(DefaultVal)); + } +} } // namespace std::array const Prefs::Items{ @@ -182,224 +242,119 @@ namespace } } // namespace -/*** -**** -***/ +// --- -Prefs::Prefs(QString config_dir) - : config_dir_{ std::move(config_dir) } +Prefs::Prefs() { static_assert(sizeof(Items) / sizeof(Items[0]) == PREFS_COUNT); - #ifndef NDEBUG for (int i = 0; i < PREFS_COUNT; ++i) { assert(Items[i].id == i); } - #endif - auto const app_defaults = get_default_app_settings(); - auto settings = tr_sessionLoadSettings(config_dir_.toStdString(), &app_defaults); - ensureSoundCommandIsAList(&settings); + load(defaults()); +} - for (int i = 0; i < PREFS_COUNT; ++i) +void Prefs::loadFromConfigDir(QString const dir) +{ + auto settings = tr_sessionLoadSettings(dir.toStdString()); + if (auto* const map = settings.get_if()) { - tr_variant const* b = tr_variantDictFind(&settings, getKey(i)); + ensureSoundCommandIsAList(*map); + load(*map); + } +} - switch (Items[i].type) +void Prefs::load(tr_variant::Map const& settings) +{ + for (int idx = 0; idx < PREFS_COUNT; ++idx) + { + if (auto const iter = settings.find(getKey(idx)); iter != settings.end()) { - case QMetaType::Int: - if (auto const value = getValue(b); value) - { - values_[i].setValue(*value); - } - break; - - case CustomVariantType::EncryptionModeType: - if (auto const val = to_value(*b)) - { - values_[i] = QVariant::fromValue(*val); - } - break; - - case CustomVariantType::SortModeType: - if (auto const val = to_value(*b)) - { - values_[i] = QVariant::fromValue(*val); - } - break; - - case CustomVariantType::ShowModeType: - if (auto const val = to_value(*b)) - { - values_[i] = QVariant::fromValue(*val); - } - break; - - case QMetaType::QString: - if (auto const value = getValue(b); value) - { - values_[i].setValue(*value); - } - break; - - case QMetaType::QStringList: - if (auto const value = getValue(b); value) - { - values_[i].setValue(*value); - } - break; - - case QMetaType::Bool: - if (auto const value = getValue(b); value) - { - values_[i].setValue(*value); - } - break; - - case QMetaType::Double: - if (auto const value = getValue(b); value) - { - values_[i].setValue(*value); - } - break; - - case QMetaType::QDateTime: - if (auto const value = getValue(b); value) - { - values_[i].setValue(QDateTime::fromSecsSinceEpoch(*value)); - } - break; - - default: - assert(false && "unhandled type"); - break; + values_[idx] = qvarFromTVar(iter->second, Items[idx].type); } } } -Prefs::~Prefs() +tr_variant::Map Prefs::current_settings() const { - // make a dict from settings.json - tr_variant current_settings; - tr_variantInitDict(¤t_settings, PREFS_COUNT); + auto map = tr_variant::Map{ PREFS_COUNT }; - for (int i = 0; i < PREFS_COUNT; ++i) + for (int idx = 0; idx < PREFS_COUNT; ++idx) { - if (!prefIsSavable(i)) + if (prefIsSavable(idx)) { - continue; - } - - auto const key = getKey(i); - auto const& val = values_[i]; - - switch (Items[i].type) - { - case QMetaType::Int: - dictAdd(¤t_settings, key, val.toInt()); - break; - - case CustomVariantType::EncryptionModeType: - *tr_variantDictAdd(¤t_settings, key) = to_variant(val.value()); - break; - - case CustomVariantType::SortModeType: - *tr_variantDictAdd(¤t_settings, key) = to_variant(val.value()); - break; - - case CustomVariantType::ShowModeType: - *tr_variantDictAdd(¤t_settings, key) = to_variant(val.value()); - break; - - case QMetaType::QString: - dictAdd(¤t_settings, key, val.toString()); - break; - - case QMetaType::QStringList: - dictAdd(¤t_settings, key, val.toStringList()); - break; - - case QMetaType::Bool: - dictAdd(¤t_settings, key, val.toBool()); - break; - - case QMetaType::Double: - dictAdd(¤t_settings, key, val.toDouble()); - break; - - case QMetaType::QDateTime: - dictAdd(¤t_settings, key, int64_t{ val.toDateTime().toSecsSinceEpoch() }); - break; - - default: - assert(false && "unhandled type"); - break; + map.try_emplace(Items[idx].key, trvarFromQVar(values_[idx], Items[idx].type)); } } - // update settings.json with our settings + return map; +} + +void Prefs::save(QString const& filename) const +{ + auto const filename_str = filename.toStdString(); auto serde = tr_variant_serde::json(); - auto const file = QFile{ QDir{ config_dir_ }.absoluteFilePath(QStringLiteral("settings.json")) }; - auto const filename = file.fileName().toStdString(); - auto settings = tr_variant::make_map(PREFS_COUNT); - if (auto const file_settings = serde.parse_file(filename); file_settings) - { - settings.merge(*file_settings); - } - settings.merge(current_settings); + auto settings = tr_variant::make_map(PREFS_COUNT); + if (auto const var = serde.parse_file(filename_str)) + { + settings.merge(*var); + } + settings.merge(tr_variant{ current_settings() }); api_compat::convert_outgoing_data(settings); - serde.to_file(settings, filename); + serde.to_file(settings, filename_str); } /** * This is where we initialize the preferences file with the default values. * If you add a new preferences key, you /must/ add a default value here. */ -tr_variant Prefs::get_default_app_settings() +// static +tr_variant::Map Prefs::defaults() { auto const download_dir = tr_getDefaultDownloadDir(); - auto settings = tr_variant::Map{ 64U }; - settings.try_emplace(TR_KEY_blocklist_date, 0); - settings.try_emplace(TR_KEY_blocklist_updates_enabled, true); - settings.try_emplace(TR_KEY_compact_view, false); - settings.try_emplace(TR_KEY_download_dir, download_dir); - settings.try_emplace(TR_KEY_filter_mode, to_variant(DefaultShowMode)); - settings.try_emplace(TR_KEY_inhibit_desktop_hibernation, false); - settings.try_emplace(TR_KEY_main_window_height, 500); - settings.try_emplace(TR_KEY_main_window_layout_order, tr_variant::unmanaged_string("menu,toolbar,filter,list,statusbar"sv)); - settings.try_emplace(TR_KEY_main_window_width, 600); - settings.try_emplace(TR_KEY_main_window_x, 50); - settings.try_emplace(TR_KEY_main_window_y, 50); - settings.try_emplace(TR_KEY_open_dialog_dir, QDir::home().absolutePath().toStdString()); - settings.try_emplace(TR_KEY_prompt_before_exit, true); - settings.try_emplace(TR_KEY_read_clipboard, false); - settings.try_emplace(TR_KEY_remote_session_enabled, false); - settings.try_emplace(TR_KEY_remote_session_host, tr_variant::unmanaged_string("localhost"sv)); - settings.try_emplace(TR_KEY_remote_session_https, false); - settings.try_emplace(TR_KEY_remote_session_password, tr_variant::unmanaged_string(""sv)); - settings.try_emplace(TR_KEY_remote_session_port, TrDefaultRpcPort); - settings.try_emplace(TR_KEY_remote_session_requires_authentication, false); - settings.try_emplace(TR_KEY_remote_session_url_base_path, tr_variant::unmanaged_string(TrHttpServerDefaultBasePath)); - settings.try_emplace(TR_KEY_remote_session_username, tr_variant::unmanaged_string(""sv)); - settings.try_emplace(TR_KEY_show_backup_trackers, false); - settings.try_emplace(TR_KEY_show_filterbar, true); - settings.try_emplace(TR_KEY_show_notification_area_icon, false); - settings.try_emplace(TR_KEY_show_options_window, true); - settings.try_emplace(TR_KEY_show_statusbar, true); - settings.try_emplace(TR_KEY_show_toolbar, true); - settings.try_emplace(TR_KEY_show_tracker_scrapes, false); - settings.try_emplace(TR_KEY_sort_mode, to_variant(DefaultSortMode)); - settings.try_emplace(TR_KEY_sort_reversed, false); - settings.try_emplace(TR_KEY_start_minimized, false); - settings.try_emplace(TR_KEY_statusbar_stats, tr_variant::unmanaged_string("total-ratio")); - settings.try_emplace(TR_KEY_torrent_added_notification_enabled, true); - settings.try_emplace(TR_KEY_torrent_complete_notification_enabled, true); - settings.try_emplace(TR_KEY_torrent_complete_sound_enabled, true); - settings.try_emplace(TR_KEY_watch_dir, download_dir); - settings.try_emplace(TR_KEY_watch_dir_enabled, false); - return tr_variant{ std::move(settings) }; + auto map = tr_variant::Map{ 64U }; + map.try_emplace(TR_KEY_blocklist_date, 0); + map.try_emplace(TR_KEY_blocklist_updates_enabled, true); + map.try_emplace(TR_KEY_compact_view, false); + map.try_emplace(TR_KEY_download_dir, download_dir); + map.try_emplace(TR_KEY_filter_mode, ser::to_variant(DefaultShowMode)); + map.try_emplace(TR_KEY_inhibit_desktop_hibernation, false); + map.try_emplace(TR_KEY_main_window_height, 500); + map.try_emplace(TR_KEY_main_window_layout_order, tr_variant::unmanaged_string("menu,toolbar,filter,list,statusbar"sv)); + map.try_emplace(TR_KEY_main_window_width, 600); + map.try_emplace(TR_KEY_main_window_x, 50); + map.try_emplace(TR_KEY_main_window_y, 50); + map.try_emplace(TR_KEY_open_dialog_dir, QDir::home().absolutePath().toStdString()); + map.try_emplace(TR_KEY_prompt_before_exit, true); + map.try_emplace(TR_KEY_read_clipboard, false); + map.try_emplace(TR_KEY_remote_session_enabled, false); + map.try_emplace(TR_KEY_remote_session_host, tr_variant::unmanaged_string("localhost"sv)); + map.try_emplace(TR_KEY_remote_session_https, false); + map.try_emplace(TR_KEY_remote_session_password, tr_variant::unmanaged_string(""sv)); + map.try_emplace(TR_KEY_remote_session_port, TrDefaultRpcPort); + map.try_emplace(TR_KEY_remote_session_requires_authentication, false); + map.try_emplace(TR_KEY_remote_session_url_base_path, tr_variant::unmanaged_string(TrHttpServerDefaultBasePath)); + map.try_emplace(TR_KEY_remote_session_username, tr_variant::unmanaged_string(""sv)); + map.try_emplace(TR_KEY_show_backup_trackers, false); + map.try_emplace(TR_KEY_show_filterbar, true); + map.try_emplace(TR_KEY_show_notification_area_icon, false); + map.try_emplace(TR_KEY_show_options_window, true); + map.try_emplace(TR_KEY_show_statusbar, true); + map.try_emplace(TR_KEY_show_toolbar, true); + map.try_emplace(TR_KEY_show_tracker_scrapes, false); + map.try_emplace(TR_KEY_sort_mode, ser::to_variant(DefaultSortMode)); + map.try_emplace(TR_KEY_sort_reversed, false); + map.try_emplace(TR_KEY_start_minimized, false); + map.try_emplace(TR_KEY_statusbar_stats, tr_variant::unmanaged_string("total-ratio")); + map.try_emplace(TR_KEY_torrent_added_notification_enabled, true); + map.try_emplace(TR_KEY_torrent_complete_notification_enabled, true); + map.try_emplace(TR_KEY_torrent_complete_sound_enabled, true); + map.try_emplace(TR_KEY_watch_dir, download_dir); + map.try_emplace(TR_KEY_watch_dir_enabled, false); + return map; } diff --git a/qt/Prefs.h b/qt/Prefs.h index d91ed7610..c6210f642 100644 --- a/qt/Prefs.h +++ b/qt/Prefs.h @@ -12,16 +12,10 @@ #include #include +#include #include -class QDateTime; - -extern "C" -{ - struct tr_variant; -} - class Prefs : public QObject { Q_OBJECT @@ -131,37 +125,36 @@ public: PREFS_COUNT }; - explicit Prefs(QString config_dir); + Prefs(); Prefs(Prefs&&) = delete; Prefs(Prefs const&) = delete; Prefs& operator=(Prefs&&) = delete; Prefs& operator=(Prefs const&) = delete; - ~Prefs() override; + ~Prefs() override = default; - [[nodiscard]] constexpr auto isCore(int key) const noexcept + [[nodiscard]] static auto constexpr isCore(int const idx) { - return FIRST_CORE_PREF <= key && key <= LAST_CORE_PREF; + return FIRST_CORE_PREF <= idx && idx <= LAST_CORE_PREF; } - [[nodiscard]] constexpr auto getKey(int i) const noexcept + [[nodiscard]] static auto constexpr getKey(int const idx) { - return Items[i].key; + return Items[idx].key; } - [[nodiscard]] constexpr auto type(int i) const noexcept + [[nodiscard]] static auto constexpr type(int const idx) { - return Items[i].type; + return Items[idx].type; } - [[nodiscard]] constexpr auto const& variant(int i) const noexcept - { - return values_[i]; - } + void loadFromConfigDir(QString dir); - template - [[nodiscard]] T get(int const key) const + void load(tr_variant::Map const& settings); + + // DEPRECATED + [[nodiscard]] constexpr auto const& variant(int const idx) const noexcept { - return values_[key].value(); + return values_[idx]; } template @@ -177,8 +170,18 @@ public: } } + template + [[nodiscard]] T get(int const idx) const + { + return values_[idx].value(); + } + + [[nodiscard]] tr_variant::Map current_settings() const; + + void save(QString const& filename) const; + signals: - void changed(int key); + void changed(int idx); private: struct PrefItem @@ -188,13 +191,11 @@ private: int type; }; - [[nodiscard]] static tr_variant get_default_app_settings(); + static std::array const Items; + + [[nodiscard]] static tr_variant::Map defaults(); void set(int key, char const* value) = delete; - QString const config_dir_; - std::array mutable values_; - - static std::array const Items; }; diff --git a/qt/Session.cc b/qt/Session.cc index 0c5c7f970..0cd54d497 100644 --- a/qt/Session.cc +++ b/qt/Session.cc @@ -63,7 +63,7 @@ void Session::sessionSet(tr_quark const key, QVariant const& value) #if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) switch (value.typeId()) #else - switch (static_cast(value.type())) + switch (value.userType()) #endif { case QMetaType::Bool: @@ -166,7 +166,7 @@ void Session::copyMagnetLinkToClipboard(int torrent_id) void Session::updatePref(int key) { - if (prefs_.isCore(key)) + if (Prefs::isCore(key)) { switch (key) { @@ -209,11 +209,11 @@ void Session::updatePref(int key) case Prefs::USPEED: case Prefs::USPEED_ENABLED: case Prefs::UTP_ENABLED: - sessionSet(prefs_.getKey(key), prefs_.variant(key)); + sessionSet(Prefs::getKey(key), prefs_.variant(key)); break; case Prefs::DOWNLOAD_DIR: - sessionSet(prefs_.getKey(key), prefs_.variant(key)); + sessionSet(Prefs::getKey(key), prefs_.variant(key)); /* this will change the 'freespace' argument, so refresh */ refreshSessionInfo(); break; @@ -890,14 +890,14 @@ void Session::updateInfo(tr_variant* args_dict) for (int i = Prefs::FIRST_CORE_PREF; i <= Prefs::LAST_CORE_PREF; ++i) { - tr_variant const* b(tr_variantDictFind(args_dict, prefs_.getKey(i))); + tr_variant const* b(tr_variantDictFind(args_dict, Prefs::getKey(i))); if (b == nullptr) { continue; } - switch (prefs_.type(i)) + switch (Prefs::type(i)) { case QMetaType::Int: if (auto const value = getValue(b); value) diff --git a/qt/TrQtInit.cc b/qt/TrQtInit.cc new file mode 100644 index 000000000..c5d37fbd1 --- /dev/null +++ b/qt/TrQtInit.cc @@ -0,0 +1,21 @@ +// This file Copyright © Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#include "TrQtInit.h" + +#include + +#include "VariantHelpers.h" + +namespace trqt +{ + +void trqt_init() +{ + transmission::app::init(); + trqt::variant_helpers::register_qt_converters(); +} + +} // namespace trqt diff --git a/qt/TrQtInit.h b/qt/TrQtInit.h new file mode 100644 index 000000000..d45e8bdb1 --- /dev/null +++ b/qt/TrQtInit.h @@ -0,0 +1,13 @@ +// This file Copyright © Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#pragma once + +namespace trqt +{ + +void trqt_init(); + +} // namespace trqt diff --git a/qt/Utils.cc b/qt/Utils.cc index 78d1c1a85..1c0cdde39 100644 --- a/qt/Utils.cc +++ b/qt/Utils.cc @@ -43,7 +43,7 @@ QIcon Utils::getIconFromIndex(QModelIndex const& index) #if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) switch (variant.typeId()) #else - switch (static_cast(variant.type())) + switch (variant.userType()) #endif { case QMetaType::QIcon: diff --git a/qt/VariantHelpers.cc b/qt/VariantHelpers.cc index e71ee7d8b..f96bd9a5e 100644 --- a/qt/VariantHelpers.cc +++ b/qt/VariantHelpers.cc @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -19,6 +20,8 @@ #include "Speed.h" #include "Torrent.h" +namespace ser = libtransmission::serializer; + namespace trqt::variant_helpers { @@ -253,6 +256,42 @@ tr_variant fromInt(int const& val) { return static_cast(val); } + +// --- + +bool toQDateTime(tr_variant const& src, QDateTime* tgt) +{ + if (auto const val = ser::to_value(src)) + { + *tgt = QDateTime::fromSecsSinceEpoch(*val); + return true; + } + + return false; +} + +tr_variant fromQDateTime(QDateTime const& src) +{ + return ser::to_variant(int64_t{ src.toSecsSinceEpoch() }); +} + +// --- + +bool toQString(tr_variant const& src, QString* tgt) +{ + if (auto const val = src.value_if()) + { + *tgt = QString::fromUtf8(std::data(*val), std::size(*val)); + return true; + } + + return false; +} + +tr_variant fromQString(QString const& val) +{ + return val.toStdString(); +} } // namespace void register_qt_converters() @@ -264,6 +303,8 @@ void register_qt_converters() { using namespace libtransmission::serializer; Converters::add(toInt, fromInt); + Converters::add(toQDateTime, fromQDateTime); + Converters::add(toQString, fromQString); }); } diff --git a/qt/main.cc b/qt/main.cc index 05c56ed74..6dada0cdb 100644 --- a/qt/main.cc +++ b/qt/main.cc @@ -7,6 +7,8 @@ #include #include +#include + #include #include @@ -203,40 +205,41 @@ int tr_main(int argc, char** argv) } // initialize the prefs - auto prefs = std::make_unique(config_dir); + auto prefs = Prefs{}; + prefs.loadFromConfigDir(config_dir); if (!host.isNull()) { - prefs->set(Prefs::SESSION_REMOTE_HOST, host); + prefs.set(Prefs::SESSION_REMOTE_HOST, host); } if (!port.isNull()) { - prefs->set(Prefs::SESSION_REMOTE_PORT, port.toUInt()); + prefs.set(Prefs::SESSION_REMOTE_PORT, port.toUInt()); } if (!username.isNull()) { - prefs->set(Prefs::SESSION_REMOTE_USERNAME, username); + prefs.set(Prefs::SESSION_REMOTE_USERNAME, username); } if (!password.isNull()) { - prefs->set(Prefs::SESSION_REMOTE_PASSWORD, password); + prefs.set(Prefs::SESSION_REMOTE_PASSWORD, password); } if (!host.isNull() || !port.isNull() || !username.isNull() || !password.isNull()) { - prefs->set(Prefs::SESSION_IS_REMOTE, true); + prefs.set(Prefs::SESSION_IS_REMOTE, true); } - if (prefs->get(Prefs::START_MINIMIZED)) + if (prefs.get(Prefs::START_MINIMIZED)) { minimized = true; } // start as minimized only if the system tray present - if (!prefs->get(Prefs::SHOW_TRAY_ICON)) + if (!prefs.get(Prefs::SHOW_TRAY_ICON)) { minimized = false; } @@ -247,8 +250,14 @@ int tr_main(int argc, char** argv) qt_argv.insert(qt_argv.end(), &argv[qt_args_start_idx], &argv[argc]); } + // run the app auto qt_argc = static_cast(std::size(qt_argv)); + auto const app = Application{ prefs, minimized, config_dir, filenames, qt_argc, std::data(qt_argv) }; + auto const ret = QApplication::exec(); - Application const app(std::move(prefs), minimized, config_dir, filenames, qt_argc, std::data(qt_argv)); - return QApplication::exec(); + // save prefs before exiting + auto const filename = QDir{ config_dir }.absoluteFilePath(QStringLiteral("settings.json")); + prefs.save(filename); + + return ret; } diff --git a/release/windows/build-qt5.ps1 b/release/windows/build-qt5.ps1 index 5901b3525..4630261b1 100644 --- a/release/windows/build-qt5.ps1 +++ b/release/windows/build-qt5.ps1 @@ -58,8 +58,6 @@ function global:Build-Qt5([string] $PrefixDir, [string] $Arch, [string] $DepsPre '-no-sql-sqlite2' '-no-sql-tds' '-nomake'; 'examples' - '-nomake'; 'tests' - '-nomake'; 'tools' '-I'; (Join-Path $DepsPrefixDir include) '-L'; (Join-Path $DepsPrefixDir lib) ) diff --git a/release/windows/build-qt6.ps1 b/release/windows/build-qt6.ps1 index 7a852b8d9..80b1c15e0 100644 --- a/release/windows/build-qt6.ps1 +++ b/release/windows/build-qt6.ps1 @@ -89,7 +89,6 @@ function global:Build-Qt6([string] $PrefixDir, [string] $Arch, [string] $DepsPre '-no-feature-syntaxhighlighter' '-no-feature-systemsemaphore' '-no-feature-tablewidget' - '-no-feature-testlib' '-no-feature-textmarkdownreader' '-no-feature-textmarkdownwriter' '-no-feature-textodfwriter' diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8e6c70384..4aa971546 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,3 +3,25 @@ add_subdirectory(libtransmission) if(ENABLE_UTILS) add_subdirectory(utils) endif() + +if(ENABLE_QT) + add_subdirectory(qt) +endif() + +add_custom_target(all-tests) + +set_property( + TARGET all-tests + PROPERTY FOLDER "tests") + +if(TARGET libtransmission-test) + add_dependencies(all-tests libtransmission-test) +endif() + +if(TARGET qt-tests) + add_dependencies(all-tests qt-tests) +endif() + +if(TARGET transmission-show) + add_dependencies(all-tests transmission-show) +endif() diff --git a/tests/qt/.clang-tidy b/tests/qt/.clang-tidy new file mode 100644 index 000000000..7f202edc2 --- /dev/null +++ b/tests/qt/.clang-tidy @@ -0,0 +1,60 @@ +--- +HeaderFilterRegex: .*/libtransmission/.* + +# TODO: Enable `portability-template-virtual-member-function` after https://github.com/llvm/llvm-project/issues/139031 is fixed +# TODO: Enable `cppcoreguidelines-pro-bounds-pointer-arithmetic` after converting all pointers to std::span +# PRs welcome to fix & re-enable any of these explicitly-disabled checks +Checks: > + bugprone-*, + -bugprone-branch-clone, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-narrowing-conversions, + cert-*, + -cert-err58-cpp, + -cert-int09-c, + clang-analyzer-*, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-c-arrays, + -cppcoreguidelines-avoid-const-or-ref-data-members, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-narrowing-conversions, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-const-cast, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-type-vararg, + google-explicit-constructor, + misc-*, + -misc-include-cleaner, + -misc-no-recursion, + -misc-non-private-member-variables-in-classes, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + -performance-move-const-arg, + portability-*, + -portability-template-virtual-member-function, + readability-*, + -readability-enum-initial-value, + -readability-function-cognitive-complexity, + -readability-identifier-length, + -readability-magic-numbers, + -readability-qualified-auto, + +CheckOptions: + - { key: cppcoreguidelines-avoid-do-while.IgnoreMacros, value: true } + - { key: cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreUnnamedParams, value: true } + - { key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor, value: true } + - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } + - { key: readability-identifier-naming.ParameterCase, value: lower_case } + - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } + - { key: readability-identifier-naming.VariableCase, value: lower_case } + - { key: readability-implicit-bool-conversion.UseUpperCaseLiteralSuffix, value: true } diff --git a/tests/qt/CMakeLists.txt b/tests/qt/CMakeLists.txt new file mode 100644 index 000000000..b1bff2b4a --- /dev/null +++ b/tests/qt/CMakeLists.txt @@ -0,0 +1,27 @@ +find_package(Qt${Qt_VERSION_MAJOR}Test ${QT_MINIMUM} REQUIRED) + +set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES + ${CMAKE_BINARY_DIR}/qt/${TR_NAME}-qt-lib_autogen/include) +set_property(DIRECTORY PROPERTY LINK_LIBRARIES + ${TR_NAME}-qt-lib + Qt${Qt_VERSION_MAJOR}::Test) + +function(add_trqt_test source) + get_filename_component(name_no_ext ${source} NAME_WE) + string(REGEX REPLACE "-test$" "" test_name ${name_no_ext}) + set(target qt-test-${test_name}) + add_executable(${target} ${source} ${CMAKE_CURRENT_SOURCE_DIR}/../../qt/application.qrc) + set_property(TARGET ${target} PROPERTY AUTOMOC ON) + set_property(TARGET ${target} PROPERTY AUTORCC ON) + add_test(NAME QT.${test_name} COMMAND ${target}) +endfunction() + +add_trqt_test(prefs-test.cc) + +add_custom_target(qt-tests + DEPENDS + qt-test-prefs) + +set_property( + TARGET qt-tests + PROPERTY FOLDER "tests") diff --git a/tests/qt/prefs-test.cc b/tests/qt/prefs-test.cc new file mode 100644 index 000000000..5b4268f33 --- /dev/null +++ b/tests/qt/prefs-test.cc @@ -0,0 +1,262 @@ +// This file Copyright © Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "CustomVariantType.h" +#include "Filters.h" +#include "Prefs.h" +#include "TrQtInit.h" + +#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) +#define QCOMPARE_EQ(actual, expected) QCOMPARE(actual, expected) +#define QCOMPARE_NE(actual, expected) QVERIFY((actual) != (expected)) +#endif + +using namespace std::literals; + +class PrefsTest : public QObject +{ + Q_OBJECT + + [[nodiscard]] static std::string get_json_member_str(int const idx, std::string_view const valstr) + { + auto const json_key = tr_quark_get_string_view(Prefs::getKey(idx)); + return fmt::format(R"("{:s}":{:s})", json_key, valstr); + } + + static void verify_json_contains(tr_variant const& var, std::string_view const substr) + { + auto serde = tr_variant_serde::json(); + serde.compact(); + auto const str = serde.to_string(var); + QVERIFY2(tr_strv_contains(str, substr), str.c_str()); + } + + static void verify_json_contains(tr_variant const& var, int const idx, std::string_view const val) + { + auto serde = tr_variant_serde::json(); + serde.compact(); + auto const str = serde.to_string(var); + auto const substr = get_json_member_str(idx, val); + QVERIFY2(tr_strv_contains(str, substr), str.c_str()); + } + + template + void verify_get_set_by_property(Prefs& prefs, int const idx, T const& val1, T const& val2) + { + QCOMPARE_NE(val1, val2); + + prefs.set(idx, val1); + QCOMPARE_EQ(prefs.get(idx), val1); + QCOMPARE_NE(prefs.get(idx), val2); + + prefs.set(idx, val2); + QCOMPARE_NE(prefs.get(idx), val1); + QCOMPARE_EQ(prefs.get(idx), val2); + } + + template + void verify_get_by_json(Prefs& prefs, int const idx, T const& val, std::string_view const valstr) + { + prefs.set(idx, val); + QCOMPARE_EQ(prefs.get(idx), val); + verify_json_contains(prefs.current_settings(), idx, valstr); + } + + template + void verify_set_by_json(Prefs& prefs, int const idx, T const& val, std::string_view const valstr) + { + auto const json_object_str = fmt::format(R"({{{:s}}})", get_json_member_str(idx, valstr)); + auto serde = tr_variant_serde::json(); + auto const var = serde.parse(json_object_str); + QVERIFY(var.has_value()); + // IDK why clang-tidy doesn't see the QVERIFY check above? + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + auto const* const map = var->get_if(); + QVERIFY(map != nullptr); + prefs.load(*map); + QCOMPARE_EQ(prefs.get(idx), val); + } + +private slots: + void handles_bool() + { + auto constexpr Idx = Prefs::SORT_REVERSED; + auto constexpr ValA = false; + auto constexpr ValAStr = "false"sv; + auto constexpr ValB = true; + auto constexpr ValBStr = "true"sv; + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, ValA, ValB); + verify_set_by_json(prefs, Idx, ValA, ValAStr); + verify_get_by_json(prefs, Idx, ValB, ValBStr); + } + + void handles_int() + { + auto constexpr Idx = Prefs::MAIN_WINDOW_HEIGHT; + auto constexpr ValA = 4242; + auto constexpr ValAStr = "4242"sv; + auto constexpr ValB = 2323; + auto constexpr ValBStr = "2323"sv; + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, ValA, ValB); + verify_set_by_json(prefs, Idx, ValA, ValAStr); + verify_get_by_json(prefs, Idx, ValB, ValBStr); + } + + void handles_double() + { + auto constexpr Idx = Prefs::RATIO; + auto constexpr ValA = 1.234; + auto constexpr ValB = 5.678; + auto const val_a_str = fmt::format("{}", ValA); + auto const val_b_str = fmt::format("{}", ValB); + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, ValA, ValB); + verify_set_by_json(prefs, Idx, ValA, val_a_str); + verify_get_by_json(prefs, Idx, ValB, val_b_str); + } + + void handles_qstring() + { + auto constexpr Idx = Prefs::DOWNLOAD_DIR; + auto constexpr ValAStr = R"("/tmp/transmission-test-download-dir")"sv; + auto constexpr ValBStr = R"("/tmp/transmission-test-download-dir-b")"sv; + auto const val_a = QStringLiteral("/tmp/transmission-test-download-dir"); + auto const val_b = QStringLiteral("/tmp/transmission-test-download-dir-b"); + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, val_a, val_b); + verify_set_by_json(prefs, Idx, val_a, ValAStr); + verify_get_by_json(prefs, Idx, val_b, ValBStr); + } + + void handles_qstringlist() + { + auto constexpr Idx = Prefs::COMPLETE_SOUND_COMMAND; + auto constexpr ValAStr = R"(["one","two","three"])"sv; + auto constexpr ValBStr = R"(["alpha","beta"])"sv; + auto const val_a = QStringList{ QStringLiteral("one"), QStringLiteral("two"), QStringLiteral("three") }; + auto const val_b = QStringList{ QStringLiteral("alpha"), QStringLiteral("beta") }; + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, val_a, val_b); + verify_set_by_json(prefs, Idx, val_a, ValAStr); + verify_get_by_json(prefs, Idx, val_b, ValBStr); + } + + void handles_qdatetime() + { + auto constexpr Idx = Prefs::BLOCKLIST_DATE; + auto const val_a = QDateTime::fromMSecsSinceEpoch(1700000000000LL).toUTC(); + auto const val_a_str = fmt::format("{}", val_a.toSecsSinceEpoch()); + auto const val_b = QDateTime::fromMSecsSinceEpoch(1700000000000LL + 123000LL).toUTC(); + auto const val_b_str = fmt::format("{}", val_b.toSecsSinceEpoch()); + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, val_a, val_b); + verify_set_by_json(prefs, Idx, val_a, val_a_str); + verify_get_by_json(prefs, Idx, val_b, val_b_str); + } + + void handles_sortmode() + { + auto constexpr Idx = Prefs::SORT_MODE; + auto constexpr ValA = SortMode::SortBySize; + auto constexpr ValAStr = R"("sort_by_size")"sv; + auto constexpr ValB = SortMode::SortByName; + auto constexpr ValBStr = R"("sort_by_name")"sv; + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, ValA, ValB); + verify_set_by_json(prefs, Idx, ValA, ValAStr); + verify_get_by_json(prefs, Idx, ValB, ValBStr); + } + + void handles_showmode() + { + auto constexpr Idx = Prefs::FILTER_MODE; + auto constexpr ValA = ShowMode::ShowAll; + auto constexpr ValAStr = R"("show_all")"sv; + auto constexpr ValB = ShowMode::ShowActive; + auto constexpr ValBStr = R"("show_active")"sv; + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, ValA, ValB); + verify_set_by_json(prefs, Idx, ValA, ValAStr); + verify_get_by_json(prefs, Idx, ValB, ValBStr); + } + + void handles_encryptionmode() + { + auto constexpr Idx = Prefs::ENCRYPTION; + auto constexpr ValA = TR_ENCRYPTION_REQUIRED; + auto constexpr ValAStr = R"("required")"sv; + auto constexpr ValB = TR_ENCRYPTION_PREFERRED; + auto constexpr ValBStr = R"("preferred")"sv; + + auto prefs = Prefs{}; + verify_get_set_by_property(prefs, Idx, ValA, ValB); + verify_set_by_json(prefs, Idx, ValA, ValAStr); + verify_get_by_json(prefs, Idx, ValB, ValBStr); + } + + // --- + + static void changed_signal_emits_when_change() + { + static auto constexpr Idx = Prefs::SORT_REVERSED; + + auto prefs = Prefs{}; + auto const spy = QSignalSpy{ &prefs, &Prefs::changed }; + + auto const old_value = prefs.get(Idx); + auto const new_value = !old_value; + prefs.set(Idx, new_value); + QCOMPARE(spy.count(), 1); + auto const& signal_args = spy.first(); + QCOMPARE(signal_args.at(0).toInt(), Idx); + } + + static void changed_signal_does_not_emit_when_unchanged() + { + static auto constexpr Idx = Prefs::SORT_REVERSED; + + auto prefs = Prefs{}; + auto const spy = QSignalSpy{ &prefs, &Prefs::changed }; + + auto const current_value = prefs.get(Idx); + prefs.set(Idx, current_value); + QCOMPARE(spy.count(), 0); + } +}; + +int main(int argc, char** argv) +{ + trqt::trqt_init(); + auto const app = QApplication{ argc, argv }; + auto test = PrefsTest{}; + return QTest::qExec(&test, argc, argv); +} + +#include "prefs-test.moc"