feat: include ai first step works
This commit is contained in:
78
example/qwfassoc/CMakeLists.txt
Normal file
78
example/qwfassoc/CMakeLists.txt
Normal file
@@ -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}
|
||||
)
|
||||
@@ -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部分的任务规划就非常符合这种范式。
|
||||
|
||||
@@ -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 `<string>` (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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
4
example/qwfassoc/i18n/qwfassoc_zh_CN.ts
Normal file
4
example/qwfassoc/i18n/qwfassoc_zh_CN.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="zh_CN">
|
||||
</TS>
|
||||
23
example/qwfassoc/src/icon_utils.cpp
Normal file
23
example/qwfassoc/src/icon_utils.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <QImage>
|
||||
|
||||
// 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 <windows.h> in the order Qt expects, so it
|
||||
// is included after all Qt headers.
|
||||
#include <qt_windows.h>
|
||||
|
||||
namespace qwfassoc {
|
||||
namespace icon_utils {
|
||||
|
||||
QPixmap fromHicon(wfassocpp::HICON handle) {
|
||||
if (handle == wfassocpp::INVALID_HICON) {
|
||||
return QPixmap();
|
||||
}
|
||||
QImage image = QImage::fromHICON(static_cast<HICON>(handle));
|
||||
return QPixmap::fromImage(std::move(image));
|
||||
}
|
||||
|
||||
} // namespace icon_utils
|
||||
} // namespace qwfassoc
|
||||
24
example/qwfassoc/src/icon_utils.h
Normal file
24
example/qwfassoc/src/icon_utils.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_ICON_UTILS_H_
|
||||
#define QWFASSOC_ICON_UTILS_H_
|
||||
|
||||
#include <QPixmap>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
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_
|
||||
152
example/qwfassoc/src/main.cpp
Normal file
152
example/qwfassoc/src/main.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#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.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;
|
||||
}
|
||||
486
example/qwfassoc/src/main_window.cpp
Normal file
486
example/qwfassoc/src/main_window.cpp
Normal file
@@ -0,0 +1,486 @@
|
||||
#include "main_window.h"
|
||||
#include "ui_main_window.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidgetItem>
|
||||
|
||||
// icon_utils.h pulls in <qt_windows.h>, so it is included after all Qt
|
||||
// headers to keep the Windows include ordering that Qt expects.
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
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<int>(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<int>(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<int>(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<int>(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<int>(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
|
||||
142
example/qwfassoc/src/main_window.h
Normal file
142
example/qwfassoc/src/main_window.h
Normal file
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_MAIN_WINDOW_H_
|
||||
#define QWFASSOC_MAIN_WINDOW_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
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::MainWindow> ui_;
|
||||
wfassocpp::Program program_;
|
||||
TargetScope scope_;
|
||||
|
||||
QString programName_;
|
||||
QPixmap programIcon_;
|
||||
QString userName_;
|
||||
|
||||
std::vector<ExtRow> 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_
|
||||
267
example/qwfassoc/src/main_window.ui
Normal file
267
example/qwfassoc/src/main_window.ui
Normal file
@@ -0,0 +1,267 @@
|
||||
<?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 class="QWidget" name="tabApplications">
|
||||
<attribute name="title">
|
||||
<string>Applications</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="applicationsLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Install and Uninstall</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="groupLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="headerLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="appIconLabel">
|
||||
<property name="text">
|
||||
<string notr="true">[icon]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="appDescLabel">
|
||||
<property name="text">
|
||||
<string>Install or uninstall the application here.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="actionLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="installButton">
|
||||
<property name="text">
|
||||
<string>Install</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uninstallButton">
|
||||
<property name="text">
|
||||
<string>Uninstall</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="applicationsVerticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tabAssociations">
|
||||
<attribute name="title">
|
||||
<string>File Associations</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="associationsLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="assocHeaderLabel">
|
||||
<property name="text">
|
||||
<string>File types associated with this application:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="selectButtonsLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="selectUserButton">
|
||||
<property name="toolTip">
|
||||
<string>Select all for current user</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">+</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="selectSystemButton">
|
||||
<property name="toolTip">
|
||||
<string>Select all for all users</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">+</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="selectButtonsSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="assocTable">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectItems</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>User</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>All Users</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="bottomButtonsLayout">
|
||||
<item>
|
||||
<spacer name="bottomButtonsSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="okButton">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="applyButton">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
141
example/qwfassoc/src/manifest.cpp
Normal file
141
example/qwfassoc/src/manifest.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "manifest.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <toml.hpp>
|
||||
|
||||
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<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 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
|
||||
55
example/qwfassoc/src/manifest.h
Normal file
55
example/qwfassoc/src/manifest.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
#ifndef QWFASSOC_MANIFEST_H_
|
||||
#define QWFASSOC_MANIFEST_H_
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <wfassoc++.h>
|
||||
|
||||
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<std::string> name;
|
||||
std::optional<std::string> icon;
|
||||
std::optional<std::string> behavior;
|
||||
|
||||
std::map<std::string, std::string> strs;
|
||||
std::map<std::string, std::string> icons;
|
||||
std::map<std::string, std::string> behaviors;
|
||||
std::map<std::string, ManifestExt> 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_
|
||||
Reference in New Issue
Block a user