Archetype: cpp_qt_mdi_window.cpp.mustache

Table of Contents

List view window hosted in the main MDI area. 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.

Template

The full template source. Edit here and re-tangle with compass build --direct tangle_codegen_templates to regenerate library/templates/cpp_qt_mdi_window.cpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/cpp_qt.org. Edit the org source. }}
{{! Template to generate Qt MDI window source for domain entities }}
{{{cpp_license}}}
#include "ores.qt/{{domain_entity.entity_pascal}}MdiWindow.hpp"

#include <QVBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QtConcurrent>
#include <QFutureWatcher>
{{#domain_entity.qt.has_uuid_primary_key}}
#include <boost/uuid/uuid_io.hpp>
{{/domain_entity.qt.has_uuid_primary_key}}
#include "ores.qt/IconUtils.hpp"
#include "ores.qt/MessageBoxHelper.hpp"
#include "ores.qt/ColorConstants.hpp"
{{#domain_entity.qt.has_badge_columns}}
#include "ores.qt/EntityItemDelegate.hpp"
#include "ores.qt/BadgeCache.hpp"
{{/domain_entity.qt.has_badge_columns}}
#include "{{domain_entity.qt.protocol_include}}"

namespace ores::qt {

using namespace ores::logging;

{{domain_entity.entity_pascal}}MdiWindow::{{domain_entity.entity_pascal}}MdiWindow(
    ClientManager* clientManager,
    const QString& username,
{{#domain_entity.qt.has_badge_columns}}
    BadgeCache* badgeCache,
{{/domain_entity.qt.has_badge_columns}}
    QWidget* parent)
    : EntityListMdiWindow(parent),
      clientManager_(clientManager),
      username_(username),
{{#domain_entity.qt.has_badge_columns}}
      badgeCache_(badgeCache),
{{/domain_entity.qt.has_badge_columns}}
      toolbar_(nullptr),
      tableView_(nullptr),
      model_(nullptr),
      proxyModel_(nullptr),
      paginationWidget_(nullptr),
      reloadAction_(nullptr),
      addAction_(nullptr),
      editAction_(nullptr),
      deleteAction_(nullptr),
      historyAction_(nullptr) {

    setupUi();
    setupConnections();
    reload();
}

void {{domain_entity.entity_pascal}}MdiWindow::setupUi() {
    auto* layout = new QVBoxLayout(this);

    setupToolbar();
    layout->addWidget(toolbar_);
    layout->addWidget(loadingBar());

    setupTable();
    layout->addWidget(tableView_);

    paginationWidget_ = new PaginationWidget(this);
    layout->addWidget(paginationWidget_);
}

void {{domain_entity.entity_pascal}}MdiWindow::setupToolbar() {
    toolbar_ = new QToolBar(this);
    toolbar_->setMovable(false);
    toolbar_->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
    toolbar_->setIconSize(QSize(20, 20));

    reloadAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::ArrowClockwise, IconUtils::DefaultIconColor),
        tr("Reload"));
    connect(reloadAction_, &QAction::triggered, this,
            &EntityListMdiWindow::reload);

    initializeStaleIndicator(reloadAction_, IconUtils::iconPath(Icon::ArrowClockwise));

    toolbar_->addSeparator();

    addAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::Add, IconUtils::DefaultIconColor),
        tr("Add"));
    addAction_->setToolTip(tr("Add new {{domain_entity.entity_singular_words}}"));
    connect(addAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::addNew);

    editAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::Edit, IconUtils::DefaultIconColor),
        tr("Edit"));
    editAction_->setToolTip(tr("Edit selected {{domain_entity.entity_singular_words}}"));
    editAction_->setEnabled(false);
    connect(editAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::editSelected);

    deleteAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::Delete, IconUtils::DefaultIconColor),
        tr("Delete"));
    deleteAction_->setToolTip(tr("Delete selected {{domain_entity.entity_singular_words}}"));
    deleteAction_->setEnabled(false);
    connect(deleteAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::deleteSelected);

    historyAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::History, IconUtils::DefaultIconColor),
        tr("History"));
    historyAction_->setToolTip(tr("View {{domain_entity.entity_singular_words}} history"));
    historyAction_->setEnabled(false);
    connect(historyAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::viewHistorySelected);
}

void {{domain_entity.entity_pascal}}MdiWindow::setupTable() {
    model_ = new Client{{domain_entity.entity_pascal}}Model(clientManager_, this);
    proxyModel_ = new QSortFilterProxyModel(this);
    proxyModel_->setSourceModel(model_);
    proxyModel_->setSortCaseSensitivity(Qt::CaseInsensitive);

    tableView_ = new QTableView(this);
    tableView_->setModel(proxyModel_);
    tableView_->setSelectionBehavior(QAbstractItemView::SelectRows);
    tableView_->setSelectionMode(QAbstractItemView::SingleSelection);
    tableView_->setSortingEnabled(true);
    tableView_->setAlternatingRowColors(true);
    tableView_->verticalHeader()->setVisible(false);

{{#domain_entity.qt.has_badge_columns}}
    using cs = column_style;
    auto* delegate = new EntityItemDelegate({
{{#domain_entity.qt.columns}}
        {{column_style}},
{{/domain_entity.qt.columns}}
    }, tableView_);
{{#domain_entity.qt.columns}}
{{#is_badge}}
    delegate->set_badge_color_resolver({{column_index}}, [cache = badgeCache_](const QString& value) -> badge_color_pair {
        static const badge_color_pair default_gray{QColor(0x6B, 0x72, 0x80), Qt::white};
        if (!cache) return default_gray;
        auto* def = cache->resolve("{{badge_key}}", value.toStdString());
        if (!def) return default_gray;
        return {QColor(QString::fromStdString(def->background_colour)),
                QColor(QString::fromStdString(def->text_colour))};
    });
{{/is_badge}}
{{/domain_entity.qt.columns}}
    tableView_->setItemDelegate(delegate);
{{/domain_entity.qt.has_badge_columns}}

    initializeTableSettings(tableView_, model_,
        "{{domain_entity.qt.settings_group}}",
{{#domain_entity.qt.has_description_column}}
        {Client{{domain_entity.entity_pascal}}Model::Description},
{{/domain_entity.qt.has_description_column}}
{{^domain_entity.qt.has_description_column}}
        {},
{{/domain_entity.qt.has_description_column}}
        {900, 400}, 1);
}

void {{domain_entity.entity_pascal}}MdiWindow::setupConnections() {
    connect(model_, &Client{{domain_entity.entity_pascal}}Model::dataLoaded,
            this, &{{domain_entity.entity_pascal}}MdiWindow::onDataLoaded);
    connect(model_, &Client{{domain_entity.entity_pascal}}Model::loadError,
            this, &{{domain_entity.entity_pascal}}MdiWindow::onLoadError);

    connect(tableView_->selectionModel(), &QItemSelectionModel::selectionChanged,
            this, &{{domain_entity.entity_pascal}}MdiWindow::onSelectionChanged);
    connect(tableView_, &QTableView::doubleClicked,
            this, &{{domain_entity.entity_pascal}}MdiWindow::onDoubleClicked);

    connect(paginationWidget_, &PaginationWidget::page_size_changed,
            this, [this](std::uint32_t size) {
        model_->set_page_size(size);
        model_->refresh();
    });

    connect(paginationWidget_, &PaginationWidget::load_all_requested,
            this, [this]() {
        const auto total = model_->total_available_count();
        if (total > 0 && total <= 1000) {
            model_->set_page_size(total);
            model_->refresh();
        }
    });

    connect(paginationWidget_, &PaginationWidget::page_requested,
            this, [this](std::uint32_t offset, std::uint32_t limit) {
        model_->load_page(offset, limit);
    });

    connectModel(model_);
}

void {{domain_entity.entity_pascal}}MdiWindow::doReload() {
    BOOST_LOG_SEV(lg(), debug) << "Reloading {{domain_entity.entity_plural_words}}";
    clearStaleIndicator();
    emit statusChanged(tr("Loading {{domain_entity.entity_plural_words}}..."));
    model_->refresh();
}

void {{domain_entity.entity_pascal}}MdiWindow::onDataLoaded() {
    const auto loaded = model_->rowCount();
    const auto total = model_->total_available_count();
    emit statusChanged(tr("Loaded %1 of %2 {{domain_entity.entity_plural_words}}").arg(loaded).arg(total));

    paginationWidget_->update_state(loaded, total);
    paginationWidget_->set_load_all_enabled(
        loaded < static_cast<int>(total) && total > 0 && total <= 1000);
}

void {{domain_entity.entity_pascal}}MdiWindow::onLoadError(const QString& error_message,
                                          const QString& details) {
    BOOST_LOG_SEV(lg(), error) << "Load error: " << error_message.toStdString();
    emit errorOccurred(error_message);
    MessageBoxHelper::critical(this, tr("Load Error"), error_message, details);
}

void {{domain_entity.entity_pascal}}MdiWindow::onSelectionChanged() {
    updateActionStates();
}

void {{domain_entity.entity_pascal}}MdiWindow::onDoubleClicked(const QModelIndex& index) {
    if (!index.isValid())
        return;

    auto sourceIndex = proxyModel_->mapToSource(index);
    if (auto* {{domain_entity.qt.item_var}} = model_->get{{domain_entity.entity_pascal_short}}(sourceIndex.row())) {
        emit show{{domain_entity.entity_pascal_short}}Details(*{{domain_entity.qt.item_var}});
    }
}

void {{domain_entity.entity_pascal}}MdiWindow::updateActionStates() {
    const bool hasSelection = tableView_->selectionModel()->hasSelection();
    editAction_->setEnabled(hasSelection);
    deleteAction_->setEnabled(hasSelection);
    historyAction_->setEnabled(hasSelection);
}

void {{domain_entity.entity_pascal}}MdiWindow::addNew() {
    BOOST_LOG_SEV(lg(), debug) << "Add new {{domain_entity.entity_singular_words}} requested";
    emit addNewRequested();
}

void {{domain_entity.entity_pascal}}MdiWindow::editSelected() {
    const auto selected = tableView_->selectionModel()->selectedRows();
    if (selected.isEmpty()) {
        BOOST_LOG_SEV(lg(), warn) << "Edit requested but no row selected";
        return;
    }

    auto sourceIndex = proxyModel_->mapToSource(selected.first());
    if (auto* {{domain_entity.qt.item_var}} = model_->get{{domain_entity.entity_pascal_short}}(sourceIndex.row())) {
        emit show{{domain_entity.entity_pascal_short}}Details(*{{domain_entity.qt.item_var}});
    }
}

void {{domain_entity.entity_pascal}}MdiWindow::viewHistorySelected() {
    const auto selected = tableView_->selectionModel()->selectedRows();
    if (selected.isEmpty()) {
        BOOST_LOG_SEV(lg(), warn) << "View history requested but no row selected";
        return;
    }

    auto sourceIndex = proxyModel_->mapToSource(selected.first());
    if (auto* {{domain_entity.qt.item_var}} = model_->get{{domain_entity.entity_pascal_short}}(sourceIndex.row())) {
        BOOST_LOG_SEV(lg(), debug) << "Emitting show{{domain_entity.entity_pascal_short}}History for code: "
                                   << {{domain_entity.qt.item_var}}->{{domain_entity.qt.key_field}};
        emit show{{domain_entity.entity_pascal_short}}History(*{{domain_entity.qt.item_var}});
    }
}

void {{domain_entity.entity_pascal}}MdiWindow::deleteSelected() {
    const auto selected = tableView_->selectionModel()->selectedRows();
    if (selected.isEmpty()) {
        BOOST_LOG_SEV(lg(), warn) << "Delete requested but no row selected";
        return;
    }

    if (!clientManager_->isConnected()) {
        MessageBoxHelper::warning(this, "Disconnected",
            "Cannot delete {{domain_entity.entity_singular_words}} while disconnected.");
        return;
    }

{{#domain_entity.qt.has_uuid_primary_key}}
    std::vector<boost::uuids::uuid> ids;
    std::vector<std::string> codes;  // For display purposes
    for (const auto& index : selected) {
        auto sourceIndex = proxyModel_->mapToSource(index);
        if (auto* {{domain_entity.qt.item_var}} = model_->get{{domain_entity.entity_pascal_short}}(sourceIndex.row())) {
            ids.push_back({{domain_entity.qt.item_var}}->id);
            codes.push_back({{domain_entity.qt.item_var}}->{{domain_entity.qt.key_field}});
        }
    }

    if (ids.empty()) {
        BOOST_LOG_SEV(lg(), warn) << "No valid {{domain_entity.entity_plural_words}} to delete";
        return;
    }

    BOOST_LOG_SEV(lg(), debug) << "Delete requested for " << ids.size()
                               << " {{domain_entity.entity_plural_words}}";

    QString confirmMessage;
    if (ids.size() == 1) {
        confirmMessage = QString("Are you sure you want to delete {{domain_entity.entity_singular_words}} '%1'?")
            .arg(QString::fromStdString(codes.front()));
    } else {
        confirmMessage = QString("Are you sure you want to delete %1 {{domain_entity.entity_plural_words}}?")
            .arg(ids.size());
    }
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
    std::vector<std::string> codes;
    for (const auto& index : selected) {
        auto sourceIndex = proxyModel_->mapToSource(index);
        if (auto* {{domain_entity.qt.item_var}} = model_->get{{domain_entity.entity_pascal_short}}(sourceIndex.row())) {
            codes.push_back({{domain_entity.qt.item_var}}->{{domain_entity.qt.key_field}});
        }
    }

    if (codes.empty()) {
        BOOST_LOG_SEV(lg(), warn) << "No valid {{domain_entity.entity_plural_words}} to delete";
        return;
    }

    BOOST_LOG_SEV(lg(), debug) << "Delete requested for " << codes.size()
                               << " {{domain_entity.entity_plural_words}}";

    QString confirmMessage;
    if (codes.size() == 1) {
        confirmMessage = QString("Are you sure you want to delete {{domain_entity.entity_singular_words}} '%1'?")
            .arg(QString::fromStdString(codes.front()));
    } else {
        confirmMessage = QString("Are you sure you want to delete %1 {{domain_entity.entity_plural_words}}?")
            .arg(codes.size());
    }
{{/domain_entity.qt.has_uuid_primary_key}}

    auto reply = MessageBoxHelper::question(this, "Delete {{domain_entity.entity_title}}",
        confirmMessage, QMessageBox::Yes | QMessageBox::No);

    if (reply != QMessageBox::Yes) {
        BOOST_LOG_SEV(lg(), debug) << "Delete cancelled by user";
        return;
    }

    QPointer<{{domain_entity.entity_pascal}}MdiWindow> self = this;
{{#domain_entity.qt.has_uuid_primary_key}}
    using DeleteResult = std::vector<std::tuple<boost::uuids::uuid, std::string, bool, std::string>>;

    auto task = [self, ids, codes]() -> DeleteResult {
        DeleteResult results;
        if (!self) return {};

        BOOST_LOG_SEV(lg(), debug) << "Making delete request for "
                                   << ids.size() << " {{domain_entity.entity_plural_words}}";

{{#domain_entity.qt.delete_is_single}}
        for (std::size_t i = 0; i < ids.size(); ++i) {
            {{domain_entity.qt.delete_request_class}} request;
            request.{{domain_entity.qt.delete_request_id_field}} = boost::uuids::to_string(ids[i]);
            request.modified_by = self->username_.toStdString();
            auto response_result = self->clientManager_->process_authenticated_request(
                std::move(request));
            if (!response_result) {
                results.push_back({ids[i], codes[i], false, "Failed to communicate with server"});
            } else {
                results.push_back({ids[i], codes[i], response_result->success, response_result->message});
            }
        }
{{/domain_entity.qt.delete_is_single}}
{{^domain_entity.qt.delete_is_single}}
        {{domain_entity.qt.delete_request_class}} request;
        request.ids = ids;
        auto response_result = self->clientManager_->process_authenticated_request(
            std::move(request));

        if (!response_result) {
            BOOST_LOG_SEV(lg(), error) << "Failed to send batch delete request";
            for (std::size_t i = 0; i < ids.size(); ++i) {
                results.push_back({ids[i], codes[i], false, "Failed to communicate with server"});
            }
            return results;
        }

        for (std::size_t i = 0; i < ids.size(); ++i) {
            results.push_back({ids[i], codes[i], response_result->success, response_result->message});
        }
{{/domain_entity.qt.delete_is_single}}

        return results;
    };

    auto* watcher = new QFutureWatcher<DeleteResult>(self);
    connect(watcher, &QFutureWatcher<DeleteResult>::finished,
            self, [self, watcher]() {
        auto results = watcher->result();
        watcher->deleteLater();

        int success_count = 0;
        int failure_count = 0;
        QString first_error;

        for (const auto& [id, code, success, message] : results) {
            if (success) {
                BOOST_LOG_SEV(lg(), debug) << "{{domain_entity.entity_title}} deleted: " << code;
                success_count++;
                emit self->{{domain_entity.qt.item_var}}Deleted(QString::fromStdString(code));
            } else {
                BOOST_LOG_SEV(lg(), error) << "{{domain_entity.entity_title}} deletion failed: "
                                           << code << " - " << message;
                failure_count++;
                if (first_error.isEmpty()) {
                    first_error = QString::fromStdString(message);
                }
            }
        }

        self->model_->refresh();

        if (failure_count == 0) {
            QString msg = success_count == 1
                ? "Successfully deleted 1 {{domain_entity.entity_singular_words}}"
                : QString("Successfully deleted %1 {{domain_entity.entity_plural_words}}").arg(success_count);
            emit self->statusChanged(msg);
        } else if (success_count == 0) {
            QString msg = QString("Failed to delete %1 %2: %3")
                .arg(failure_count)
                .arg(failure_count == 1 ? "{{domain_entity.entity_singular_words}}" : "{{domain_entity.entity_plural_words}}")
                .arg(first_error);
            emit self->errorOccurred(msg);
            MessageBoxHelper::critical(self, "Delete Failed", msg);
        } else {
            QString msg = QString("Deleted %1, failed to delete %2")
                .arg(success_count)
                .arg(failure_count);
            emit self->statusChanged(msg);
            MessageBoxHelper::warning(self, "Partial Success", msg);
        }
    });
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
    using DeleteResult = std::vector<std::pair<std::string, std::pair<bool, std::string>>>;

    auto task = [self, codes]() -> DeleteResult {
        DeleteResult results;
        if (!self) return {};

        BOOST_LOG_SEV(lg(), debug) << "Making delete request for "
                                   << codes.size() << " {{domain_entity.entity_plural_words}}";

{{#domain_entity.qt.delete_request_id_field}}
{{#domain_entity.qt.delete_request_id_is_plural}}
        {{domain_entity.qt.delete_request_class}} request;
        request.{{domain_entity.qt.delete_request_id_field}} = codes;
        auto response_result = self->clientManager_->process_authenticated_request(
            std::move(request));

        if (!response_result) {
            BOOST_LOG_SEV(lg(), error) << "Failed to send batch delete request";
            for (const auto& code : codes) {
                results.push_back({code, {false, "Failed to communicate with server"}});
            }
            return results;
        }

        for (const auto& code : codes) {
            results.push_back({code, {response_result->success, response_result->message}});
        }
{{/domain_entity.qt.delete_request_id_is_plural}}
{{^domain_entity.qt.delete_request_id_is_plural}}
        for (const auto& code : codes) {
            {{domain_entity.qt.delete_request_class}} request;
            request.{{domain_entity.qt.delete_request_id_field}} = code;
            auto response_result = self->clientManager_->process_authenticated_request(
                std::move(request));
            if (!response_result) {
                results.push_back({code, {false, "Failed to communicate with server"}});
            } else {
                results.push_back({code, {response_result->success, response_result->message}});
            }
        }
{{/domain_entity.qt.delete_request_id_is_plural}}
{{/domain_entity.qt.delete_request_id_field}}
{{^domain_entity.qt.delete_request_id_field}}
        {{domain_entity.qt.delete_request_class}} request;
        request.codes = codes;
        auto response_result = self->clientManager_->process_authenticated_request(
            std::move(request));

        if (!response_result) {
            BOOST_LOG_SEV(lg(), error) << "Failed to send batch delete request";
            for (const auto& code : codes) {
                results.push_back({code, {false, "Failed to communicate with server"}});
            }
            return results;
        }

        for (const auto& code : codes) {
            results.push_back({code, {response_result->success, response_result->message}});
        }
{{/domain_entity.qt.delete_request_id_field}}

        return results;
    };

    auto* watcher = new QFutureWatcher<DeleteResult>(self);
    connect(watcher, &QFutureWatcher<DeleteResult>::finished,
            self, [self, watcher]() {
        auto results = watcher->result();
        watcher->deleteLater();

        int success_count = 0;
        int failure_count = 0;
        QString first_error;

        for (const auto& [code, result] : results) {
            if (result.first) {
                BOOST_LOG_SEV(lg(), debug) << "{{domain_entity.entity_title}} deleted: " << code;
                success_count++;
                emit self->{{domain_entity.qt.item_var}}Deleted(QString::fromStdString(code));
            } else {
                BOOST_LOG_SEV(lg(), error) << "{{domain_entity.entity_title}} deletion failed: "
                                           << code << " - " << result.second;
                failure_count++;
                if (first_error.isEmpty()) {
                    first_error = QString::fromStdString(result.second);
                }
            }
        }

        self->model_->refresh();

        if (failure_count == 0) {
            QString msg = success_count == 1
                ? "Successfully deleted 1 {{domain_entity.entity_singular_words}}"
                : QString("Successfully deleted %1 {{domain_entity.entity_plural_words}}").arg(success_count);
            emit self->statusChanged(msg);
        } else if (success_count == 0) {
            QString msg = QString("Failed to delete %1 %2: %3")
                .arg(failure_count)
                .arg(failure_count == 1 ? "{{domain_entity.entity_singular_words}}" : "{{domain_entity.entity_plural_words}}")
                .arg(first_error);
            emit self->errorOccurred(msg);
            MessageBoxHelper::critical(self, "Delete Failed", msg);
        } else {
            QString msg = QString("Deleted %1, failed to delete %2")
                .arg(success_count)
                .arg(failure_count);
            emit self->statusChanged(msg);
            MessageBoxHelper::warning(self, "Partial Success", msg);
        }
    });
{{/domain_entity.qt.has_uuid_primary_key}}

    QFuture<DeleteResult> future = QtConcurrent::run(task);
    watcher->setFuture(future);
}

}

See also

Emacs 29.1 (Org mode 9.6.6)