feat: include ai first step works
This commit is contained in:
486
example/qwfassoc/src/main_window.cpp
Normal file
486
example/qwfassoc/src/main_window.cpp
Normal file
@@ -0,0 +1,486 @@
|
||||
#include "main_window.h"
|
||||
#include "ui_main_window.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidgetItem>
|
||||
|
||||
// icon_utils.h pulls in <qt_windows.h>, so it is included after all Qt
|
||||
// headers to keep the Windows include ordering that Qt expects.
|
||||
#include "icon_utils.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace qwfassoc {
|
||||
|
||||
// region: Helpers
|
||||
|
||||
namespace {
|
||||
|
||||
// Convert our internal TargetScope enum to the wfassocpp::Scope value used by
|
||||
// the program APIs (register / unregister / link / unlink / is_registered).
|
||||
wfassocpp::Scope toWfassocScope(TargetScope scope) {
|
||||
return scope == TargetScope::User ? wfassocpp::Scope::User
|
||||
: wfassocpp::Scope::System;
|
||||
}
|
||||
|
||||
// Decide which CellState a queried ExtStatus corresponds to, given the
|
||||
// resolved "self" name for this extension. We treat the cell as Self when the
|
||||
// resolved display name matches our own; otherwise it is treated as Other.
|
||||
CellState classifyCell(const QString& selfName, const QString& observedName) {
|
||||
return observedName == selfName ? CellState::Self : CellState::Other;
|
||||
}
|
||||
|
||||
// Effective icon to draw for a cell, based on its state.
|
||||
QPixmap effectiveCellIcon(const ExtRow& row, const CellData& cell) {
|
||||
switch (cell.state) {
|
||||
case CellState::Blank:
|
||||
return QPixmap();
|
||||
case CellState::Self:
|
||||
return row.selfIcon;
|
||||
case CellState::Other:
|
||||
return cell.icon;
|
||||
}
|
||||
return QPixmap();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Construction
|
||||
|
||||
MainWindow::MainWindow(wfassocpp::Program program,
|
||||
TargetScope scope,
|
||||
QWidget* parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui::MainWindow),
|
||||
program_(std::move(program)),
|
||||
scope_(scope) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Resolve program metadata that several labels depend on. These calls may
|
||||
// throw std::runtime_error on failure; the caller (main.cpp) is expected
|
||||
// to handle that and present an error dialog before exiting.
|
||||
programName_ = QString::fromUtf8(program_.ResolveName());
|
||||
|
||||
{
|
||||
auto iconRc = program_.ResolveIcon();
|
||||
auto handle = iconRc.GetIcon();
|
||||
programIcon_ = icon_utils::fromHicon(handle);
|
||||
}
|
||||
|
||||
// Fetch the current user name for the second column header. The USERNAME
|
||||
// environment variable is good enough on Windows; fall back to a static
|
||||
// translatable placeholder if it is unset for any reason.
|
||||
userName_ = QProcessEnvironment::systemEnvironment().value(
|
||||
QStringLiteral("USERNAME"), tr("User"));
|
||||
|
||||
// Give the table as much vertical room as possible inside its layout.
|
||||
ui_->associationsLayout->setStretch(2, 1);
|
||||
|
||||
// Reasonable default column widths so dotted extensions and ProgId names
|
||||
// stay readable in the 480px dialog.
|
||||
ui_->assocTable->setColumnWidth(0, 90);
|
||||
ui_->assocTable->setColumnWidth(1, 175);
|
||||
ui_->assocTable->setColumnWidth(2, 175);
|
||||
ui_->assocTable->verticalHeader()->setVisible(false);
|
||||
ui_->assocTable->setShowGrid(true);
|
||||
|
||||
// Wire signals.
|
||||
connect(ui_->installButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onInstallClicked);
|
||||
connect(ui_->uninstallButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onUninstallClicked);
|
||||
connect(ui_->selectUserButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onSelectUserClicked);
|
||||
connect(ui_->selectSystemButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onSelectSystemClicked);
|
||||
connect(ui_->assocTable, &QTableWidget::cellClicked, this,
|
||||
&MainWindow::onCellClicked);
|
||||
connect(ui_->okButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onOkClicked);
|
||||
connect(ui_->cancelButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onCancelClicked);
|
||||
connect(ui_->applyButton, &QPushButton::clicked, this,
|
||||
&MainWindow::onApplyClicked);
|
||||
|
||||
retranslateUi();
|
||||
refreshProgramState();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: UI Text
|
||||
|
||||
void MainWindow::retranslateUi() {
|
||||
// The window title embeds the program name, so it is composed at runtime
|
||||
// through tr() + QString::arg to stay translatable.
|
||||
setWindowTitle(tr("%1 Options").arg(programName_));
|
||||
|
||||
// Application tab.
|
||||
if (!programIcon_.isNull()) {
|
||||
ui_->appIconLabel->setPixmap(
|
||||
programIcon_.scaled(32, 32, Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
}
|
||||
ui_->appDescLabel->setText(
|
||||
tr("Install or uninstall %1 here.").arg(programName_));
|
||||
|
||||
// File association tab.
|
||||
ui_->assocHeaderLabel->setText(
|
||||
tr("File types associated with %1:").arg(programName_));
|
||||
|
||||
QStringList headers;
|
||||
headers << tr("Type") << userName_ << tr("All Users");
|
||||
ui_->assocTable->setHorizontalHeaderLabels(headers);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: State Refresh
|
||||
|
||||
void MainWindow::refreshProgramState() {
|
||||
const bool registered = program_.IsRegistered(toWfassocScope(scope_));
|
||||
|
||||
ui_->installButton->setEnabled(!registered);
|
||||
ui_->uninstallButton->setEnabled(registered);
|
||||
|
||||
// The whole file-association tab is disabled until the application has
|
||||
// been registered in the active scope. Additionally, the system column is
|
||||
// permanently disabled when running in user mode.
|
||||
const bool userColumnActive = registered;
|
||||
const bool systemColumnActive = registered && isSystemColumnEnabled();
|
||||
|
||||
ui_->selectUserButton->setEnabled(userColumnActive);
|
||||
ui_->selectSystemButton->setEnabled(systemColumnActive);
|
||||
ui_->assocTable->setEnabled(registered);
|
||||
|
||||
rebuildTable();
|
||||
}
|
||||
|
||||
void MainWindow::rebuildTable() {
|
||||
refreshing_ = true;
|
||||
|
||||
rows_.clear();
|
||||
const size_t count = program_.ExtsLen();
|
||||
rows_.reserve(count);
|
||||
|
||||
ui_->assocTable->setRowCount(static_cast<int>(count));
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
ExtRow row;
|
||||
row.index = i;
|
||||
|
||||
// Self extension info: dotted body, display name and cached icon.
|
||||
auto selfExt = program_.ResolveExt(i);
|
||||
row.extBody = QString::fromUtf8(selfExt.GetExt());
|
||||
row.dottedExt = QString::fromUtf8(selfExt.GetDottedExt());
|
||||
row.selfName = QString::fromUtf8(selfExt.GetName());
|
||||
row.selfIcon = icon_utils::fromHicon(selfExt.GetIcon());
|
||||
|
||||
// Query the user-view and system-view states. None means blank;
|
||||
// a match against our self name means Self; anything else is Other
|
||||
// and we keep the original name/icon around for display.
|
||||
auto userStatus = program_.QueryExt(wfassocpp::View::User, i);
|
||||
if (userStatus) {
|
||||
const QString observedName =
|
||||
QString::fromUtf8(userStatus->GetName());
|
||||
row.initialUser.state = classifyCell(row.selfName, observedName);
|
||||
row.initialUser.name = observedName;
|
||||
row.initialUser.icon =
|
||||
icon_utils::fromHicon(userStatus->GetIcon());
|
||||
}
|
||||
|
||||
auto systemStatus = program_.QueryExt(wfassocpp::View::System, i);
|
||||
if (systemStatus) {
|
||||
const QString observedName =
|
||||
QString::fromUtf8(systemStatus->GetName());
|
||||
row.initialSystem.state =
|
||||
classifyCell(row.selfName, observedName);
|
||||
row.initialSystem.name = observedName;
|
||||
row.initialSystem.icon =
|
||||
icon_utils::fromHicon(systemStatus->GetIcon());
|
||||
}
|
||||
|
||||
row.pendingUser = row.initialUser;
|
||||
row.pendingSystem = row.initialSystem;
|
||||
|
||||
rows_.push_back(std::move(row));
|
||||
|
||||
// Create the QTableWidgetItem cells once; subsequent refreshes only
|
||||
// update their text/icon and flags.
|
||||
const int rowIdx = static_cast<int>(i);
|
||||
|
||||
auto* typeItem = new QTableWidgetItem(row.dottedExt);
|
||||
ui_->assocTable->setItem(rowIdx, 0, typeItem);
|
||||
|
||||
auto* userItem = new QTableWidgetItem;
|
||||
userItem->setTextAlignment(Qt::AlignCenter);
|
||||
ui_->assocTable->setItem(rowIdx, 1, userItem);
|
||||
|
||||
auto* systemItem = new QTableWidgetItem;
|
||||
systemItem->setTextAlignment(Qt::AlignCenter);
|
||||
ui_->assocTable->setItem(rowIdx, 2, systemItem);
|
||||
|
||||
refreshRowDisplay(rowIdx);
|
||||
}
|
||||
|
||||
refreshing_ = false;
|
||||
|
||||
updateApplyButtonEnabled();
|
||||
}
|
||||
|
||||
void MainWindow::refreshRowDisplay(int row) {
|
||||
if (row < 0 || row >= static_cast<int>(rows_.size())) {
|
||||
return;
|
||||
}
|
||||
const ExtRow& r = rows_[row];
|
||||
|
||||
// Column 0: hybrid icon (user-preferred) + dotted extension.
|
||||
QPixmap hybridIcon;
|
||||
if (r.pendingUser.state != CellState::Blank) {
|
||||
hybridIcon = effectiveCellIcon(r, r.pendingUser);
|
||||
} else if (r.pendingSystem.state != CellState::Blank) {
|
||||
hybridIcon = effectiveCellIcon(r, r.pendingSystem);
|
||||
}
|
||||
|
||||
QTableWidgetItem* typeItem = ui_->assocTable->item(row, 0);
|
||||
if (typeItem) {
|
||||
typeItem->setIcon(QIcon(hybridIcon));
|
||||
typeItem->setText(r.dottedExt);
|
||||
}
|
||||
|
||||
// Column 1: user scope display name.
|
||||
QTableWidgetItem* userItem = ui_->assocTable->item(row, 1);
|
||||
if (userItem) {
|
||||
userItem->setText(r.pendingUser.state == CellState::Blank
|
||||
? QString()
|
||||
: r.pendingUser.name);
|
||||
}
|
||||
|
||||
// Column 2: system scope display name. When the system column is
|
||||
// inactive (user-only run), the cells are flagged as disabled so that
|
||||
// clicks are ignored and the rendering is greyed out.
|
||||
QTableWidgetItem* systemItem = ui_->assocTable->item(row, 2);
|
||||
if (systemItem) {
|
||||
systemItem->setText(r.pendingSystem.state == CellState::Blank
|
||||
? QString()
|
||||
: r.pendingSystem.name);
|
||||
|
||||
const Qt::ItemFlags enabledFlags =
|
||||
Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
// Without Qt::ItemIsEnabled the cell renders disabled (greyed out)
|
||||
// and cellClicked is not emitted, so clicks are silently ignored.
|
||||
const Qt::ItemFlags disabledFlags = Qt::ItemIsSelectable;
|
||||
if (isSystemColumnEnabled()) {
|
||||
systemItem->setFlags(enabledFlags);
|
||||
} else {
|
||||
systemItem->setFlags(disabledFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateApplyButtonEnabled() {
|
||||
bool dirty = false;
|
||||
for (const ExtRow& r : rows_) {
|
||||
if (r.pendingUser.state != r.initialUser.state ||
|
||||
r.pendingSystem.state != r.initialSystem.state) {
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ui_->applyButton->setEnabled(dirty);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Cell Interaction
|
||||
|
||||
void MainWindow::toggleCell(int row, int column) {
|
||||
if (refreshing_) {
|
||||
return;
|
||||
}
|
||||
if (row < 0 || row >= static_cast<int>(rows_.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
CellData* cell = nullptr;
|
||||
const ExtRow* rowPtr = &rows_[row];
|
||||
if (column == 1) {
|
||||
cell = &rows_[row].pendingUser;
|
||||
} else if (column == 2) {
|
||||
if (!isSystemColumnEnabled()) {
|
||||
return;
|
||||
}
|
||||
cell = &rows_[row].pendingSystem;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle: Self -> Blank, anything else -> Self.
|
||||
if (cell->state == CellState::Self) {
|
||||
cell->state = CellState::Blank;
|
||||
cell->name.clear();
|
||||
cell->icon = QPixmap();
|
||||
} else {
|
||||
cell->state = CellState::Self;
|
||||
cell->name = rowPtr->selfName;
|
||||
cell->icon = rowPtr->selfIcon;
|
||||
}
|
||||
|
||||
refreshRowDisplay(row);
|
||||
updateApplyButtonEnabled();
|
||||
}
|
||||
|
||||
void MainWindow::selectAllInScope(bool isUser) {
|
||||
if (!isUser && !isSystemColumnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Progressively select more. If there is at least one blank cell, the
|
||||
// first click only fills blanks; otherwise the click overrides cells
|
||||
// pointing at other handlers as well.
|
||||
bool hasBlank = false;
|
||||
for (ExtRow& r : rows_) {
|
||||
const CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
|
||||
if (cell.state == CellState::Blank) {
|
||||
hasBlank = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (ExtRow& r : rows_) {
|
||||
CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
|
||||
if (hasBlank) {
|
||||
if (cell.state == CellState::Blank) {
|
||||
cell.state = CellState::Self;
|
||||
cell.name = r.selfName;
|
||||
cell.icon = r.selfIcon;
|
||||
}
|
||||
} else if (cell.state != CellState::Self) {
|
||||
cell.state = CellState::Self;
|
||||
cell.name = r.selfName;
|
||||
cell.icon = r.selfIcon;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < rows_.size(); ++i) {
|
||||
refreshRowDisplay(static_cast<int>(i));
|
||||
}
|
||||
updateApplyButtonEnabled();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Apply
|
||||
|
||||
void MainWindow::applyAllChanges() {
|
||||
// Walk through every row and commit any cell whose pending state differs
|
||||
// from the initial snapshot. wfassoc's link/unlink take an index rather
|
||||
// than a scope/view, so we map columns back to (scope, index) pairs.
|
||||
for (const ExtRow& r : rows_) {
|
||||
if (r.pendingUser.state != r.initialUser.state) {
|
||||
if (r.pendingUser.state == CellState::Self) {
|
||||
program_.LinkExt(wfassocpp::Scope::User, r.index);
|
||||
} else {
|
||||
program_.UnlinkExt(wfassocpp::Scope::User, r.index);
|
||||
}
|
||||
}
|
||||
if (r.pendingSystem.state != r.initialSystem.state) {
|
||||
if (r.pendingSystem.state == CellState::Self) {
|
||||
program_.LinkExt(wfassocpp::Scope::System, r.index);
|
||||
} else {
|
||||
program_.UnlinkExt(wfassocpp::Scope::System, r.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-query and rebuild the table so the UI reflects the live registry.
|
||||
rebuildTable();
|
||||
}
|
||||
|
||||
bool MainWindow::isSystemColumnEnabled() const {
|
||||
return scope_ == TargetScope::System;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Slots
|
||||
|
||||
void MainWindow::onInstallClicked() {
|
||||
try {
|
||||
program_.Register(toWfassocScope(scope_));
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
QString::fromUtf8(e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, windowTitle(),
|
||||
tr("Application installed successfully."));
|
||||
refreshProgramState();
|
||||
}
|
||||
|
||||
void MainWindow::onUninstallClicked() {
|
||||
try {
|
||||
program_.Unregister(toWfassocScope(scope_));
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
QString::fromUtf8(e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, windowTitle(),
|
||||
tr("Application uninstalled successfully."));
|
||||
refreshProgramState();
|
||||
}
|
||||
|
||||
void MainWindow::onSelectUserClicked() {
|
||||
selectAllInScope(/*isUser=*/true);
|
||||
}
|
||||
|
||||
void MainWindow::onSelectSystemClicked() {
|
||||
selectAllInScope(/*isUser=*/false);
|
||||
}
|
||||
|
||||
void MainWindow::onCellClicked(int row, int column) {
|
||||
toggleCell(row, column);
|
||||
}
|
||||
|
||||
void MainWindow::onOkClicked() {
|
||||
try {
|
||||
applyAllChanges();
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
QString::fromUtf8(e.what()));
|
||||
// Sync the table with the live registry, since some changes may have
|
||||
// been committed before the failure.
|
||||
refreshProgramState();
|
||||
return;
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
void MainWindow::onCancelClicked() {
|
||||
reject();
|
||||
}
|
||||
|
||||
void MainWindow::onApplyClicked() {
|
||||
try {
|
||||
applyAllChanges();
|
||||
} catch (const std::exception& e) {
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
QString::fromUtf8(e.what()));
|
||||
refreshProgramState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace qwfassoc
|
||||
Reference in New Issue
Block a user