mirror of
https://github.com/zealdocs/zeal.git
synced 2026-08-29 08:34:50 +08:00
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ add_library(Core STATIC
|
|||||||
httpserver.cpp
|
httpserver.cpp
|
||||||
networkaccessmanager.cpp
|
networkaccessmanager.cpp
|
||||||
settings.cpp
|
settings.cpp
|
||||||
|
session.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(Core PRIVATE Registry)
|
target_link_libraries(Core PRIVATE Registry)
|
||||||
@@ -38,6 +39,15 @@ else()
|
|||||||
target_link_libraries(Core PRIVATE ${LibArchive_LIBRARIES})
|
target_link_libraries(Core PRIVATE ${LibArchive_LIBRARIES})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Configure toml++ (header-only).
|
||||||
|
find_package(tomlplusplus CONFIG QUIET)
|
||||||
|
if(tomlplusplus_FOUND)
|
||||||
|
target_link_libraries(Core PRIVATE tomlplusplus::tomlplusplus)
|
||||||
|
else()
|
||||||
|
# Use bundled version of toml++ if not found.
|
||||||
|
target_include_directories(Core PRIVATE "${CMAKE_SOURCE_DIR}/src/contrib/tomlplusplus")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configure cpp-httplib.
|
# Configure cpp-httplib.
|
||||||
add_definitions(-DCPPHTTPLIB_USE_POLL)
|
add_definitions(-DCPPHTTPLIB_USE_POLL)
|
||||||
|
|
||||||
@@ -55,3 +65,8 @@ if(NOT WIN32)
|
|||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
target_link_libraries(Core PRIVATE Threads::Threads)
|
target_link_libraries(Core PRIVATE Threads::Threads)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_subdirectory(tests)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "filemanager.h"
|
#include "filemanager.h"
|
||||||
#include "httpserver.h"
|
#include "httpserver.h"
|
||||||
#include "networkaccessmanager.h"
|
#include "networkaccessmanager.h"
|
||||||
|
#include "session.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
|
||||||
#include <registry/docsetregistry.h>
|
#include <registry/docsetregistry.h>
|
||||||
@@ -39,6 +40,9 @@ Application::Application(QObject *parent)
|
|||||||
m_instance = this;
|
m_instance = this;
|
||||||
|
|
||||||
m_settings = new Settings(this);
|
m_settings = new Settings(this);
|
||||||
|
m_session = new Session();
|
||||||
|
m_session->load();
|
||||||
|
|
||||||
m_networkManager = new NetworkAccessManager(this);
|
m_networkManager = new NetworkAccessManager(this);
|
||||||
|
|
||||||
m_fileManager = new FileManager(this);
|
m_fileManager = new FileManager(this);
|
||||||
@@ -75,6 +79,9 @@ Application::~Application()
|
|||||||
m_extractorThread->wait();
|
m_extractorThread->wait();
|
||||||
delete m_extractor;
|
delete m_extractor;
|
||||||
delete m_docsetRegistry;
|
delete m_docsetRegistry;
|
||||||
|
|
||||||
|
m_session->save();
|
||||||
|
delete m_session;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@@ -92,6 +99,11 @@ QNetworkAccessManager *Application::networkManager() const
|
|||||||
return m_networkManager;
|
return m_networkManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session *Application::session() const
|
||||||
|
{
|
||||||
|
return m_session;
|
||||||
|
}
|
||||||
|
|
||||||
Settings *Application::settings() const
|
Settings *Application::settings() const
|
||||||
{
|
{
|
||||||
return m_settings;
|
return m_settings;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Extractor;
|
|||||||
class FileManager;
|
class FileManager;
|
||||||
class HttpServer;
|
class HttpServer;
|
||||||
class Settings;
|
class Settings;
|
||||||
|
struct Session;
|
||||||
|
|
||||||
class Application final : public QObject
|
class Application final : public QObject
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,7 @@ public:
|
|||||||
static Application *instance();
|
static Application *instance();
|
||||||
|
|
||||||
QNetworkAccessManager *networkManager() const;
|
QNetworkAccessManager *networkManager() const;
|
||||||
|
Session *session() const;
|
||||||
Settings *settings() const;
|
Settings *settings() const;
|
||||||
|
|
||||||
Registry::DocsetRegistry *docsetRegistry() const;
|
Registry::DocsetRegistry *docsetRegistry() const;
|
||||||
@@ -65,6 +67,7 @@ private:
|
|||||||
|
|
||||||
static Application *m_instance;
|
static Application *m_instance;
|
||||||
|
|
||||||
|
Session *m_session = nullptr;
|
||||||
Settings *m_settings = nullptr;
|
Settings *m_settings = nullptr;
|
||||||
|
|
||||||
QNetworkAccessManager *m_networkManager = nullptr;
|
QNetworkAccessManager *m_networkManager = nullptr;
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "session.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <string_view>
|
||||||
|
#include <toml++/toml.hpp>
|
||||||
|
|
||||||
|
namespace Zeal::Core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Q_LOGGING_CATEGORY(log, "zeal.core.session")
|
||||||
|
|
||||||
|
constexpr std::string_view ArrayWindows = "windows";
|
||||||
|
constexpr std::string_view KeyGeometry = "geometry";
|
||||||
|
constexpr std::string_view KeySplitter = "splitter";
|
||||||
|
constexpr std::string_view KeyTocSplitter = "toc_splitter";
|
||||||
|
|
||||||
|
QByteArray readBlob(const toml::table &tbl, std::string_view key)
|
||||||
|
{
|
||||||
|
const auto *node = tbl.get(key);
|
||||||
|
if (node == nullptr || !node->is_string()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string &encoded = node->as_string()->get();
|
||||||
|
return QByteArray::fromBase64(QByteArray::fromStdString(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeBlob(toml::table &tbl, std::string_view key, const QByteArray &blob)
|
||||||
|
{
|
||||||
|
tbl.insert_or_assign(key, blob.toBase64().toStdString());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WindowState &Session::primaryWindow()
|
||||||
|
{
|
||||||
|
if (windows.isEmpty()) {
|
||||||
|
windows.append(WindowState());
|
||||||
|
}
|
||||||
|
|
||||||
|
return windows.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Session::defaultFilePath()
|
||||||
|
{
|
||||||
|
#ifdef PORTABLE_BUILD
|
||||||
|
return QCoreApplication::applicationDirPath() + QLatin1String("/session.toml");
|
||||||
|
#else
|
||||||
|
// Test mode: isolate test runs under ~/.qttest/zeal/, mirroring how
|
||||||
|
// QStandardPaths handles its own path categories in test mode.
|
||||||
|
if (QStandardPaths::isTestModeEnabled()) {
|
||||||
|
return QDir::homePath() + QLatin1String("/.qttest/zeal/session.toml");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the canonical platform path directly rather than via
|
||||||
|
// QStandardPaths::StateLocation, which (a) is Qt 6.7+ only and (b) appends
|
||||||
|
// "<organizationName>/<applicationName>" yielding the doubled "Zeal/Zeal"
|
||||||
|
// path tracked in #1104. The path must not depend on Qt version — distro
|
||||||
|
// Qt upgrades shouldn't relocate the state file.
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
QString base = QString::fromLocal8Bit(qgetenv("LOCALAPPDATA"));
|
||||||
|
if (base.isEmpty()) {
|
||||||
|
base = QDir::homePath() + QLatin1String("/AppData/Local");
|
||||||
|
}
|
||||||
|
return base + QLatin1String("/Zeal/session.toml");
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
return QDir::homePath() + QLatin1String("/Library/Application Support/Zeal/session.toml");
|
||||||
|
#else
|
||||||
|
const QByteArray xdgState = qgetenv("XDG_STATE_HOME");
|
||||||
|
const QString base = xdgState.isEmpty() ? QDir::homePath() + QLatin1String("/.local/state")
|
||||||
|
: QString::fromLocal8Bit(xdgState);
|
||||||
|
return base + QLatin1String("/zeal/session.toml");
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::load()
|
||||||
|
{
|
||||||
|
const QString path = defaultFilePath();
|
||||||
|
if (QFile::exists(path)) {
|
||||||
|
loadFromFile(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First launch with the new state system: migrate the three blobs from the
|
||||||
|
// legacy QSettings 'state' group. Persist session.toml before removing the
|
||||||
|
// legacy keys so a crash between the two steps doesn't lose user data —
|
||||||
|
// migration retries on the next launch.
|
||||||
|
#ifndef PORTABLE_BUILD
|
||||||
|
QSettings legacy;
|
||||||
|
#else
|
||||||
|
QSettings legacy(QCoreApplication::applicationDirPath() + QLatin1String("/zeal.ini"), QSettings::IniFormat);
|
||||||
|
#endif
|
||||||
|
legacy.beginGroup(QStringLiteral("state"));
|
||||||
|
const QByteArray geometry = legacy.value(QStringLiteral("window_geometry")).toByteArray();
|
||||||
|
const QByteArray splitter = legacy.value(QStringLiteral("splitter_geometry")).toByteArray();
|
||||||
|
const QByteArray tocSplitter = legacy.value(QStringLiteral("toc_splitter_state")).toByteArray();
|
||||||
|
legacy.endGroup();
|
||||||
|
|
||||||
|
if (geometry.isEmpty() && splitter.isEmpty() && tocSplitter.isEmpty()) {
|
||||||
|
return; // Nothing to migrate.
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowState ws;
|
||||||
|
ws.geometry = geometry;
|
||||||
|
ws.splitterState = splitter;
|
||||||
|
ws.tocSplitterState = tocSplitter;
|
||||||
|
windows.append(ws);
|
||||||
|
|
||||||
|
if (!saveToFile(path)) {
|
||||||
|
return; // Leave legacy keys intact so migration retries next launch.
|
||||||
|
}
|
||||||
|
|
||||||
|
legacy.beginGroup(QStringLiteral("state"));
|
||||||
|
legacy.remove(QStringLiteral("window_geometry"));
|
||||||
|
legacy.remove(QStringLiteral("splitter_geometry"));
|
||||||
|
legacy.remove(QStringLiteral("toc_splitter_state"));
|
||||||
|
legacy.endGroup();
|
||||||
|
legacy.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::save() const
|
||||||
|
{
|
||||||
|
return saveToFile(defaultFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::loadFromFile(const QString &path)
|
||||||
|
{
|
||||||
|
windows.clear();
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.exists()) {
|
||||||
|
return; // Defaults.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
qCWarning(log, "Failed to open state file '%s': %s", qPrintable(path), qPrintable(file.errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray contents = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
toml::table root;
|
||||||
|
try {
|
||||||
|
root = toml::parse(std::string_view(contents.constData(), static_cast<size_t>(contents.size())));
|
||||||
|
} catch (const toml::parse_error &err) {
|
||||||
|
qCWarning(log,
|
||||||
|
"Failed to parse state file '%s': %s",
|
||||||
|
qPrintable(path),
|
||||||
|
qPrintable(QString::fromUtf8(err.description())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto *windowsNode = root.get(ArrayWindows);
|
||||||
|
if (windowsNode == nullptr || !windowsNode->is_array()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &node : *windowsNode->as_array()) {
|
||||||
|
const auto *tbl = node.as_table();
|
||||||
|
if (tbl == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowState ws;
|
||||||
|
ws.geometry = readBlob(*tbl, KeyGeometry);
|
||||||
|
ws.splitterState = readBlob(*tbl, KeySplitter);
|
||||||
|
ws.tocSplitterState = readBlob(*tbl, KeyTocSplitter);
|
||||||
|
windows.append(ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::saveToFile(const QString &path) const
|
||||||
|
{
|
||||||
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||||
|
|
||||||
|
toml::array windowsArray;
|
||||||
|
for (const WindowState &ws : windows) {
|
||||||
|
toml::table tbl;
|
||||||
|
writeBlob(tbl, KeyGeometry, ws.geometry);
|
||||||
|
writeBlob(tbl, KeySplitter, ws.splitterState);
|
||||||
|
writeBlob(tbl, KeyTocSplitter, ws.tocSplitterState);
|
||||||
|
windowsArray.push_back(std::move(tbl));
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::table root;
|
||||||
|
root.insert_or_assign(ArrayWindows, std::move(windowsArray));
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "# Managed by Zeal. Do not edit manually.\n\n";
|
||||||
|
oss << root;
|
||||||
|
const std::string text = oss.str();
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
||||||
|
qCWarning(log, "Failed to write state file '%s': %s", qPrintable(path), qPrintable(file.errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto expected = static_cast<qint64>(text.size());
|
||||||
|
const qint64 written = file.write(text.c_str(), expected);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (written != expected) {
|
||||||
|
qCWarning(log, "Partial write to state file '%s': %s", qPrintable(path), qPrintable(file.errorString()));
|
||||||
|
// Don't leave a truncated/corrupt file behind — next launch should retry from scratch.
|
||||||
|
QFile::remove(path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Zeal::Core
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#ifndef ZEAL_CORE_SESSION_H
|
||||||
|
#define ZEAL_CORE_SESSION_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Zeal::Core {
|
||||||
|
|
||||||
|
struct WindowState final
|
||||||
|
{
|
||||||
|
QByteArray geometry;
|
||||||
|
QByteArray splitterState;
|
||||||
|
QByteArray tocSplitterState;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Session final
|
||||||
|
{
|
||||||
|
QList<WindowState> windows;
|
||||||
|
|
||||||
|
// Returns a reference to the first window's state, creating an empty entry
|
||||||
|
// if the list is empty. Until multi-window support lands, all callers go
|
||||||
|
// through this accessor.
|
||||||
|
WindowState &primaryWindow();
|
||||||
|
|
||||||
|
void load();
|
||||||
|
bool save() const;
|
||||||
|
|
||||||
|
void loadFromFile(const QString &path);
|
||||||
|
bool saveToFile(const QString &path) const;
|
||||||
|
|
||||||
|
static QString defaultFilePath();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Zeal::Core
|
||||||
|
|
||||||
|
#endif // ZEAL_CORE_SESSION_H
|
||||||
@@ -38,7 +38,6 @@ constexpr char GroupGlobalShortcuts[] = "global_shortcuts";
|
|||||||
constexpr char GroupSearch[] = "search";
|
constexpr char GroupSearch[] = "search";
|
||||||
constexpr char GroupTabs[] = "tabs";
|
constexpr char GroupTabs[] = "tabs";
|
||||||
constexpr char GroupInternal[] = "internal";
|
constexpr char GroupInternal[] = "internal";
|
||||||
constexpr char GroupState[] = "state";
|
|
||||||
constexpr char GroupProxy[] = "proxy";
|
constexpr char GroupProxy[] = "proxy";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -277,12 +276,6 @@ void Settings::load()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings->beginGroup(GroupState);
|
|
||||||
windowGeometry = settings->value(QStringLiteral("window_geometry")).toByteArray();
|
|
||||||
verticalSplitterGeometry = settings->value(QStringLiteral("splitter_geometry")).toByteArray();
|
|
||||||
tocSplitterState = settings->value(QStringLiteral("toc_splitter_state")).toByteArray();
|
|
||||||
settings->endGroup();
|
|
||||||
|
|
||||||
settings->beginGroup(GroupInternal);
|
settings->beginGroup(GroupInternal);
|
||||||
installId = settings
|
installId = settings
|
||||||
->value(QStringLiteral("install_id"),
|
->value(QStringLiteral("install_id"),
|
||||||
@@ -352,12 +345,6 @@ void Settings::save()
|
|||||||
settings->setValue(QStringLiteral("path"), docsetPath);
|
settings->setValue(QStringLiteral("path"), docsetPath);
|
||||||
settings->endGroup();
|
settings->endGroup();
|
||||||
|
|
||||||
settings->beginGroup(GroupState);
|
|
||||||
settings->setValue(QStringLiteral("window_geometry"), windowGeometry);
|
|
||||||
settings->setValue(QStringLiteral("splitter_geometry"), verticalSplitterGeometry);
|
|
||||||
settings->setValue(QStringLiteral("toc_splitter_state"), tocSplitterState);
|
|
||||||
settings->endGroup();
|
|
||||||
|
|
||||||
settings->beginGroup(GroupInternal);
|
settings->beginGroup(GroupInternal);
|
||||||
settings->setValue(QStringLiteral("install_id"), installId);
|
settings->setValue(QStringLiteral("install_id"), installId);
|
||||||
// Version of configuration file format, should match Zeal version. Used for migration rules.
|
// Version of configuration file format, should match Zeal version. Used for migration rules.
|
||||||
@@ -416,17 +403,6 @@ void Settings::migrate(QSettings *settings) const
|
|||||||
settings->endGroup();
|
settings->endGroup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Pre 0.3
|
|
||||||
//
|
|
||||||
|
|
||||||
// Unset 'state/splitter_geometry', because custom styles were removed.
|
|
||||||
if (version < QVersionNumber(0, 3, 0)) {
|
|
||||||
settings->beginGroup(GroupState);
|
|
||||||
settings->remove(QStringLiteral("splitter_geometry"));
|
|
||||||
settings->endGroup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|||||||
@@ -100,11 +100,6 @@ public:
|
|||||||
// Other
|
// Other
|
||||||
QString docsetPath;
|
QString docsetPath;
|
||||||
|
|
||||||
// State
|
|
||||||
QByteArray windowGeometry;
|
|
||||||
QByteArray verticalSplitterGeometry;
|
|
||||||
QByteArray tocSplitterState;
|
|
||||||
|
|
||||||
explicit Settings(QObject *parent = nullptr);
|
explicit Settings(QObject *parent = nullptr);
|
||||||
~Settings() override;
|
~Settings() override;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||||
|
|
||||||
|
add_executable(session_test session_test.cpp)
|
||||||
|
target_link_libraries(session_test PRIVATE Core Qt6::Test)
|
||||||
|
|
||||||
|
zeal_add_test(session_test)
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "../session.h"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
using Zeal::Core::Session;
|
||||||
|
using Zeal::Core::WindowState;
|
||||||
|
|
||||||
|
class SessionTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
void loadMissingFile_usesDefaults();
|
||||||
|
void saveThenLoad_roundTrip();
|
||||||
|
void loadCorruptFile_usesDefaults();
|
||||||
|
void loadEmptyFile_usesDefaults();
|
||||||
|
void loadPartialFile_missingKeysUseDefaults();
|
||||||
|
void load_migratesFromLegacyQSettings();
|
||||||
|
void load_skipsMigrationWhenStateFileExists();
|
||||||
|
};
|
||||||
|
|
||||||
|
void SessionTest::initTestCase()
|
||||||
|
{
|
||||||
|
QStandardPaths::setTestModeEnabled(true);
|
||||||
|
QCoreApplication::setOrganizationName(QStringLiteral("ZealTest"));
|
||||||
|
QCoreApplication::setApplicationName(QStringLiteral("ZealTest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::loadMissingFile_usesDefaults()
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QVERIFY(dir.isValid());
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.loadFromFile(dir.filePath("missing.toml"));
|
||||||
|
|
||||||
|
QVERIFY(session.windows.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::saveThenLoad_roundTrip()
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QVERIFY(dir.isValid());
|
||||||
|
const QString path = dir.filePath("session.toml");
|
||||||
|
|
||||||
|
const QByteArray geom = QByteArray::fromHex("0102030405deadbeef");
|
||||||
|
const QByteArray splitter = QByteArray::fromHex("aabbccdd");
|
||||||
|
const QByteArray tocSplitter = QByteArray::fromHex("cafebabe");
|
||||||
|
|
||||||
|
{
|
||||||
|
Session out;
|
||||||
|
WindowState ws;
|
||||||
|
ws.geometry = geom;
|
||||||
|
ws.splitterState = splitter;
|
||||||
|
ws.tocSplitterState = tocSplitter;
|
||||||
|
out.windows.append(ws);
|
||||||
|
out.saveToFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Session in;
|
||||||
|
in.loadFromFile(path);
|
||||||
|
QCOMPARE(in.windows.size(), 1);
|
||||||
|
QCOMPARE(in.windows.first().geometry, geom);
|
||||||
|
QCOMPARE(in.windows.first().splitterState, splitter);
|
||||||
|
QCOMPARE(in.windows.first().tocSplitterState, tocSplitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::loadCorruptFile_usesDefaults()
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QVERIFY(dir.isValid());
|
||||||
|
const QString path = dir.filePath("session.toml");
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||||
|
file.write("this is not [valid toml");
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.loadFromFile(path);
|
||||||
|
QVERIFY(session.windows.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::loadEmptyFile_usesDefaults()
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QVERIFY(dir.isValid());
|
||||||
|
const QString path = dir.filePath("session.toml");
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.loadFromFile(path);
|
||||||
|
QVERIFY(session.windows.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::loadPartialFile_missingKeysUseDefaults()
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QVERIFY(dir.isValid());
|
||||||
|
const QString path = dir.filePath("session.toml");
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||||
|
file.write("[[windows]]\ngeometry = \"AQIDBA==\"\n");
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.loadFromFile(path);
|
||||||
|
QCOMPARE(session.windows.size(), 1);
|
||||||
|
QCOMPARE(session.windows.first().geometry, QByteArray::fromHex("01020304"));
|
||||||
|
QCOMPARE(session.windows.first().splitterState, QByteArray());
|
||||||
|
QCOMPARE(session.windows.first().tocSplitterState, QByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::load_migratesFromLegacyQSettings()
|
||||||
|
{
|
||||||
|
const QByteArray geom = QByteArray::fromHex("deadbeef0102");
|
||||||
|
const QByteArray splitter = QByteArray::fromHex("aabbccdd");
|
||||||
|
const QByteArray tocSplitter = QByteArray::fromHex("11223344");
|
||||||
|
|
||||||
|
// Populate legacy QSettings and make sure session.toml does not exist yet.
|
||||||
|
{
|
||||||
|
QSettings legacy;
|
||||||
|
legacy.beginGroup(QStringLiteral("state"));
|
||||||
|
legacy.setValue(QStringLiteral("window_geometry"), geom);
|
||||||
|
legacy.setValue(QStringLiteral("splitter_geometry"), splitter);
|
||||||
|
legacy.setValue(QStringLiteral("toc_splitter_state"), tocSplitter);
|
||||||
|
legacy.endGroup();
|
||||||
|
legacy.sync();
|
||||||
|
}
|
||||||
|
QFile::remove(Session::defaultFilePath());
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.load();
|
||||||
|
|
||||||
|
QCOMPARE(session.windows.size(), 1);
|
||||||
|
QCOMPARE(session.windows.first().geometry, geom);
|
||||||
|
QCOMPARE(session.windows.first().splitterState, splitter);
|
||||||
|
QCOMPARE(session.windows.first().tocSplitterState, tocSplitter);
|
||||||
|
|
||||||
|
// Legacy keys should be gone after migration.
|
||||||
|
QSettings legacy;
|
||||||
|
legacy.beginGroup(QStringLiteral("state"));
|
||||||
|
QVERIFY(!legacy.contains(QStringLiteral("window_geometry")));
|
||||||
|
QVERIFY(!legacy.contains(QStringLiteral("splitter_geometry")));
|
||||||
|
QVERIFY(!legacy.contains(QStringLiteral("toc_splitter_state")));
|
||||||
|
|
||||||
|
// Migration must persist the state so future runs load from TOML, not QSettings.
|
||||||
|
QVERIFY(QFile::exists(Session::defaultFilePath()));
|
||||||
|
|
||||||
|
// Cleanup so other tests start from a known state.
|
||||||
|
QFile::remove(Session::defaultFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionTest::load_skipsMigrationWhenStateFileExists()
|
||||||
|
{
|
||||||
|
const QByteArray legacyGeom = QByteArray::fromHex("deadbeef");
|
||||||
|
const QByteArray tomlGeom = QByteArray::fromHex("aabbccdd");
|
||||||
|
|
||||||
|
// Populate legacy QSettings with values that should NOT be loaded.
|
||||||
|
{
|
||||||
|
QSettings legacy;
|
||||||
|
legacy.beginGroup(QStringLiteral("state"));
|
||||||
|
legacy.setValue(QStringLiteral("window_geometry"), legacyGeom);
|
||||||
|
legacy.endGroup();
|
||||||
|
legacy.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session.toml with different values that SHOULD be loaded.
|
||||||
|
{
|
||||||
|
Session out;
|
||||||
|
WindowState ws;
|
||||||
|
ws.geometry = tomlGeom;
|
||||||
|
out.windows.append(ws);
|
||||||
|
QVERIFY(out.saveToFile(Session::defaultFilePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.load();
|
||||||
|
|
||||||
|
// Loaded from TOML, not legacy.
|
||||||
|
QCOMPARE(session.windows.size(), 1);
|
||||||
|
QCOMPARE(session.windows.first().geometry, tomlGeom);
|
||||||
|
|
||||||
|
// Legacy keys must be untouched (migration was skipped).
|
||||||
|
QSettings legacy;
|
||||||
|
legacy.beginGroup(QStringLiteral("state"));
|
||||||
|
QCOMPARE(legacy.value(QStringLiteral("window_geometry")).toByteArray(), legacyGeom);
|
||||||
|
legacy.endGroup();
|
||||||
|
|
||||||
|
// Cleanup so other tests start from a known state.
|
||||||
|
{
|
||||||
|
QSettings cleanup;
|
||||||
|
cleanup.beginGroup(QStringLiteral("state"));
|
||||||
|
cleanup.remove(QStringLiteral("window_geometry"));
|
||||||
|
cleanup.endGroup();
|
||||||
|
}
|
||||||
|
QFile::remove(Session::defaultFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(SessionTest)
|
||||||
|
#include "session_test.moc"
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
#include "view.h"
|
#include "view.h"
|
||||||
|
|
||||||
#include <core/application.h>
|
#include <core/application.h>
|
||||||
#include <core/settings.h>
|
#include <core/session.h>
|
||||||
#include <ui/widgets/layouthelper.h>
|
#include <ui/widgets/layouthelper.h>
|
||||||
|
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
@@ -23,7 +23,7 @@ Container::Container(QWidget *parent)
|
|||||||
m_splitter = new QSplitter();
|
m_splitter = new QSplitter();
|
||||||
m_splitter->setOrientation(Qt::Vertical);
|
m_splitter->setOrientation(Qt::Vertical);
|
||||||
connect(m_splitter, &QSplitter::splitterMoved, this, [this]() {
|
connect(m_splitter, &QSplitter::splitterMoved, this, [this]() {
|
||||||
Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState();
|
Core::Application::instance()->session()->primaryWindow().tocSplitterState = m_splitter->saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup main layout.
|
// Setup main layout.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <browser/webbridge.h>
|
#include <browser/webbridge.h>
|
||||||
#include <browser/webcontrol.h>
|
#include <browser/webcontrol.h>
|
||||||
#include <core/application.h>
|
#include <core/application.h>
|
||||||
|
#include <core/session.h>
|
||||||
#include <core/settings.h>
|
#include <core/settings.h>
|
||||||
#include <qxtglobalshortcut/qxtglobalshortcut.h>
|
#include <qxtglobalshortcut/qxtglobalshortcut.h>
|
||||||
#include <sidebar/container.h>
|
#include <sidebar/container.h>
|
||||||
@@ -73,7 +74,8 @@ MainWindow::MainWindow(Core::Application *app, QWidget *parent)
|
|||||||
|
|
||||||
setCentralWidget(centralWidget);
|
setCentralWidget(centralWidget);
|
||||||
|
|
||||||
restoreGeometry(m_settings->windowGeometry);
|
Core::WindowState &windowState = m_application->session()->primaryWindow();
|
||||||
|
restoreGeometry(windowState.geometry);
|
||||||
|
|
||||||
// Setup sidebar.
|
// Setup sidebar.
|
||||||
auto *sbViewProvider = new SidebarViewProvider(this);
|
auto *sbViewProvider = new SidebarViewProvider(this);
|
||||||
@@ -84,7 +86,7 @@ MainWindow::MainWindow(Core::Application *app, QWidget *parent)
|
|||||||
|
|
||||||
// Setup splitter.
|
// Setup splitter.
|
||||||
m_splitter->insertWidget(0, sb);
|
m_splitter->insertWidget(0, sb);
|
||||||
m_splitter->restoreState(m_settings->verticalSplitterGeometry);
|
m_splitter->restoreState(windowState.splitterState);
|
||||||
|
|
||||||
// Setup web settings.
|
// Setup web settings.
|
||||||
new Browser::Settings(m_settings, this);
|
new Browser::Settings(m_settings, this);
|
||||||
@@ -108,8 +110,9 @@ MainWindow::MainWindow(Core::Application *app, QWidget *parent)
|
|||||||
|
|
||||||
MainWindow::~MainWindow()
|
MainWindow::~MainWindow()
|
||||||
{
|
{
|
||||||
m_settings->verticalSplitterGeometry = m_splitter->saveState();
|
Core::WindowState &windowState = m_application->session()->primaryWindow();
|
||||||
m_settings->windowGeometry = saveGeometry();
|
windowState.splitterState = m_splitter->saveState();
|
||||||
|
windowState.geometry = saveGeometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::search(const Registry::SearchQuery &query)
|
void MainWindow::search(const Registry::SearchQuery &query)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "widgets/toolbarframe.h"
|
#include "widgets/toolbarframe.h"
|
||||||
|
|
||||||
#include <core/application.h>
|
#include <core/application.h>
|
||||||
|
#include <core/session.h>
|
||||||
#include <core/settings.h>
|
#include <core/settings.h>
|
||||||
#include <registry/docset.h>
|
#include <registry/docset.h>
|
||||||
#include <registry/docsetregistry.h>
|
#include <registry/docsetregistry.h>
|
||||||
@@ -104,7 +105,7 @@ SearchSidebar::SearchSidebar(const SearchSidebar *other, QWidget *parent)
|
|||||||
m_pageTocView->hide();
|
m_pageTocView->hide();
|
||||||
} else {
|
} else {
|
||||||
m_pageTocView->show();
|
m_pageTocView->show();
|
||||||
m_splitter->restoreState(Core::Application::instance()->settings()->tocSplitterState);
|
m_splitter->restoreState(Core::Application::instance()->session()->primaryWindow().tocSplitterState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
m_pageTocView->setModel(m_pageTocModel);
|
m_pageTocView->setModel(m_pageTocModel);
|
||||||
@@ -192,7 +193,7 @@ SearchSidebar::SearchSidebar(const SearchSidebar *other, QWidget *parent)
|
|||||||
m_splitter->addWidget(m_treeView);
|
m_splitter->addWidget(m_treeView);
|
||||||
m_splitter->addWidget(m_pageTocView);
|
m_splitter->addWidget(m_pageTocView);
|
||||||
connect(m_splitter, &QSplitter::splitterMoved, this, [this]() {
|
connect(m_splitter, &QSplitter::splitterMoved, this, [this]() {
|
||||||
Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState();
|
Core::Application::instance()->session()->primaryWindow().tocSplitterState = m_splitter->saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup main layout.
|
// Setup main layout.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"default-features": false
|
"default-features": false
|
||||||
},
|
},
|
||||||
"sqlite3",
|
"sqlite3",
|
||||||
|
"tomlplusplus",
|
||||||
"vulkan-headers"
|
"vulkan-headers"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user