Archetype: cpp_qt_mdi_window.cpp.mustache
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
- Parent facet: C++ Qt templates
- Template variable reference