1
0

feat: update qwfassoc

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

View File

@@ -1,13 +1,14 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(qwfassoc LANGUAGES CXX) project(qwfassoc_suite LANGUAGES CXX)
# Qt 6 requires C++17 at minimum. # Qt 6 requires C++17 at minimum.
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
# Let CMake auto-process Q_OBJECT, .ui files and .qrc resources. # Let CMake auto-process Q_OBJECT, .ui files and .qrc resources for every
# subproject below.
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
@@ -15,64 +16,21 @@ set(CMAKE_AUTORCC ON)
# Make the bundled Findwfassoc.cmake module visible to find_package(). # Make the bundled Findwfassoc.cmake module visible to find_package().
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# Locate third-party dependencies. The user is responsible for making these # Qt and LinguistTools are used by both subprojects, so they are looked up at
# discoverable through the usual CMake mechanisms (CMAKE_PREFIX_PATH, etc.). # the top level. The same is true for wfassoc: although only the qwfassoc
# LinguistTools is required to wire up Qt's translation pipeline # library links against it directly, PUBLIC propagation from the library
# (lupdate + lrelease) so that .ts files are kept up to date and .qm files # target makes the dependency available to qwfassoc-standalone as well.
# are embedded as Qt resources. # toml11 is only needed when the standalone executable is built, so it is
# looked up conditionally below.
find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools) 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) find_package(wfassoc REQUIRED)
set(QWFASSOC_SOURCES # The standalone executable is optional: embedders may want only the library.
"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" option(QWFASSOC_BUILD_STANDALONE "Build the qwfassoc-standalone executable" ON)
"${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 add_subdirectory(qwfassoc)
"${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 if(QWFASSOC_BUILD_STANDALONE)
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.ui" find_package(toml11 REQUIRED)
) add_subdirectory(qwfassoc-standalone)
endif()
# 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,137 +1,154 @@
# qwfassoc # qwfassoc (suite)
A Qt Widgets based GUI front-end for the [wfassoc](../../wfassoc) library. A Qt Widgets based GUI for the [wfassoc](../../wfassoc) library, split into a
reusable shared library and a small standalone executable that exercises it.
`qwfassoc` exposes the same install / uninstall / file-association operations The project is organized as two CMake subprojects:
as the `wfassoc-exec` command line tool, but in a small tabbed dialog aimed at
end users rather than scripts.
## Layout
``` ```
qwfassoc/ qwfassoc/ Parent directory (this README)
├── CMakeLists.txt CMake build script ├── CMakeLists.txt Top-level CMake; add_subdirectory's both
├── README.md This file subprojects and finds Qt, wfassoc, toml11
├── cmake/ ├── cmake/
│ ├── Findwfassoc.cmake Verbatim copy of wfassoc's Find module │ ├── Findwfassoc.cmake Verbatim copy of wfassoc's Find module
│ └── README.md Provenance notes for the copy │ └── README.md Provenance notes for the copy
├── i18n/ ├── qwfassoc/ Shared library subproject
── qwfassoc_zh_CN.ts Empty placeholder translation file ── CMakeLists.txt
└── src/ │ ├── i18n/
├── main.cpp Entry point, CLI parsing, translator loading │ └── qwfassoc_zh_CN.ts Empty placeholder translation file
── main_window.h/.cpp Main configuration dialog ── src/
├── main_window.ui Qt Designer description of the dialog ├── qwfassoc_global.h QWFASSOC_EXPORT macro
├── manifest.h/.cpp TOML manifest loader (toml11) and schema builder ├── scope.h Shared TargetScope enum
└── icon_utils.h/.cpp wfassocpp::HICON -> QPixmap conversion helper ├── manifest.h Manifest data struct (no TOML dependency)
│ ├── icon_utils.h/.cpp wfassocpp::HICON -> QPixmap conversion
│ ├── application_widget.h/.cpp/.ui
│ │ Install / uninstall widget
│ └── association_widget.h/.cpp/.ui
│ File associations widget
└── qwfassoc-standalone/ Executable subproject
├── CMakeLists.txt
├── i18n/
│ └── qwfassoc-standalone_zh_CN.ts
│ Empty placeholder translation file
└── src/
├── main.cpp Entry point, CLI parsing, translator loading
├── main_window.h/.cpp/.ui
│ QDialog hosting the two widgets in a tab widget
└── manifest_parser.h/.cpp
TOML -> Manifest, Manifest -> Schema
``` ```
## Subprojects at a glance
### `qwfassoc` (shared library)
Exports two reusable widgets that wrap wfassoc:
* `qwfassoc::ApplicationWidget` — install / uninstall the program in the
configured scope.
* `qwfassoc::AssociationWidget` — stage and apply per-extension link / unlink
operations.
Both widgets follow the **two-phase initialization** pattern expected by Qt
Designer promoted widgets: the constructor only takes a `QWidget*` parent and
leaves the widget disabled. A `setConfig(Config)` method injects the
`wfassocpp::Program` pointer and `TargetScope` (and, for the association
widget, whether the OK/Cancel buttons are visible). Each widget also exposes:
* a `refresh()` slot that re-queries the live wfassoc state, intended to be
called by the host when another component has mutated the registry;
* a `changed()` signal emitted whenever the widget itself mutates the
registry (install / uninstall / apply);
* (`AssociationWidget` only) a `finished(bool accepted)` signal emitted when
the user clicks OK (after `changed()`) or Cancel, so the host can close the
dialog.
The library also exposes the plain `qwfassoc::Manifest` data struct and a
`qwfassoc::icon_utils::fromHicon()` helper, but the TOML parsing logic (which
depends on toml11) lives in the standalone executable.
### `qwfassoc-standalone` (executable)
Reproduces the original tabbed wfassoc configurator by:
1. parsing `-c/--manifest <path>` and `-f/--for <user|system>` from the
command line;
2. building a `wfassocpp::Program` via `parseManifestFile` + `buildSchema`;
3. hosting `ApplicationWidget` and `AssociationWidget` inside a `QTabWidget`
in a `MainWindow` dialog;
4. wiring the widgets' `changed()` and `finished()` signals so that any
registry mutation refreshes both pages and OK/Cancel drive the dialog's
acceptance.
## Requirements ## Requirements
* **CMake** 3.20 or newer. * **CMake** 3.20 or newer (3.21+ recommended for `qt6_add_translations`).
* A C++17 compiler. * A C++17 compiler.
* **Qt 6** (6.3 or newer is recommended for `qt6_add_translations`). The * **Qt 6** with the `Widgets` and `LinguistTools` components.
`Widgets` and `LinguistTools` components are required: * **wfassoc**, with `wfassoc_ROOT` pointing at an installed tree (see
`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 [`cmake/Findwfassoc.cmake`](cmake/Findwfassoc.cmake) for the expected
directory layout). directory layout).
* **toml11** — only required when building the standalone executable.
## Building ## Building
```bat ```bat
cmake -S . -B build -DCMAKE_PREFIX_PATH=C:\Qt\6.x.x\msvc2022_64 ^ cmake -S . -B build ^
-DCMAKE_PREFIX_PATH=C:\Qt\6.x.x\msvc2022_64 ^
-Dwfassoc_ROOT=C:\path\to\wfassoc\install ^ -Dwfassoc_ROOT=C:\path\to\wfassoc\install ^
-Dtoml11_DIR=C:\path\to\toml11\share\toml11\cmake -Dtoml11_DIR=C:\path\to\toml11\share\toml11\cmake
cmake --build build --config Release cmake --build build --config Release
``` ```
The resulting executable is `build/Release/qwfassoc.exe` (or a similar path To skip the standalone executable (and the toml11 dependency):
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 ```bat
qwfassoc -c C:\path\to\ppic.toml -f user cmake -S . -B build -DQWFASSOC_BUILD_STANDALONE=OFF ...
``` ```
When `-f user` is used the "All Users" column of the file-association table is The standalone executable is `build/qwfassoc-standalone/Release/qwfassoc-standalone.exe`
read-only (cells render greyed out and clicks are ignored). Use `-f system` (or similar, depending on the generator); the library is
(with appropriate administrator privileges) to make changes that affect all `build/qwfassoc/Release/qwfassoc.dll`.
users.
## Running the standalone executable
| 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`. |
```bat
qwfassoc-standalone -c C:\path\to\ppic.toml -f user
```
## Internationalization ## Internationalization
The user-facing strings shipped in the source code and the `.ui` file are Source strings are English and every user-facing string is wrapped in `tr()`
written in English. Every translatable string is wrapped in `tr()` (in code) (in code) or is a plain `<string>` element in the `.ui` file (which `uic`
or marked as a regular `<string>` (in the `.ui` file, which `uic` then wraps wraps in `QCoreApplication::translate`).
in `QCoreApplication::translate`).
The CMake build wires up the standard Qt translation pipeline via Each subproject ships its own empty placeholder `.ts` file under its
`qt6_add_translations()`: `i18n/` directory and registers it with `qt6_add_translations()`:
* the listed `.ts` files under `i18n/` are kept in sync with the source by * `qwfassoc/i18n/qwfassoc_zh_CN.ts` — covers the library widgets.
`lupdate`, * `qwfassoc-standalone/i18n/qwfassoc-standalone_zh_CN.ts` — covers the
* `lrelease` compiles them into `.qm` files which are embedded under the executable-specific messages (CLI errors, tab titles, dialog window
`:/i18n/` resource prefix, title, etc.).
* `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**. At runtime, `installTranslators()` in `qwfassoc-standalone/src/main.cpp`
Translators are expected to fill it in (or add new language files and list loads both `.qm` files for the user's preferred UI language from the
them in `CMakeLists.txt`). No actual translation work is performed by the `:/i18n/` resource prefix. Translators are expected to fill in the `.ts`
build on its own. files; 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 ## Notes and Limitations
* The "self" detection in the file-association table is based on comparing 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 display name returned by wfassoc with the display name this program would
use. Two programs sharing the exact same display name could therefore be use. Two programs sharing the exact same display name could therefore be
confused. confused.
* The dialog uses `Qt::ItemIsSelectable` (without `Qt::ItemIsEnabled`) to * The system column in `AssociationWidget` is rendered disabled (using
render disabled system-column cells in user mode; their text is still shown `Qt::ItemIsSelectable` without `Qt::ItemIsEnabled`) when `TargetScope` is
but they cannot be clicked. `User`; the cells stay visible but cannot be clicked.
* All errors originating from wfassoc are surfaced through `QMessageBox` * All errors originating from wfassoc are surfaced through `QMessageBox`
dialogs; fatal errors during startup cause the process to exit with a dialogs; fatal errors during startup cause the process to exit with a
non-zero status code. non-zero status code.

View File

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

View File

@@ -13,21 +13,18 @@
#include <wfassoc++.h> #include <wfassoc++.h>
#include "main_window.h" #include "main_window.h"
#include "manifest.h" #include "manifest_parser.h"
#include "scope.h"
namespace { namespace {
// Context used for translatable strings that live outside of any QObject. // Context used for translatable strings that live outside of any QObject.
// Keeping it stable lets translators find these messages under the same key constexpr const char* kTranslationContext = "qwfassoc-standalone";
// across builds.
constexpr const char* kTranslationContext = "qwfassoc";
// Show a modal error dialog with the given message and return a non-zero // 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 // exit code. Used for the various fatal conditions that may occur before the
// main dialog can be shown. // main dialog can be shown.
int fatal(QWidget* parent, const QString& message) { 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); QMessageBox::critical(parent, QApplication::applicationName(), message);
return 1; return 1;
} }
@@ -46,18 +43,24 @@ qwfassoc::TargetScope parseScope(const QString& value) {
"Invalid value for --for. Use \"user\" or \"system\"."); "Invalid value for --for. Use \"user\" or \"system\".");
} }
// Load the translation matching the user's preferred UI language, if any. // Install the translation(s) matching the user's preferred UI language, if
// Qt's qt6_add_translations() compiles .ts files into .qm files and embeds // any. Both the qwfassoc library's .qm and this executable's .qm are loaded
// them under the ":/i18n" resource prefix. // (their .ts files live under the per-project i18n/ directories and are
// embedded under the ":/i18n" resource prefix by qt6_add_translations).
void installTranslators(QApplication& app) { void installTranslators(QApplication& app) {
QTranslator* translator = new QTranslator(&app);
const QStringList uiLanguages = QLocale::system().uiLanguages(); const QStringList uiLanguages = QLocale::system().uiLanguages();
for (const QString& locale : uiLanguages) { for (const QString& locale : uiLanguages) {
const QString baseName = const QString name = QLocale(locale).name();
QStringLiteral("qwfassoc_") + QLocale(locale).name();
if (translator->load(QStringLiteral(":/i18n/") + baseName)) { QTranslator* libTranslator = new QTranslator(&app);
app.installTranslator(translator); if (libTranslator->load(QStringLiteral(":/i18n/qwfassoc_") + name)) {
return; app.installTranslator(libTranslator);
}
QTranslator* appTranslator = new QTranslator(&app);
if (appTranslator->load(
QStringLiteral(":/i18n/qwfassoc-standalone_") + name)) {
app.installTranslator(appTranslator);
} }
} }
} }
@@ -66,11 +69,11 @@ void installTranslators(QApplication& app) {
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
QApplication app(argc, argv); QApplication app(argc, argv);
QApplication::setApplicationName(QStringLiteral("qwfassoc")); QApplication::setApplicationName(QStringLiteral("qwfassoc-standalone"));
// Install available translations before any translatable string is // Install available translations before any translatable string is
// resolved (including the application display name below) so that tr() // resolved so that tr() and QCoreApplication::translate() pick up the
// and QCoreApplication::translate() pick up the right language. // right language.
installTranslators(app); installTranslators(app);
QApplication::setApplicationDisplayName( QApplication::setApplicationDisplayName(
@@ -80,8 +83,9 @@ int main(int argc, char* argv[]) {
// Parse command line arguments using Qt's built-in parser. // Parse command line arguments using Qt's built-in parser.
QCommandLineParser parser; QCommandLineParser parser;
parser.setApplicationDescription( parser.setApplicationDescription(
QCoreApplication::translate(kTranslationContext, QCoreApplication::translate(
"Qt-based GUI for the wfassoc library.")); kTranslationContext,
"Qt-based GUI executable for the wfassoc library."));
parser.addHelpOption(); parser.addHelpOption();
QCommandLineOption manifestOption( QCommandLineOption manifestOption(
@@ -136,8 +140,8 @@ int main(int argc, char* argv[]) {
int exitCode = 0; int exitCode = 0;
try { try {
qwfassoc::Manifest manifest = qwfassoc::Manifest manifest =
qwfassoc::Manifest::fromFile(manifestPath.toStdString()); qwfassoc::parseManifestFile(manifestPath.toStdString());
wfassocpp::Schema schema = manifest.toSchema(); wfassocpp::Schema schema = qwfassoc::buildSchema(manifest);
wfassocpp::Program program(std::move(schema)); wfassocpp::Program program(std::move(schema));
qwfassoc::MainWindow window(std::move(program), scope); qwfassoc::MainWindow window(std::move(program), scope);

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
#include "manifest.h" #include "manifest_parser.h"
#include <stdexcept> #include <stdexcept>
@@ -6,9 +6,9 @@
namespace qwfassoc { namespace qwfassoc {
// region: Manifest Parsing // region: TOML Parsing
Manifest Manifest::fromFile(const std::string& path) { Manifest parseManifestFile(const std::string& path) {
toml::value root; toml::value root;
try { try {
root = toml::parse(path); root = toml::parse(path);
@@ -103,30 +103,33 @@ Manifest Manifest::fromFile(const std::string& path) {
// region: Schema Conversion // region: Schema Conversion
wfassocpp::Schema Manifest::toSchema() const { wfassocpp::Schema buildSchema(const Manifest& manifest) {
wfassocpp::Schema schema; wfassocpp::Schema schema;
// The wfassocpp wrappers translate any underlying failure into a // The wfassocpp wrappers translate any underlying failure into a
// std::runtime_error via _Check, so we let those propagate untouched. // std::runtime_error via _Check, so we let those propagate untouched.
schema.SetIdentifier(identifier.c_str()); schema.SetIdentifier(manifest.identifier.c_str());
schema.SetPath(path.c_str()); schema.SetPath(manifest.path.c_str());
schema.SetClsid(clsid.c_str()); schema.SetClsid(manifest.clsid.c_str());
// Optional fields: passing nullptr tells wfassoc to clear the value. // Optional fields: passing nullptr tells wfassoc to clear the value.
schema.SetName(name.has_value() ? name->c_str() : nullptr); schema.SetName(manifest.name.has_value() ? manifest.name->c_str()
schema.SetIcon(icon.has_value() ? icon->c_str() : nullptr); : nullptr);
schema.SetBehavior(behavior.has_value() ? behavior->c_str() : nullptr); schema.SetIcon(manifest.icon.has_value() ? manifest.icon->c_str()
: nullptr);
schema.SetBehavior(manifest.behavior.has_value() ? manifest.behavior->c_str()
: nullptr);
for (const auto& [key, value] : strs) { for (const auto& [key, value] : manifest.strs) {
schema.AddStr(key.c_str(), value.c_str()); schema.AddStr(key.c_str(), value.c_str());
} }
for (const auto& [key, value] : icons) { for (const auto& [key, value] : manifest.icons) {
schema.AddIcon(key.c_str(), value.c_str()); schema.AddIcon(key.c_str(), value.c_str());
} }
for (const auto& [key, value] : behaviors) { for (const auto& [key, value] : manifest.behaviors) {
schema.AddBehavior(key.c_str(), value.c_str()); schema.AddBehavior(key.c_str(), value.c_str());
} }
for (const auto& [key, value] : exts) { for (const auto& [key, value] : manifest.exts) {
schema.AddExt(key.c_str(), schema.AddExt(key.c_str(),
value.name.c_str(), value.name.c_str(),
value.icon.c_str(), value.icon.c_str(),

View File

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

View File

@@ -0,0 +1,58 @@
# qwfassoc: shared library exporting reusable Qt widgets that wrap wfassoc.
# Qt 6 requires C++17 at minimum.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(QWFASSOC_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.cpp"
)
set(QWFASSOC_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/src/qwfassoc_global.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/scope.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.h"
)
set(QWFASSOC_UI
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.ui"
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.ui"
)
add_library(qwfassoc SHARED
${QWFASSOC_SOURCES}
${QWFASSOC_HEADERS}
${QWFASSOC_UI}
)
# QWFASSOC_LIBRARY switches QWFASSOC_EXPORT from import to export mode.
target_compile_definitions(qwfassoc PRIVATE QWFASSOC_LIBRARY)
# Consumers (and the library itself) need to find the public headers under
# src/. PUBLIC propagates the include path to anyone linking against qwfassoc.
target_include_directories(qwfassoc PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_link_libraries(qwfassoc PUBLIC
Qt6::Widgets
wfassoc::wfassoc
)
# Translation pipeline for the library. qt6_add_translations() runs lupdate
# against the target's sources and embeds the lrelease output under the
# ":/i18n" resource prefix, where installTranslators() in the executable
# looks it up at runtime.
set(QWFASSOC_TS_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc_zh_CN.ts"
)
qt6_add_translations(qwfassoc
TS_FILES ${QWFASSOC_TS_FILES}
)

View File

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

View File

@@ -0,0 +1,119 @@
#include "application_widget.h"
#include "ui_application_widget.h"
#include "icon_utils.h"
#include <QMessageBox>
#include <QPushButton>
#include <stdexcept>
namespace qwfassoc {
namespace {
// Convert the library's 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;
}
} // namespace
ApplicationWidget::ApplicationWidget(QWidget* parent)
: QWidget(parent), ui_(new Ui::ApplicationWidget) {
ui_->setupUi(this);
connect(ui_->installButton, &QPushButton::clicked, this,
&ApplicationWidget::onInstallClicked);
connect(ui_->uninstallButton, &QPushButton::clicked, this,
&ApplicationWidget::onUninstallClicked);
// Until setConfig() is called we have no program to operate on; keep the
// whole widget disabled.
setEnabled(false);
}
ApplicationWidget::~ApplicationWidget() = default;
void ApplicationWidget::setConfig(const Config& config) {
program_ = config.program;
scope_ = config.scope;
if (program_ != nullptr) {
// Resolve program metadata that several labels depend on. These calls
// may throw std::runtime_error on failure; the caller is expected to
// wrap setConfig() in a try/catch and present an error dialog.
programName_ = QString::fromUtf8(program_->ResolveName());
auto iconRc = program_->ResolveIcon();
auto handle = iconRc.GetIcon();
programIcon_ = icon_utils::fromHicon(handle);
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_));
setEnabled(true);
refresh();
} else {
programName_.clear();
programIcon_ = QPixmap();
ui_->appIconLabel->setPixmap(QPixmap());
ui_->appIconLabel->setText(QString());
ui_->appDescLabel->setText(QString());
setEnabled(false);
}
}
void ApplicationWidget::refresh() {
if (program_ == nullptr) {
ui_->installButton->setEnabled(false);
ui_->uninstallButton->setEnabled(false);
return;
}
const bool registered = program_->IsRegistered(toWfassocScope(scope_));
ui_->installButton->setEnabled(!registered);
ui_->uninstallButton->setEnabled(registered);
}
void ApplicationWidget::onInstallClicked() {
if (program_ == nullptr) {
return;
}
try {
program_->Register(toWfassocScope(scope_));
} catch (const std::exception& e) {
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
return;
}
QMessageBox::information(this, tr("Information"),
tr("Application installed successfully."));
refresh();
emit changed();
}
void ApplicationWidget::onUninstallClicked() {
if (program_ == nullptr) {
return;
}
try {
program_->Unregister(toWfassocScope(scope_));
} catch (const std::exception& e) {
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
return;
}
QMessageBox::information(this, tr("Information"),
tr("Application uninstalled successfully."));
refresh();
emit changed();
}
} // namespace qwfassoc

View File

@@ -0,0 +1,74 @@
#pragma once
#ifndef QWFASSOC_APPLICATION_WIDGET_H_
#define QWFASSOC_APPLICATION_WIDGET_H_
#include <QPixmap>
#include <QString>
#include <QWidget>
#include <wfassoc++.h>
#include "qwfassoc_global.h"
#include "scope.h"
namespace Ui {
class ApplicationWidget;
}
namespace qwfassoc {
// Widget exposing install / uninstall actions for a single wfassoc program.
//
// The widget follows the two-phase initialization pattern required by Qt
// Designer promoted widgets: the constructor only takes a parent, and the
// caller must invoke setConfig() with the target program and scope before the
// widget becomes usable. Until setConfig() is called the widget is disabled.
class QWFASSOC_EXPORT ApplicationWidget : public QWidget {
Q_OBJECT
public:
// Configuration bundle passed to setConfig().
struct Config {
// Non-owning pointer to the wfassoc program. Must outlive the widget.
wfassocpp::Program* program = nullptr;
// Scope that install/unregister operations apply to.
TargetScope scope = TargetScope::User;
};
explicit ApplicationWidget(QWidget* parent = nullptr);
~ApplicationWidget() override;
// Two-phase initialization. Calling this with a non-null program enables
// the widget and triggers an initial refresh. Calling it with a null
// program (or not calling it at all) leaves the widget disabled.
void setConfig(const Config& config);
// Re-query the live wfassoc state and update the enabled state of the
// install / uninstall buttons. Called automatically by setConfig() and
// also intended to be called by the host whenever another component has
// mutated wfassoc state.
void refresh();
signals:
// Emitted whenever the user performs an action that mutates wfassoc
// state (i.e. install or uninstall). The host should refresh every
// widget that depends on wfassoc state in response.
void changed();
private slots:
void onInstallClicked();
void onUninstallClicked();
private:
Ui::ApplicationWidget* ui_;
wfassocpp::Program* program_ = nullptr;
TargetScope scope_ = TargetScope::User;
// Cached metadata used to fill the widget labels.
QString programName_;
QPixmap programIcon_;
};
} // namespace qwfassoc
#endif // QWFASSOC_APPLICATION_WIDGET_H_

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ApplicationWidget</class>
<widget class="QWidget" name="ApplicationWidget">
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<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="verticalSpacer">
<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>
<resources/>
<connections/>
</ui>

View File

@@ -1,5 +1,6 @@
#include "main_window.h" #include "association_widget.h"
#include "ui_main_window.h" #include "ui_association_widget.h"
#include "icon_utils.h"
#include <QHeaderView> #include <QHeaderView>
#include <QMessageBox> #include <QMessageBox>
@@ -7,19 +8,13 @@
#include <QPushButton> #include <QPushButton>
#include <QTableWidgetItem> #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> #include <stdexcept>
namespace qwfassoc { namespace qwfassoc {
// region: Helpers
namespace { namespace {
// Convert our internal TargetScope enum to the wfassocpp::Scope value used by // Convert the library's TargetScope enum to the wfassocpp::Scope value used by
// the program APIs (register / unregister / link / unlink / is_registered). // the program APIs (register / unregister / link / unlink / is_registered).
wfassocpp::Scope toWfassocScope(TargetScope scope) { wfassocpp::Scope toWfassocScope(TargetScope scope) {
return scope == TargetScope::User ? wfassocpp::Scope::User return scope == TargetScope::User ? wfassocpp::Scope::User
@@ -29,12 +24,16 @@ wfassocpp::Scope toWfassocScope(TargetScope scope) {
// Decide which CellState a queried ExtStatus corresponds to, given the // 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 "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. // resolved display name matches our own; otherwise it is treated as Other.
CellState classifyCell(const QString& selfName, const QString& observedName) { detail::CellState classifyCell(const QString& selfName,
return observedName == selfName ? CellState::Self : CellState::Other; const QString& observedName) {
return observedName == selfName ? detail::CellState::Self
: detail::CellState::Other;
} }
// Effective icon to draw for a cell, based on its state. // Effective icon to draw for a cell, based on its state.
QPixmap effectiveCellIcon(const ExtRow& row, const CellData& cell) { QPixmap effectiveCellIcon(const detail::ExtRow& row,
const detail::CellData& cell) {
using detail::CellState;
switch (cell.state) { switch (cell.state) {
case CellState::Blank: case CellState::Blank:
return QPixmap(); return QPixmap();
@@ -48,30 +47,10 @@ QPixmap effectiveCellIcon(const ExtRow& row, const CellData& cell) {
} // namespace } // namespace
// endregion AssociationWidget::AssociationWidget(QWidget* parent)
: QWidget(parent), ui_(new Ui::AssociationWidget) {
// 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); 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 // Fetch the current user name for the second column header. The USERNAME
// environment variable is good enough on Windows; fall back to a static // environment variable is good enough on Windows; fall back to a static
// translatable placeholder if it is unset for any reason. // translatable placeholder if it is unset for any reason.
@@ -79,105 +58,114 @@ MainWindow::MainWindow(wfassocpp::Program program,
QStringLiteral("USERNAME"), tr("User")); QStringLiteral("USERNAME"), tr("User"));
// Give the table as much vertical room as possible inside its layout. // Give the table as much vertical room as possible inside its layout.
ui_->associationsLayout->setStretch(2, 1); ui_->mainLayout->setStretch(2, 1);
// Reasonable default column widths so dotted extensions and ProgId names // Reasonable default column widths so dotted extensions and ProgId names
// stay readable in the 480px dialog. // stay readable in the 480px dialog the widget typically lives in.
ui_->assocTable->setColumnWidth(0, 90); ui_->assocTable->setColumnWidth(0, 90);
ui_->assocTable->setColumnWidth(1, 175); ui_->assocTable->setColumnWidth(1, 175);
ui_->assocTable->setColumnWidth(2, 175); ui_->assocTable->setColumnWidth(2, 175);
ui_->assocTable->verticalHeader()->setVisible(false); ui_->assocTable->verticalHeader()->setVisible(false);
ui_->assocTable->setShowGrid(true); 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, connect(ui_->selectUserButton, &QPushButton::clicked, this,
&MainWindow::onSelectUserClicked); &AssociationWidget::onSelectUserClicked);
connect(ui_->selectSystemButton, &QPushButton::clicked, this, connect(ui_->selectSystemButton, &QPushButton::clicked, this,
&MainWindow::onSelectSystemClicked); &AssociationWidget::onSelectSystemClicked);
connect(ui_->assocTable, &QTableWidget::cellClicked, this, connect(ui_->assocTable, &QTableWidget::cellClicked, this,
&MainWindow::onCellClicked); &AssociationWidget::onCellClicked);
connect(ui_->okButton, &QPushButton::clicked, this, connect(ui_->okButton, &QPushButton::clicked, this,
&MainWindow::onOkClicked); &AssociationWidget::onOkClicked);
connect(ui_->cancelButton, &QPushButton::clicked, this, connect(ui_->cancelButton, &QPushButton::clicked, this,
&MainWindow::onCancelClicked); &AssociationWidget::onCancelClicked);
connect(ui_->applyButton, &QPushButton::clicked, this, connect(ui_->applyButton, &QPushButton::clicked, this,
&MainWindow::onApplyClicked); &AssociationWidget::onApplyClicked);
retranslateUi(); // Until setConfig() is called we have no program to operate on; keep the
refreshProgramState(); // whole widget disabled.
setEnabled(false);
} }
MainWindow::~MainWindow() = default; AssociationWidget::~AssociationWidget() = default;
// endregion void AssociationWidget::setConfig(const Config& config) {
program_ = config.program;
scope_ = config.scope;
showOkCancelButtons_ = config.showOkCancelButtons;
// region: UI Text ui_->okButton->setVisible(showOkCancelButtons_);
ui_->cancelButton->setVisible(showOkCancelButtons_);
void MainWindow::retranslateUi() { if (program_ != nullptr) {
// The window title embeds the program name, so it is composed at runtime programName_ = QString::fromUtf8(program_->ResolveName());
// through tr() + QString::arg to stay translatable. ui_->assocHeaderLabel->setText(
setWindowTitle(tr("%1 Options").arg(programName_)); tr("File types associated with %1:").arg(programName_));
// Application tab. QStringList headers;
if (!programIcon_.isNull()) { headers << tr("Type") << userName_ << tr("All Users");
ui_->appIconLabel->setPixmap( ui_->assocTable->setHorizontalHeaderLabels(headers);
programIcon_.scaled(32, 32, Qt::KeepAspectRatio,
Qt::SmoothTransformation)); setEnabled(true);
refresh();
} else {
programName_.clear();
ui_->assocHeaderLabel->setText(QString());
ui_->assocTable->setRowCount(0);
rows_.clear();
setEnabled(false);
} }
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 void AssociationWidget::refresh() {
if (program_ == nullptr) {
return;
}
updateEnabledState();
rebuildTable();
}
// region: State Refresh void AssociationWidget::updateEnabledState() {
if (program_ == nullptr) {
ui_->selectUserButton->setEnabled(false);
ui_->selectSystemButton->setEnabled(false);
ui_->assocTable->setEnabled(false);
ui_->applyButton->setEnabled(false);
return;
}
void MainWindow::refreshProgramState() { const bool registered = program_->IsRegistered(toWfassocScope(scope_));
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 userColumnActive = registered;
const bool systemColumnActive = registered && isSystemColumnEnabled(); const bool systemColumnActive = registered && isSystemColumnEnabled();
ui_->selectUserButton->setEnabled(userColumnActive); ui_->selectUserButton->setEnabled(userColumnActive);
ui_->selectSystemButton->setEnabled(systemColumnActive); ui_->selectSystemButton->setEnabled(systemColumnActive);
ui_->assocTable->setEnabled(registered); ui_->assocTable->setEnabled(registered);
// The Apply button enable state is driven by pending changes too; only
rebuildTable(); // touch it here to make sure it's disabled when nothing is registered.
if (!registered) {
ui_->applyButton->setEnabled(false);
}
} }
void MainWindow::rebuildTable() { void AssociationWidget::rebuildTable() {
if (program_ == nullptr) {
return;
}
refreshing_ = true; refreshing_ = true;
rows_.clear(); rows_.clear();
const size_t count = program_.ExtsLen(); const size_t count = program_->ExtsLen();
rows_.reserve(count); rows_.reserve(count);
ui_->assocTable->setRowCount(static_cast<int>(count)); ui_->assocTable->setRowCount(static_cast<int>(count));
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
ExtRow row; detail::ExtRow row;
row.index = i; row.index = i;
// Self extension info: dotted body, display name and cached icon. // Self extension info: dotted body, display name and cached icon.
auto selfExt = program_.ResolveExt(i); auto selfExt = program_->ResolveExt(i);
row.extBody = QString::fromUtf8(selfExt.GetExt()); row.extBody = QString::fromUtf8(selfExt.GetExt());
row.dottedExt = QString::fromUtf8(selfExt.GetDottedExt()); row.dottedExt = QString::fromUtf8(selfExt.GetDottedExt());
row.selfName = QString::fromUtf8(selfExt.GetName()); row.selfName = QString::fromUtf8(selfExt.GetName());
@@ -186,7 +174,7 @@ void MainWindow::rebuildTable() {
// Query the user-view and system-view states. None means blank; // Query the user-view and system-view states. None means blank;
// a match against our self name means Self; anything else is Other // a match against our self name means Self; anything else is Other
// and we keep the original name/icon around for display. // and we keep the original name/icon around for display.
auto userStatus = program_.QueryExt(wfassocpp::View::User, i); auto userStatus = program_->QueryExt(wfassocpp::View::User, i);
if (userStatus) { if (userStatus) {
const QString observedName = const QString observedName =
QString::fromUtf8(userStatus->GetName()); QString::fromUtf8(userStatus->GetName());
@@ -196,7 +184,7 @@ void MainWindow::rebuildTable() {
icon_utils::fromHicon(userStatus->GetIcon()); icon_utils::fromHicon(userStatus->GetIcon());
} }
auto systemStatus = program_.QueryExt(wfassocpp::View::System, i); auto systemStatus = program_->QueryExt(wfassocpp::View::System, i);
if (systemStatus) { if (systemStatus) {
const QString observedName = const QString observedName =
QString::fromUtf8(systemStatus->GetName()); QString::fromUtf8(systemStatus->GetName());
@@ -216,7 +204,7 @@ void MainWindow::rebuildTable() {
// update their text/icon and flags. // update their text/icon and flags.
const int rowIdx = static_cast<int>(i); const int rowIdx = static_cast<int>(i);
auto* typeItem = new QTableWidgetItem(row.dottedExt); auto* typeItem = new QTableWidgetItem(rows_.back().dottedExt);
ui_->assocTable->setItem(rowIdx, 0, typeItem); ui_->assocTable->setItem(rowIdx, 0, typeItem);
auto* userItem = new QTableWidgetItem; auto* userItem = new QTableWidgetItem;
@@ -235,11 +223,13 @@ void MainWindow::rebuildTable() {
updateApplyButtonEnabled(); updateApplyButtonEnabled();
} }
void MainWindow::refreshRowDisplay(int row) { void AssociationWidget::refreshRowDisplay(int row) {
using detail::CellState;
if (row < 0 || row >= static_cast<int>(rows_.size())) { if (row < 0 || row >= static_cast<int>(rows_.size())) {
return; return;
} }
const ExtRow& r = rows_[row]; const detail::ExtRow& r = rows_[row];
// Column 0: hybrid icon (user-preferred) + dotted extension. // Column 0: hybrid icon (user-preferred) + dotted extension.
QPixmap hybridIcon; QPixmap hybridIcon;
@@ -285,9 +275,9 @@ void MainWindow::refreshRowDisplay(int row) {
} }
} }
void MainWindow::updateApplyButtonEnabled() { void AssociationWidget::updateApplyButtonEnabled() {
bool dirty = false; bool dirty = false;
for (const ExtRow& r : rows_) { for (const detail::ExtRow& r : rows_) {
if (r.pendingUser.state != r.initialUser.state || if (r.pendingUser.state != r.initialUser.state ||
r.pendingSystem.state != r.initialSystem.state) { r.pendingSystem.state != r.initialSystem.state) {
dirty = true; dirty = true;
@@ -297,11 +287,9 @@ void MainWindow::updateApplyButtonEnabled() {
ui_->applyButton->setEnabled(dirty); ui_->applyButton->setEnabled(dirty);
} }
// endregion void AssociationWidget::toggleCell(int row, int column) {
using detail::CellState;
// region: Cell Interaction
void MainWindow::toggleCell(int row, int column) {
if (refreshing_) { if (refreshing_) {
return; return;
} }
@@ -309,8 +297,8 @@ void MainWindow::toggleCell(int row, int column) {
return; return;
} }
CellData* cell = nullptr; detail::CellData* cell = nullptr;
const ExtRow* rowPtr = &rows_[row]; const detail::ExtRow* rowPtr = &rows_[row];
if (column == 1) { if (column == 1) {
cell = &rows_[row].pendingUser; cell = &rows_[row].pendingUser;
} else if (column == 2) { } else if (column == 2) {
@@ -337,7 +325,9 @@ void MainWindow::toggleCell(int row, int column) {
updateApplyButtonEnabled(); updateApplyButtonEnabled();
} }
void MainWindow::selectAllInScope(bool isUser) { void AssociationWidget::selectAllInScope(bool isUser) {
using detail::CellState;
if (!isUser && !isSystemColumnEnabled()) { if (!isUser && !isSystemColumnEnabled()) {
return; return;
} }
@@ -346,16 +336,16 @@ void MainWindow::selectAllInScope(bool isUser) {
// first click only fills blanks; otherwise the click overrides cells // first click only fills blanks; otherwise the click overrides cells
// pointing at other handlers as well. // pointing at other handlers as well.
bool hasBlank = false; bool hasBlank = false;
for (ExtRow& r : rows_) { for (detail::ExtRow& r : rows_) {
const CellData& cell = isUser ? r.pendingUser : r.pendingSystem; const detail::CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
if (cell.state == CellState::Blank) { if (cell.state == CellState::Blank) {
hasBlank = true; hasBlank = true;
break; break;
} }
} }
for (ExtRow& r : rows_) { for (detail::ExtRow& r : rows_) {
CellData& cell = isUser ? r.pendingUser : r.pendingSystem; detail::CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
if (hasBlank) { if (hasBlank) {
if (cell.state == CellState::Blank) { if (cell.state == CellState::Blank) {
cell.state = CellState::Self; cell.state = CellState::Self;
@@ -375,27 +365,23 @@ void MainWindow::selectAllInScope(bool isUser) {
updateApplyButtonEnabled(); updateApplyButtonEnabled();
} }
// endregion void AssociationWidget::applyAllChanges() {
// region: Apply
void MainWindow::applyAllChanges() {
// Walk through every row and commit any cell whose pending state differs // Walk through every row and commit any cell whose pending state differs
// from the initial snapshot. wfassoc's link/unlink take an index rather // 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. // than a scope/view, so we map columns back to (scope, index) pairs.
for (const ExtRow& r : rows_) { for (const detail::ExtRow& r : rows_) {
if (r.pendingUser.state != r.initialUser.state) { if (r.pendingUser.state != r.initialUser.state) {
if (r.pendingUser.state == CellState::Self) { if (r.pendingUser.state == detail::CellState::Self) {
program_.LinkExt(wfassocpp::Scope::User, r.index); program_->LinkExt(wfassocpp::Scope::User, r.index);
} else { } else {
program_.UnlinkExt(wfassocpp::Scope::User, r.index); program_->UnlinkExt(wfassocpp::Scope::User, r.index);
} }
} }
if (r.pendingSystem.state != r.initialSystem.state) { if (r.pendingSystem.state != r.initialSystem.state) {
if (r.pendingSystem.state == CellState::Self) { if (r.pendingSystem.state == detail::CellState::Self) {
program_.LinkExt(wfassocpp::Scope::System, r.index); program_->LinkExt(wfassocpp::Scope::System, r.index);
} else { } else {
program_.UnlinkExt(wfassocpp::Scope::System, r.index); program_->UnlinkExt(wfassocpp::Scope::System, r.index);
} }
} }
} }
@@ -404,83 +390,49 @@ void MainWindow::applyAllChanges() {
rebuildTable(); rebuildTable();
} }
bool MainWindow::isSystemColumnEnabled() const { bool AssociationWidget::isSystemColumnEnabled() const {
return scope_ == TargetScope::System; return scope_ == TargetScope::System;
} }
// endregion void AssociationWidget::onSelectUserClicked() {
// 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); selectAllInScope(/*isUser=*/true);
} }
void MainWindow::onSelectSystemClicked() { void AssociationWidget::onSelectSystemClicked() {
selectAllInScope(/*isUser=*/false); selectAllInScope(/*isUser=*/false);
} }
void MainWindow::onCellClicked(int row, int column) { void AssociationWidget::onCellClicked(int row, int column) {
toggleCell(row, column); toggleCell(row, column);
} }
void MainWindow::onOkClicked() { void AssociationWidget::onOkClicked() {
try { try {
applyAllChanges(); applyAllChanges();
} catch (const std::exception& e) { } catch (const std::exception& e) {
QMessageBox::critical(this, windowTitle(), QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
QString::fromUtf8(e.what()));
// Sync the table with the live registry, since some changes may have // Sync the table with the live registry, since some changes may have
// been committed before the failure. // been committed before the failure.
refreshProgramState(); refresh();
return; return;
} }
accept(); emit changed();
emit finished(/*accepted=*/true);
} }
void MainWindow::onCancelClicked() { void AssociationWidget::onCancelClicked() {
reject(); emit finished(/*accepted=*/false);
} }
void MainWindow::onApplyClicked() { void AssociationWidget::onApplyClicked() {
try { try {
applyAllChanges(); applyAllChanges();
} catch (const std::exception& e) { } catch (const std::exception& e) {
QMessageBox::critical(this, windowTitle(), QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
QString::fromUtf8(e.what())); refresh();
refreshProgramState();
return; return;
} }
emit changed();
} }
// endregion
} // namespace qwfassoc } // namespace qwfassoc

View File

@@ -1,32 +1,29 @@
#pragma once #pragma once
#ifndef QWFASSOC_MAIN_WINDOW_H_ #ifndef QWFASSOC_ASSOCIATION_WIDGET_H_
#define QWFASSOC_MAIN_WINDOW_H_ #define QWFASSOC_ASSOCIATION_WIDGET_H_
#include <QDialog>
#include <QPixmap> #include <QPixmap>
#include <QString> #include <QString>
#include <QWidget>
#include <memory>
#include <vector> #include <vector>
#include <wfassoc++.h> #include <wfassoc++.h>
#include "qwfassoc_global.h"
#include "scope.h"
namespace Ui { namespace Ui {
class MainWindow; class AssociationWidget;
} }
namespace qwfassoc { namespace qwfassoc {
// The target scope the application is being managed for. // Internal helper types used by AssociationWidget. They live in a `detail`
// This value comes from the --for command line argument and decides which // namespace to signal that they are not part of the public API even though
// columns in the file association table are interactive, as well as which // they need to be visible in the header.
// scope install/uninstall operate on. namespace detail {
enum class TargetScope {
User,
System,
};
// The pending state of a single (extension, scope) cell.
enum class CellState { enum class CellState {
// The extension has no associated handler in this scope. // The extension has no associated handler in this scope.
Blank, Blank,
@@ -68,42 +65,64 @@ struct ExtRow {
CellData pendingSystem; CellData pendingSystem;
}; };
// Main configuration dialog. Owns the wfassoc Program for its lifetime. } // namespace detail
class MainWindow : public QDialog {
// Widget showing the per-extension file-association status of a wfassoc
// program, and letting the user stage link / unlink operations.
//
// The widget follows the two-phase initialization pattern: the constructor
// only takes a parent, and the caller invokes setConfig() with the target
// program and scope before the widget becomes usable.
//
// By default the OK and Cancel buttons are hidden because they imply a
// dialog-level operation (close). Hosts that embed this widget in a dialog
// can enable them through Config::showOkCancelButtons and react to the
// finished() signal.
class QWFASSOC_EXPORT AssociationWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit MainWindow(wfassocpp::Program program, struct Config {
TargetScope scope, // Non-owning pointer to the wfassoc program. Must outlive the widget.
QWidget* parent = nullptr); wfassocpp::Program* program = nullptr;
~MainWindow() override; // Scope that link / unlink operations apply to. Also controls whether
// the system column is interactive (only System scope unlocks it).
TargetScope scope = TargetScope::User;
// Whether the OK and Cancel buttons are visible. They are hidden by
// default since closing the host window is a host-level decision.
bool showOkCancelButtons = false;
};
explicit AssociationWidget(QWidget* parent = nullptr);
~AssociationWidget() override;
// Two-phase initialization.
void setConfig(const Config& config);
// Re-query the live wfassoc state and rebuild the table.
void refresh();
signals:
// Emitted after the user applies pending changes (OK or Apply). The host
// should refresh every widget that depends on wfassoc state in response.
void changed();
// Emitted when the widget wants its host window to close. `accepted` is
// true when the OK button was used (after the changes were applied and
// changed() was emitted) and false when the Cancel button was used.
void finished(bool accepted);
private slots: 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(); void onSelectUserClicked();
// Slot connected to the "+" button above the system column.
void onSelectSystemClicked(); void onSelectSystemClicked();
// Slot connected to QTableWidget::cellClicked.
void onCellClicked(int row, int column); void onCellClicked(int row, int column);
// Slot connected to the "OK" button: apply pending changes then close.
void onOkClicked(); void onOkClicked();
// Slot connected to the "Cancel" button: close without applying.
void onCancelClicked(); void onCancelClicked();
// Slot connected to the "Apply" button: apply pending changes, stay open.
void onApplyClicked(); void onApplyClicked();
private: private:
// Re-apply labels that depend on the resolved program name and the // Refresh install / apply button enable state based on the live registry
// current user name. Translatable strings use tr() so they get picked up // and the pending edits.
// by Qt's translation tooling. void updateEnabledState();
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. // Drop and rebuild the table contents from the live registry.
void rebuildTable(); void rebuildTable();
// Refresh a single row's displayed cells from its pending state. // Refresh a single row's displayed cells from its pending state.
@@ -119,19 +138,18 @@ private:
void selectAllInScope(bool isUser); void selectAllInScope(bool isUser);
// Commit every pending change to the registry via wfassoc. // Commit every pending change to the registry via wfassoc.
void applyAllChanges(); void applyAllChanges();
// True when the system column should be interactive. // True when the system column should be interactive.
bool isSystemColumnEnabled() const; bool isSystemColumnEnabled() const;
std::unique_ptr<Ui::MainWindow> ui_; Ui::AssociationWidget* ui_;
wfassocpp::Program program_; wfassocpp::Program* program_ = nullptr;
TargetScope scope_; TargetScope scope_ = TargetScope::User;
bool showOkCancelButtons_ = false;
QString programName_; QString programName_;
QPixmap programIcon_;
QString userName_; QString userName_;
std::vector<ExtRow> rows_; std::vector<detail::ExtRow> rows_;
// Re-entrancy guard used while rebuilding the table to avoid feeding // Re-entrancy guard used while rebuilding the table to avoid feeding
// model-change signals back into toggleCell(). // model-change signals back into toggleCell().
bool refreshing_ = false; bool refreshing_ = false;
@@ -139,4 +157,4 @@ private:
} // namespace qwfassoc } // namespace qwfassoc
#endif // QWFASSOC_MAIN_WINDOW_H_ #endif // QWFASSOC_ASSOCIATION_WIDGET_H_

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AssociationWidget</class>
<widget class="QWidget" name="AssociationWidget">
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<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>
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="visible">
<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>
<resources/>
<connections/>
</ui>

View File

@@ -6,6 +6,8 @@
#include <wfassoc++.h> #include <wfassoc++.h>
#include "qwfassoc_global.h"
namespace qwfassoc { namespace qwfassoc {
namespace icon_utils { namespace icon_utils {
@@ -16,7 +18,7 @@ namespace icon_utils {
// Windows) and then QPixmap::fromImage() using its rvalue-reference overload // Windows) and then QPixmap::fromImage() using its rvalue-reference overload
// so that no extra pixel buffer copy is performed. The returned pixmap is null // so that no extra pixel buffer copy is performed. The returned pixmap is null
// if the input handle is null. // if the input handle is null.
QPixmap fromHicon(wfassocpp::HICON handle); QWFASSOC_EXPORT QPixmap fromHicon(wfassocpp::HICON handle);
} // namespace icon_utils } // namespace icon_utils
} // namespace qwfassoc } // namespace qwfassoc

View File

@@ -6,8 +6,6 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <wfassoc++.h>
namespace qwfassoc { namespace qwfassoc {
// Description of a single extension declared in the manifest. // Description of a single extension declared in the manifest.
@@ -23,9 +21,11 @@ struct ManifestExt {
// In-memory representation of a wfassoc manifest TOML file. // In-memory representation of a wfassoc manifest TOML file.
// //
// This struct mirrors the Rust `Manifest` type defined in // This struct mirrors the Rust `Manifest` type defined in
// `wfassoc-exec/src/manifest.rs`. All validation that requires the wfassoc // `wfassoc-exec/src/manifest.rs`. It is a plain data struct: parsing from
// library itself (identifier regex, dangling references, etc.) is deferred to // TOML and conversion into a wfassocpp::Schema are intentionally kept out of
// `wfassocpp::Program` construction; this struct only performs TOML parsing. // the library so that the library itself does not depend on a TOML parser.
// Consumers (such as qwfassoc-standalone) are responsible for filling the
// fields in.
struct Manifest { struct Manifest {
std::string identifier; std::string identifier;
std::string path; std::string path;
@@ -39,15 +39,6 @@ struct Manifest {
std::map<std::string, std::string> icons; std::map<std::string, std::string> icons;
std::map<std::string, std::string> behaviors; std::map<std::string, std::string> behaviors;
std::map<std::string, ManifestExt> exts; 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 } // namespace qwfassoc

View File

@@ -0,0 +1,20 @@
#pragma once
#ifndef QWFASSOC_GLOBAL_H_
#define QWFASSOC_GLOBAL_H_
#include <qglobal.h>
// Standard Qt shared-library export/import macros.
//
// When the qwfassoc library itself is being built, QWFASSOC_LIBRARY is
// defined (see the library's CMakeLists.txt) and QWFASSOC_EXPORT expands to
// Q_DECL_EXPORT so that symbols are exported from the .dll. Consumers of the
// library leave QWFASSOC_LIBRARY undefined, so QWFASSOC_EXPORT expands to
// Q_DECL_IMPORT and the same symbols are imported.
#if defined(QWFASSOC_LIBRARY)
# define QWFASSOC_EXPORT Q_DECL_EXPORT
#else
# define QWFASSOC_EXPORT Q_DECL_IMPORT
#endif
#endif // QWFASSOC_GLOBAL_H_

View File

@@ -0,0 +1,20 @@
#pragma once
#ifndef QWFASSOC_SCOPE_H_
#define QWFASSOC_SCOPE_H_
namespace qwfassoc {
// The target scope an application is being managed for.
//
// This value typically comes from a `--for user` / `--for system` command
// line argument and decides which columns of the file-association table are
// interactive, as well as which scope install / unregister operations apply
// to.
enum class TargetScope {
User,
System,
};
} // namespace qwfassoc
#endif // QWFASSOC_SCOPE_H_

View File

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