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