ores.cpp.qt.controller_impl
Table of Contents
Dialog lifecycle and service calls. Qt UI component: model/view class or dialog wired to the service layer via request/response messages.
See the Template variable reference for the complete list of available variables and their semantics.
Extension points
<<paste:UUID>> markers in this template are paste-block injection
points — see paste blocks: injecting custom code into generated files
for the full mechanism. This template defines two:
52E70F3B-5903-4359-91FB-B533054A18AC— end ofshowListWindow(), after the generated signal connections. Extraconnect()wiring, e.g. relaying a cross-navigation signal declared via the header seam (66E078FB-D6A6-4DF6-814B-65995D948090in ores.cpp.qt.controller_header).DF72DFCF-FD65-4FE5-98BA-CB8168CE0FDF— end of the generatedwireDetailDialogCommon()helper (the single place every detail window construction site – add/edit/history-version/revert – wires a freshly-created<Entity>DetailDialog). Extraconnect(detailDialog, ...)wiring, e.g. relaying a custom dialog signal declared via the dialog header seam (EC19A4EE-363D-4D41-A33A-D5F85A139C80in ores.cpp.qt.detail_dialog_header) up through the controller signal declared via the seam above.
Template
The full template source. Edit here and re-tangle with
compass build --direct tangle_codegen_templates to regenerate
library/templates/cpp_qt_controller.cpp.mustache.
{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/cpp_qt.org. Edit the org source. }}
{{! Template to generate Qt controller source for domain entities }}
{{{cpp_license}}}
#include "ores.qt/{{domain_entity.entity_pascal}}Controller.hpp"
{{#domain_entity.qt.has_change_reason_cache}}
#include "ores.qt/ChangeReasonCache.hpp"
{{/domain_entity.qt.has_change_reason_cache}}
{{#domain_entity.qt.changed_event_class}}
#include "ores.eventing.api/domain/event_traits.hpp"
#include "{{domain_entity.qt.changed_event_include}}"
{{/domain_entity.qt.changed_event_class}}
#include <QFutureWatcher>
#include <QMdiSubWindow>
#include <QMessageBox>
#include <QPointer>
#include <QtConcurrent>
#include <algorithm>
#include "ores.qt/IconUtils.hpp"
#include "ores.qt/{{domain_entity.entity_pascal}}MdiWindow.hpp"
{{^domain_entity.qt.has_readonly_paginated_list}}
#include "ores.qt/{{domain_entity.entity_pascal}}DetailDialog.hpp"
#include "ores.qt/HistoryDialog.hpp"
{{/domain_entity.qt.has_readonly_paginated_list}}
#include "ores.qt/DetachableMdiSubWindow.hpp"
#include "ores.qt/UiPersistence.hpp"
#include "{{domain_entity.qt.protocol_include}}"
{{#domain_entity.qt.history_protocol_include}}
#include "{{domain_entity.qt.history_protocol_include}}"
{{/domain_entity.qt.history_protocol_include}}
{{#domain_entity.qt.has_uuid_primary_key}}
#include <boost/uuid/uuid_io.hpp>
{{/domain_entity.qt.has_uuid_primary_key}}
namespace ores::qt {
using namespace ores::logging;
{{#domain_entity.qt.changed_event_class}}
namespace {
constexpr std::string_view {{domain_entity.qt.item_var}}_event_name =
eventing::domain::event_traits<{{domain_entity.qt.changed_event_class}}>::name;
}
{{/domain_entity.qt.changed_event_class}}
{{domain_entity.entity_pascal}}Controller::{{domain_entity.entity_pascal}}Controller(
QMainWindow* mainWindow,
QMdiArea* mdiArea,
ClientManager* clientManager,
{{#domain_entity.qt.needs_image_cache}}
ImageCache* imageCache,
{{/domain_entity.qt.needs_image_cache}}
{{#domain_entity.qt.has_change_reason_cache}}
ChangeReasonCache* changeReasonCache,
{{/domain_entity.qt.has_change_reason_cache}}
const QString& username,
{{#domain_entity.qt.has_badge_columns}}
BadgeCache* badgeCache,
{{/domain_entity.qt.has_badge_columns}}
QObject* parent)
: EntityController(mainWindow, mdiArea, clientManager, username,
{{#domain_entity.qt.changed_event_class}}
{{domain_entity.qt.item_var}}_event_name,
{{/domain_entity.qt.changed_event_class}}
{{^domain_entity.qt.changed_event_class}}
std::string_view{},
{{/domain_entity.qt.changed_event_class}}
parent),
{{#domain_entity.qt.has_change_reason_cache}}
changeReasonCache_(changeReasonCache),
{{/domain_entity.qt.has_change_reason_cache}}
{{#domain_entity.qt.has_badge_columns}}
badgeCache_(badgeCache),
{{/domain_entity.qt.has_badge_columns}}
{{#domain_entity.qt.has_parent_scoped_list}}
{{domain_entity.qt.parent_key_param}}_(),
{{/domain_entity.qt.has_parent_scoped_list}}
listWindow_(nullptr),
listMdiSubWindow_(nullptr) {
{{#domain_entity.qt.needs_image_cache}}
setImageCache(imageCache);
{{/domain_entity.qt.needs_image_cache}}
BOOST_LOG_SEV(lg(), debug) << "{{domain_entity.entity_pascal}}Controller created";
}
{{#domain_entity.qt.has_parent_scoped_list}}
void {{domain_entity.entity_pascal}}Controller::showListWindow() {
showListWindow(QString());
}
void {{domain_entity.entity_pascal}}Controller::openForParent(
const QString& {{domain_entity.qt.parent_key_param}}) {
showListWindow({{domain_entity.qt.parent_key_param}});
}
void {{domain_entity.entity_pascal}}Controller::showListWindow(
const QString& {{domain_entity.qt.parent_key_param}}) {
BOOST_LOG_SEV(lg(), debug) << "showListWindow called for {{domain_entity.qt.parent_key_param}}="
<< {{domain_entity.qt.parent_key_param}}.toStdString();
{{domain_entity.qt.parent_key_param}}_ = {{domain_entity.qt.parent_key_param}};
const QString key = build_window_key("list", "{{domain_entity.entity_plural}}_" + {{domain_entity.qt.parent_key_param}});
{{/domain_entity.qt.has_parent_scoped_list}}
{{^domain_entity.qt.has_parent_scoped_list}}
void {{domain_entity.entity_pascal}}Controller::showListWindow() {
BOOST_LOG_SEV(lg(), debug) << "showListWindow called";
const QString key = build_window_key("list", "{{domain_entity.entity_plural}}");
{{/domain_entity.qt.has_parent_scoped_list}}
if (try_reuse_window(key)) {
BOOST_LOG_SEV(lg(), debug) << "Reusing existing list window";
return;
}
// Create new window
listWindow_ = new {{domain_entity.entity_pascal}}MdiWindow(clientManager_, username_{{#domain_entity.qt.has_badge_columns}}, badgeCache_{{/domain_entity.qt.has_badge_columns}}{{#domain_entity.qt.needs_image_cache}}, imageCache_{{/domain_entity.qt.needs_image_cache}}{{#domain_entity.qt.has_parent_scoped_list}}, {{domain_entity.qt.parent_key_param}}_{{/domain_entity.qt.has_parent_scoped_list}});
// Connect signals
connect(listWindow_, &{{domain_entity.entity_pascal}}MdiWindow::statusChanged,
this, &{{domain_entity.entity_pascal}}Controller::statusMessage);
connect(listWindow_, &{{domain_entity.entity_pascal}}MdiWindow::errorOccurred,
this, &{{domain_entity.entity_pascal}}Controller::errorMessage);
{{^domain_entity.qt.has_readonly_paginated_list}}
connect(listWindow_, &{{domain_entity.entity_pascal}}MdiWindow::show{{domain_entity.entity_pascal_short}}Details,
this, &{{domain_entity.entity_pascal}}Controller::onShowDetails);
connect(listWindow_, &{{domain_entity.entity_pascal}}MdiWindow::addNewRequested,
this, &{{domain_entity.entity_pascal}}Controller::onAddNewRequested);
connect(listWindow_, &{{domain_entity.entity_pascal}}MdiWindow::show{{domain_entity.entity_pascal_short}}History,
this, &{{domain_entity.entity_pascal}}Controller::onShowHistory);
{{/domain_entity.qt.has_readonly_paginated_list}}
{{#domain_entity.qt.related_entity_shortcuts}}
connect(listWindow_, &{{domain_entity.entity_pascal}}MdiWindow::show{{signal}}Requested,
this, &{{domain_entity.entity_pascal}}Controller::show{{signal}}Requested);
{{/domain_entity.qt.related_entity_shortcuts}}
{{! see * Extension points above }}
<<paste:52E70F3B-5903-4359-91FB-B533054A18AC>>
// Create MDI subwindow
listMdiSubWindow_ = new DetachableMdiSubWindow(mainWindow_);
listMdiSubWindow_->setWidget(listWindow_);
listMdiSubWindow_->setWindowTitle("{{domain_entity.qt.window_title}}");
listMdiSubWindow_->setWindowIcon(IconUtils::createRecoloredIcon(
Icon::{{domain_entity.qt.icon}}, IconUtils::DefaultIconColor));
listMdiSubWindow_->setAttribute(Qt::WA_DeleteOnClose);
listMdiSubWindow_->resize(listWindow_->sizeHint());
mdiArea_->addSubWindow(listMdiSubWindow_);
listMdiSubWindow_->show();
// Track window
track_window(key, listMdiSubWindow_);
register_detachable_window(listMdiSubWindow_);
listMdiSubWindow_->setGeometryKey(key);
UiPersistence::restoreMdiGeometry(key, listMdiSubWindow_);
// Cleanup when closed
{{#domain_entity.qt.has_parent_scoped_list}}
connect(listMdiSubWindow_, &QObject::destroyed, this,
[self = QPointer<{{domain_entity.entity_pascal}}Controller>(this), key,
destroyedSubWindow = listMdiSubWindow_]() {
if (!self) return;
self->untrack_window(key);
// A different parent's window may have opened (and become the
// tracked listWindow_/listMdiSubWindow_) since this one did --
// only clear the pointers if they still reference the window
// actually being destroyed, so closing an older parent-scoped
// window can't orphan a still-open, more-recently-opened one.
if (self->listMdiSubWindow_ == destroyedSubWindow) {
self->listWindow_ = nullptr;
self->listMdiSubWindow_ = nullptr;
}
});
{{/domain_entity.qt.has_parent_scoped_list}}
{{^domain_entity.qt.has_parent_scoped_list}}
connect(listMdiSubWindow_, &QObject::destroyed, this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this), key]() {
if (!self) return;
self->untrack_window(key);
self->listWindow_ = nullptr;
self->listMdiSubWindow_ = nullptr;
});
{{/domain_entity.qt.has_parent_scoped_list}}
BOOST_LOG_SEV(lg(), debug) << "{{domain_entity.entity_title}} list window created";
}
void {{domain_entity.entity_pascal}}Controller::closeAllWindows() {
BOOST_LOG_SEV(lg(), debug) << "closeAllWindows called";
// Close all managed windows
QList<QString> keys = managed_windows_.keys();
for (const QString& key : keys) {
if (auto* window = managed_windows_.value(key)) {
window->close();
}
}
managed_windows_.clear();
listWindow_ = nullptr;
listMdiSubWindow_ = nullptr;
}
void {{domain_entity.entity_pascal}}Controller::reloadListWindow() {
{{#domain_entity.qt.has_parent_scoped_list}}
// EntityController's listWindow()/reloadListWindow() contract tracks
// a single window per controller. With multiple parent-scoped list
// windows open concurrently (e.g. two different parents' lists),
// only the most-recently-opened one is reloaded/marked stale here --
// a known limitation of that single-window contract, not something
// this knob's templates alone can fix without widening
// EntityController itself to track N windows.
{{/domain_entity.qt.has_parent_scoped_list}}
if (listWindow_) {
listWindow_->reload();
}
}
{{^domain_entity.qt.has_readonly_paginated_list}}
void {{domain_entity.entity_pascal}}Controller::onShowDetails(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
BOOST_LOG_SEV(lg(), debug) << "Show details for: " << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
showDetailWindow({{domain_entity.qt.item_var}});
}
void {{domain_entity.entity_pascal}}Controller::onAddNewRequested() {
BOOST_LOG_SEV(lg(), info) << "Add new {{domain_entity.entity_singular_words}} requested";
showAddWindow();
}
{{#domain_entity.qt.has_explorer_api}}
void {{domain_entity.entity_pascal}}Controller::openAdd() {
showAddWindow();
}
{{#domain_entity.qt.has_parent_relationship}}
void {{domain_entity.entity_pascal}}Controller::openAddWithParent(
boost::uuids::uuid {{domain_entity.qt.parent_id_field_camel}}) {
showAddWindow({{domain_entity.qt.parent_id_field_camel}});
}
{{/domain_entity.qt.has_parent_relationship}}
void {{domain_entity.entity_pascal}}Controller::openEdit(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
showDetailWindow({{domain_entity.qt.item_var}});
}
void {{domain_entity.entity_pascal}}Controller::openHistory(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
showHistoryWindow({{domain_entity.qt.item_var}});
}
{{/domain_entity.qt.has_explorer_api}}
{{#domain_entity.qt.has_uuid_primary_key}}
void {{domain_entity.entity_pascal}}Controller::onShowHistory(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
BOOST_LOG_SEV(lg(), debug) << "Show history requested for: " << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
showHistoryWindow({{domain_entity.qt.item_var}});
}
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
void {{domain_entity.entity_pascal}}Controller::onShowHistory(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
BOOST_LOG_SEV(lg(), debug) << "Show history requested for: " << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
showHistoryWindow(QString::fromStdString({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}}));
}
{{/domain_entity.qt.has_uuid_primary_key}}
void {{domain_entity.entity_pascal}}Controller::wireDetailDialogCommon(
{{domain_entity.entity_pascal}}DetailDialog* detailDialog) {
{{#domain_entity.qt.has_change_reason_cache}}
if (changeReasonCache_)
detailDialog->setChangeReasonCache(changeReasonCache_);
{{/domain_entity.qt.has_change_reason_cache}}
{{#domain_entity.qt.needs_image_cache}}
detailDialog->setImageCache(imageCache_);
{{/domain_entity.qt.needs_image_cache}}
{{#domain_entity.qt.has_combo_badge_source}}
detailDialog->setBadgeCache(badgeCache_);
{{/domain_entity.qt.has_combo_badge_source}}
detailDialog->setClientManager(clientManager_);
detailDialog->setUsername(username_.toStdString());
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::statusMessage,
this, &{{domain_entity.entity_pascal}}Controller::statusMessage);
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::errorMessage,
this, &{{domain_entity.entity_pascal}}Controller::errorMessage);
{{! see * Extension points above }}
<<paste:DF72DFCF-FD65-4FE5-98BA-CB8168CE0FDF>>
}
{{#domain_entity.qt.has_parent_relationship}}
void {{domain_entity.entity_pascal}}Controller::showAddWindow(
boost::uuids::uuid {{domain_entity.qt.parent_id_field_camel}}) {
{{/domain_entity.qt.has_parent_relationship}}
{{^domain_entity.qt.has_parent_relationship}}
void {{domain_entity.entity_pascal}}Controller::showAddWindow() {
{{/domain_entity.qt.has_parent_relationship}}
BOOST_LOG_SEV(lg(), debug) << "Creating add window for new {{domain_entity.entity_singular_words}}";
auto* detailDialog = new {{domain_entity.entity_pascal}}DetailDialog(mainWindow_);
wireDetailDialogCommon(detailDialog);
{{#domain_entity.qt.has_parent_relationship}}
if (!{{domain_entity.qt.parent_id_field_camel}}.is_nil()) {
{{domain_entity.qt.domain_class}} prefilled;
prefilled.{{domain_entity.qt.parent_id_field}} = {{domain_entity.qt.parent_id_field_camel}};
detailDialog->set{{domain_entity.entity_pascal_short}}(prefilled);
}
{{/domain_entity.qt.has_parent_relationship}}
detailDialog->setCreateMode(true);
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::{{domain_entity.qt.item_var}}Saved,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](const QString& code) {
if (!self) return;
BOOST_LOG_SEV(lg(), info) << "{{domain_entity.entity_title}} saved: " << code.toStdString();
self->handleEntitySaved();
});
auto* detailWindow = new DetachableMdiSubWindow(mainWindow_);
detailWindow->setAttribute(Qt::WA_DeleteOnClose);
detailWindow->setWidget(detailDialog);
detailWindow->setWindowTitle("New {{domain_entity.entity_title}}");
detailWindow->setWindowIcon(IconUtils::createRecoloredIcon(
Icon::{{domain_entity.qt.icon}}, IconUtils::DefaultIconColor));
register_detachable_window(detailWindow);
connect_dialog_close(detailDialog, detailWindow);
show_managed_window(detailWindow, listMdiSubWindow_);
}
void {{domain_entity.entity_pascal}}Controller::showDetailWindow(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
const QString identifier = QString::fromStdString({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}});
const QString key = build_window_key("details", identifier);
if (try_reuse_window(key)) {
BOOST_LOG_SEV(lg(), debug) << "Reusing existing detail window";
return;
}
BOOST_LOG_SEV(lg(), debug) << "Creating detail window for: " << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
auto* detailDialog = new {{domain_entity.entity_pascal}}DetailDialog(mainWindow_);
wireDetailDialogCommon(detailDialog);
detailDialog->setCreateMode(false);
detailDialog->set{{domain_entity.entity_pascal_short}}({{domain_entity.qt.item_var}});
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::{{domain_entity.qt.item_var}}Saved,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](const QString& code) {
if (!self) return;
BOOST_LOG_SEV(lg(), info) << "{{domain_entity.entity_title}} saved: " << code.toStdString();
self->handleEntitySaved();
});
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::{{domain_entity.qt.item_var}}Deleted,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this), key](const QString& code) {
if (!self) return;
BOOST_LOG_SEV(lg(), info) << "{{domain_entity.entity_title}} deleted: " << code.toStdString();
self->handleEntityDeleted();
});
auto* detailWindow = new DetachableMdiSubWindow(mainWindow_);
detailWindow->setAttribute(Qt::WA_DeleteOnClose);
detailWindow->setWidget(detailDialog);
detailWindow->setWindowTitle(QString("{{domain_entity.entity_title}}: %1").arg(identifier));
detailWindow->setWindowIcon(IconUtils::createRecoloredIcon(
Icon::{{domain_entity.qt.icon}}, IconUtils::DefaultIconColor));
// Track window
track_window(key, detailWindow);
register_detachable_window(detailWindow);
detailWindow->setGeometryKey(key);
QPointer<{{domain_entity.entity_pascal}}Controller> self = this;
connect(detailWindow, &QObject::destroyed, this,
[self, key]() {
if (self) {
self->untrack_window(key);
}
});
connect_dialog_close(detailDialog, detailWindow);
show_managed_window(detailWindow, listMdiSubWindow_);
}
{{#domain_entity.qt.has_uuid_primary_key}}
void {{domain_entity.entity_pascal}}Controller::showHistoryWindow(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
const QString code = QString::fromStdString({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}});
BOOST_LOG_SEV(lg(), info) << "Opening history window for {{domain_entity.entity_singular_words}}: "
<< {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
const QString windowKey = build_window_key("history", code);
// Try to reuse existing window
if (try_reuse_window(windowKey)) {
BOOST_LOG_SEV(lg(), info) << "Reusing existing history window for: "
<< {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
return;
}
BOOST_LOG_SEV(lg(), info) << "Creating new history window for: "
<< {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
const QString entityId = QString::fromStdString(boost::uuids::to_string(
{{domain_entity.qt.item_var}}.{{#domain_entity.qt.id_access}}{{domain_entity.qt.id_access}}{{/domain_entity.qt.id_access}}{{^domain_entity.qt.id_access}}id{{/domain_entity.qt.id_access}}));
auto* historyDialog = new HistoryDialog(
std::string(entity_type_of({{domain_entity.qt.domain_class}}{})),
entityId.toStdString(), clientManager_, mainWindow_);
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
void {{domain_entity.entity_pascal}}Controller::showHistoryWindow(const QString& code) {
BOOST_LOG_SEV(lg(), info) << "Opening history window for {{domain_entity.entity_singular_words}}: "
<< code.toStdString();
const QString windowKey = build_window_key("history", code);
// Try to reuse existing window
if (try_reuse_window(windowKey)) {
BOOST_LOG_SEV(lg(), info) << "Reusing existing history window for: "
<< code.toStdString();
return;
}
BOOST_LOG_SEV(lg(), info) << "Creating new history window for: "
<< code.toStdString();
auto* historyDialog = new HistoryDialog(
std::string(entity_type_of({{domain_entity.qt.domain_class}}{})),
code.toStdString(), clientManager_, mainWindow_);
{{/domain_entity.qt.has_uuid_primary_key}}
connect(historyDialog, &HistoryDialog::statusChanged,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](const QString& message) {
if (!self) return;
emit self->statusMessage(message);
});
connect(historyDialog, &HistoryDialog::errorOccurred,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](const QString& message) {
if (!self) return;
emit self->errorMessage(message);
});
connect(historyDialog, &HistoryDialog::revertVersionRequested,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](
const QString& /*entityType*/, const QString& entityId, int version) {
if (!self) return;
self->onRevertHistoryVersion(entityId, version);
});
connect(historyDialog, &HistoryDialog::openVersionRequested,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](
const QString& /*entityType*/, const QString& entityId, int version) {
if (!self) return;
self->onOpenHistoryVersion(entityId, version);
});
// Load history data
historyDialog->loadHistory();
auto* historyWindow = new DetachableMdiSubWindow(mainWindow_);
historyWindow->setAttribute(Qt::WA_DeleteOnClose);
historyWindow->setWidget(historyDialog);
historyWindow->setWindowTitle(QString("{{domain_entity.entity_title}} History: %1").arg(code));
historyWindow->setWindowIcon(IconUtils::createRecoloredIcon(
Icon::History, IconUtils::DefaultIconColor));
connect_dialog_close(historyDialog, historyWindow);
// Track this history window
track_window(windowKey, historyWindow);
register_detachable_window(historyWindow);
historyWindow->setGeometryKey(windowKey);
QPointer<{{domain_entity.entity_pascal}}Controller> self = this;
connect(historyWindow, &QObject::destroyed, this,
[self, windowKey]() {
if (self) {
self->untrack_window(windowKey);
}
});
show_managed_window(historyWindow, listMdiSubWindow_);
}
void {{domain_entity.entity_pascal}}Controller::onOpenVersion(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}, int versionNumber{{#domain_entity.qt.has_version_navigation}},
const std::vector<{{domain_entity.qt.domain_class}}>& fullHistory{{/domain_entity.qt.has_version_navigation}}) {
BOOST_LOG_SEV(lg(), info) << "Opening historical version " << versionNumber
<< " for {{domain_entity.entity_singular_words}}: " << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};
const QString code = QString::fromStdString({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}});
const QString windowKey = build_window_key("version", QString("%1_v%2")
.arg(code).arg(versionNumber));
// Try to reuse existing window
if (try_reuse_window(windowKey)) {
BOOST_LOG_SEV(lg(), info) << "Reusing existing version window";
return;
}
auto* detailDialog = new {{domain_entity.entity_pascal}}DetailDialog(mainWindow_);
wireDetailDialogCommon(detailDialog);
{{#domain_entity.qt.has_version_navigation}}
// A single version-nav toolbar (first/prev/next/last) only means
// something if the dialog has the *full* history to navigate, not
// just this one version.
if (!fullHistory.empty()) {
detailDialog->setHistory(fullHistory, versionNumber);
} else {
detailDialog->set{{domain_entity.entity_pascal_short}}({{domain_entity.qt.item_var}});
detailDialog->setReadOnly(true, versionNumber);
}
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::revertRequested,
this, &{{domain_entity.entity_pascal}}Controller::onRevertVersion);
{{/domain_entity.qt.has_version_navigation}}
{{^domain_entity.qt.has_version_navigation}}
detailDialog->set{{domain_entity.entity_pascal_short}}({{domain_entity.qt.item_var}});
detailDialog->setReadOnly(true);
{{/domain_entity.qt.has_version_navigation}}
auto* detailWindow = new DetachableMdiSubWindow(mainWindow_);
detailWindow->setAttribute(Qt::WA_DeleteOnClose);
detailWindow->setWidget(detailDialog);
detailWindow->setWindowTitle(QString("{{domain_entity.entity_title}}: %1 (Version %2)")
.arg(code).arg(versionNumber));
detailWindow->setWindowIcon(IconUtils::createRecoloredIcon(
Icon::History, IconUtils::DefaultIconColor));
track_window(windowKey, detailWindow);
register_detachable_window(detailWindow);
QPointer<{{domain_entity.entity_pascal}}Controller> self = this;
connect(detailWindow, &QObject::destroyed, this,
[self, windowKey]() {
if (self) {
self->untrack_window(windowKey);
}
});
connect_dialog_close(detailDialog, detailWindow);
show_managed_window(detailWindow, listMdiSubWindow_, QPoint(60, 60));
}
void {{domain_entity.entity_pascal}}Controller::fetch{{domain_entity.entity_pascal}}History(
const QString& entityId,
std::function<void(std::expected<std::vector<{{domain_entity.qt.domain_class}}>, QString>)> callback) {
{{domain_entity.qt.history_request_class}} request;
{{#domain_entity.qt.has_uuid_primary_key}}
request.id = entityId.toStdString();
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
{{#domain_entity.qt.history_request_id_field}}
request.{{domain_entity.qt.history_request_id_field}} = entityId.toStdString();
{{/domain_entity.qt.history_request_id_field}}
{{^domain_entity.qt.history_request_id_field}}
request.{{domain_entity.primary_key.column}} = entityId.toStdString();
{{/domain_entity.qt.history_request_id_field}}
{{/domain_entity.qt.has_uuid_primary_key}}
using FetchResult = std::expected<std::vector<{{domain_entity.qt.domain_class}}>, QString>;
QPointer<{{domain_entity.entity_pascal}}Controller> self = this;
QPointer<ClientManager> clientManager = clientManager_;
auto future = QtConcurrent::run([clientManager, request = std::move(request)]() -> FetchResult {
if (!clientManager || !clientManager->isConnected())
return std::unexpected(QString("Not connected to server"));
auto result = clientManager->process_authenticated_request(std::move(request));
if (!result)
return std::unexpected(QString::fromStdString(result.error()));
if (!result->success)
return std::unexpected(QString::fromStdString(result->message));
return std::move(result->{{domain_entity.qt.history_response_data_field}});
});
auto* watcher = new QFutureWatcher<FetchResult>(this);
connect(watcher, &QFutureWatcher<FetchResult>::finished, this,
[self, watcher, callback = std::move(callback)]() mutable {
auto result = watcher->result();
watcher->deleteLater();
if (!self) return;
callback(std::move(result));
});
watcher->setFuture(future);
}
void {{domain_entity.entity_pascal}}Controller::onOpenHistoryVersion(const QString& entityId, int versionNumber) {
QPointer<{{domain_entity.entity_pascal}}Controller> self = this;
fetch{{domain_entity.entity_pascal}}History(entityId,
[self, entityId, versionNumber](std::expected<std::vector<{{domain_entity.qt.domain_class}}>, QString> result) {
if (!self) return;
if (!result) {
emit self->errorMessage(QString("Failed to load history for '%1': %2")
.arg(entityId).arg(result.error()));
return;
}
const auto& history = *result;
const auto it = std::find_if(history.begin(), history.end(),
[&](const auto& v) { return v.version == versionNumber; });
if (it == history.end()) {
emit self->errorMessage(
QString("Version %1 not found for '%2'").arg(versionNumber).arg(entityId));
return;
}
{{#domain_entity.qt.has_version_navigation}}
self->onOpenVersion(*it, versionNumber, history);
{{/domain_entity.qt.has_version_navigation}}
{{^domain_entity.qt.has_version_navigation}}
self->onOpenVersion(*it, versionNumber);
{{/domain_entity.qt.has_version_navigation}}
});
}
void {{domain_entity.entity_pascal}}Controller::onRevertHistoryVersion(const QString& entityId, int versionNumber) {
QPointer<{{domain_entity.entity_pascal}}Controller> self = this;
fetch{{domain_entity.entity_pascal}}History(entityId,
[self, entityId, versionNumber](std::expected<std::vector<{{domain_entity.qt.domain_class}}>, QString> result) {
if (!self) return;
if (!result) {
emit self->errorMessage(QString("Failed to load history for '%1': %2")
.arg(entityId).arg(result.error()));
return;
}
const auto& history = *result;
const auto it = std::find_if(history.begin(), history.end(),
[&](const auto& v) { return v.version == versionNumber; });
if (it == history.end()) {
emit self->errorMessage(
QString("Version %1 not found for '%2'").arg(versionNumber).arg(entityId));
return;
}
self->onRevertVersion(*it);
});
}
{{/domain_entity.qt.has_readonly_paginated_list}}
Reverting to a historical version
onRevertVersion is invoked when the user picks a row in the history
dialog and clicks Revert. The history dialog hands us the full domain
object as it looked at that past version — every field populated with the
values that were live back then, including that row's old version number.
The user's mental model of "revert" is: bring the entity's current state back to how it looked at version N. Crucially this is not a rewrite of history and not an in-place edit of the old row — the bitemporal store is append-only. Reverting must therefore create a brand-new latest version whose field values happen to match version N. After the revert the version sequence reads 1, 2, …, current, current+1, where current+1 is a faithful copy of N's fields. Nothing earlier is mutated; the revert itself is just another audited change in the timeline.
The one subtlety — and the bug this guards against — is the version
field. The save path sends the domain object to the ..._insert_fn trigger,
which enforces optimistic concurrency:
if found and new.version <> 0 and new.version <> current_version then
raise exception 'Version conflict'
If we handed the dialog the historical object untouched, new.version
would be N (a stale number, not the current head), so the trigger would
reject the save as a conflict — the symptom being "I click Save and nothing
happens". We therefore reset version to the 0 sentinel. Zero tells the
trigger "I am not asserting any particular base version — just append":
it skips the conflict check and assigns new.version = current_version + 1.
Everything else is the normal amend flow: the dialog opens in edit mode
(setCreateMode(false)), the user is still prompted for a change reason
(so the revert is properly audited), and on success the list refreshes via
handleEntitySaved(). The window is titled "Revert …" and carries the
counter-clockwise-arrow icon to distinguish it from a plain edit.
{{^domain_entity.qt.has_readonly_paginated_list}}
void {{domain_entity.entity_pascal}}Controller::onRevertVersion(
const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
BOOST_LOG_SEV(lg(), info) << "Reverting {{domain_entity.entity_singular_words}} to version: "
<< {{domain_entity.qt.item_var}}.{{#domain_entity.qt.version_access}}{{domain_entity.qt.version_access}}{{/domain_entity.qt.version_access}}{{^domain_entity.qt.version_access}}version{{/domain_entity.qt.version_access}};
// Open detail dialog with the old version data for editing
auto* detailDialog = new {{domain_entity.entity_pascal}}DetailDialog(mainWindow_);
wireDetailDialogCommon(detailDialog);
auto reverted_{{domain_entity.qt.item_var}} = {{domain_entity.qt.item_var}};
reverted_{{domain_entity.qt.item_var}}.{{#domain_entity.qt.version_access}}{{domain_entity.qt.version_access}}{{/domain_entity.qt.version_access}}{{^domain_entity.qt.version_access}}version{{/domain_entity.qt.version_access}} = 0;
detailDialog->set{{domain_entity.entity_pascal_short}}(reverted_{{domain_entity.qt.item_var}});
detailDialog->setCreateMode(false);
detailDialog->markDirty();
connect(detailDialog, &{{domain_entity.entity_pascal}}DetailDialog::{{domain_entity.qt.item_var}}Saved,
this, [self = QPointer<{{domain_entity.entity_pascal}}Controller>(this)](const QString& code) {
if (!self) return;
BOOST_LOG_SEV(lg(), info) << "{{domain_entity.entity_title}} reverted: " << code.toStdString();
emit self->statusMessage(QString("{{domain_entity.entity_title}} '%1' reverted successfully").arg(code));
self->handleEntitySaved();
});
auto* detailWindow = new DetachableMdiSubWindow(mainWindow_);
detailWindow->setAttribute(Qt::WA_DeleteOnClose);
detailWindow->setWidget(detailDialog);
detailWindow->setWindowTitle(QString("Revert {{domain_entity.entity_title}}: %1")
.arg(QString::fromStdString({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}})));
detailWindow->setWindowIcon(IconUtils::createRecoloredIcon(
Icon::ArrowRotateCounterclockwise, IconUtils::DefaultIconColor));
register_detachable_window(detailWindow);
connect_dialog_close(detailDialog, detailWindow);
show_managed_window(detailWindow, listMdiSubWindow_);
}
{{/domain_entity.qt.has_readonly_paginated_list}}
Remaining controller methods
The window-management accessors and lifecycle helpers below carry no entity-specific logic beyond the generated type names.
EntityListMdiWindow* {{domain_entity.entity_pascal}}Controller::listWindow() const {
return listWindow_;
}
{{#domain_entity.qt.changed_event_class}}
void {{domain_entity.entity_pascal}}Controller::notifyOpenDialogs(const QStringList& entityIds) {
for (auto it = managed_windows_.begin(); it != managed_windows_.end(); ++it) {
auto* window = it.value();
if (!window)
continue;
if (it.key().startsWith("details.")) {
if (auto* dialog = qobject_cast<DetailDialogBase*>(window->widget())) {
if (entityIds.isEmpty() || entityIds.contains(dialog->code())) {
dialog->markAsStale();
}
}
} else if (it.key().startsWith("history.")) {
if (auto* dialog = qobject_cast<HistoryDialogBase*>(window->widget())) {
if (entityIds.isEmpty() || entityIds.contains(dialog->code())) {
dialog->markAsStale();
}
}
}
}
}
{{/domain_entity.qt.changed_event_class}}
}
See also
- Parent facet: ores.cpp.qt
- Template variable reference