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 of showListWindow(), after the generated signal connections. Extra connect() wiring, e.g. relaying a cross-navigation signal declared via the header seam (66E078FB-D6A6-4DF6-814B-65995D948090 in ores.cpp.qt.controller_header).
  • DF72DFCF-FD65-4FE5-98BA-CB8168CE0FDF — end of the generated wireDetailDialogCommon() helper (the single place every detail window construction site – add/edit/history-version/revert – wires a freshly-created <Entity>DetailDialog). Extra connect(detailDialog, ...) wiring, e.g. relaying a custom dialog signal declared via the dialog header seam (EC19A4EE-363D-4D41-A33A-D5F85A139C80 in 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

Emacs 29.3 (Org mode 9.6.15)