ores.cpp.qt.mdi_window_impl

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.

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 one:

  • 18B9ED63-78C2-4259-93AC-CCDFBC88EFBD — end of setupToolbar(), after the generated History action. Extra toolbar actions, e.g. a cross-navigation button to a related entity's list window.

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_pair_icon_column}}
#include "ores.qt/FlagIconHelper.hpp"
{{/domain_entity.qt.has_pair_icon_column}}
{{^domain_entity.qt.has_pair_icon_column}}
{{#domain_entity.qt.has_any_flag_icon}}
#include "ores.qt/FlagIconHelper.hpp"
{{/domain_entity.qt.has_any_flag_icon}}
{{/domain_entity.qt.has_pair_icon_column}}
{{#domain_entity.qt.has_csv_xml_io}}
#include "ores.qt/ImportEntityDialog.hpp"
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QUrl>
{{/domain_entity.qt.has_csv_xml_io}}
{{#domain_entity.qt.needs_item_delegate}}
#include "ores.qt/EntityItemDelegate.hpp"
{{/domain_entity.qt.needs_item_delegate}}
{{#domain_entity.qt.has_badge_columns}}
#include "ores.qt/BadgeCache.hpp"
{{/domain_entity.qt.has_badge_columns}}
{{#domain_entity.qt.needs_image_cache}}
#include "ores.qt/ImageCache.hpp"
{{/domain_entity.qt.needs_image_cache}}
#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}}
{{#domain_entity.qt.needs_image_cache}}
    ImageCache* imageCache,
{{/domain_entity.qt.needs_image_cache}}
{{#domain_entity.qt.has_parent_scoped_list}}
    const QString& {{domain_entity.qt.parent_key_param}},
{{/domain_entity.qt.has_parent_scoped_list}}
    QWidget* parent)
    : EntityListMdiWindow(parent),
      clientManager_(clientManager),
      username_(username),
{{#domain_entity.qt.has_parent_scoped_list}}
      {{domain_entity.qt.parent_key_param}}_({{domain_entity.qt.parent_key_param}}),
{{/domain_entity.qt.has_parent_scoped_list}}
{{#domain_entity.qt.has_badge_columns}}
      badgeCache_(badgeCache),
{{/domain_entity.qt.has_badge_columns}}
{{#domain_entity.qt.needs_image_cache}}
      imageCache_(imageCache),
{{/domain_entity.qt.needs_image_cache}}
      toolbar_(nullptr),
      tableView_(nullptr),
      model_(nullptr),
      proxyModel_(nullptr),
      paginationWidget_(nullptr),
      reloadAction_(nullptr){{^domain_entity.qt.has_readonly_paginated_list}},
      addAction_(nullptr),
      editAction_(nullptr),
      deleteAction_(nullptr){{/domain_entity.qt.has_readonly_paginated_list}}{{#domain_entity.qt.has_csv_xml_io}},
      importXMLAction_(nullptr),
      exportCSVAction_(nullptr),
      exportXMLAction_(nullptr){{/domain_entity.qt.has_csv_xml_io}}{{^domain_entity.qt.has_readonly_paginated_list}},
      historyAction_(nullptr){{/domain_entity.qt.has_readonly_paginated_list}}{{#domain_entity.qt.has_setting_gated_actions}},
{{#domain_entity.qt.setting_gated_actions}}
      {{action}}_(nullptr),
{{/domain_entity.qt.setting_gated_actions}}
      settingGatedActions_(new SettingGatedActionController(clientManager, this)){{/domain_entity.qt.has_setting_gated_actions}} {

    setupUi();
    setupConnections();
{{#domain_entity.qt.has_setting_gated_actions}}
{{#domain_entity.qt.setting_gated_actions}}
    settingGatedActions_->registerAction({{action}}_, "{{setting}}");
{{/domain_entity.qt.setting_gated_actions}}
    if (clientManager_ && clientManager_->isLoggedIn())
        settingGatedActions_->refresh();
{{/domain_entity.qt.has_setting_gated_actions}}
    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));

{{^domain_entity.qt.has_readonly_paginated_list}}
    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);
{{/domain_entity.qt.has_readonly_paginated_list}}
{{! see * Extension points above }}
<<paste:18B9ED63-78C2-4259-93AC-CCDFBC88EFBD>>

{{#domain_entity.qt.has_csv_xml_io}}
    toolbar_->addSeparator();

    importXMLAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::ImportOre, IconUtils::DefaultIconColor),
        tr("Import XML"));
    importXMLAction_->setToolTip(tr("Import {{domain_entity.entity_plural_words}} from an ORE XML file"));
    connect(importXMLAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::importFromXML);

    exportCSVAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::ExportCsv, IconUtils::DefaultIconColor),
        tr("Export CSV"));
    exportCSVAction_->setToolTip(tr("Export {{domain_entity.entity_plural_words}} to CSV"));
    connect(exportCSVAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::exportToCSV);

    exportXMLAction_ = toolbar_->addAction(
        IconUtils::createRecoloredIcon(
            Icon::ExportOre, IconUtils::DefaultIconColor),
        tr("Export XML"));
    exportXMLAction_->setToolTip(tr("Export {{domain_entity.entity_plural_words}} to ORE XML"));
    connect(exportXMLAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}MdiWindow::exportToXML);
{{/domain_entity.qt.has_csv_xml_io}}
{{#domain_entity.qt.has_related_entity_shortcuts}}

    toolbar_->addSeparator();
{{#domain_entity.qt.related_entity_shortcuts}}

    {
        auto* action = toolbar_->addAction(
            IconUtils::createRecoloredIcon(Icon::{{icon}}, IconUtils::DefaultIconColor),
            tr("{{label}}"));
        action->setToolTip(tr("{{tooltip}}"));
        connect(action, &QAction::triggered, this, [this]() {
            emit show{{signal}}Requested();
        });
    }
{{/domain_entity.qt.related_entity_shortcuts}}
{{/domain_entity.qt.has_related_entity_shortcuts}}
}

void {{domain_entity.entity_pascal}}MdiWindow::setupTable() {
    model_ = new Client{{domain_entity.entity_pascal}}Model(clientManager_,
{{#domain_entity.qt.has_parent_scoped_list}}
        {{domain_entity.qt.parent_key_param}}_,
{{/domain_entity.qt.has_parent_scoped_list}}
        this);
{{#domain_entity.qt.needs_image_cache}}
    model_->setImageCache(imageCache_);
{{/domain_entity.qt.needs_image_cache}}
    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_pair_icon_column}}
    tableView_->setIconSize(currency_pair_icon_size());
{{/domain_entity.qt.has_pair_icon_column}}
{{^domain_entity.qt.has_pair_icon_column}}
{{#domain_entity.qt.has_any_flag_icon}}
    tableView_->setIconSize(single_flag_icon_size());
{{/domain_entity.qt.has_any_flag_icon}}
{{/domain_entity.qt.has_pair_icon_column}}

{{#domain_entity.qt.needs_item_delegate}}
    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 hardcoded_fallback{color_constants::badge_fallback,
                                                         color_constants::badge_fallback_text,
                                                         true};
        if (!cache) return hardcoded_fallback;
        auto* def = cache->resolve("{{badge_key}}", value.toStdString());
        if (!def) {
            auto* reserved = cache->fallback();
            if (!reserved) return hardcoded_fallback;
            return {QColor(QString::fromStdString(reserved->background_colour)),
                    QColor(QString::fromStdString(reserved->text_colour)),
                    true};
        }
        return {QColor(QString::fromStdString(def->background_colour)),
                QColor(QString::fromStdString(def->text_colour))};
    });
{{/is_badge}}
{{#self_colour}}
    delegate->set_badge_color_resolver({{column_index}}, [](const QString& value) -> badge_color_pair {
        QColor bg(value);
        if (!bg.isValid()) bg = color_constants::badge_fallback;
        const QColor fg = bg.lightnessF() > 0.5 ? QColor(Qt::black) : QColor(Qt::white);
        return {bg, fg};
    });
{{/self_colour}}
{{/domain_entity.qt.columns}}
    tableView_->setItemDelegate(delegate);
{{#domain_entity.qt.has_badge_columns}}
    if (badgeCache_) {
        if (badgeCache_->isLoaded())
            tableView_->viewport()->update();
        connect(badgeCache_, &BadgeCache::loaded, tableView_->viewport(), [this]() {
            tableView_->viewport()->update();
        });
    }
{{/domain_entity.qt.has_badge_columns}}
{{/domain_entity.qt.needs_item_delegate}}

    initializeTableSettings(tableView_, model_,
        "{{domain_entity.qt.settings_group}}",
        {
{{#domain_entity.qt.hidden_columns}}
            Client{{domain_entity.entity_pascal}}Model::{{enum_name}}{{^is_last}},{{/is_last}}
{{/domain_entity.qt.hidden_columns}}
        },
        {900, 400}, {{domain_entity.qt.qt_settings_version}});
}

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);
            paginationWidget_->reset_page();
            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_->load_page(paginationWidget_->current_offset(), paginationWidget_->page_size());
}

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() {
{{^domain_entity.qt.has_readonly_paginated_list}}
    updateActionStates();
{{/domain_entity.qt.has_readonly_paginated_list}}
}

{{^domain_entity.qt.has_readonly_paginated_list}}
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}});
    }
}
{{/domain_entity.qt.has_readonly_paginated_list}}
{{#domain_entity.qt.has_readonly_paginated_list}}
void {{domain_entity.entity_pascal}}MdiWindow::onDoubleClicked(const QModelIndex&) {
}
{{/domain_entity.qt.has_readonly_paginated_list}}

{{^domain_entity.qt.has_readonly_paginated_list}}
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}});
    }
}
{{/domain_entity.qt.has_readonly_paginated_list}}

{{^domain_entity.qt.has_readonly_paginated_list}}
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<std::string> 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(boost::uuids::to_string({{domain_entity.qt.item_var}}->id));
            codes.push_back({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}->{{domain_entity.qt.key_field}}{{domain_entity.qt.key_to_string_suffix}});
        }
    }

    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<std::string, 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}} = 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_->load_page(
            self->paginationWidget_->current_offset(), self->paginationWidget_->page_size());

        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_->load_page(
            self->paginationWidget_->current_offset(), self->paginationWidget_->page_size());

        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);
}
{{/domain_entity.qt.has_readonly_paginated_list}}

{{#domain_entity.qt.has_csv_xml_io}}
void {{domain_entity.entity_pascal}}MdiWindow::exportToCSV() {
    if (model_->rowCount() == 0) {
        QMessageBox::information(this, "No Data", "There are no {{domain_entity.entity_plural_words}} to export.");
        return;
    }

    auto {{domain_entity.qt.collection_name}} = model_->get{{domain_entity.entity_pascal_short_plural}}();

    QString fileName = QFileDialog::getSaveFileName(
        this, "Export to CSV", "{{domain_entity.qt.collection_name}}.csv", "CSV Files (*.csv);;All Files (*)");
    if (fileName.isEmpty())
        return;

    try {
        std::string csvData = {{domain_entity.qt.csv_export_class}}::{{domain_entity.qt.csv_export_method}}({{domain_entity.qt.collection_name}});

        QFile file(fileName);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            MessageBoxHelper::critical(
                this, "File Error", QString("Could not open file for writing: %1").arg(fileName));
            return;
        }
        file.write(csvData.c_str(), csvData.length());
        file.close();

        QDesktopServices::openUrl(QUrl::fromLocalFile(fileName));
        emit statusChanged(QString("Successfully exported {{domain_entity.entity_plural_words}} to %1").arg(fileName));
    } catch (const std::exception& e) {
        MessageBoxHelper::critical(
            this, "Export Error", QString("Error during CSV export: %1").arg(e.what()));
    }
}

void {{domain_entity.entity_pascal}}MdiWindow::exportToXML() {
    if (model_->rowCount() == 0) {
        QMessageBox::information(this, "No Data", "There are no {{domain_entity.entity_plural_words}} to export.");
        return;
    }

    auto {{domain_entity.qt.collection_name}} = model_->get{{domain_entity.entity_pascal_short_plural}}();

    QString fileName = QFileDialog::getSaveFileName(
        this, "Export to ORE XML", "{{domain_entity.qt.collection_name}}.xml", "XML Files (*.xml);;All Files (*)");
    if (fileName.isEmpty())
        return;

    try {
        std::string xmlData = {{domain_entity.qt.xml_export_class}}::{{domain_entity.qt.xml_export_method}}({{domain_entity.qt.collection_name}});

        QFile file(fileName);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            MessageBoxHelper::critical(
                this, "File Error", QString("Could not open file for writing: %1").arg(fileName));
            return;
        }
        file.write(xmlData.c_str(), xmlData.length());
        file.close();

        QDesktopServices::openUrl(QUrl::fromLocalFile(fileName));
        emit statusChanged(QString("Successfully exported {{domain_entity.entity_plural_words}} to %1").arg(fileName));
    } catch (const std::exception& e) {
        MessageBoxHelper::critical(
            this, "Export Error", QString("Error during XML export: %1").arg(e.what()));
    }
}

void {{domain_entity.entity_pascal}}MdiWindow::importFromXML() {
    if (!clientManager_->isLoggedIn()) {
        MessageBoxHelper::warning(
            this, "Not Logged In", "Cannot import {{domain_entity.entity_plural_words}} while not logged in.");
        return;
    }

    QString fileName = QFileDialog::getOpenFileName(
        this, "Select ORE XML File to Import", QString(), "ORE XML Files (*.xml);;All Files (*)");
    if (fileName.isEmpty())
        return;

    emit statusChanged("Parsing XML file...");

    try {
        std::filesystem::path path(fileName.toStdString());
        auto {{domain_entity.qt.collection_name}} = {{domain_entity.qt.xml_import_class}}::{{domain_entity.qt.xml_import_method}}(path);

        if ({{domain_entity.qt.collection_name}}.empty()) {
            MessageBoxHelper::information(this,
                                          "No {{domain_entity.entity_title}} Found",
                                          "The selected XML file does not contain any {{domain_entity.entity_plural_words}}.");
            emit statusChanged("Import cancelled - no {{domain_entity.entity_plural_words}} found");
            return;
        }

        emit statusChanged(
            QString("Found %1 {{domain_entity.entity_plural_words}} - opening import dialog...").arg({{domain_entity.qt.collection_name}}.size()));

        std::vector<ImportEntityRow> rows;
        rows.reserve({{domain_entity.qt.collection_name}}.size());
        for (const auto& {{domain_entity.qt.item_var}} : {{domain_entity.qt.collection_name}}) {
            const auto validation_error = {{domain_entity.qt.xml_import_class}}::{{domain_entity.qt.xml_validate_method}}({{domain_entity.qt.item_var}});
            rows.push_back(
                {.display_values = {
{{#domain_entity.qt.import_preview_columns}}
{{#is_string}}
                    QString::fromStdString({{domain_entity.qt.item_var}}.{{field}}),
{{/is_string}}
{{#is_int}}
                    QString::number({{domain_entity.qt.item_var}}.{{field}}),
{{/is_int}}
{{#is_bool}}
                    {{domain_entity.qt.item_var}}.{{field}} ? "true" : "false",
{{/is_bool}}
{{#is_double}}
                    QString::number({{domain_entity.qt.item_var}}.{{field}}),
{{/is_double}}
{{#is_uuid}}
                    QString::fromStdString(boost::uuids::to_string({{domain_entity.qt.item_var}}.{{field}})),
{{/is_uuid}}
{{/domain_entity.qt.import_preview_columns}}
                 },
                 .is_valid = validation_error.empty(),
                 .invalid_reason = QString::fromStdString(validation_error)});
        }

        auto client_manager = clientManager_;
        auto username = username_;
        auto {{domain_entity.qt.collection_name}}_by_row = {{domain_entity.qt.collection_name}};
        auto label_of = [{{domain_entity.qt.collection_name}}_by_row](std::size_t index) {
            return QString::fromStdString({{domain_entity.qt.collection_name}}_by_row[index].{{domain_entity.qt.key_field}});
        };
        auto import_one = [client_manager, username, {{domain_entity.qt.collection_name}}_by_row](std::size_t index) {
            auto {{domain_entity.qt.item_var}}_to_import = {{domain_entity.qt.collection_name}}_by_row[index];
            {{domain_entity.qt.item_var}}_to_import.modified_by = username.toStdString();
            try {
                auto request = {{domain_entity.qt.save_request_class}}::from({{domain_entity.qt.item_var}}_to_import);
                auto response_result = client_manager->process_authenticated_request(std::move(request));
                return response_result.has_value() && response_result->success;
            } catch (const std::exception&) {
                return false;
            }
        };

        auto* dialog = new ImportEntityDialog(
            "{{domain_entity.entity_plural_words}}",
            fileName,
            {
{{#domain_entity.qt.import_preview_columns}}
                "{{header}}",
{{/domain_entity.qt.import_preview_columns}}
            },
            std::move(rows),
            label_of,
            import_one,
            this);

        connect(dialog, &ImportEntityDialog::importCompleted, this,
                [this](int success_count, int total_count) {
            if (success_count > 0) {
                paginationWidget_->reset_page();
                model_->load_page(0, paginationWidget_->page_size());
                QString message = QString("Successfully imported %1 of %2 {{domain_entity.entity_plural_words}}")
                                      .arg(success_count).arg(total_count);
                emit statusChanged(message);
                MessageBoxHelper::information(this, "Import Complete", message);
            } else {
                emit statusChanged("Import failed - no {{domain_entity.entity_plural_words}} imported");
                MessageBoxHelper::warning(
                    this, "Import Failed", "Failed to import {{domain_entity.entity_plural_words}}. Check the log for details.");
            }
        });

        connect(dialog, &ImportEntityDialog::importCancelled, this, [this]() {
            emit statusChanged("Import cancelled");
        });

        if (dialog->exec() != QDialog::Accepted) {
            emit statusChanged("Import cancelled");
        }
        dialog->deleteLater();

    } catch (const std::exception& e) {
        MessageBoxHelper::critical(
            this, "Import Error", QString("Failed to import XML file:\n%1").arg(e.what()));
        emit statusChanged("Import failed");
    }
}
{{/domain_entity.qt.has_csv_xml_io}}

}

See also

Emacs 29.3 (Org mode 9.6.15)