1
0

feat: include ai first step works

This commit is contained in:
2026-06-23 20:46:56 +08:00
parent 821b865f2d
commit 071347f4d4
14 changed files with 1525 additions and 1561 deletions

View 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}
)

View File

@@ -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状态。
- 用户可以在"文件关联"选项卡中设置是否以当前应用程序打开某些扩展名。用户可以点击全选按钮或单元格来进行设置,应用程序暂存修改,等用户点击确认或应用后再应用修改。如果点击的是应用,则刷新当前页面。
# 额外要求
- 不要尝试去编译来检查错误。我会安排其他人来检查程序是否能正常运行,并汇报回来,你再修改。
- 你不需要关心能否找到Qtwfassoc和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部分的任务规划就非常符合这种范式。

View File

@@ -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

View File

@@ -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.

View File

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

View File

@@ -0,0 +1,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

View 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_

View 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;
}

View 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

View 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_

View 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>

View 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

View 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_