diff --git a/example/qwfassoc/CMakeLists.txt b/example/qwfassoc/CMakeLists.txt new file mode 100644 index 0000000..d4cb0ec --- /dev/null +++ b/example/qwfassoc/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.20) + +project(qwfassoc LANGUAGES CXX) + +# Qt 6 requires C++17 at minimum. +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Let CMake auto-process Q_OBJECT, .ui files and .qrc resources. +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +# Make the bundled Findwfassoc.cmake module visible to find_package(). +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Locate third-party dependencies. The user is responsible for making these +# discoverable through the usual CMake mechanisms (CMAKE_PREFIX_PATH, etc.). +# LinguistTools is required to wire up Qt's translation pipeline +# (lupdate + lrelease) so that .ts files are kept up to date and .qm files +# are embedded as Qt resources. +find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools) +find_package(toml11 REQUIRED) +# Findwfassoc.cmake requires the wfassoc_ROOT variable to be set. +find_package(wfassoc REQUIRED) + +set(QWFASSOC_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/manifest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.cpp" +) + +set(QWFASSOC_HEADERS + "${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/manifest.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.h" +) + +set(QWFASSOC_UI + "${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.ui" +) + +# WIN32 makes this a windowed application on Windows (no console window). +add_executable(qwfassoc WIN32 + ${QWFASSOC_SOURCES} + ${QWFASSOC_HEADERS} + ${QWFASSOC_UI} +) + +target_include_directories(qwfassoc PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + +target_link_libraries(qwfassoc PRIVATE + Qt6::Widgets + toml11::toml11 + wfassoc::wfassoc +) + +# Translation pipeline. +# +# qt6_add_translations() runs lupdate against the target's sources (keeping +# the .ts files below in sync) and then runs lrelease to compile them into +# .qm files. The .qm files are embedded under the ":/i18n" resource prefix, +# where installTranslators() in main.cpp looks them up at runtime. +# +# The .ts files are intentionally shipped empty: translators are expected to +# fill them in. Re-running cmake / building the target refreshes the entries +# found by lupdate without dropping already-translated ones. +set(QWFASSOC_TS_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc_zh_CN.ts" +) + +qt6_add_translations(qwfassoc + TS_FILES ${QWFASSOC_TS_FILES} +) diff --git a/example/qwfassoc/PROMPT.txt b/example/qwfassoc/PROMPT.txt deleted file mode 100644 index ceb0570..0000000 --- a/example/qwfassoc/PROMPT.txt +++ /dev/null @@ -1,241 +0,0 @@ -我要求你使用Qt Widget编写一个GUI程序。该GUI程序是wfassoc的一个可视化界面。wfassoc是一个由Rust编写,并暴露出C接口,可以操作Windows注册表,来管理应用程序的注册和卸载,以及文件关联的动态链接库。 - -我要求你在@example/qwfassoc/TASKS.md 中先做好详细的规划,而不是上来就写代码。我会安排其他人来负责执行你做的规划。 - -# 项目要求 - -- 使用Qt Widget编写界面,而不是QML。 -- 使用UI文件而不是C++语句来构建界面。 -- 我使用的是Qt 6,使用CMake作为构建系统,不要使用Qt的qmake。 -- 使用toml11作为TOML读取库。 - -# 界面要求 - -这是一个基于 Qt Widgets 的标准对话框界面描述。你可以按照以下层级结构来构建代码: - -## 主窗口容器 (Main Window) - -* 类: `QDialog`。 -* 窗口标题: "xxx选项"。xxx在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。 -* 窗口图标:在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。 -* 大小限制:固定大小480x600 - -## 选项卡 (Top Tabs) - -* 组件: `QTabWidget`。 -* 标签页 (Tabs): 从左到右依次添加以下标签页: - 1. 应用程序 - 2. 文件关联 -* 大小:选项卡占据对话框全部内容 - -## "应用程序"选项卡内容 - -该选项卡内部使用垂直布局 (`QVBoxLayout`),包含一个主要的分组区域: - -### 区域内容 - -* 容器: `QGroupBox`。 -* 区域标题:安装与卸载 -* 布局: 垂直布局 (`QVBoxLayout`)。 - * 上半部分 - * 水平布局(`QHBoxLayout`)。 - * 左侧: 一个 `QLabel` 显示图标,该图标表示要设定的应用程序的图标。在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。 - * 右侧: 一个 `QLabel` 显示文本"在此安装或卸载xxx",表示要设定的应用程序的名称。xxx在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。 (文本需要设置自动换行 `setWordWrap(true)`)。 - * 下半部分 - * 水平布局(`QHBoxLayout`)。 - * 内容为两个 `QPushButton`,文本分别为: - * 安装:为当前对象(系统或当前用户,由应用程序初始化时从命令行参数获取)安装应用。如果应用程序已经安装,则不可点击。 - * 卸载:为当前对象(系统或当前用户,由应用程序初始化时从命令行参数获取)卸载应用。如果应用程序没有安装,则不可点击。 - -## "文件关联"选项卡内容 - -在这个选项页内部,使用一个垂直布局 (`QVBoxLayout`) 来排列以下控件: - -* 说明文本: - * 组件: `QLabel`。 - * 文本: "使用 xxx 关联的文件类型:"。xxx在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。 - -* 功能按钮行: - * 布局: `QHBoxLayout` (水平布局)。 - * 组件: 两个 `QPushButton`。 - * 文本: 两个按钮的文本都是 "+"。 - * 位置: 位于列表上方,用于“全选”操作(第一次点击,将所有没关联的文件扩展名(显示为空白)设置为应用程序提供的打开方式。如果没有空白内容,或第二次点击,将所有文件全部设置为应用程序提供的打开方式)。 - -* 文件类型列表 (核心组件): - * 组件: `QTableWidget` (表格控件)。 - * 列数: 3列。 - * 表头 (Header): - * 第1列标题: "类型" - * 第2列标题: "uuu" (uuu在运行时进行获取,为当前用户名) - * 第3列标题: "所有用户" - * 行内容示例: - * 第一列:一个文件类型图标,右边跟着对应的文本。表示当前文件扩展名,和当前混合视图(hybrid)下的图标。图标和文本均从wfassoc函数获取。 - * 第二列:用户视图(user)下的名称。文本从wfassoc函数获取。 - * 第三列:系统视图(system)下的名称。文本从wfassoc函数获取。 - * 滚动条: 右侧有一个垂直滚动条 (`QScrollBar`),表示内容超出可视区域。 - * 操作方式: - * 第二列和第三列的元素可点击。 - * 如果元素为空白或其它打开方式,则点击后设置为当前应用程序指定的打开方式(link)。 - * 如果是自身打开方式,点击后设置为空白(unlink) - * 点击后,第一列的图标需要改变,也因此你需要暂存当前应用程序提供打开方式的图标。如果第二第三列均为空,则不显示图标(仍然占位,显示为空白)。 - * 点击操作并不会实时操作注册表,程序需要暂存用户的需求,然后在点击确认或应用按钮后再统一执行。 - -* 底部按钮栏 - * 布局: `QHBoxLayout` (水平布局),通常右对齐或使用 `QDialogButtonBox`。 - * 组件: 三个 `QPushButton`。 - * 按钮文本 (从左到右): - 1. "确定" (通常设为默认按钮 `setDefault(true)`)。 - 2. "取消"。 - 3. "应用":点击后应用修改,并留在页面 (如果没有修改,则不可用)。 - -额外注意: - -* 如果应用程序没有安装,则本页面下所有内容均不启用。 -* 如果启动时命令行指定以为当前用户安装的模式启动,则系统那一栏所有按钮都不可用 - -# 代码要求 - -- 有关wfassoc的接口,请查阅@wfassoc-cdylib/codegen/wfassoc++.h 我要求你使用这个头文件中提供的内容来进行编写。 - - wfassoc++.h文件中没有注释,如果你想查看注释,请访问@wfassoc-cdylib/codegen/wfassoc.h 文件。wfassoc++.h是wfassoc.h的C++包装。 - - wfassoc.h所暴露的接口是由Rust编写的,通常查看wfassoc.h可满足所有需求。如果仍有不确定的内容,可查看其对应Rust项目的源码,位于@wfassoc-cdylib/src 目录下。或更进一步地查看其依赖的源码,位于@wfassoc/src 目录下。 -- 该GUI程序需要接受两个必要的命令行参数,请使用Qt内置的命令行解析器进行解析: - - `-c`或`--manifest`:指定要配置的应用程序的清单文件。 - - `-f`或`--for`:指定应用程序要安装到的 - - 清单文件的样例是@example/manifest/ppic.toml -- 在应用程序加载时,或者执行操作时,如果发生错误(例如底层wfassoc发生错误,丢失命令行选项等,则弹出对话框报错,然后立即退出程序) -- 程序基本流程: - - 接受命令行,检查命令行参数是否合法 - - 加载命令行指定的manifest文件,并使用sanitizer检查错误。你可以阅读@wfassoc-exec/src/manifest.rs 文件来看看我是如何在Rust中检查它的。 - - 按照给定的manifest文件,使用wfassoc库构建schema,然后再构建program。 - - 初始化窗口。 - - 调用wfassoc program提供的函数,检查应用程序是否安装,设置窗口的安装部分的按钮enable。 - - 调用wfassoc program提供的函数,遍历所有文件扩展名和关联情况,设置窗口的文件关联表格。 - - 用户可以在"应用程序"选项卡中注册和卸载应用程序,点击后弹出窗口表示安装或卸载成功,然后检测是否安装,并刷新各个控件的enable状态。 - - 用户可以在"文件关联"选项卡中设置是否以当前应用程序打开某些扩展名。用户可以点击全选按钮或单元格来进行设置,应用程序暂存修改,等用户点击确认或应用后再应用修改。如果点击的是应用,则刷新当前页面。 - -# 额外要求 - -- 不要尝试去编译来检查错误。我会安排其他人来检查程序是否能正常运行,并汇报回来,你再修改。 -- 你不需要关心能否找到Qt,wfassoc和toml11这些库。 - - 对于wfassoc,你只需要将@wfassoc-cdylib/codegen/Findwfassoc.cmake 复制到@example/qwfassoc/cmake 目录下,并在此目录下编写一个README.md,表明这个文件是从哪里复制来的即可。然后把复制的cmake文件所在目录加入find_package目录,然后使用find_package寻找wfassoc即可。至于去哪里找这个库,我会安排其他人来做。 - - 对于Qt和toml11我会安排其他人来做,你只需要用find_package来找他们就行,不需要操心能不能找到。 -- 如果你对某项需求有疑问,请问我,而不是进行猜测。 - - - - - - - - - - - -### 总结代码结构示意 (伪代码): - -```cpp -QDialog *dialog = new QDialog(); -dialog->setWindowTitle("选项"); - -QVBoxLayout *mainLayout = new QVBoxLayout(dialog); - -// 1. Tab Widget -QTabWidget *tabWidget = new QTabWidget(); -tabWidget->addTab(new QWidget(), "系统"); -tabWidget->addTab(new QWidget(), "7-Zip"); -// ... 其他 tabs - -// 2. System Tab Content -QWidget *systemTab = tabWidget->widget(0); -QVBoxLayout *systemLayout = new QVBoxLayout(systemTab); - -// Label -QLabel *label = new QLabel("使用 7-Zip 关联的文件类型:"); -systemLayout->addWidget(label); - -// Buttons (+) -QHBoxLayout *btnLayout = new QHBoxLayout(); -QPushButton *btnPlus1 = new QPushButton("+"); -QPushButton *btnPlus2 = new QPushButton("+"); -btnLayout->addWidget(btnPlus1); -btnLayout->addWidget(btnPlus2); -systemLayout->addLayout(btnLayout); - -// List (TreeWidget) -QTreeWidget *treeWidget = new QTreeWidget(); -treeWidget->setColumnCount(3); -treeWidget->setHeaderLabels(QStringList() << "类型" << "yyc12345" << "所有用户"); -// 添加 items... -systemLayout->addWidget(treeWidget); - -// 3. Bottom Buttons -QHBoxLayout *bottomLayout = new QHBoxLayout(); -bottomLayout->addStretch(); // 推挤按钮到右边 -QPushButton *btnOK = new QPushButton("确定"); -QPushButton *btnCancel = new QPushButton("取消"); -QPushButton *btnApply = new QPushButton("应用(A)"); -btnApply->setEnabled(false); // 禁用 -QPushButton *btnHelp = new QPushButton("帮助"); - -bottomLayout->addWidget(btnOK); -bottomLayout->addWidget(btnCancel); -bottomLayout->addWidget(btnApply); -bottomLayout->addWidget(btnHelp); - -mainLayout->addWidget(tabWidget); -mainLayout->addLayout(bottomLayout); -``` - - - - - - - - - -这是一个标准的 Windows 风格属性对话框,可以通过以下 Qt Widgets 结构来描述: - -### 1. 主窗口容器 -* 类: `QDialog`。 -* 标题: "系统属性"。 -* 布局: 垂直布局 (`QVBoxLayout`)。 - -### 2. 顶部选项卡 (Tabs) -* 组件: `QTabWidget`。 -* 标签页: 包含 "计算机名", "硬件", "高级", "系统保护", "远程"。 -* 当前选中: "硬件" 标签页。 - -### 3. "硬件" 选项卡内容 -该选项卡内部使用垂直布局 (`QVBoxLayout`),包含两个主要的分组区域(视觉上类似 `QGroupBox` 或带有边框的 `QFrame`): - -#### 区域 A: 设备管理器 (上半部分) -* 容器: 一个带有边框的容器。 -* 布局: 水平布局 (`QHBoxLayout`)。 - * 左侧: 一个 `QLabel` 显示电脑图标。 - * 中间: 一个 `QLabel` 显示说明文本:"设备管理器列出所有安装在计算机上的硬件设备。请使用设备管理器来更改设备的属性。" (文本需要设置自动换行 `setWordWrap(true)`)。 - * 右侧/底部: 一个 `QPushButton`,文本为 "设备管理器(D)"。 - -#### 区域 B: 设备安装设置 (下半部分) -* 容器: 一个带有边框的容器。 -* 布局: 垂直布局 (`QVBoxLayout`)。 - * 顶部行: 水平布局 (`QHBoxLayout`)。 - * 左侧: 一个 `QLabel` 显示列表/勾选图标。 - * 右侧: 一个 `QLabel` 显示说明文本:"选择 Windows 是否下载制造商提供的可用于你的设备的应用和自定义图标。" (文本需要设置自动换行)。 - * 底部: 一个 `QPushButton`,文本为 "设备安装设置(S)",靠右对齐。 - -### 4. 底部按钮栏 -* 布局: 水平布局 (`QHBoxLayout`),右对齐 (通常通过 `addStretch()` 实现)。 -* 组件: 三个 `QPushButton`。 - * "确定" - * "取消" - * "应用(A)" (注意:截图中该按钮呈灰色,代码中需设置 `setEnabled(false)`)。 - - - -你制定的计划有一些问题,请按照下述标出的问题一一修正: - -- 编写的代码和说明文件需要使用英文注释。 -- 是manifest而非manifesto,表示清单文件,请修正这个拼写错误。 -- 我看到你在mainwindow篇章中编写了大量的C++代码,这没有必要。你是计划者而非执行者。你需要把需要在这个头文件中实现什么?该怎么做?需要使用哪些wfassoc函数,这些函数该怎么调用?在哪里查看他们怎么调用?详细的告诉将要执行这些任务的执行者,而不是直接为他们编写好代码。你在manifest部分的任务规划就非常符合这种范式。 - diff --git a/example/qwfassoc/README.md b/example/qwfassoc/README.md index 5b223d0..4c6b75c 100644 --- a/example/qwfassoc/README.md +++ b/example/qwfassoc/README.md @@ -1,3 +1,137 @@ -# Q WFAssoc +# qwfassoc -TODO +A Qt Widgets based GUI front-end for the [wfassoc](../../wfassoc) library. + +`qwfassoc` exposes the same install / uninstall / file-association operations +as the `wfassoc-exec` command line tool, but in a small tabbed dialog aimed at +end users rather than scripts. + +## Layout + +``` +qwfassoc/ +├── CMakeLists.txt CMake build script +├── README.md This file +├── cmake/ +│ ├── Findwfassoc.cmake Verbatim copy of wfassoc's Find module +│ └── README.md Provenance notes for the copy +├── i18n/ +│ └── qwfassoc_zh_CN.ts Empty placeholder translation file +└── src/ + ├── main.cpp Entry point, CLI parsing, translator loading + ├── main_window.h/.cpp Main configuration dialog + ├── main_window.ui Qt Designer description of the dialog + ├── manifest.h/.cpp TOML manifest loader (toml11) and schema builder + └── icon_utils.h/.cpp wfassocpp::HICON -> QPixmap conversion helper +``` + +## Requirements + +* **CMake** 3.20 or newer. +* A C++17 compiler. +* **Qt 6** (6.3 or newer is recommended for `qt6_add_translations`). The + `Widgets` and `LinguistTools` components are required: + `find_package(Qt6 COMPONENTS Widgets LinguistTools)`. +* **toml11**. `find_package(toml11)` is used. +* **wfassoc**. `find_package(wfassoc)` is used, which requires `wfassoc_ROOT` + to point at an installed wfassoc tree (see + [`cmake/Findwfassoc.cmake`](cmake/Findwfassoc.cmake) for the expected + directory layout). + +## Building + +```bat +cmake -S . -B build -DCMAKE_PREFIX_PATH=C:\Qt\6.x.x\msvc2022_64 ^ + -Dwfassoc_ROOT=C:\path\to\wfassoc\install ^ + -Dtoml11_DIR=C:\path\to\toml11\share\toml11\cmake +cmake --build build --config Release +``` + +The resulting executable is `build/Release/qwfassoc.exe` (or a similar path +depending on the generator). + +## Running + +`qwfassoc` requires two command line arguments: + +| Short | Long | Meaning | +| ----- | ------------ | ------------------------------------------------------------- | +| `-c` | `--manifest` | Path to the application manifest TOML file (see [`example/manifest/ppic.toml`](../manifest/ppic.toml)). | +| `-f` | `--for` | Target scope: `user` or `system`. | + +Example: + +```bat +qwfassoc -c C:\path\to\ppic.toml -f user +``` + +When `-f user` is used the "All Users" column of the file-association table is +read-only (cells render greyed out and clicks are ignored). Use `-f system` +(with appropriate administrator privileges) to make changes that affect all +users. + +## Internationalization + +The user-facing strings shipped in the source code and the `.ui` file are +written in English. Every translatable string is wrapped in `tr()` (in code) +or marked as a regular `` (in the `.ui` file, which `uic` then wraps +in `QCoreApplication::translate`). + +The CMake build wires up the standard Qt translation pipeline via +`qt6_add_translations()`: + +* the listed `.ts` files under `i18n/` are kept in sync with the source by + `lupdate`, +* `lrelease` compiles them into `.qm` files which are embedded under the + `:/i18n/` resource prefix, +* `installTranslators()` in `src/main.cpp` loads the `.qm` file matching the + current locale at startup. + +The repository ships `i18n/qwfassoc_zh_CN.ts` as an **empty placeholder**. +Translators are expected to fill it in (or add new language files and list +them in `CMakeLists.txt`). No actual translation work is performed by the +build on its own. + +## UI Overview + +The dialog is a fixed 480x600 `QDialog` with two tabs. + +### Applications tab + +Lets the user install or uninstall the program described by the manifest in +the scope selected by `--for`. The currently-active action button is enabled +based on whether the program is already registered; the other one is disabled. + +### File associations tab + +Lists every extension declared in the manifest. Each row shows: + +* the dotted extension (`.jpg`) with a hybrid-view icon, +* the display name of the handler currently registered for the current user, +* the display name of the handler currently registered for all users. + +Clicking a cell in the user or all-users column toggles the cell state between +the program-provided handler (link) and no handler (unlink). The "+ " buttons +above each column progressively select more: the first click only fills blank +cells, the next click overrides any cell pointing at another handler. + +Changes are buffered in memory and only written to the registry when **OK** or +**Apply** is pressed. **OK** writes and closes the dialog; **Apply** writes +and refreshes the table; **Cancel** discards the pending changes and closes +the dialog. The **Apply** button is disabled when no changes are pending. + +If the program is not installed in the active scope, the whole file +associations tab is disabled. + +## Notes and Limitations + +* The "self" detection in the file-association table is based on comparing the + display name returned by wfassoc with the display name this program would + use. Two programs sharing the exact same display name could therefore be + confused. +* The dialog uses `Qt::ItemIsSelectable` (without `Qt::ItemIsEnabled`) to + render disabled system-column cells in user mode; their text is still shown + but they cannot be clicked. +* All errors originating from wfassoc are surfaced through `QMessageBox` + dialogs; fatal errors during startup cause the process to exit with a + non-zero status code. diff --git a/example/qwfassoc/TASKS.md b/example/qwfassoc/TASKS.md deleted file mode 100644 index 07bee8b..0000000 --- a/example/qwfassoc/TASKS.md +++ /dev/null @@ -1,1313 +0,0 @@ -# qwfassoc 实施计划 - -本文档是 qwfassoc GUI 程序的详细实施计划。qwfassoc 是 wfassoc 动态链接库的可视化界面,用于管理 Windows 应用程序的注册/卸载以及文件关联。 - ---- - -## 1. 项目结构 - -``` -example/qwfassoc/ -├── TASKS.md # 本文件 -├── CMakeLists.txt # CMake 构建文件 -├── cmake/ -│ ├── Findwfassoc.cmake # 从 wfassoc-cdylib/codegen/Findwfassoc.cmake 复制 -│ └── README.md # 说明 Findwfassoc.cmake 的来源 -└── src/ - ├── main.cpp # 程序入口 + 命令行解析 - ├── manifesto.hpp # TOML manifest 解析与校检模块的头文件 - ├── manifesto.cpp # TOML manifest 解析与校检模块的实现文件 - ├── mainwindow.ui # Qt Designer UI 文件(主对话框) - ├── mainwindow.hpp # MainWindow 类头文件 - └── mainwindow.cpp # MainWindow 类实现文件 -``` - ---- - -## 2. 任务分解 - -### 任务 2.1: 复制 cmake 模块文件 - -**操作内容:** - -1. 将 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制到 `example/qwfassoc/cmake/Findwfassoc.cmake`。 -2. 在 `example/qwfassoc/cmake/README.md` 中写入以下内容(纯文本说明文件): - -```markdown -# cmake 模块说明 - -此目录下的 `Findwfassoc.cmake` 是从项目根目录 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制而来。 - -该文件提供 `wfassoc::wfassoc` imported target。使用前需要设置 `wfassoc_ROOT` 变量指向 wfassoc 安装目录。 -``` - ---- - -### 任务 2.2: 编写 CMakeLists.txt - -**产出文件:** `example/qwfassoc/CMakeLists.txt` - -**内容要求:** - -```cmake -cmake_minimum_required(VERSION 3.16) -project(qwfassoc VERSION 0.1.0 LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Qt 自动处理 MOC/UIC -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) - -# --- 查找依赖库 --- - -# 查找 Qt6 Widgets -find_package(Qt6 REQUIRED COMPONENTS Widgets) - -# 查找 toml11 -find_package(toml11 REQUIRED) - -# 添加 cmake 模块搜索路径(用于 Findwfassoc.cmake) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") - -# 查找 wfassoc (需要用户设置 wfassoc_ROOT) -find_package(wfassoc REQUIRED) - -# --- 源文件 --- - -set(SOURCES - src/main.cpp - src/mainwindow.cpp - src/manifesto.cpp -) - -set(HEADERS - src/mainwindow.hpp - src/manifesto.hpp -) - -set(UI_FILES - src/mainwindow.ui -) - -# --- 构建目标 --- - -qt_add_executable(qwfassoc ${SOURCES} ${HEADERS} ${UI_FILES}) - -target_link_libraries(qwfassoc PRIVATE - Qt6::Widgets - toml11::toml11 - wfassoc::wfassoc -) - -# 将 wfassoc DLL 复制到输出目录 -add_custom_command(TARGET qwfassoc POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${wfassoc_DLL}" - $ - COMMENT "Copying wfassoc DLL to output directory" -) -``` - -**注意事项:** -- 使用 `qt_add_executable` 而不是 `add_executable`,因为 Qt6 推荐用此命令自动设置 WIN32 等属性。 -- 使用 `CMAKE_AUTOUIC ON` 自动处理 `.ui` 文件。 -- 使用 `CMAKE_AUTOMOC ON` 自动处理 QObject 的 MOC。 -- `wfassoc_DLL` 变量由 `Findwfassoc.cmake` 提供。 - ---- - -### 任务 2.3: 编写 Manifest 解析与校检模块 - -本模块负责从 TOML 文件加载 manifest、进行校检,然后构建 `wfassocpp::Schema` 并创建 `wfassocpp::Program`。 - -**涉及 API:** wfassoc++.h 中的 `Schema`, `Program` 类。 - -**校检逻辑:** 参照 `wfassoc-exec/src/manifest.rs` 的实现: -- `identifier`, `path`, `clsid` 是必填字段 -- `name`, `icon`, `behavior` 是可选字段 -- `[strs]`, `[icons]`, `[behaviors]`, `[exts]` 表格全部可选(可为空) -- 使用 Schema 的各种 setter 方法写入值,任何底层验证失败都会通过 `_Check` 抛出 `std::runtime_error` - ---- - -#### 任务 2.3.1: 编写 manifesto.hpp - -**产出文件:** `example/qwfassoc/src/manifesto.hpp` - -**头文件内容规范:** - -```cpp -#pragma once - -#include "wfassoc++.h" -#include -#include - -// 从 TOML manifest 文件构建 Program -// 内部完成 Schema 创建、字段填充、校检、Program 创建 -// 如果任何步骤失败,抛出 std::runtime_error(由 wfassoc++ 的 _Check 机制产生) -// 也可能抛出 toml11 的解析异常 -std::unique_ptr BuildProgramFromManifest(const std::string& tomlPath); -``` - -所有实现集中在 `manifesto.cpp` 中即可。此模块不对外暴露内部数据结构。 - ---- - -#### 任务 2.3.2: 编写 manifesto.cpp - -**产出文件:** `example/qwfassoc/src/manifesto.cpp` - -**实现流程:** - -1. 使用 `toml::parse(tomlPath)` 解析 TOML 文件。如果解析失败,toml11 会抛出异常(通常是 `toml::syntax_error`)。直接让异常向上传播。 - -2. 从解析结果中提取数据: - - 必填字段:`identifier`(字符串)、`path`(字符串)、`clsid`(字符串) - - 可选字段:`name`(字符串,可为空)、`icon`(字符串,可为空)、`behavior`(字符串,可为空) - - 表格:`strs`(键值对表)、`icons`(键值对表)、`behaviors`(键值对表)、`exts`(表,每个键对应一个子表) - - toml11 读取语法示例: - ```cpp - const auto& data = toml::parse(path); - std::string identifier = toml::find(data, "identifier"); - std::string path_val = toml::find(data, "path"); - std::string clsid = toml::find(data, "clsid"); - ``` - - 可选字段读取: - ```cpp - std::string name; - bool has_name = data.contains("name"); - if (has_name) { - name = toml::find(data, "name"); - } - ``` - (icon、behavior 同理) - - 表格读取: - ```cpp - const auto& strs = toml::find(data, "strs").as_table(); - // 遍历 strs: for (const auto& [key, value] : strs) { ... value.as_string() ... } - ``` - - exts 表格读取(每个值是嵌套表,包含 name、icon、behavior 字段): - ```cpp - const auto& exts = toml::find(data, "exts").as_table(); - for (const auto& [ext_key, ext_value] : exts) { - const auto& ext_table = ext_value.as_table(); - // 这实际上是 toml::value 的嵌套访问,toml11 中 as_table() 返回的是 std::unordered_map> - // 但更简单的是在解析后使用 toml::find(data, "exts") 然后再逐个读取 - } - ``` - - **toml11 访问注意事项:** 对于嵌套的 TOML 表(如 `[exts.jpg]`),toml11 中使用 `toml::find` 时路径可以用点号拼接,例如 `toml::find(data, "exts", "jpg", "name")` 或通过迭代 as_table 的结果来遍历。具体可以参考 toml11 的文档。 - -3. 创建 `wfassocpp::Schema` 对象并填充字段: - - ```cpp - wfassocpp::Schema schema; - - // 必填字段 - schema.SetIdentifier(identifier.c_str()); - schema.SetPath(path_val.c_str()); - schema.SetClsid(clsid.c_str()); - - // 可选字段:如果不存在则传 nullptr - schema.SetName(has_name ? name.c_str() : nullptr); - schema.SetIcon(has_icon ? icon.c_str() : nullptr); - schema.SetBehavior(has_behavior ? behavior.c_str() : nullptr); - - // 遍历并添加 strs - for (const auto& [key, value] : strs) { - std::string v = toml::find(value, ""); // value 本身是字符串 - // 实际上,遍历 as_table() 得到的 value 是 shared_ptr - // 需要用 value->as_string() 或者解引用后获取 - schema.AddStr(key.c_str(), /* value string */.c_str()); - } - - // 同理遍历 icons 和 behaviors - - // 遍历 exts: ext_key 是纯扩展名(不含点),ext_table 有 name, icon, behavior 三个字符串字段 - for (const auto& [ext_key, ext_table_value] : exts) { - // 从 ext_table_value 中读取 name, icon, behavior - schema.AddExt(ext_key.c_str(), ext_name.c_str(), ext_icon.c_str(), ext_behavior.c_str()); - } - ``` - -4. 用 Schema 创建 Program: - - ```cpp - return std::make_unique(std::make_unique(std::move(schema))); - ``` - - **注意:** Program 的构造函数接受 `std::unique_ptr&&`。Schema 会被消费(move),之后不能再使用。 - -**错误处理:** -- toml 解析错误 → 抛出 toml::syntax_error,上层捕获后弹出错误对话框 -- Schema 设置错误 → wfassoc++ 抛出 std::runtime_error,上层捕获 -- Program 创建错误 → wfassoc++ 抛出 std::runtime_error,上层捕获 - ---- - -### 任务 2.4: 编写 UI 文件 (mainwindow.ui) - -**产出文件:** `example/qwfassoc/src/mainwindow.ui` - -**使用 Qt Designer 或手动编写 XML 格式的 .ui 文件。** - -#### 层级结构概览 - -``` -QDialog (窗口, objectName="MainWindow") -├── windowTitle: "" (运行时动态设置) -├── minimumSize: 480x600, maximumSize: 480x600 -├── Layout: QVBoxLayout -│ └── QTabWidget (objectName="tabWidget") -│ ├── Tab 0: "应用程序" -│ │ └── QGroupBox (objectName="groupBoxApp", title="安装与卸载") -│ │ └── Layout: QVBoxLayout -│ │ ├── [上半部分] QHBoxLayout -│ │ │ ├── QLabel (objectName="lblAppIcon", 显示应用程序图标) -│ │ │ └── QLabel (objectName="lblAppDesc", wordWrap=true) -│ │ └── [下半部分] QHBoxLayout -│ │ ├── QPushButton (objectName="btnInstall", text="安装") -│ │ └── QPushButton (objectName="btnUninstall", text="卸载") -│ │ -│ └── Tab 1: "文件关联" -│ └── Layout: QVBoxLayout -│ ├── QLabel (objectName="lblAssocDesc", wordWrap=true) -│ ├── QHBoxLayout -│ │ ├── QPushButton (objectName="btnSelectAllUser", text="+") -│ │ └── QPushButton (objectName="btnSelectAllSystem", text="+") -│ ├── QTableWidget (objectName="tableAssoc", columnCount=3) -│ │ ├── 列 0 标题: "类型" -│ │ ├── 列 1 标题: "" (运行时设为当前用户名) -│ │ └── 列 2 标题: "所有用户" -│ └── QHBoxLayout (底部按钮栏) -│ ├── QPushButton (objectName="btnOk", text="确定", default=true) -│ ├── QPushButton (objectName="btnCancel", text="取消") -│ └── QPushButton (objectName="btnApply", text="应用") -``` - -#### 详细属性设置 - -**QDialog (MainWindow):** -- `windowTitle`: 留空(运行时设置) -- `minimumSize` / `maximumSize`: 宽度 480,高度 600 -- `sizePolicy`: Fixed(水平和垂直都设为 Fixed) - -**QTabWidget (tabWidget):** -- 添加两个标签页,标题分别为 `"应用程序"` 和 `"文件关联"`。 - -**QLabel (lblAppIcon):** -- 用于显示从 `Program::ResolveIcon()` 获取的图标。需要设置 `scaledContents` 为 false,通过代码将 HICON 转换为 QPixmap 并设置。 -- 建议 size: 48x48 左右(通过 `minimumSize` / `maximumSize` 固定)。 - -**QLabel (lblAppDesc):** -- `wordWrap` 设为 true。 - -**QPushButton (btnInstall / btnUninstall):** -- 初始都设为 enabled(代码中根据注册状态动态调整)。 - -**QLabel (lblAssocDesc):** -- `wordWrap` 设为 true。 - -**QTableWidget (tableAssoc):** -- `columnCount`: 3 -- 列 0 标题:`"类型"` -- 列 1 标题:`""`(运行时设为标准格式的用户名,如 "DESKTOP-XXX\username") -- 列 2 标题:`"所有用户"` -- `selectionBehavior`: `SelectItems` -- `editTriggers`: `NoEditTriggers`(禁止编辑) -- 水平表头设置:`stretchLastSection` 为 true(可选) -- 添加垂直滚动条(默认行为即可) - -**底部按钮 (btnOk, btnCancel, btnApply):** -- `btnOk`: `default` 属性设为 true(设为默认按钮) -- `btnApply`: 初始 disabled(无修改时不可用) - -#### UI 文件中的布局提示 - -- **"应用程序"选项卡**中的 QGroupBox 应该拉伸占满标签页空间。 -- QGroupBox 内部的布局使用 `QVBoxLayout`,上半部分(图标+文本描述)和下半部分(安装/卸载按钮)是其中的两个水平布局。 -- 图标和文本之间可以用 `addStretch()` 或设置间距来保证合适的视觉效果。 -- **"文件关联"选项卡**的最底部按钮栏(确定/取消/应用)应使用右对齐的布局。可以使用 `QHBoxLayout` 并在前面添加 `addStretch()` 来实现右对齐。 - -#### QTableWidget 列宽设置建议 - -在 .ui 文件中给列设置初始宽度: -- 列 0 ("类型"):约 120 像素 -- 列 1 (用户名):约 160 像素 -- 列 2 ("所有用户"):约 160 像素 -- (总共约 440 像素,接近对话框宽度 480 减去边距) - -可以关闭水平表头的 `stretchLastSection`,然后手动设置各列宽度;或开启 `stretchLastSection` 让最后一列自行填充。 - ---- - -### 任务 2.5: 编写 MainWindow 头文件 - -**产出文件:** `example/qwfassoc/src/mainwindow.hpp` - -```cpp -#pragma once - -#include "wfassoc++.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Ui { -class MainWindow; -} - -class MainWindow : public QDialog { - Q_OBJECT - -public: - explicit MainWindow( - std::unique_ptr program, - wfassoc::Scope scope, - QWidget* parent = nullptr - ); - ~MainWindow() override; - - // 初始化窗口:调用 WFStartup,检查程序状态,填充 UI - void Initialize(); - -private slots: - void onInstallClicked(); - void onUninstallClicked(); - void onAssocCellClicked(int row, int column); - void onSelectAllUserClicked(); - void onSelectAllSystemClicked(); - void onOkClicked(); - void onCancelClicked(); - void onApplyClicked(); - -private: - // 初始化"应用程序"选项卡 - void initAppTab(); - - // 刷新"应用程序"选项卡(根据注册状态启用/禁用按钮) - void refreshAppTab(); - - // 初始化"文件关联"选项卡(首次填充表格) - void initAssocTab(); - - // 刷新"文件关联"选项卡(重新查询所有扩展名状态并更新表格) - void refreshAssocTab(); - - // 更新单行的显示(根据当前状态和待处理状态) - void refreshAssocRow(int row); - - // 更新"应用"按钮的启用状态 - void updateApplyButtonState(); - - // 判断在当前待处理状态下,某个 scope 的某个 ext 是否为 linked - // 优先看 pending,再看实际状态 - bool isExtLinked(wfassoc::Scope scope, size_t extIndex) const; - - // 判断当前程序是否已安装(使用给定的 scope) - bool isProgramRegistered() const; - - // HICON 转 QPixmap 的工具函数 - static QPixmap hiconToPixmap(void* hicon, int width = 32, int height = 32); - - // UI 元素(通过 UIC 自动生成,使用成员指针) - std::unique_ptr ui_; - - // 核心数据 - std::unique_ptr program_; - wfassoc::Scope scope_; - - // 缓存的程序图标(用于在表格中表示"已关联到本程序"的状态) - // 在初始化时获取,通过 Program::ResolveIcon() -> IconRc::GetIcon() 得到 HICON - // 转换为 QPixmap 后缓存 - QPixmap programIcon_; - - // 缓存的程序名称(用于判断某个 ExtStatus 是否属于本程序) - std::string programName_; - - // 各个扩展名的内部信息 - struct ExtInfo { - std::string innerName; // 纯扩展名(不含点),如 "jpg" - std::string dottedInnerName; // 带点的扩展名,如 ".jpg" - }; - std::vector extInfos_; // 索引对应 WFProgramGetExt 的 index - - // 待处理的链接/取消链接操作 - // 键:extIndex - // 值:true=待链接(link), false=待取消链接(unlink) - // 如果某 extIndex 在此 map 中无记录,则无待处理操作 - std::map pendingUserOps_; // 用户视图的待处理操作 - std::map pendingSystemOps_; // 系统视图的待处理操作 -}; -``` - ---- - -### 任务 2.6: 编写 MainWindow 实现文件 (mainwindow.cpp) - -这是最复杂的部分,按功能区域逐步实现。 - ---- - -#### 2.6.1 构造函数 - -```cpp -MainWindow::MainWindow( - std::unique_ptr program, - wfassoc::Scope scope, - QWidget* parent) - : QDialog(parent) - , ui_(std::make_unique()) - , program_(std::move(program)) - , scope_(scope) -{ - ui_->setupUi(this); - - // 固定窗口大小 - setFixedSize(480, 600); - - // 初始化 wfassoc 库 - if (!wfassoc::WFStartup()) { - // WFStartup 失败(此时无法调用 WFGetLastError) - QMessageBox::critical(this, "错误", "无法初始化 wfassoc 库。"); - // 抛出异常或标记失败,由 main.cpp 处理 - throw std::runtime_error("WFStartup failed"); - } - - // 解析程序名称并设置窗口标题 - programName_ = program_->ResolveName(); - setWindowTitle(QString::fromStdString(programName_) + "选项"); - - // 解析程序图标并设置窗口图标 + 应用程序选项卡图标 + 表格占位图标 - auto iconRc = program_->ResolveIcon(); - if (iconRc) { - void* hicon = iconRc->GetIcon(); - QPixmap pixmap = hiconToPixmap(hicon, 48, 48); - if (!pixmap.isNull()) { - programIcon_ = hiconToPixmap(hicon, 32, 32); // 表格中用小图标 - } - setWindowIcon(QIcon(pixmap)); - } - - // 遍历扩展名列表,缓存 extInfo - size_t extCount = program_->ExtsLen(); - extInfos_.reserve(extCount); - for (size_t i = 0; i < extCount; ++i) { - auto ext = program_->GetExt(i); - extInfos_.push_back({ - ext->GetInner(), - ext->GetDottedInner() - }); - } - - // 连接信号与槽 - connect(ui_->btnInstall, &QPushButton::clicked, this, &MainWindow::onInstallClicked); - connect(ui_->btnUninstall, &QPushButton::clicked, this, &MainWindow::onUninstallClicked); - connect(ui_->tableAssoc, &QTableWidget::cellClicked, this, &MainWindow::onAssocCellClicked); - connect(ui_->btnSelectAllUser, &QPushButton::clicked, this, &MainWindow::onSelectAllUserClicked); - connect(ui_->btnSelectAllSystem, &QPushButton::clicked, this, &MainWindow::onSelectAllSystemClicked); - connect(ui_->btnOk, &QPushButton::clicked, this, &MainWindow::onOkClicked); - connect(ui_->btnCancel, &QPushButton::clicked, this, &MainWindow::onCancelClicked); - connect(ui_->btnApply, &QPushButton::clicked, this, &MainWindow::onApplyClicked); -} -``` - -**析构函数:** - -```cpp -MainWindow::~MainWindow() { - // program_ 先销毁(在其析构中调用 WFProgramDestroy) - // 然后调用 WFShutdown 清理库 - program_.reset(); - wfassoc::WFShutdown(); -} -``` - -**WFStartup/WFShutdown 注意事项:** -- `WFStartup` 在构造函数中调用。 -- `WFShutdown` 在析构函数中调用。 -- `program_` 必须在 `WFShutdown` 之前销毁,因为 Program 的析构函数会调用 `WFProgramDestroy`。 - ---- - -#### 2.6.2 HICON 到 QPixmap 的转换 - -```cpp -QPixmap MainWindow::hiconToPixmap(void* hicon, int width, int height) { - // 使用 Qt6 的 QtWin::fromHICON - // 需要 #include - // 注意:QtWin 在 Qt6 中属于 QtGui 模块,需要链接 Qt6::Gui(Widgets 模块已经包含) - QPixmap pixmap = QtWin::fromHICON(reinterpret_cast(hicon)); - if (!pixmap.isNull() && width > 0 && height > 0) { - pixmap = pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - return pixmap; -} -``` - -**注意:** `QtWin::fromHICON` 需要 `#include ` 头文件。此函数复制了 HICON 的数据,返回的 QPixmap 是独立的,不依赖原 HICON 的生命周期。 - ---- - -#### 2.6.3 Initialize() 方法 - -```cpp -void MainWindow::Initialize() { - initAppTab(); - initAssocTab(); - refreshAppTab(); -} -``` - -按顺序初始化两个选项卡,然后根据注册状态刷新按钮可用性。 - ---- - -#### 2.6.4 "应用程序"选项卡实现 - -##### initAppTab() - -设置图标 Label 和文本 Label 的内容: -```cpp -void MainWindow::initAppTab() { - // 设置图标 - auto iconRc = program_->ResolveIcon(); - if (iconRc) { - void* hicon = iconRc->GetIcon(); - QPixmap pixmap = hiconToPixmap(hicon, 48, 48); - if (!pixmap.isNull()) { - ui_->lblAppIcon->setPixmap(pixmap); - // 可能还需要设置 scaledContents 或 fixed size - } - } - - // 设置描述文本 - QString desc = QString::fromStdString("在此安装或卸载" + programName_); - ui_->lblAppDesc->setText(desc); -} -``` - -##### refreshAppTab() - -```cpp -void MainWindow::refreshAppTab() { - bool registered = isProgramRegistered(); - ui_->btnInstall->setEnabled(!registered); - ui_->btnUninstall->setEnabled(registered); - - // 同时启用/禁用"文件关联"选项卡的整体内容 - // 可使用 QTabWidget 的 setTabEnabled 或直接操作内部控件的 setEnabled - QWidget* assocTab = ui_->tabWidget->widget(1); // 索引 1 是"文件关联"选项卡 - assocTab->setEnabled(registered); -} -``` - -##### isProgramRegistered() - -```cpp -bool MainWindow::isProgramRegistered() const { - try { - return program_->IsRegistered(scope_); - } catch (const std::runtime_error&) { - // 查询失败时视为未注册 - return false; - } -} -``` - -##### onInstallClicked() - -```cpp -void MainWindow::onInstallClicked() { - try { - program_->Register(scope_); - QMessageBox::information(this, "成功", "应用程序安装成功!"); - refreshAppTab(); - } catch (const std::runtime_error& e) { - QMessageBox::critical(this, "错误", - QString::fromStdString("安装失败:" + std::string(e.what()))); - } -} -``` - -##### onUninstallClicked() - -```cpp -void MainWindow::onUninstallClicked() { - try { - program_->Unregister(scope_); - QMessageBox::information(this, "成功", "应用程序卸载成功!"); - refreshAppTab(); - } catch (const std::runtime_error& e) { - QMessageBox::critical(this, "错误", - QString::fromStdString("卸载失败:" + std::string(e.what()))); - } -} -``` - ---- - -#### 2.6.5 "文件关联"选项卡实现 - -这是最复杂的部分。 - -##### initAssocTab() - -```cpp -void MainWindow::initAssocTab() { - // 设置描述标签 - QString desc = QString::fromStdString("使用 " + programName_ + " 关联的文件类型:"); - ui_->lblAssocDesc->setText(desc); - - // 设置表格列标题 - // 列 1 标题设为当前用户名(标准格式,如 "DESKTOP-XXXX\username") - QString userName = QString::fromLocal8Bit(qgetenv("USERNAME")); - // 更完整的方式是使用 Windows API GetUserNameEx 或类似方法, - // 但简化的用户名也足够 - ui_->tableAssoc->setHorizontalHeaderItem(1, - new QTableWidgetItem(userName)); - // 列 0 标题 "类型" 和列 2 标题 "所有用户" 已在 UI 文件中设置 - - // 为表格添加行 - size_t extCount = program_->ExtsLen(); - ui_->tableAssoc->setRowCount(static_cast(extCount)); - - // 设置列宽 - ui_->tableAssoc->setColumnWidth(0, 120); - ui_->tableAssoc->setColumnWidth(1, 160); - ui_->tableAssoc->setColumnWidth(2, 160); - - // 禁止编辑 - ui_->tableAssoc->setEditTriggers(QAbstractItemView::NoEditTriggers); - // 设置选择行为:选中单元格 - ui_->tableAssoc->setSelectionBehavior(QAbstractItemView::SelectItems); - - // 初始刷新 - refreshAssocTab(); - - // 设置"系统"列的可用性(如果 scope 是 User,则系统列不可点击) - if (scope_ == wfassoc::Scope::User) { - // 禁用第 2 列的交互(仍可显示,但不能点击) - for (int row = 0; row < static_cast(extCount); ++row) { - QTableWidgetItem* item = ui_->tableAssoc->item(row, 2); - if (!item) { - item = new QTableWidgetItem(); - ui_->tableAssoc->setItem(row, 2, item); - } - item->setFlags(item->flags() & ~Qt::ItemIsEnabled); - } - } -} -``` - -##### refreshAssocTab() - -完全重新查询所有扩展名的状态,覆盖式刷新整个表格: - -```cpp -void MainWindow::refreshAssocTab() { - size_t extCount = program_->ExtsLen(); - for (size_t i = 0; i < extCount; ++i) { - refreshAssocRow(static_cast(i)); - } - updateApplyButtonState(); -} -``` - -##### refreshAssocRow(int row) - -单行刷新逻辑: - -```cpp -void MainWindow::refreshAssocRow(int row) { - size_t extIndex = static_cast(row); - - // --- 列 0:类型(扩展名 + 图标) --- - // 先查询 Hybrid 视图获取图标 - // 然后根据用户/系统视图是否关联来决定最终显示的图标 - auto hybridStatus = program_->QueryExt(wfassoc::View::Hybrid, extIndex); - HICON hybridIcon = nullptr; - if (hybridStatus) { - hybridIcon = hybridStatus->GetIcon(); - } - - // 判断当前(包含 pending)的用户和系统是否链接到本程序 - bool userLinked = isExtLinked(wfassoc::Scope::User, extIndex); - bool systemLinked = isExtLinked(wfassoc::Scope::System, extIndex); - - QTableWidgetItem* typeItem = ui_->tableAssoc->item(row, 0); - if (!typeItem) { - typeItem = new QTableWidgetItem(); - ui_->tableAssoc->setItem(row, 0, typeItem); - } - - // 设置文本:扩展名 - typeItem->setText(QString::fromStdString(extInfos_[extIndex].dottedInnerName)); - - // 设置图标: - // - 如果 user 或 system 任一 pending/actual 是 linked → 显示本程序图标 - // - 否则如果 hybrid 有图标 → 显示 hybrid 图标 - // - 否则空白 - if (userLinked || systemLinked) { - if (!programIcon_.isNull()) { - typeItem->setIcon(QIcon(programIcon_)); - } - } else if (hybridIcon != nullptr) { - QPixmap pixmap = hiconToPixmap(hybridIcon); - if (!pixmap.isNull()) { - typeItem->setIcon(QIcon(pixmap)); - } else { - typeItem->setIcon(QIcon()); - } - } else { - typeItem->setIcon(QIcon()); - } - typeItem->setFlags(typeItem->flags() & ~Qt::ItemIsEditable); - - // --- 列 1:用户视图状态 --- - auto userStatus = program_->QueryExt(wfassoc::View::User, extIndex); - std::string userName; - if (userStatus) { - userName = userStatus->GetName(); - } - QTableWidgetItem* userItem = ui_->tableAssoc->item(row, 1); - if (!userItem) { - userItem = new QTableWidgetItem(); - ui_->tableAssoc->setItem(row, 1, userItem); - } - // 如果有 pending 操作,则 pending 优先显示 - auto pendingUserIt = pendingUserOps_.find(extIndex); - if (pendingUserIt != pendingUserOps_.end()) { - // pending link → 显示本程序名 - // pending unlink → 显示空白 - userItem->setText(pendingUserIt->second - ? QString::fromStdString(programName_) - : QString()); - } else { - // 无 pending:如果状态名等于本程序名,说明关联到了本程序 - // 否则显示实际关联的程序名(或空白) - if (!userName.empty() && userName == programName_) { - userItem->setText(QString::fromStdString(programName_)); - } else { - userItem->setText(QString::fromStdString(userName)); - } - } - userItem->setFlags((userItem->flags() | Qt::ItemIsEnabled) & ~Qt::ItemIsEditable); - - // --- 列 2:系统视图状态 --- - auto systemStatus = program_->QueryExt(wfassoc::View::System, extIndex); - std::string systemName; - if (systemStatus) { - systemName = systemStatus->GetName(); - } - QTableWidgetItem* systemItem = ui_->tableAssoc->item(row, 2); - if (!systemItem) { - systemItem = new QTableWidgetItem(); - ui_->tableAssoc->setItem(row, 2, systemItem); - } - auto pendingSysIt = pendingSystemOps_.find(extIndex); - if (pendingSysIt != pendingSystemOps_.end()) { - systemItem->setText(pendingSysIt->second - ? QString::fromStdString(programName_) - : QString()); - } else { - if (!systemName.empty() && systemName == programName_) { - systemItem->setText(QString::fromStdString(programName_)); - } else { - systemItem->setText(QString::fromStdString(systemName)); - } - } - // 如果是 User scope 模式,系统列不可交互 - if (scope_ == wfassoc::Scope::User) { - systemItem->setFlags((systemItem->flags() & ~Qt::ItemIsEnabled) & ~Qt::ItemIsEditable); - } else { - systemItem->setFlags((systemItem->flags() | Qt::ItemIsEnabled) & ~Qt::ItemIsEditable); - } -} -``` - -##### isExtLinked(size_t scope, size_t extIndex) - -```cpp -bool MainWindow::isExtLinked(wfassoc::Scope scope, size_t extIndex) const { - // 先检查 pending 状态 - auto& pendingMap = (scope == wfassoc::Scope::User) ? pendingUserOps_ : pendingSystemOps_; - auto it = pendingMap.find(extIndex); - if (it != pendingMap.end()) { - return it->second; // pending link → true, pending unlink → false - } - - // 否则查询实际状态 - auto view = (scope == wfassoc::Scope::User) ? wfassoc::View::User : wfassoc::View::System; - auto status = program_->QueryExt(view, extIndex); - if (status) { - std::string name = status->GetName(); - return !name.empty() && name == programName_; - } - return false; -} -``` - -##### onAssocCellClicked(int row, int column) - -```cpp -void MainWindow::onAssocCellClicked(int row, int column) { - // 只处理列 1(用户视图)和列 2(系统视图) - if (column != 1 && column != 2) { - return; - } - // 如果 scope 是 User 且点击的是系统列,忽略 - if (column == 2 && scope_ == wfassoc::Scope::User) { - return; - } - - wfassoc::Scope targetScope = (column == 1) ? wfassoc::Scope::User : wfassoc::Scope::System; - size_t extIndex = static_cast(row); - - bool currentlyLinked = isExtLinked(targetScope, extIndex); - auto& pendingMap = (targetScope == wfassoc::Scope::User) ? pendingUserOps_ : pendingSystemOps_; - - if (currentlyLinked) { - // 当前已链接 → 待取消链接 - pendingMap[extIndex] = false; // false = pending unlink - } else { - // 当前未链接 → 待链接 - pendingMap[extIndex] = true; // true = pending link - } - - // 刷新该行 - refreshAssocRow(row); - updateApplyButtonState(); -} -``` - -##### onSelectAllUserClicked() - -全选按钮操作("+"按钮,作用域 = User): - -```cpp -void MainWindow::onSelectAllUserClicked() { - onSelectAllWithScope(wfassoc::Scope::User); -} - -void MainWindow::onSelectAllSystemClicked() { - // 如果 scope 是 User,此按钮不应起作用 - if (scope_ == wfassoc::Scope::User) { - return; - } - onSelectAllWithScope(wfassoc::Scope::System); -} - -// 内部全选逻辑 -void MainWindow::onSelectAllWithScope(wfassoc::Scope targetScope) { - auto& pendingMap = (targetScope == wfassoc::Scope::User) ? pendingUserOps_ : pendingSystemOps_; - - // 第一阶段:检查是否还有未关联的项(显示为空白的行) - bool allLinked = true; - size_t extCount = program_->ExtsLen(); - for (size_t i = 0; i < extCount; ++i) { - if (!isExtLinked(targetScope, i)) { - allLinked = false; - break; - } - } - - if (!allLinked) { - // 有未关联的项 → 将所有未关联的设置为 link - for (size_t i = 0; i < extCount; ++i) { - if (!isExtLinked(targetScope, i)) { - pendingMap[i] = true; - } - } - } else { - // 所有项都已关联 → 将所有项全部设为 link(第二次点击) - for (size_t i = 0; i < extCount; ++i) { - pendingMap[i] = true; - } - } - - // 刷新整个表格 - refreshAssocTab(); -} -``` - -**注意:** 需要在 `mainwindow.hpp` 中补充声明 `void onSelectAllWithScope(wfassoc::Scope targetScope);`。 - -##### onOkClicked() - -```cpp -void MainWindow::onOkClicked() { - // 应用所有待处理的修改 - applyPendingChanges(); - accept(); // 关闭对话框(QDialog::accept()) -} -``` - -##### onCancelClicked() - -```cpp -void MainWindow::onCancelClicked() { - // 丢弃所有待处理修改,直接关闭 - reject(); // QDialog::reject() -} -``` - -##### onApplyClicked() - -```cpp -void MainWindow::onApplyClicked() { - applyPendingChanges(); - // 刷新视图(清除 pending 状态,重新查询) - pendingUserOps_.clear(); - pendingSystemOps_.clear(); - refreshAssocTab(); -} -``` - -##### applyPendingChanges() - -实际执行注册表修改的私有方法: - -```cpp -void MainWindow::applyPendingChanges() { - // 应用用户视图的待处理操作 - for (const auto& [extIndex, link] : pendingUserOps_) { - try { - if (link) { - program_->LinkExt(wfassoc::Scope::User, extIndex); - } else { - program_->UnlinkExt(wfassoc::Scope::User, extIndex); - } - } catch (const std::runtime_error& e) { - QMessageBox::critical(this, "错误", - QString::fromStdString("操作文件扩展名 " + extInfos_[extIndex].dottedInnerName + " 失败:" + std::string(e.what()))); - } - } - - // 应用系统视图的待处理操作 - for (const auto& [extIndex, link] : pendingSystemOps_) { - try { - if (link) { - program_->LinkExt(wfassoc::Scope::System, extIndex); - } else { - program_->UnlinkExt(wfassoc::Scope::System, extIndex); - } - } catch (const std::runtime_error& e) { - QMessageBox::critical(this, "错误", - QString::fromStdString("操作文件扩展名 " + extInfos_[extIndex].dottedInnerName + " 失败:" + std::string(e.what()))); - } - } -} -``` - -##### updateApplyButtonState() - -```cpp -void MainWindow::updateApplyButtonState() { - bool hasPending = !pendingUserOps_.empty() || !pendingSystemOps_.empty(); - ui_->btnApply->setEnabled(hasPending); -} -``` - ---- - -### 任务 2.7: 编写程序入口 main.cpp - -**产出文件:** `example/qwfassoc/src/main.cpp` - -```cpp -#include -#include -#include -#include "mainwindow.hpp" -#include "manifesto.hpp" -#include "wfassoc++.h" - -int main(int argc, char* argv[]) { - // --- 初始化 Qt --- - // 设置 Application 属性(一些 Qt 平台需要,特别是 Windows) - QApplication app(argc, argv); - QApplication::setOrganizationName("wfassoc"); - QApplication::setApplicationName("qwfassoc"); - // 不设置默认窗口图标,将由程序动态获取 - - // --- 解析命令行 --- - QCommandLineParser parser; - parser.setApplicationDescription("wfassoc 可视化配置工具"); - parser.addHelpOption(); - - // -c / --manifest:指定 TOML manifest 文件路径 - QCommandLineOption manifestOption( - QStringList() << "c" << "manifest", - "指定要配置的应用程序的清单文件路径(TOML 格式)", - "manifest_path" - ); - parser.addOption(manifestOption); - - // -f / --for:指定作用的范围(user / system) - QCommandLineOption forOption( - QStringList() << "f" << "for", - "指定应用程序注册的范围:user(当前用户)或 system(所有用户)", - "scope" - ); - parser.addOption(forOption); - - parser.process(app); - - // 检查必填选项 - if (!parser.isSet(manifestOption)) { - QMessageBox::critical(nullptr, "错误", "缺少命令行选项 -c/--manifest。\n请指定 manifest 文件路径。"); - return 1; - } - if (!parser.isSet(forOption)) { - QMessageBox::critical(nullptr, "错误", "缺少命令行选项 -f/--for。\n请指定作用范围:user 或 system。"); - return 1; - } - - // 解析 scope - QString scopeStr = parser.value(forOption).trimmed().toLower(); - wfassoc::Scope scope; - if (scopeStr == "user") { - scope = wfassoc::Scope::User; - } else if (scopeStr == "system") { - scope = wfassoc::Scope::System; - } else { - QMessageBox::critical(nullptr, "错误", - QString("无效的 --for 参数值 '%1'。\n有效值:user 或 system。").arg(scopeStr)); - return 1; - } - - // --- 加载 manifest 并构建 Program --- - std::unique_ptr program; - try { - std::string manifestPath = parser.value(manifestOption).toStdString(); - program = BuildProgramFromManifest(manifestPath); - } catch (const std::exception& e) { - QMessageBox::critical(nullptr, "错误", - QString::fromStdString("无法加载 manifest 文件:\n" + std::string(e.what()))); - return 1; - } - - // --- 创建并运行主窗口 --- - try { - MainWindow window(std::move(program), scope); - window.Initialize(); - window.exec(); // 模态显示对话框 - } catch (const std::exception& e) { - QMessageBox::critical(nullptr, "错误", - QString::fromStdString("程序初始化失败:\n" + std::string(e.what()))); - return 1; - } - - return 0; -} -``` - -**注意事项:** -- QCommandLineParser 是 Qt 内置的命令行解析器(`#include `)。 -- 所有错误条件(缺少参数、无效参数、manifest 解析失败、Program 创建失败等)都通过 QMessageBox::critical 弹出错误对话框,然后 `return 1` 退出程序。 -- MainWindow 如果初始化失败,也会显示错误对话框并退出。 -- MainWindow::exec() 进入 Qt 事件循环,直到对话框关闭。 - ---- - -## 3. 数据流与状态模型总结 - -### 3.1 程序启动流程 - -``` -main.cpp 解析命令行 - ↓ -BuildProgramFromManifest(tomlPath) - ↓ 读取 TOML → 创建 Schema → 创建 Program - ↓ -创建 MainWindow(Program, Scope) - ↓ -MainWindow::Initialize() - ├── WFStartup() // 初始化 wfassoc 库 - ├── Program::ResolveName() // 获取程序名称 → 设置窗口标题 - ├── Program::ResolveIcon() // 获取程序图标 → 设置窗口图标 + 缓存 - ├── Program::ExtsLen() + GetExt() // 遍历扩展名列表 → 缓存 extInfos_ - ├── initAppTab() // 设置"应用程序"选项卡内容 - │ └── 调用 RefreshAppTab() // 根据 isRegistered 设置按钮状态 - └── initAssocTab() // 设置"文件关联"选项卡内容 - └── refreshAssocTab() // 遍历所有扩展名 → QueryExt → 填充表格 -``` - -### 3.2 安装/卸载操作流程 - -``` -用户点击"安装" / "卸载" - ↓ -Program::Register(scope) / Program::Unregister(scope) - ↓ 成功 -QMessageBox 显示成功 → refreshAppTab() - ↓ 失败 -QMessageBox 显示错误(含 WFGetLastError 的信息) -``` - -### 3.3 文件关联操作流程 - -``` -用户点击表格单元格(列 1 或列 2) - ↓ -判断当前状态(pending优先,否则实际状态) - ↓ -如果当前已关联 → 设置 pending = unlink -如果当前未关联 → 设置 pending = link - ↓ -refreshAssocRow(row) → 更新显示 -updateApplyButtonState() → 启用/禁用"应用"按钮 -``` - -### 3.4 应用/确定操作流程 - -``` -用户点击"应用"或"确定" - ↓ -遍历 pendingUserOps_ → 逐个调用 LinkExt/UnlinkExt -遍历 pendingSystemOps_ → 逐个调用 LinkExt/UnlinkExt - ↓ -清空 pending maps - ↓ -如果是"确定" → accept() 关闭对话框 -如果是"应用" → refreshAssocTab() 重新查询显示 -``` - -### 3.5 状态机说明 - -表格中每个单元格(行=扩展名,列=作用域)有三种可能的状态: - -| 实际注册表状态 | Pending 状态 | 显示内容 | 图标 | -|---|---|---|---| -| 关联到本程序 | 无 pending | 本程序名 | 本程序图标 | -| 关联到本程序 | pending unlink | 空白 | 空白(如果没有另一个scope已关联) | -| 未关联/关联到其他 | 无 pending | 空白/其他程序名 | Hybrid 图标或空白 | -| 未关联/关联到其他 | pending link | 本程序名 | 本程序图标 | - ---- - -## 4. 额外注意事项 - -### 4.1 用户名获取 - -为获取列 1 的表头用户名,可以使用以下方式(按推荐顺序): - -1. 通过 `GetUserNameExW` Windows API 获取完整用户名(格式:`DOMAIN\username` 或 `COMPUTERNAME\username`) -2. 通过环境变量 `USERNAME` 获取简单用户名(仅 `username`) - -推荐使用方法 1,因为与 Windows 资源管理器中显示的格式一致。实现代码示例如下: - -```cpp -#include -#include - -QString getFullUserName() { - WCHAR buf[256]; - ULONG size = 256; - if (GetUserNameExW(NameSamCompatible, buf, &size)) { - return QString::fromWCharArray(buf); - } - return QString::fromLocal8Bit(qgetenv("USERNAME")); -} -``` - -使用 `NameSamCompatible` 格式可得到如 `DESKTOP-XXXX\username` 的完整用户名。 - -### 4.2 图标生命周期 - -`IconRc` 对象的 `GetIcon()` 返回的 HICON 指针仅在 `IconRc` 对象存活期间有效。必须先通过 `QtWin::fromHICON` 转换为 `QPixmap`(该函数会复制图标数据),然后缓存 `QPixmap` 而非 HICON 句柄。 - -`ExtStatus` 的 `GetIcon()` 同理,仅在 `ExtStatus` 对象存活期间有效。在 `refreshAssocRow` 中使用完后就销毁了。 - -### 4.3 Tab 页的索引 - -在 `.ui` 文件中,"应用程序"选项卡是索引 0,"文件关联"选项卡是索引 1。代码中通过以下方式获取: - -```cpp -QWidget* assocTab = ui_->tabWidget->widget(1); -// 或者直接在 UI 文件中设置 objectName,如 tabApp, tabAssoc -``` - -### 4.4 UI 安装选项卡中 QPushButton 的布局约束 - -安装和卸载按钮是 QHBoxLayout 中仅有的两个元素。如果希望按钮居左排列,可使用默认对齐方式。如果希望按钮之间有间距,可添加 Stretch。 - -### 4.5 错误处理策略 - -- 命令行参数错误 → QMessageBox::critical → return 1 -- Manifest 加载/解析/校检错误 → QMessageBox::critical → return 1 -- WFStartup 失败 → QMessageBox::critical → 无法继续 -- 操作过程中的错误(安装/卸载/链接/取消链接失败)→ QMessageBox::critical → 留在当前页面,不退出程序 - -### 4.6 WFStartup / WFShutdown 调用时机 - -根据 `wfassoc.h` 的文档: -- `WFStartup` 必须在 `MainWindow` 构造函数中调用(在创建 Program 之后,用户开始交互之前)。 -- `WFShutdown` 在 `MainWindow` 析构函数中调用。 -- **关键:** `program_` 的析构必须在 `WFShutdown()` 之前,因为 Program 的析构会调用 `WFProgramDestroy`。因此析构函数的实现应为: - -```cpp -MainWindow::~MainWindow() { - program_.reset(); // 先销毁 Program(调用 WFProgramDestroy) - wfassoc::WFShutdown(); // 再调用 WFShutdown 清理库 -} -``` - -### 4.7 字符串编码 - -- wfassoc 所有 API 使用 UTF-8 编码的字符串。 -- Qt 默认在 QString 中使用 UTF-16。 -- 使用 `QString::fromStdString(std::string)` 将 std::string(UTF-8)转换为 QString。 -- 使用 `std::string s = qStr.toStdString()` 将 QString 转换为 std::string(UTF-8)。 -- TOML 文件中的字符串也是 UTF-8 编码。 - -### 4.8 禁用的列处理 - -- 当 scope 为 User 时,表格的第 2 列("所有用户")所有单元格的 `Qt::ItemIsEnabled` flag 被清除。 -- 同时 `onAssocCellClicked` 中检查如果 `column == 2 && scope_ == User` 则直接 return。 -- "全选"按钮中 "+" 按钮的行为了两个相同的——一个是针对 user 的,一个是针对 system 的。当 scope 为 User 时,system 的全选按钮也需要判断禁用(不执行任何操作)。 - ---- - -## 5. 测试指导 - -实施完成后,建议通过以下场景测试: - -1. **命令行参数测试**: - - 缺少 -c 参数 → 应弹出错误对话框后退出 - - 缺少 -f 参数 → 应弹出错误对话框后退出 - - -f 参数无效 → 应弹出错误对话框后退出 - - manifest 文件不存在 → 应弹出错误对话框后退出 - - manifest 文件格式有误 → 应弹出错误对话框后退出 - -2. **"应用程序"选项卡测试**: - - 程序未安装时:安装按钮可点击,卸载按钮禁用,文件关联选项卡全部禁用 - - 点击安装 → 成功后按钮状态颠倒 - - 点击卸载 → 成功后按钮状态颠倒 - - 操作失败 → 弹出错误消息,保持当前状态 - -3. **"文件关联"选项卡测试**: - - 程序未安装时:整个选项卡内容禁用 - - 程序已安装时:可以正常交互 - - 点击用户视图单元格 → 切换 pending 状态,图标更新 - - 点击系统视图单元格 → 切换 pending 状态,图标更新 - - User 模式下系统列单元格不可点击 - - 点击"+"全选按钮 → 正确批量设置 - - 有 pending 时"应用"按钮可点击,无 pending 时禁用 - - 点击"应用" → 待处理操作执行,表格刷新 - - 点击"确定" → 待处理操作执行,对话框关闭 - - 点击"取消" → 待处理操作丢弃,对话框关闭 diff --git a/example/qwfassoc/cmake/README.md b/example/qwfassoc/cmake/README.md index 057f2ad..61ba811 100644 --- a/example/qwfassoc/cmake/README.md +++ b/example/qwfassoc/cmake/README.md @@ -1,7 +1,19 @@ -# CMake Module Notes +# qwfassoc/cmake -The `Findwfassoc.cmake` file in this directory was copied from the project root at -`wfassoc-cdylib/codegen/Findwfassoc.cmake`. +This directory holds CMake helper modules used by the `qwfassoc` project. -It provides the `wfassoc::wfassoc` imported target. Before using it, the `wfassoc_ROOT` -variable must be set to point to the wfassoc installation directory. +## `Findwfassoc.cmake` + +This file is a verbatim copy of the upstream `Findwfassoc.cmake` shipped with +the wfassoc C dynamic library, located at: + +``` +wfassoc-cdylib/cbindgen/Findwfassoc.cmake +``` + +The copy is committed here so that `qwfassoc` can locate the wfassoc library +through a standard `find_package(wfassoc)` call without depending on the source +tree layout at configure time. + +To keep this copy in sync with the upstream version, re-run the copy command +shown above whenever `wfassoc-cdylib/cbindgen/Findwfassoc.cmake` is updated. diff --git a/example/qwfassoc/i18n/qwfassoc_zh_CN.ts b/example/qwfassoc/i18n/qwfassoc_zh_CN.ts new file mode 100644 index 0000000..e5ca8aa --- /dev/null +++ b/example/qwfassoc/i18n/qwfassoc_zh_CN.ts @@ -0,0 +1,4 @@ + + + + diff --git a/example/qwfassoc/src/icon_utils.cpp b/example/qwfassoc/src/icon_utils.cpp new file mode 100644 index 0000000..2af7de6 --- /dev/null +++ b/example/qwfassoc/src/icon_utils.cpp @@ -0,0 +1,23 @@ +#include "icon_utils.h" + +#include + +// QImage::fromHICON is only available on Windows and requires the real Win32 +// HICON typedef to be visible. The qt_windows.h wrapper (shipped with Qt 6 on +// Windows) takes care of including in the order Qt expects, so it +// is included after all Qt headers. +#include + +namespace qwfassoc { +namespace icon_utils { + +QPixmap fromHicon(wfassocpp::HICON handle) { + if (handle == wfassocpp::INVALID_HICON) { + return QPixmap(); + } + QImage image = QImage::fromHICON(static_cast(handle)); + return QPixmap::fromImage(std::move(image)); +} + +} // namespace icon_utils +} // namespace qwfassoc diff --git a/example/qwfassoc/src/icon_utils.h b/example/qwfassoc/src/icon_utils.h new file mode 100644 index 0000000..b5bab11 --- /dev/null +++ b/example/qwfassoc/src/icon_utils.h @@ -0,0 +1,24 @@ +#pragma once +#ifndef QWFASSOC_ICON_UTILS_H_ +#define QWFASSOC_ICON_UTILS_H_ + +#include + +#include + +namespace qwfassoc { +namespace icon_utils { + +// Convert a wfassocpp::HICON handle (the C++ wrapper around wfassoc's opaque +// icon handle) into a QPixmap suitable for use in Qt widgets. +// +// The conversion goes through QImage::fromHICON() (available since Qt 6.0 on +// Windows) and then QPixmap::fromImage() using its rvalue-reference overload +// so that no extra pixel buffer copy is performed. The returned pixmap is null +// if the input handle is null. +QPixmap fromHicon(wfassocpp::HICON handle); + +} // namespace icon_utils +} // namespace qwfassoc + +#endif // QWFASSOC_ICON_UTILS_H_ diff --git a/example/qwfassoc/src/main.cpp b/example/qwfassoc/src/main.cpp new file mode 100644 index 0000000..f47ba83 --- /dev/null +++ b/example/qwfassoc/src/main.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "main_window.h" +#include "manifest.h" + +namespace { + +// Context used for translatable strings that live outside of any QObject. +// Keeping it stable lets translators find these messages under the same key +// across builds. +constexpr const char* kTranslationContext = "qwfassoc"; + +// 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) { + // The application name is treated as a brand identifier and is not + // translated; the message text itself is already translated by callers. + 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\"."); +} + +// Load the translation matching the user's preferred UI language, if any. +// Qt's qt6_add_translations() compiles .ts files into .qm files and embeds +// them under the ":/i18n" resource prefix. +void installTranslators(QApplication& app) { + QTranslator* translator = new QTranslator(&app); + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString& locale : uiLanguages) { + const QString baseName = + QStringLiteral("qwfassoc_") + QLocale(locale).name(); + if (translator->load(QStringLiteral(":/i18n/") + baseName)) { + app.installTranslator(translator); + return; + } + } +} + +} // namespace + +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + QApplication::setApplicationName(QStringLiteral("qwfassoc")); + + // Install available translations before any translatable string is + // resolved (including the application display name below) 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 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::Manifest::fromFile(manifestPath.toStdString()); + wfassocpp::Schema schema = manifest.toSchema(); + 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; +} diff --git a/example/qwfassoc/src/main_window.cpp b/example/qwfassoc/src/main_window.cpp new file mode 100644 index 0000000..2b71201 --- /dev/null +++ b/example/qwfassoc/src/main_window.cpp @@ -0,0 +1,486 @@ +#include "main_window.h" +#include "ui_main_window.h" + +#include +#include +#include +#include +#include + +// icon_utils.h pulls in , so it is included after all Qt +// headers to keep the Windows include ordering that Qt expects. +#include "icon_utils.h" + +#include + +namespace qwfassoc { + +// region: Helpers + +namespace { + +// Convert our internal TargetScope enum to the wfassocpp::Scope value used by +// the program APIs (register / unregister / link / unlink / is_registered). +wfassocpp::Scope toWfassocScope(TargetScope scope) { + return scope == TargetScope::User ? wfassocpp::Scope::User + : wfassocpp::Scope::System; +} + +// Decide which CellState a queried ExtStatus corresponds to, given the +// resolved "self" name for this extension. We treat the cell as Self when the +// resolved display name matches our own; otherwise it is treated as Other. +CellState classifyCell(const QString& selfName, const QString& observedName) { + return observedName == selfName ? CellState::Self : CellState::Other; +} + +// Effective icon to draw for a cell, based on its state. +QPixmap effectiveCellIcon(const ExtRow& row, const CellData& cell) { + switch (cell.state) { + case CellState::Blank: + return QPixmap(); + case CellState::Self: + return row.selfIcon; + case CellState::Other: + return cell.icon; + } + return QPixmap(); +} + +} // namespace + +// endregion + +// region: Construction + +MainWindow::MainWindow(wfassocpp::Program program, + TargetScope scope, + QWidget* parent) + : QDialog(parent), + ui_(new Ui::MainWindow), + program_(std::move(program)), + scope_(scope) { + ui_->setupUi(this); + + // Resolve program metadata that several labels depend on. These calls may + // throw std::runtime_error on failure; the caller (main.cpp) is expected + // to handle that and present an error dialog before exiting. + programName_ = QString::fromUtf8(program_.ResolveName()); + + { + auto iconRc = program_.ResolveIcon(); + auto handle = iconRc.GetIcon(); + programIcon_ = icon_utils::fromHicon(handle); + } + + // Fetch the current user name for the second column header. The USERNAME + // environment variable is good enough on Windows; fall back to a static + // translatable placeholder if it is unset for any reason. + userName_ = QProcessEnvironment::systemEnvironment().value( + QStringLiteral("USERNAME"), tr("User")); + + // Give the table as much vertical room as possible inside its layout. + ui_->associationsLayout->setStretch(2, 1); + + // Reasonable default column widths so dotted extensions and ProgId names + // stay readable in the 480px dialog. + ui_->assocTable->setColumnWidth(0, 90); + ui_->assocTable->setColumnWidth(1, 175); + ui_->assocTable->setColumnWidth(2, 175); + ui_->assocTable->verticalHeader()->setVisible(false); + ui_->assocTable->setShowGrid(true); + + // Wire signals. + connect(ui_->installButton, &QPushButton::clicked, this, + &MainWindow::onInstallClicked); + connect(ui_->uninstallButton, &QPushButton::clicked, this, + &MainWindow::onUninstallClicked); + connect(ui_->selectUserButton, &QPushButton::clicked, this, + &MainWindow::onSelectUserClicked); + connect(ui_->selectSystemButton, &QPushButton::clicked, this, + &MainWindow::onSelectSystemClicked); + connect(ui_->assocTable, &QTableWidget::cellClicked, this, + &MainWindow::onCellClicked); + connect(ui_->okButton, &QPushButton::clicked, this, + &MainWindow::onOkClicked); + connect(ui_->cancelButton, &QPushButton::clicked, this, + &MainWindow::onCancelClicked); + connect(ui_->applyButton, &QPushButton::clicked, this, + &MainWindow::onApplyClicked); + + retranslateUi(); + refreshProgramState(); +} + +MainWindow::~MainWindow() = default; + +// endregion + +// region: UI Text + +void MainWindow::retranslateUi() { + // The window title embeds the program name, so it is composed at runtime + // through tr() + QString::arg to stay translatable. + setWindowTitle(tr("%1 Options").arg(programName_)); + + // Application tab. + if (!programIcon_.isNull()) { + ui_->appIconLabel->setPixmap( + programIcon_.scaled(32, 32, Qt::KeepAspectRatio, + Qt::SmoothTransformation)); + } + ui_->appDescLabel->setText( + tr("Install or uninstall %1 here.").arg(programName_)); + + // File association tab. + ui_->assocHeaderLabel->setText( + tr("File types associated with %1:").arg(programName_)); + + QStringList headers; + headers << tr("Type") << userName_ << tr("All Users"); + ui_->assocTable->setHorizontalHeaderLabels(headers); +} + +// endregion + +// region: State Refresh + +void MainWindow::refreshProgramState() { + const bool registered = program_.IsRegistered(toWfassocScope(scope_)); + + ui_->installButton->setEnabled(!registered); + ui_->uninstallButton->setEnabled(registered); + + // The whole file-association tab is disabled until the application has + // been registered in the active scope. Additionally, the system column is + // permanently disabled when running in user mode. + const bool userColumnActive = registered; + const bool systemColumnActive = registered && isSystemColumnEnabled(); + + ui_->selectUserButton->setEnabled(userColumnActive); + ui_->selectSystemButton->setEnabled(systemColumnActive); + ui_->assocTable->setEnabled(registered); + + rebuildTable(); +} + +void MainWindow::rebuildTable() { + refreshing_ = true; + + rows_.clear(); + const size_t count = program_.ExtsLen(); + rows_.reserve(count); + + ui_->assocTable->setRowCount(static_cast(count)); + + for (size_t i = 0; i < count; ++i) { + ExtRow row; + row.index = i; + + // Self extension info: dotted body, display name and cached icon. + auto selfExt = program_.ResolveExt(i); + row.extBody = QString::fromUtf8(selfExt.GetExt()); + row.dottedExt = QString::fromUtf8(selfExt.GetDottedExt()); + row.selfName = QString::fromUtf8(selfExt.GetName()); + row.selfIcon = icon_utils::fromHicon(selfExt.GetIcon()); + + // Query the user-view and system-view states. None means blank; + // a match against our self name means Self; anything else is Other + // and we keep the original name/icon around for display. + auto userStatus = program_.QueryExt(wfassocpp::View::User, i); + if (userStatus) { + const QString observedName = + QString::fromUtf8(userStatus->GetName()); + row.initialUser.state = classifyCell(row.selfName, observedName); + row.initialUser.name = observedName; + row.initialUser.icon = + icon_utils::fromHicon(userStatus->GetIcon()); + } + + auto systemStatus = program_.QueryExt(wfassocpp::View::System, i); + if (systemStatus) { + const QString observedName = + QString::fromUtf8(systemStatus->GetName()); + row.initialSystem.state = + classifyCell(row.selfName, observedName); + row.initialSystem.name = observedName; + row.initialSystem.icon = + icon_utils::fromHicon(systemStatus->GetIcon()); + } + + row.pendingUser = row.initialUser; + row.pendingSystem = row.initialSystem; + + rows_.push_back(std::move(row)); + + // Create the QTableWidgetItem cells once; subsequent refreshes only + // update their text/icon and flags. + const int rowIdx = static_cast(i); + + auto* typeItem = new QTableWidgetItem(row.dottedExt); + ui_->assocTable->setItem(rowIdx, 0, typeItem); + + auto* userItem = new QTableWidgetItem; + userItem->setTextAlignment(Qt::AlignCenter); + ui_->assocTable->setItem(rowIdx, 1, userItem); + + auto* systemItem = new QTableWidgetItem; + systemItem->setTextAlignment(Qt::AlignCenter); + ui_->assocTable->setItem(rowIdx, 2, systemItem); + + refreshRowDisplay(rowIdx); + } + + refreshing_ = false; + + updateApplyButtonEnabled(); +} + +void MainWindow::refreshRowDisplay(int row) { + if (row < 0 || row >= static_cast(rows_.size())) { + return; + } + const ExtRow& r = rows_[row]; + + // Column 0: hybrid icon (user-preferred) + dotted extension. + QPixmap hybridIcon; + if (r.pendingUser.state != CellState::Blank) { + hybridIcon = effectiveCellIcon(r, r.pendingUser); + } else if (r.pendingSystem.state != CellState::Blank) { + hybridIcon = effectiveCellIcon(r, r.pendingSystem); + } + + QTableWidgetItem* typeItem = ui_->assocTable->item(row, 0); + if (typeItem) { + typeItem->setIcon(QIcon(hybridIcon)); + typeItem->setText(r.dottedExt); + } + + // Column 1: user scope display name. + QTableWidgetItem* userItem = ui_->assocTable->item(row, 1); + if (userItem) { + userItem->setText(r.pendingUser.state == CellState::Blank + ? QString() + : r.pendingUser.name); + } + + // Column 2: system scope display name. When the system column is + // inactive (user-only run), the cells are flagged as disabled so that + // clicks are ignored and the rendering is greyed out. + QTableWidgetItem* systemItem = ui_->assocTable->item(row, 2); + if (systemItem) { + systemItem->setText(r.pendingSystem.state == CellState::Blank + ? QString() + : r.pendingSystem.name); + + const Qt::ItemFlags enabledFlags = + Qt::ItemIsEnabled | Qt::ItemIsSelectable; + // Without Qt::ItemIsEnabled the cell renders disabled (greyed out) + // and cellClicked is not emitted, so clicks are silently ignored. + const Qt::ItemFlags disabledFlags = Qt::ItemIsSelectable; + if (isSystemColumnEnabled()) { + systemItem->setFlags(enabledFlags); + } else { + systemItem->setFlags(disabledFlags); + } + } +} + +void MainWindow::updateApplyButtonEnabled() { + bool dirty = false; + for (const ExtRow& r : rows_) { + if (r.pendingUser.state != r.initialUser.state || + r.pendingSystem.state != r.initialSystem.state) { + dirty = true; + break; + } + } + ui_->applyButton->setEnabled(dirty); +} + +// endregion + +// region: Cell Interaction + +void MainWindow::toggleCell(int row, int column) { + if (refreshing_) { + return; + } + if (row < 0 || row >= static_cast(rows_.size())) { + return; + } + + CellData* cell = nullptr; + const ExtRow* rowPtr = &rows_[row]; + if (column == 1) { + cell = &rows_[row].pendingUser; + } else if (column == 2) { + if (!isSystemColumnEnabled()) { + return; + } + cell = &rows_[row].pendingSystem; + } else { + return; + } + + // Toggle: Self -> Blank, anything else -> Self. + if (cell->state == CellState::Self) { + cell->state = CellState::Blank; + cell->name.clear(); + cell->icon = QPixmap(); + } else { + cell->state = CellState::Self; + cell->name = rowPtr->selfName; + cell->icon = rowPtr->selfIcon; + } + + refreshRowDisplay(row); + updateApplyButtonEnabled(); +} + +void MainWindow::selectAllInScope(bool isUser) { + if (!isUser && !isSystemColumnEnabled()) { + return; + } + + // Progressively select more. If there is at least one blank cell, the + // first click only fills blanks; otherwise the click overrides cells + // pointing at other handlers as well. + bool hasBlank = false; + for (ExtRow& r : rows_) { + const CellData& cell = isUser ? r.pendingUser : r.pendingSystem; + if (cell.state == CellState::Blank) { + hasBlank = true; + break; + } + } + + for (ExtRow& r : rows_) { + CellData& cell = isUser ? r.pendingUser : r.pendingSystem; + if (hasBlank) { + if (cell.state == CellState::Blank) { + cell.state = CellState::Self; + cell.name = r.selfName; + cell.icon = r.selfIcon; + } + } else if (cell.state != CellState::Self) { + cell.state = CellState::Self; + cell.name = r.selfName; + cell.icon = r.selfIcon; + } + } + + for (size_t i = 0; i < rows_.size(); ++i) { + refreshRowDisplay(static_cast(i)); + } + updateApplyButtonEnabled(); +} + +// endregion + +// region: Apply + +void MainWindow::applyAllChanges() { + // Walk through every row and commit any cell whose pending state differs + // from the initial snapshot. wfassoc's link/unlink take an index rather + // than a scope/view, so we map columns back to (scope, index) pairs. + for (const ExtRow& r : rows_) { + if (r.pendingUser.state != r.initialUser.state) { + if (r.pendingUser.state == CellState::Self) { + program_.LinkExt(wfassocpp::Scope::User, r.index); + } else { + program_.UnlinkExt(wfassocpp::Scope::User, r.index); + } + } + if (r.pendingSystem.state != r.initialSystem.state) { + if (r.pendingSystem.state == CellState::Self) { + program_.LinkExt(wfassocpp::Scope::System, r.index); + } else { + program_.UnlinkExt(wfassocpp::Scope::System, r.index); + } + } + } + + // Re-query and rebuild the table so the UI reflects the live registry. + rebuildTable(); +} + +bool MainWindow::isSystemColumnEnabled() const { + return scope_ == TargetScope::System; +} + +// endregion + +// region: Slots + +void MainWindow::onInstallClicked() { + try { + program_.Register(toWfassocScope(scope_)); + } catch (const std::exception& e) { + QMessageBox::critical(this, windowTitle(), + QString::fromUtf8(e.what())); + return; + } + + QMessageBox::information(this, windowTitle(), + tr("Application installed successfully.")); + refreshProgramState(); +} + +void MainWindow::onUninstallClicked() { + try { + program_.Unregister(toWfassocScope(scope_)); + } catch (const std::exception& e) { + QMessageBox::critical(this, windowTitle(), + QString::fromUtf8(e.what())); + return; + } + + QMessageBox::information(this, windowTitle(), + tr("Application uninstalled successfully.")); + refreshProgramState(); +} + +void MainWindow::onSelectUserClicked() { + selectAllInScope(/*isUser=*/true); +} + +void MainWindow::onSelectSystemClicked() { + selectAllInScope(/*isUser=*/false); +} + +void MainWindow::onCellClicked(int row, int column) { + toggleCell(row, column); +} + +void MainWindow::onOkClicked() { + try { + applyAllChanges(); + } catch (const std::exception& e) { + QMessageBox::critical(this, windowTitle(), + QString::fromUtf8(e.what())); + // Sync the table with the live registry, since some changes may have + // been committed before the failure. + refreshProgramState(); + return; + } + accept(); +} + +void MainWindow::onCancelClicked() { + reject(); +} + +void MainWindow::onApplyClicked() { + try { + applyAllChanges(); + } catch (const std::exception& e) { + QMessageBox::critical(this, windowTitle(), + QString::fromUtf8(e.what())); + refreshProgramState(); + return; + } +} + +// endregion + +} // namespace qwfassoc diff --git a/example/qwfassoc/src/main_window.h b/example/qwfassoc/src/main_window.h new file mode 100644 index 0000000..fe9e4de --- /dev/null +++ b/example/qwfassoc/src/main_window.h @@ -0,0 +1,142 @@ +#pragma once +#ifndef QWFASSOC_MAIN_WINDOW_H_ +#define QWFASSOC_MAIN_WINDOW_H_ + +#include +#include +#include + +#include +#include + +#include + +namespace Ui { +class MainWindow; +} + +namespace qwfassoc { + +// The target scope the application is being managed for. +// This value comes from the --for command line argument and decides which +// columns in the file association table are interactive, as well as which +// scope install/uninstall operate on. +enum class TargetScope { + User, + System, +}; + +// The pending state of a single (extension, scope) cell. +enum class CellState { + // The extension has no associated handler in this scope. + Blank, + // The extension is associated with the application described by the + // current manifest. + Self, + // The extension is associated with some other application. + Other, +}; + +// Per-cell cached data. +struct CellData { + CellState state = CellState::Blank; + // Display name. Only meaningful when state == Other, since the Self name + // is shared per row (it does not change between cells). + QString name; + // Display icon. Used for both Self and Other states. + QPixmap icon; +}; + +// All data attached to a single row in the file association table. +struct ExtRow { + // wfassoc extension index, used when calling LinkExt/UnlinkExt/QueryExt. + size_t index = 0; + // Dotted extension body, e.g. ".jpg". Displayed in column 0. + QString dottedExt; + // Body without leading dot, e.g. "jpg". + QString extBody; + // The display name when this program is the handler. + QString selfName; + // The icon when this program is the handler. + QPixmap selfIcon; + + // Snapshot taken from the live registry at load/refresh time. + CellData initialUser; + CellData initialSystem; + // Working copy edited by the user; compared against initial* on Apply. + CellData pendingUser; + CellData pendingSystem; +}; + +// Main configuration dialog. Owns the wfassoc Program for its lifetime. +class MainWindow : public QDialog { + Q_OBJECT + +public: + explicit MainWindow(wfassocpp::Program program, + TargetScope scope, + QWidget* parent = nullptr); + ~MainWindow() override; + +private slots: + // Slot connected to the "Install" button. + void onInstallClicked(); + // Slot connected to the "Uninstall" button. + void onUninstallClicked(); + // Slot connected to the "+" button above the user column. + void onSelectUserClicked(); + // Slot connected to the "+" button above the system column. + void onSelectSystemClicked(); + // Slot connected to QTableWidget::cellClicked. + void onCellClicked(int row, int column); + // Slot connected to the "OK" button: apply pending changes then close. + void onOkClicked(); + // Slot connected to the "Cancel" button: close without applying. + void onCancelClicked(); + // Slot connected to the "Apply" button: apply pending changes, stay open. + void onApplyClicked(); + +private: + // Re-apply labels that depend on the resolved program name and the + // current user name. Translatable strings use tr() so they get picked up + // by Qt's translation tooling. + void retranslateUi(); + // Refresh install/uninstall button enable state and the file association + // tab enable state from the live registry. + void refreshProgramState(); + // Drop and rebuild the table contents from the live registry. + void rebuildTable(); + // Refresh a single row's displayed cells from its pending state. + void refreshRowDisplay(int row); + // Update the "Apply" button enable state based on whether any pending + // cell differs from its initial state. + void updateApplyButtonEnabled(); + // Toggle the pending state of a (row, column) cell. Column 1 maps to the + // user scope, column 2 to the system scope. + void toggleCell(int row, int column); + // Apply the "+" semantics to a given scope: blank cells become Self, and + // if there are none, all non-Self cells become Self. + void selectAllInScope(bool isUser); + // Commit every pending change to the registry via wfassoc. + void applyAllChanges(); + + // True when the system column should be interactive. + bool isSystemColumnEnabled() const; + + std::unique_ptr ui_; + wfassocpp::Program program_; + TargetScope scope_; + + QString programName_; + QPixmap programIcon_; + QString userName_; + + std::vector rows_; + // Re-entrancy guard used while rebuilding the table to avoid feeding + // model-change signals back into toggleCell(). + bool refreshing_ = false; +}; + +} // namespace qwfassoc + +#endif // QWFASSOC_MAIN_WINDOW_H_ diff --git a/example/qwfassoc/src/main_window.ui b/example/qwfassoc/src/main_window.ui new file mode 100644 index 0000000..1e0048d --- /dev/null +++ b/example/qwfassoc/src/main_window.ui @@ -0,0 +1,267 @@ + + + MainWindow + + + + 0 + 0 + 480 + 600 + + + + + 480 + 600 + + + + + 480 + 600 + + + + Options + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + 0 + + + + Applications + + + + + + Install and Uninstall + + + + + + + + [icon] + + + + + + + Install or uninstall the application here. + + + true + + + + + + + + + + + Install + + + false + + + + + + + Uninstall + + + false + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + File Associations + + + + + + File types associated with this application: + + + true + + + + + + + + + Select all for current user + + + + + + + false + + + + + + + Select all for all users + + + + + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectItems + + + + Type + + + + + User + + + + + All Users + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + true + + + true + + + + + + + Cancel + + + false + + + + + + + Apply + + + false + + + false + + + + + + + + + + + + + + diff --git a/example/qwfassoc/src/manifest.cpp b/example/qwfassoc/src/manifest.cpp new file mode 100644 index 0000000..a593e65 --- /dev/null +++ b/example/qwfassoc/src/manifest.cpp @@ -0,0 +1,141 @@ +#include "manifest.h" + +#include + +#include + +namespace qwfassoc { + +// region: Manifest Parsing + +Manifest Manifest::fromFile(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(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 { + if (!parent.contains(key)) { + return {}; + } + std::map 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 Manifest::toSchema() const { + 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(identifier.c_str()); + schema.SetPath(path.c_str()); + schema.SetClsid(clsid.c_str()); + + // Optional fields: passing nullptr tells wfassoc to clear the value. + schema.SetName(name.has_value() ? name->c_str() : nullptr); + schema.SetIcon(icon.has_value() ? icon->c_str() : nullptr); + schema.SetBehavior(behavior.has_value() ? behavior->c_str() : nullptr); + + for (const auto& [key, value] : strs) { + schema.AddStr(key.c_str(), value.c_str()); + } + for (const auto& [key, value] : icons) { + schema.AddIcon(key.c_str(), value.c_str()); + } + for (const auto& [key, value] : behaviors) { + schema.AddBehavior(key.c_str(), value.c_str()); + } + for (const auto& [key, value] : exts) { + schema.AddExt(key.c_str(), + value.name.c_str(), + value.icon.c_str(), + value.behavior.c_str()); + } + + return schema; +} + +// endregion + +} // namespace qwfassoc diff --git a/example/qwfassoc/src/manifest.h b/example/qwfassoc/src/manifest.h new file mode 100644 index 0000000..3ccaf70 --- /dev/null +++ b/example/qwfassoc/src/manifest.h @@ -0,0 +1,55 @@ +#pragma once +#ifndef QWFASSOC_MANIFEST_H_ +#define QWFASSOC_MANIFEST_H_ + +#include +#include +#include + +#include + +namespace qwfassoc { + +// Description of a single extension declared in the manifest. +// +// For a given extension (e.g. "jpg"), the manifest references one entry from +// each of the `strs`, `icons` and `behaviors` tables by its token name. +struct ManifestExt { + std::string name; + std::string icon; + std::string behavior; +}; + +// In-memory representation of a wfassoc manifest TOML file. +// +// This struct mirrors the Rust `Manifest` type defined in +// `wfassoc-exec/src/manifest.rs`. All validation that requires the wfassoc +// library itself (identifier regex, dangling references, etc.) is deferred to +// `wfassocpp::Program` construction; this struct only performs TOML parsing. +struct Manifest { + std::string identifier; + std::string path; + std::string clsid; + + std::optional name; + std::optional icon; + std::optional behavior; + + std::map strs; + std::map icons; + std::map behaviors; + std::map exts; + + // Parse a manifest TOML file from disk. + // Throws std::runtime_error on any IO or TOML syntax error. + static Manifest fromFile(const std::string& path); + + // Build a wfassocpp::Schema from this manifest. + // Throws std::runtime_error (originating from wfassocpp::_Check) when the + // wfassoc library rejects an operation, e.g. on duplicate keys. + wfassocpp::Schema toSchema() const; +}; + +} // namespace qwfassoc + +#endif // QWFASSOC_MANIFEST_H_