1
0

feat: update qwfassoc

This commit is contained in:
2026-06-23 21:38:54 +08:00
parent 071347f4d4
commit 1a44240f88
24 changed files with 1169 additions and 684 deletions

View File

@@ -0,0 +1,48 @@
# qwfassoc-standalone: executable that uses the qwfassoc library to reproduce
# the original tabbed wfassoc configurator.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(QWFASSOC_STANDALONE_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest_parser.cpp"
)
set(QWFASSOC_STANDALONE_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest_parser.h"
)
set(QWFASSOC_STANDALONE_UI
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.ui"
)
add_executable(qwfassoc-standalone WIN32
${QWFASSOC_STANDALONE_SOURCES}
${QWFASSOC_STANDALONE_HEADERS}
${QWFASSOC_STANDALONE_UI}
)
target_include_directories(qwfassoc-standalone PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_link_libraries(qwfassoc-standalone PRIVATE
qwfassoc
Qt6::Widgets
toml11::toml11
)
# Translation pipeline for the standalone executable. The library's strings
# are translated by qwfassoc's own .ts file; this one only covers the
# executable-specific messages (CLI errors, tab titles, etc.).
set(QWFASSOC_STANDALONE_TS_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc-standalone_zh_CN.ts"
)
qt6_add_translations(qwfassoc-standalone
TS_FILES ${QWFASSOC_STANDALONE_TS_FILES}
)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
</TS>

View File

@@ -0,0 +1,156 @@
#include <QApplication>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QLocale>
#include <QMessageBox>
#include <QString>
#include <QStringList>
#include <QTranslator>
#include <stdexcept>
#include <string>
#include <wfassoc++.h>
#include "main_window.h"
#include "manifest_parser.h"
#include "scope.h"
namespace {
// Context used for translatable strings that live outside of any QObject.
constexpr const char* kTranslationContext = "qwfassoc-standalone";
// Show a modal error dialog with the given message and return a non-zero
// exit code. Used for the various fatal conditions that may occur before the
// main dialog can be shown.
int fatal(QWidget* parent, const QString& message) {
QMessageBox::critical(parent, QApplication::applicationName(), message);
return 1;
}
// Convert the --for command line value to a TargetScope. Throws if the value
// is not one of the accepted strings.
qwfassoc::TargetScope parseScope(const QString& value) {
const QString normalized = value.trimmed().toLower();
if (normalized == QStringLiteral("user")) {
return qwfassoc::TargetScope::User;
}
if (normalized == QStringLiteral("system")) {
return qwfassoc::TargetScope::System;
}
throw std::runtime_error(
"Invalid value for --for. Use \"user\" or \"system\".");
}
// Install the translation(s) matching the user's preferred UI language, if
// any. Both the qwfassoc library's .qm and this executable's .qm are loaded
// (their .ts files live under the per-project i18n/ directories and are
// embedded under the ":/i18n" resource prefix by qt6_add_translations).
void installTranslators(QApplication& app) {
const QStringList uiLanguages = QLocale::system().uiLanguages();
for (const QString& locale : uiLanguages) {
const QString name = QLocale(locale).name();
QTranslator* libTranslator = new QTranslator(&app);
if (libTranslator->load(QStringLiteral(":/i18n/qwfassoc_") + name)) {
app.installTranslator(libTranslator);
}
QTranslator* appTranslator = new QTranslator(&app);
if (appTranslator->load(
QStringLiteral(":/i18n/qwfassoc-standalone_") + name)) {
app.installTranslator(appTranslator);
}
}
}
} // namespace
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QApplication::setApplicationName(QStringLiteral("qwfassoc-standalone"));
// Install available translations before any translatable string is
// resolved so that tr() and QCoreApplication::translate() pick up the
// right language.
installTranslators(app);
QApplication::setApplicationDisplayName(
QCoreApplication::translate(kTranslationContext,
"qwfassoc - wfassoc Configurator"));
// Parse command line arguments using Qt's built-in parser.
QCommandLineParser parser;
parser.setApplicationDescription(
QCoreApplication::translate(
kTranslationContext,
"Qt-based GUI executable for the wfassoc library."));
parser.addHelpOption();
QCommandLineOption manifestOption(
QStringList() << QStringLiteral("c") << QStringLiteral("manifest"),
QCoreApplication::translate(kTranslationContext,
"Path to the application manifest TOML file."),
QStringLiteral("manifest"));
QCommandLineOption forOption(
QStringList() << QStringLiteral("f") << QStringLiteral("for"),
QCoreApplication::translate(kTranslationContext,
"Target scope: \"user\" or \"system\"."),
QStringLiteral("scope"));
parser.addOption(manifestOption);
parser.addOption(forOption);
parser.process(app);
// Validate that both mandatory options were provided with sane values.
const QString manifestPath = parser.value(manifestOption);
const QString forValue = parser.value(forOption);
if (manifestPath.isEmpty()) {
return fatal(nullptr,
QCoreApplication::translate(
kTranslationContext,
"The --manifest/-c option is required."));
}
if (forValue.isEmpty()) {
return fatal(nullptr,
QCoreApplication::translate(
kTranslationContext,
"The --for/-f option is required."));
}
qwfassoc::TargetScope scope;
try {
scope = parseScope(forValue);
} catch (const std::exception& e) {
return fatal(nullptr, QString::fromUtf8(e.what()));
}
// Initialize the wfassoc runtime. WFStartup must run before most other
// wfassoc calls; if it fails we cannot proceed.
if (!wfassoc::WFStartup()) {
return fatal(nullptr,
QString::fromUtf8(wfassoc::WFGetLastError()));
}
// Build the manifest -> schema -> program pipeline and run the dialog.
// Program construction consumes the schema (move) and performs the deep
// validation (identifier format, dangling references, etc.).
int exitCode = 0;
try {
qwfassoc::Manifest manifest =
qwfassoc::parseManifestFile(manifestPath.toStdString());
wfassocpp::Schema schema = qwfassoc::buildSchema(manifest);
wfassocpp::Program program(std::move(schema));
qwfassoc::MainWindow window(std::move(program), scope);
exitCode = window.exec();
} catch (const std::exception& e) {
wfassoc::WFShutdown();
return fatal(nullptr, QString::fromUtf8(e.what()));
}
wfassoc::WFShutdown();
return exitCode;
}

View File

@@ -0,0 +1,79 @@
#include "main_window.h"
#include "ui_main_window.h"
#include "application_widget.h"
#include "association_widget.h"
#include "icon_utils.h"
#include <QTabWidget>
namespace qwfassoc {
MainWindow::MainWindow(wfassocpp::Program program,
TargetScope scope,
QWidget* parent)
: QDialog(parent),
ui_(new Ui::MainWindow),
appTab_(nullptr),
assocTab_(nullptr),
program_(std::move(program)),
scope_(scope) {
ui_->setupUi(this);
// Resolve program metadata that several labels depend on.
programName_ = QString::fromUtf8(program_.ResolveName());
{
auto iconRc = program_.ResolveIcon();
auto handle = iconRc.GetIcon();
programIcon_ = icon_utils::fromHicon(handle);
}
// Compose the window title and icon.
setWindowTitle(tr("%1 Options").arg(programName_));
if (!programIcon_.isNull()) {
setWindowIcon(QIcon(programIcon_));
}
// Build the two tab pages from the library widgets.
appTab_ = new ApplicationWidget(this);
assocTab_ = new AssociationWidget(this);
ui_->tabWidget->addTab(appTab_, tr("Applications"));
ui_->tabWidget->addTab(assocTab_, tr("File Associations"));
// Two-phase initialization. The standalone executable wants the OK and
// Cancel buttons visible because they drive dialog acceptance.
appTab_->setConfig({&program_, scope_});
assocTab_->setConfig({&program_, scope_, /*showOkCancelButtons=*/true});
// Wire widget signals so that any wfassoc change refreshes both pages,
// and the association widget can request dialog closure.
connect(appTab_, &ApplicationWidget::changed, this,
&MainWindow::onAnyChanged);
connect(assocTab_, &AssociationWidget::changed, this,
&MainWindow::onAnyChanged);
connect(assocTab_, &AssociationWidget::finished, this,
&MainWindow::onFinished);
}
MainWindow::~MainWindow() = default;
void MainWindow::onAnyChanged() {
if (appTab_ != nullptr) {
appTab_->refresh();
}
if (assocTab_ != nullptr) {
assocTab_->refresh();
}
}
void MainWindow::onFinished(bool accepted) {
if (accepted) {
accept();
} else {
reject();
}
}
} // namespace qwfassoc

View File

@@ -0,0 +1,57 @@
#pragma once
#ifndef QWFASSOC_STANDALONE_MAIN_WINDOW_H_
#define QWFASSOC_STANDALONE_MAIN_WINDOW_H_
#include <QDialog>
#include <QPixmap>
#include <QString>
#include <wfassoc++.h>
#include "scope.h"
namespace Ui {
class MainWindow;
}
namespace qwfassoc {
class ApplicationWidget;
class AssociationWidget;
}
namespace qwfassoc {
// Top-level dialog used by the qwfassoc-standalone executable. Hosts the two
// reusable widgets from the qwfassoc library inside a QTabWidget and wires
// their changed() / finished() signals together.
class MainWindow : public QDialog {
Q_OBJECT
public:
explicit MainWindow(wfassocpp::Program program,
TargetScope scope,
QWidget* parent = nullptr);
~MainWindow() override;
private slots:
// Called whenever one of the embedded widgets reports that wfassoc state
// has changed. Refreshes both widgets so they stay in sync.
void onAnyChanged();
// Called when the association widget asks the dialog to close.
void onFinished(bool accepted);
private:
Ui::MainWindow* ui_;
ApplicationWidget* appTab_;
AssociationWidget* assocTab_;
wfassocpp::Program program_;
TargetScope scope_;
QString programName_;
QPixmap programIcon_;
};
} // namespace qwfassoc
#endif // QWFASSOC_STANDALONE_MAIN_WINDOW_H_

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QDialog" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>600</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<height>600</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>480</width>
<height>600</height>
</size>
</property>
<property name="windowTitle">
<string>Options</string>
</property>
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,144 @@
#include "manifest_parser.h"
#include <stdexcept>
#include <toml.hpp>
namespace qwfassoc {
// region: TOML Parsing
Manifest parseManifestFile(const std::string& path) {
toml::value root;
try {
root = toml::parse(path);
} catch (const std::exception& e) {
// toml::parse already produces a descriptive message including file
// path and line number; just propagate it wrapped for context.
throw std::runtime_error(
std::string("Failed to parse manifest TOML file: ") + e.what());
}
Manifest manifest;
// Helper lambda: read a string field, re-throwing with a clearer message.
auto readString = [](const toml::value& v,
const std::string& key) -> std::string {
try {
return toml::find<std::string>(v, key);
} catch (const std::exception& e) {
throw std::runtime_error(
"Manifest field \"" + key +
"\" is missing or is not a string: " + e.what());
}
};
// Required top-level scalar fields.
manifest.identifier = readString(root, "identifier");
manifest.path = readString(root, "path");
manifest.clsid = readString(root, "clsid");
// Optional top-level scalar fields.
if (root.contains("name")) {
manifest.name = readString(root, "name");
}
if (root.contains("icon")) {
manifest.icon = readString(root, "icon");
}
if (root.contains("behavior")) {
manifest.behavior = readString(root, "behavior");
}
// Helper lambda: copy a TOML table of {string -> string} into a std::map.
auto readStringTable =
[](const toml::value& parent,
const std::string& key) -> std::map<std::string, std::string> {
if (!parent.contains(key)) {
return {};
}
std::map<std::string, std::string> out;
try {
// Keep the sub-value alive as a local so that the table reference
// obtained from as_table() stays valid for the loop below,
// regardless of whether toml::find returns by reference or by
// value in the toml11 version that is linked.
const toml::value& sub = toml::find(parent, key);
for (const auto& [k, v] : sub.as_table()) {
out.emplace(k, v.as_string());
}
} catch (const std::exception& e) {
throw std::runtime_error(
"Manifest table \"" + key +
"\" contains an invalid entry: " + e.what());
}
return out;
};
manifest.strs = readStringTable(root, "strs");
manifest.icons = readStringTable(root, "icons");
manifest.behaviors = readStringTable(root, "behaviors");
// Extension table. Each entry is itself a table with name/icon/behavior.
if (root.contains("exts")) {
try {
const toml::value& exts_value = toml::find(root, "exts");
for (const auto& [ext_key, ext_value] : exts_value.as_table()) {
ManifestExt ext;
ext.name = readString(ext_value, "name");
ext.icon = readString(ext_value, "icon");
ext.behavior = readString(ext_value, "behavior");
manifest.exts.emplace(ext_key, std::move(ext));
}
} catch (const std::exception& e) {
throw std::runtime_error(
"Manifest \"exts\" table contains an invalid entry: " +
e.what());
}
}
return manifest;
}
// endregion
// region: Schema Conversion
wfassocpp::Schema buildSchema(const Manifest& manifest) {
wfassocpp::Schema schema;
// The wfassocpp wrappers translate any underlying failure into a
// std::runtime_error via _Check, so we let those propagate untouched.
schema.SetIdentifier(manifest.identifier.c_str());
schema.SetPath(manifest.path.c_str());
schema.SetClsid(manifest.clsid.c_str());
// Optional fields: passing nullptr tells wfassoc to clear the value.
schema.SetName(manifest.name.has_value() ? manifest.name->c_str()
: nullptr);
schema.SetIcon(manifest.icon.has_value() ? manifest.icon->c_str()
: nullptr);
schema.SetBehavior(manifest.behavior.has_value() ? manifest.behavior->c_str()
: nullptr);
for (const auto& [key, value] : manifest.strs) {
schema.AddStr(key.c_str(), value.c_str());
}
for (const auto& [key, value] : manifest.icons) {
schema.AddIcon(key.c_str(), value.c_str());
}
for (const auto& [key, value] : manifest.behaviors) {
schema.AddBehavior(key.c_str(), value.c_str());
}
for (const auto& [key, value] : manifest.exts) {
schema.AddExt(key.c_str(),
value.name.c_str(),
value.icon.c_str(),
value.behavior.c_str());
}
return schema;
}
// endregion
} // namespace qwfassoc

View File

@@ -0,0 +1,25 @@
#pragma once
#ifndef QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
#define QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
#include <string>
#include <wfassoc++.h>
#include "manifest.h"
namespace qwfassoc {
// Parse a manifest TOML file from disk into a Manifest value.
// Throws std::runtime_error on any IO or TOML syntax error.
Manifest parseManifestFile(const std::string& path);
// Build a wfassocpp::Schema from a manifest value.
// Throws std::runtime_error (originating from wfassocpp::_Check) when the
// wfassoc library rejects an operation, e.g. on duplicate keys or dangling
// references.
wfassocpp::Schema buildSchema(const Manifest& manifest);
} // namespace qwfassoc
#endif // QWFASSOC_STANDALONE_MANIFEST_PARSER_H_