aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/Model/Template
diff options
context:
space:
mode:
Diffstat (limited to 'app/source/Cplt/Model/Template')
-rw-r--r--app/source/Cplt/Model/Template/TableTemplate.cpp591
-rw-r--r--app/source/Cplt/Model/Template/TableTemplate.hpp223
-rw-r--r--app/source/Cplt/Model/Template/TableTemplateIterator.cpp52
-rw-r--r--app/source/Cplt/Model/Template/TableTemplateIterator.hpp35
-rw-r--r--app/source/Cplt/Model/Template/Template.hpp68
-rw-r--r--app/source/Cplt/Model/Template/Template_Main.cpp214
-rw-r--r--app/source/Cplt/Model/Template/Template_RTTI.cpp29
-rw-r--r--app/source/Cplt/Model/Template/fwd.hpp11
8 files changed, 1223 insertions, 0 deletions
diff --git a/app/source/Cplt/Model/Template/TableTemplate.cpp b/app/source/Cplt/Model/Template/TableTemplate.cpp
new file mode 100644
index 0000000..5cd9ed8
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplate.cpp
@@ -0,0 +1,591 @@
+#include "TableTemplate.hpp"
+
+#include <Cplt/Utils/IO/StringIntegration.hpp>
+#include <Cplt/Utils/IO/TslArrayIntegration.hpp>
+#include <Cplt/Utils/IO/VectorIntegration.hpp>
+
+#include <xlsxwriter.h>
+#include <algorithm>
+#include <charconv>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <map>
+
+bool TableCell::IsDataHoldingCell() const
+{
+ return IsPrimaryCell() || !IsMergedCell();
+}
+
+bool TableCell::IsPrimaryCell() const
+{
+ return PrimaryCellLocation == Location;
+}
+
+bool TableCell::IsMergedCell() const
+{
+ return PrimaryCellLocation.x == -1 || PrimaryCellLocation.y == -1;
+}
+
+template <class TTableCell, class TStream>
+void OperateStreamForTableCell(TTableCell& cell, TStream& proxy)
+{
+ proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Content);
+ proxy.Object(cell.Location);
+ proxy.Object(cell.PrimaryCellLocation);
+ proxy.Value(cell.SpanX);
+ proxy.Value(cell.SpanY);
+ proxy.Enum(cell.HorizontalAlignment);
+ proxy.Enum(cell.VerticalAlignment);
+ proxy.Enum(cell.Type);
+ proxy.Value(cell.DataId);
+}
+
+void TableCell::ReadFromDataStream(InputDataStream& stream)
+{
+ ::OperateStreamForTableCell(*this, stream);
+}
+
+void TableCell::WriteToDataStream(OutputDataStream& stream) const
+{
+ ::OperateStreamForTableCell(*this, stream);
+}
+
+Vec2i TableArrayGroup::GetLeftCell() const
+{
+ return { Row, LeftCell };
+}
+
+Vec2i TableArrayGroup::GetRightCell() const
+{
+ return { Row, RightCell };
+}
+
+int TableArrayGroup::GetCount() const
+{
+ return RightCell - LeftCell + 1;
+}
+
+Vec2i TableArrayGroup::FindCell(std::string_view name)
+{
+ // TODO
+ return Vec2i{};
+}
+
+template <class TMap>
+static bool UpdateElementName(TMap& map, std::string_view oldName, std::string_view newName)
+{
+ auto iter = map.find(oldName);
+ if (iter == map.end()) {
+ return false;
+ }
+
+ auto elm = iter.value();
+ auto [DISCARD, inserted] = map.insert(newName, elm);
+ if (!inserted) {
+ return false;
+ }
+
+ map.erase(iter);
+ return true;
+}
+
+bool TableArrayGroup::UpdateCellName(std::string_view oldName, std::string_view newName)
+{
+ return ::UpdateElementName(mName2Cell, oldName, newName);
+}
+
+template <class TTableArrayGroup, class TStream>
+void OperateStreamForTableArrayGroup(TTableArrayGroup& group, TStream& stream)
+{
+ stream.Value(group.Row);
+ stream.Value(group.LeftCell);
+ stream.Value(group.RightCell);
+}
+
+void TableArrayGroup::ReadFromDataStream(InputDataStream& stream)
+{
+ ::OperateStreamForTableArrayGroup(*this, stream);
+}
+
+void TableArrayGroup::WriteToDataStream(OutputDataStream& stream) const
+{
+ ::OperateStreamForTableArrayGroup(*this, stream);
+}
+
+TableInstantiationParameters::TableInstantiationParameters(const TableTemplate& table)
+ : mTable{ &table }
+{
+}
+
+TableInstantiationParameters& TableInstantiationParameters::ResetTable(const TableTemplate& newTable)
+{
+ mTable = &newTable;
+ return *this;
+}
+
+TableInstantiationParameters TableInstantiationParameters::RebindTable(const TableTemplate& newTable) const
+{
+ TableInstantiationParameters result(newTable);
+ result.SingularCells = this->SingularCells;
+ result.ArrayGroups = this->ArrayGroups;
+ return result;
+}
+
+const TableTemplate& TableInstantiationParameters::GetTable() const
+{
+ return *mTable;
+}
+
+bool TableTemplate::IsInstance(const Template* tmpl)
+{
+ return tmpl->GetKind() == KD_Table;
+}
+
+TableTemplate::TableTemplate()
+ : Template(KD_Table)
+{
+}
+
+int TableTemplate::GetTableWidth() const
+{
+ return mColumnWidths.size();
+}
+
+int TableTemplate::GetTableHeight() const
+{
+ return mRowHeights.size();
+}
+
+void TableTemplate::Resize(int newWidth, int newHeight)
+{
+ // TODO this doesn't gracefully handle resizing to a smaller size which trims some merged cells
+
+ std::vector<TableCell> cells;
+ cells.reserve(newWidth * newHeight);
+
+ int tableWidth = GetTableWidth();
+ int tableHeight = GetTableHeight();
+
+ for (int y = 0; y < newHeight; ++y) {
+ if (y >= tableHeight) {
+ for (int x = 0; x < newWidth; ++x) {
+ cells.push_back(TableCell{});
+ }
+ continue;
+ }
+
+ for (int x = 0; x < newWidth; ++x) {
+ if (x >= tableWidth) {
+ cells.push_back(TableCell{});
+ } else {
+ auto& cell = GetCell({ x, y });
+ cells.push_back(std::move(cell));
+ }
+ }
+ }
+
+ mCells = std::move(cells);
+ mColumnWidths.resize(newWidth, 80);
+ mRowHeights.resize(newHeight, 20);
+}
+
+int TableTemplate::GetRowHeight(int row) const
+{
+ return mRowHeights[row];
+}
+
+void TableTemplate::SetRowHeight(int row, int height)
+{
+ mRowHeights[row] = height;
+}
+
+int TableTemplate::GetColumnWidth(int column) const
+{
+ return mColumnWidths[column];
+}
+
+void TableTemplate::SetColumnWidth(int column, int width)
+{
+ mColumnWidths[column] = width;
+}
+
+const TableCell& TableTemplate::GetCell(Vec2i pos) const
+{
+ int tableWidth = GetTableWidth();
+ return mCells[pos.y * tableWidth + pos.x];
+}
+
+TableCell& TableTemplate::GetCell(Vec2i pos)
+{
+ return const_cast<TableCell&>(const_cast<const TableTemplate*>(this)->GetCell(pos));
+}
+
+void TableTemplate::SetCellType(Vec2i pos, TableCell::CellType type)
+{
+ auto& cell = GetCell(pos);
+ if (cell.Type == type) {
+ return;
+ }
+
+ switch (cell.Type) {
+ // Nothing to change
+ case TableCell::ConstantCell: break;
+
+ case TableCell::SingularParametricCell:
+ mName2Parameters.erase(cell.Content);
+ break;
+
+ case TableCell::ArrayParametricCell: {
+ auto& ag = mArrayGroups[cell.DataId];
+ if (pos.x == ag.LeftCell) {
+ ag.LeftCell++;
+ } else if (pos.x == ag.RightCell) {
+ ag.RightCell--;
+ } else {
+ }
+ } break;
+ }
+
+ switch (type) {
+ // Nothing to do
+ case TableCell::ConstantCell: break;
+
+ case TableCell::SingularParametricCell: {
+ int idx = pos.y * GetTableWidth() + pos.x;
+ auto [DISCARD, inserted] = mName2Parameters.insert(cell.Content, idx);
+
+ // Duplicate name
+ if (!inserted) {
+ return;
+ }
+ } break;
+
+ case TableCell::ArrayParametricCell: {
+ auto ptr = AddArrayGroup(pos.y, pos.x, pos.x);
+
+ // Duplicate name
+ if (ptr == nullptr) {
+ return;
+ }
+ } break;
+ }
+
+ cell.Type = type;
+}
+
+bool TableTemplate::UpdateParameterName(std::string_view oldName, std::string_view newName)
+{
+ return ::UpdateElementName(mName2Parameters, oldName, newName);
+}
+
+int TableTemplate::GetArrayGroupCount() const
+{
+ return mArrayGroups.size();
+}
+
+const TableArrayGroup& TableTemplate::GetArrayGroup(int id) const
+{
+ return mArrayGroups[id];
+}
+
+TableArrayGroup& TableTemplate::GetArrayGroup(int id)
+{
+ return mArrayGroups[id];
+}
+
+TableArrayGroup* TableTemplate::AddArrayGroup(int row, int left, int right)
+{
+ // size_t max value: 18446744073709551615
+ // ^~~~~~~~~~~~~~~~~~~~ 20 chars
+ char name[20];
+ auto res = std::to_chars(std::begin(name), std::end(name), mArrayGroups.size());
+ std::string_view nameStr(name, res.ptr - name);
+
+ return AddArrayGroup(nameStr, row, left, right);
+}
+
+TableArrayGroup* TableTemplate::AddArrayGroup(std::string_view name, int row, int left, int right)
+{
+ assert(row >= 0 && row < GetTableHeight());
+ assert(left >= 0 && left < GetTableWidth());
+ assert(right >= 0 && right < GetTableWidth());
+
+ // TODO check for overlap
+
+ if (left > right) {
+ std::swap(left, right);
+ }
+
+ auto [DISCARD, inserted] = mName2ArrayGroups.insert(name, (int)mArrayGroups.size());
+ if (!inserted) {
+ return nullptr;
+ }
+
+ mArrayGroups.push_back(TableArrayGroup{
+ .Row = row,
+ .LeftCell = left,
+ .RightCell = right,
+ });
+ auto& ag = mArrayGroups.back();
+
+ for (int x = left; x <= right; x++) {
+ auto& cell = GetCell({ x, row });
+
+ // Update type
+ cell.Type = TableCell::ArrayParametricCell;
+
+ // Insert parameter name lookup
+ while (true) {
+ auto [DISCARD, inserted] = ag.mName2Cell.insert(cell.Content, x);
+ if (inserted) {
+ break;
+ }
+
+ cell.Content += "-";
+ }
+ }
+
+ return &ag;
+}
+
+bool TableTemplate::UpdateArrayGroupName(std::string_view oldName, std::string_view newName)
+{
+ return ::UpdateElementName(mName2ArrayGroups, oldName, newName);
+}
+
+bool TableTemplate::ExtendArrayGroupLeft(int id, int n)
+{
+ assert(n > 0);
+
+ auto& ag = mArrayGroups[id];
+ ag.LeftCell -= n;
+
+ return false;
+}
+
+bool TableTemplate::ExtendArrayGroupRight(int id, int n)
+{
+ assert(n > 0);
+
+ auto& ag = mArrayGroups[id];
+ ag.RightCell += n;
+
+ return false;
+}
+
+TableCell* TableTemplate::FindCell(std::string_view name)
+{
+ auto iter = mName2Parameters.find(name);
+ if (iter != mName2Parameters.end()) {
+ return &mCells[iter.value()];
+ } else {
+ return nullptr;
+ }
+}
+
+TableArrayGroup* TableTemplate::FindArrayGroup(std::string_view name)
+{
+ auto iter = mName2ArrayGroups.find(name);
+ if (iter != mName2ArrayGroups.end()) {
+ return &mArrayGroups[iter.value()];
+ } else {
+ return nullptr;
+ }
+}
+
+TableTemplate::MergeCellsResult TableTemplate::MergeCells(Vec2i topLeft, Vec2i bottomRight)
+{
+ auto SortTwo = [](int& a, int& b) {
+ if (a > b) {
+ std::swap(a, b);
+ }
+ };
+ SortTwo(topLeft.x, bottomRight.x);
+ SortTwo(topLeft.y, bottomRight.y);
+
+ auto ResetProgress = [&]() {
+ for (int y = topLeft.y; y < bottomRight.y; ++y) {
+ for (int x = topLeft.x; x < bottomRight.x; ++x) {
+ auto& cell = GetCell({ x, y });
+ cell.PrimaryCellLocation = { -1, -1 };
+ }
+ }
+ };
+
+ for (int y = topLeft.y; y < bottomRight.y; ++y) {
+ for (int x = topLeft.x; x < bottomRight.x; ++x) {
+ auto& cell = GetCell({ x, y });
+ if (cell.IsMergedCell()) {
+ ResetProgress();
+ return MCR_CellAlreadyMerged;
+ }
+
+ cell.PrimaryCellLocation = topLeft;
+ }
+ }
+
+ auto& primaryCell = GetCell(topLeft);
+ primaryCell.SpanX = bottomRight.x - topLeft.x;
+ primaryCell.SpanY = bottomRight.y - topLeft.y;
+
+ return MCR_Success;
+}
+
+TableTemplate::BreakCellsResult TableTemplate::BreakCells(Vec2i topLeft)
+{
+ auto& primaryCell = GetCell(topLeft);
+ if (!primaryCell.IsMergedCell()) {
+ return BCR_CellNotMerged;
+ }
+
+ for (int dy = 0; dy < primaryCell.SpanY; ++dy) {
+ for (int dx = 0; dx < primaryCell.SpanX; ++dx) {
+ auto& cell = GetCell({ topLeft.x + dx, topLeft.y + dy });
+ cell.PrimaryCellLocation = { -1, -1 };
+ }
+ }
+
+ primaryCell.SpanX = 1;
+ primaryCell.SpanY = 1;
+
+ return BCR_Success;
+}
+
+lxw_workbook* TableTemplate::InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const
+{
+ auto workbook = workbook_new("Table.xlsx");
+ InstantiateToExcelWorksheet(workbook, params);
+ return workbook;
+}
+
+lxw_worksheet* TableTemplate::InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const
+{
+ auto worksheet = workbook_add_worksheet(workbook, "CpltExport.xlsx");
+
+ // Map: row number -> length of generated ranges
+ std::map<int, int> generatedRanges;
+
+ for (size_t i = 0; i < mArrayGroups.size(); ++i) {
+ auto& info = mArrayGroups[i];
+ auto& param = params.ArrayGroups[i];
+
+ auto iter = generatedRanges.find(i);
+ if (iter != generatedRanges.end()) {
+ int available = iter->second;
+ if (available >= param.size()) {
+ // Current space is enough to fit in this array group, skip
+ continue;
+ }
+ }
+
+ // Not enough space to fit in this array group, update (or insert) the appropriate amount of generated rows
+ int row = i;
+ int count = param.size();
+ generatedRanges.try_emplace(row, count);
+ }
+
+ auto GetOffset = [&](int y) -> int {
+ // std::find_if <values less than y>
+ int verticalOffset = 0;
+ for (auto it = generatedRanges.begin(); it != generatedRanges.end() && it->first < y; ++it) {
+ verticalOffset += it->second;
+ }
+ return verticalOffset;
+ };
+
+ auto WriteCell = [&](int row, int col, const TableCell& cell, const char* text) -> void {
+ if (cell.IsPrimaryCell()) {
+ int lastRow = row + cell.SpanY - 1;
+ int lastCol = col + cell.SpanX - 1;
+ // When both `string` and `format` are null, the top-left cell contents are untouched (what we just wrote in the above switch)
+ worksheet_merge_range(worksheet, row, col, lastRow, lastCol, text, nullptr);
+ } else {
+ worksheet_write_string(worksheet, row, col, text, nullptr);
+ }
+ };
+
+ // Write/instantiate all array groups
+ for (size_t i = 0; i < mArrayGroups.size(); ++i) {
+ auto& groupInfo = mArrayGroups[i];
+ auto& groupParams = params.ArrayGroups[i];
+
+ int rowCellCount = groupInfo.GetCount();
+ int rowCount = groupParams.size();
+ int baseRowIdx = groupInfo.Row + GetOffset(groupInfo.Row);
+
+ // For each row that would be generated
+ for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) {
+ auto& row = groupParams[rowIdx];
+
+ // For each cell in the row
+ for (int rowCellIdx = 0; rowCellIdx < rowCellCount; ++rowCellIdx) {
+ // TODO support merged cells in array groups
+ worksheet_write_string(worksheet, baseRowIdx + rowIdx, rowCellIdx, row[rowCellIdx].c_str(), nullptr);
+ }
+ }
+ }
+
+ int tableWidth = GetTableWidth();
+ int tableHeight = GetTableHeight();
+
+ // Write all regular and singular parameter cells
+ for (int y = 0; y < tableHeight; ++y) {
+ for (int x = 0; x < tableWidth; ++x) {
+ auto& cell = GetCell({ x, y });
+
+ if (!cell.IsDataHoldingCell()) {
+ continue;
+ }
+
+ switch (cell.Type) {
+ case TableCell::ConstantCell: {
+ int row = y + GetOffset(y);
+ int col = x;
+
+ WriteCell(row, col, cell, cell.Content.c_str());
+ } break;
+
+ case TableCell::SingularParametricCell: {
+ int row = y + GetOffset(y);
+ int col = x;
+
+ auto iter = params.SingularCells.find({ x, y });
+ if (iter != params.SingularCells.end()) {
+ WriteCell(row, col, cell, iter.value().c_str());
+ }
+ } break;
+
+ // See loop above that processes whole array groups at the same time
+ case TableCell::ArrayParametricCell: break;
+ }
+ }
+ }
+
+ return worksheet;
+}
+
+class TableTemplate::Private
+{
+public:
+ template <class TTableTemplate, class TProxy>
+ static void OperateStream(TTableTemplate& table, TProxy& proxy)
+ {
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mColumnWidths);
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mRowHeights);
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mCells);
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mArrayGroups);
+ proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2Parameters);
+ proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2ArrayGroups);
+ }
+};
+
+void TableTemplate::ReadFromDataStream(InputDataStream& stream)
+{
+ Private::OperateStream(*this, stream);
+}
+
+void TableTemplate::WriteToDataStream(OutputDataStream& stream) const
+{
+ Private::OperateStream(*this, stream);
+}
diff --git a/app/source/Cplt/Model/Template/TableTemplate.hpp b/app/source/Cplt/Model/Template/TableTemplate.hpp
new file mode 100644
index 0000000..3e931d4
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplate.hpp
@@ -0,0 +1,223 @@
+#pragma once
+
+#include <Cplt/Model/Template/Template.hpp>
+#include <Cplt/Utils/Vector.hpp>
+#include <Cplt/Utils/VectorHash.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <tsl/array_map.h>
+#include <tsl/robin_map.h>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class TableCell
+{
+public:
+ enum TextAlignment
+ {
+ /// For horizontal alignment, this means align left. For vertical alignment, this means align top.
+ AlignAxisMin,
+ /// Align middle of the text to the middle of the axis.
+ AlignCenter,
+ /// For horizontal alignment, this means align right. For vertical alignment, this means align bottom.
+ AlignAxisMax,
+ };
+
+ enum CellType
+ {
+ ConstantCell,
+ SingularParametricCell,
+ ArrayParametricCell,
+ };
+
+public:
+ /// Display content of this cell. This doesn't necessarily have to line up with the parameter name (if this cell is one).
+ std::string Content;
+ Vec2i Location;
+ /// Location of the primary (top left) cell, if this cell is a part of a merged group.
+ /// Otherwise, either component of this field shall be -1.
+ Vec2i PrimaryCellLocation{ -1, -1 };
+ int SpanX = 0;
+ int SpanY = 0;
+ TextAlignment HorizontalAlignment = AlignCenter;
+ TextAlignment VerticalAlignment = AlignCenter;
+ CellType Type = ConstantCell;
+ /// The id of the group description object, if this cell isn't a constant or singular parameter cell. Otherwise, this value is -1.
+ int DataId = -1;
+
+public:
+ /// Return whether this cell holds meaningful data, i.e. true when this cell is either unmerged or the primary cell of a merged range.
+ bool IsDataHoldingCell() const;
+ /// Return whether this cell is the primary (i.e. top left) cell of a merged range or not.
+ bool IsPrimaryCell() const;
+ /// Return whether this cell is a part of a merged range or not. Includes the primary cell.
+ bool IsMergedCell() const;
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+};
+
+// TODO support reverse (bottom to top) filling order
+// TODO support horizontal filling order
+
+/// Parameter group information for a grouped array of cells. When instantiated, an array of 0 or more
+/// elements shall be provided by the user, which will replace the group of templated cells with a list
+/// of rows, each instantiated with the n-th element in the provided array.
+/// \code
+/// [["foo", "bar", "foobar"],
+/// ["a", "b", c"],
+/// ["1", "2", "3"],
+/// ["x", "y", "z"]]
+/// // ... may be more
+/// \endcode
+/// This would create 4 rows of data in the place of the original parameter group.
+///
+/// If more than one array parameter groups are on the same row, they would share space between each other:
+/// \code
+/// | 2 elements was fed to it
+/// | | 1 element was fed to it
+/// V V
+/// {~~~~~~~~~~~~~~~~}{~~~~~~~~~~~~~~}
+/// +------+---------+---------------+
+/// | Foo | Example | Another group |
+/// +------+---------+---------------+
+/// | Cool | Example | |
+/// +------+---------+---------------+
+/// \endcode
+///
+/// \see TableCell
+/// \see TableInstantiationParameters
+/// \see TableTemplate
+class TableArrayGroup
+{
+public:
+ /// Parameter name mapped to cell location (index from LeftCell).
+ tsl::array_map<char, int> mName2Cell;
+ int Row;
+ /// Leftmost cell in this group
+ int LeftCell;
+ /// Rightmost cell in this group
+ int RightCell;
+
+public:
+ Vec2i GetLeftCell() const;
+ Vec2i GetRightCell() const;
+ int GetCount() const;
+
+ /// Find the location of the cell within this array group that has the given name.
+ Vec2i FindCell(std::string_view name);
+ bool UpdateCellName(std::string_view oldName, std::string_view newName);
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+};
+
+// Forward declaration of libxlsxwriter structs
+struct lxw_workbook;
+struct lxw_worksheet;
+
+/// An object containing the necessary information to instantiate a table template.
+/// \see TableTemplate
+class TableInstantiationParameters
+{
+private:
+ const TableTemplate* mTable;
+
+public:
+ tsl::robin_map<Vec2i, std::string> SingularCells;
+
+ using ArrayGroupRow = std::vector<std::string>;
+ using ArrayGroupData = std::vector<ArrayGroupRow>;
+ std::vector<ArrayGroupData> ArrayGroups;
+
+public:
+ TableInstantiationParameters(const TableTemplate& table);
+
+ TableInstantiationParameters& ResetTable(const TableTemplate& newTable);
+ TableInstantiationParameters RebindTable(const TableTemplate& newTable) const;
+
+ const TableTemplate& GetTable() const;
+};
+
+/// A table template, where individual cells can be filled by workflows instantiating this template. Merged cells,
+/// parametric rows/columns, and grids are also supported.
+///
+/// This current supports exporting to xlsx files.
+class TableTemplate : public Template
+{
+ friend class TableSingleParamsIter;
+ friend class TableArrayGroupsIter;
+ class Private;
+
+private:
+ /// Map from parameter name to index of the parameter cell (stored in mCells).
+ tsl::array_map<char, int> mName2Parameters;
+ /// Map from array group name to the index of the array group (stored in mArrayGroups).
+ tsl::array_map<char, int> mName2ArrayGroups;
+ std::vector<TableCell> mCells;
+ std::vector<TableArrayGroup> mArrayGroups;
+ std::vector<int> mRowHeights;
+ std::vector<int> mColumnWidths;
+
+public:
+ static bool IsInstance(const Template* tmpl);
+ TableTemplate();
+
+ int GetTableWidth() const;
+ int GetTableHeight() const;
+ void Resize(int newWidth, int newHeight);
+
+ int GetRowHeight(int row) const;
+ void SetRowHeight(int row, int height);
+ int GetColumnWidth(int column) const;
+ void SetColumnWidth(int column, int width);
+
+ const TableCell& GetCell(Vec2i pos) const;
+ TableCell& GetCell(Vec2i pos);
+ /// <ul>
+ /// <li> In case of becoming a SingularParametricCell: the parameter name is filled with TableCell::Content.
+ /// <li> In case of becoming a ArrayGroupParametricCell: the array group name is automatically generated as the nth group it would be come.
+ /// i.e., if there aRe currently 3 groups, the newly created group would be named "4".
+ /// If this name collides with an existing group, hyphens \c - will be append to the name until no collision happens.
+ /// </ul>
+ void SetCellType(Vec2i pos, TableCell::CellType type);
+
+ /// Updates the parameter cell to a new name. Returns true on success and false on failure (param not found or name duplicates).
+ bool UpdateParameterName(std::string_view oldName, std::string_view newName);
+
+ int GetArrayGroupCount() const;
+ const TableArrayGroup& GetArrayGroup(int id) const;
+ TableArrayGroup& GetArrayGroup(int id);
+ TableArrayGroup* AddArrayGroup(int row, int left, int right);
+ TableArrayGroup* AddArrayGroup(std::string_view name, int row, int left, int right);
+ bool UpdateArrayGroupName(std::string_view oldName, std::string_view newName);
+ bool ExtendArrayGroupLeft(int id, int n);
+ bool ExtendArrayGroupRight(int id, int n);
+
+ /// Find a singular parameter cell by its name. This does not include cells within an array group.
+ TableCell* FindCell(std::string_view name);
+
+ /// Find an array group by its name.
+ TableArrayGroup* FindArrayGroup(std::string_view name);
+
+ enum MergeCellsResult
+ {
+ MCR_CellAlreadyMerged,
+ MCR_Success,
+ };
+ MergeCellsResult MergeCells(Vec2i topLeft, Vec2i bottomRight);
+
+ enum BreakCellsResult
+ {
+ BCR_CellNotMerged,
+ BCR_Success,
+ };
+ BreakCellsResult BreakCells(Vec2i topLeft);
+
+ lxw_workbook* InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const;
+ lxw_worksheet* InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const;
+
+ void ReadFromDataStream(InputDataStream& stream) override;
+ void WriteToDataStream(OutputDataStream& stream) const override;
+};
diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.cpp b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp
new file mode 100644
index 0000000..19e30b9
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp
@@ -0,0 +1,52 @@
+#include "TableTemplateIterator.hpp"
+
+TableSingleParamsIter::TableSingleParamsIter(TableTemplate& tmpl)
+ : mTemplate{ &tmpl }
+ , mIter{ tmpl.mName2Parameters.begin() }
+{
+}
+
+bool TableSingleParamsIter::HasNext() const
+{
+ return mIter != mTemplate->mName2Parameters.end();
+}
+
+TableCell& TableSingleParamsIter::Next()
+{
+ int id = mIter.value();
+ ++mIter;
+
+ return mTemplate->mCells[id];
+}
+
+TableArrayGroupsIter::TableArrayGroupsIter(TableTemplate& tmpl)
+ : mTemplate{ &tmpl }
+ , mIter{ tmpl.mName2ArrayGroups.begin() }
+{
+}
+
+bool TableArrayGroupsIter::HasNext() const
+{
+ return mIter != mTemplate->mName2ArrayGroups.end();
+}
+
+TableArrayGroup& TableArrayGroupsIter::Peek() const
+{
+ int id = mIter.value();
+ return mTemplate->mArrayGroups[id];
+}
+
+std::string_view TableArrayGroupsIter::PeekName() const
+{
+ return mIter.key_sv();
+}
+
+const char* TableArrayGroupsIter::PeekNameCStr() const
+{
+ return mIter.key();
+}
+
+void TableArrayGroupsIter::Next()
+{
+ ++mIter;
+}
diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.hpp b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp
new file mode 100644
index 0000000..c4b5bf9
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <Cplt/Model/Template/TableTemplate.hpp>
+#include <Cplt/Model/Template/Template.hpp>
+
+#include <string_view>
+
+class TableSingleParamsIter
+{
+private:
+ TableTemplate* mTemplate;
+ tsl::array_map<char, int>::iterator mIter;
+
+public:
+ TableSingleParamsIter(TableTemplate& tmpl);
+
+ bool HasNext() const;
+ TableCell& Next();
+};
+
+class TableArrayGroupsIter
+{
+private:
+ TableTemplate* mTemplate;
+ tsl::array_map<char, int>::iterator mIter;
+
+public:
+ TableArrayGroupsIter(TableTemplate& tmpl);
+
+ bool HasNext() const;
+ TableArrayGroup& Peek() const;
+ std::string_view PeekName() const;
+ const char* PeekNameCStr() const;
+ void Next();
+};
diff --git a/app/source/Cplt/Model/Template/Template.hpp b/app/source/Cplt/Model/Template/Template.hpp
new file mode 100644
index 0000000..cf926d0
--- /dev/null
+++ b/app/source/Cplt/Model/Template/Template.hpp
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <Cplt/Model/Assets.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <filesystem>
+#include <iosfwd>
+#include <memory>
+#include <string>
+
+class Template : public Asset
+{
+public:
+ enum Kind
+ {
+ KD_Table,
+
+ InvalidKind,
+ KindCount = InvalidKind,
+ };
+
+ using CategoryType = TemplateAssetList;
+
+private:
+ Kind mKind;
+
+public:
+ static const char* FormatKind(Kind kind);
+ static std::unique_ptr<Template> CreateByKind(Kind kind);
+
+ static bool IsInstance(const Template* tmpl);
+
+ Template(Kind kind);
+ ~Template() override = default;
+
+ Kind GetKind() const;
+
+ virtual void ReadFromDataStream(InputDataStream& stream) = 0;
+ virtual void WriteToDataStream(OutputDataStream& stream) const = 0;
+};
+
+class TemplateAssetList final : public AssetListTyped<Template>
+{
+private:
+ // AC = Asset Creator
+ std::string mACNewName;
+ NameSelectionError mACNewNameError = NameSelectionError::Empty;
+ Template::Kind mACNewKind = Template::InvalidKind;
+
+public:
+ // Inherit constructors
+ using AssetListTyped::AssetListTyped;
+
+protected:
+ void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override;
+
+ std::string RetrieveNameFromFile(const std::filesystem::path& file) const override;
+ uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override;
+ std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override;
+
+ bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override;
+ Template* LoadInstance(const SavedAsset& assetInfo) const override;
+ Template* CreateInstance(const SavedAsset& assetInfo) const override;
+ bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override;
+
+ void DisplayAssetCreator(ListState& state) override;
+ void DisplayDetailsTable(ListState& state) const override;
+};
diff --git a/app/source/Cplt/Model/Template/Template_Main.cpp b/app/source/Cplt/Model/Template/Template_Main.cpp
new file mode 100644
index 0000000..d658231
--- /dev/null
+++ b/app/source/Cplt/Model/Template/Template_Main.cpp
@@ -0,0 +1,214 @@
+#include "Template.hpp"
+
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/IO/Archive.hpp>
+#include <Cplt/Utils/UUID.hpp>
+
+#include <imgui.h>
+#include <imgui_stdlib.h>
+#include <algorithm>
+#include <cstdint>
+#include <fstream>
+
+using namespace std::literals::string_view_literals;
+namespace fs = std::filesystem;
+
+Template::Template(Kind kind)
+ : mKind{ kind }
+{
+}
+
+Template::Kind Template::GetKind() const
+{
+ return mKind;
+}
+
+void TemplateAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const
+{
+ auto dir = GetConnectedProject().GetTemplatesDirectory();
+ DiscoverFilesByExtension(callback, dir, ".cplt-template"sv);
+}
+
+std::string TemplateAssetList::RetrieveNameFromFile(const fs::path& file) const
+{
+ auto res = DataArchive::LoadFile(file);
+ if (!res) return "";
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ return assetInfo.Name;
+}
+
+uuids::uuid TemplateAssetList::RetrieveUuidFromFile(const fs::path& file) const
+{
+ return uuids::uuid::from_string(file.stem().string());
+}
+
+fs::path TemplateAssetList::RetrievePathFromAsset(const SavedAsset& asset) const
+{
+ auto fileName = uuids::to_string(asset.Uuid);
+ return GetConnectedProject().GetTemplatePath(fileName);
+}
+
+bool TemplateAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+ auto res = DataArchive::SaveFile(path);
+ if (!res) return false;
+ auto& stream = res.value();
+
+ stream.WriteObject(assetInfo);
+ // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Template
+ if (auto tmpl = static_cast<const Template*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
+ stream.WriteObject(*tmpl);
+ }
+
+ return true;
+}
+
+static std::unique_ptr<Template> LoadTemplateFromFile(const fs::path& path)
+{
+ auto res = DataArchive::LoadFile(path);
+ if (!res) return nullptr;
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ auto kind = static_cast<Template::Kind>(assetInfo.Payload);
+ auto tmpl = Template::CreateByKind(kind);
+ stream.ReadObject(*tmpl);
+
+ return tmpl;
+}
+
+Template* TemplateAssetList::LoadInstance(const SavedAsset& assetInfo) const
+{
+ return ::LoadTemplateFromFile(RetrievePathFromAsset(assetInfo)).release();
+}
+
+Template* TemplateAssetList::CreateInstance(const SavedAsset& assetInfo) const
+{
+ auto kind = static_cast<Template::Kind>(assetInfo.Payload);
+ return Template::CreateByKind(kind).release();
+}
+
+bool TemplateAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const
+{
+ // Get asset path, which is only dependent on UUID
+ auto path = RetrievePathFromAsset(assetInfo);
+
+ auto tmpl = ::LoadTemplateFromFile(path);
+ if (!tmpl) return false;
+
+ // Rewrite the asset with the updated name (note the given assetInfo already has the update name)
+ SaveInstance(assetInfo, tmpl.get());
+
+ return true;
+}
+
+void TemplateAssetList::DisplayAssetCreator(ListState& state)
+{
+ auto ValidateNewName = [&]() -> void {
+ if (mACNewName.empty()) {
+ mACNewNameError = NameSelectionError::Empty;
+ return;
+ }
+
+ if (FindByName(mACNewName)) {
+ mACNewNameError = NameSelectionError::Duplicated;
+ return;
+ }
+
+ mACNewNameError = NameSelectionError::None;
+ };
+
+ auto ShowNewNameErrors = [&]() -> void {
+ switch (mACNewNameError) {
+ case NameSelectionError::None: break;
+ case NameSelectionError::Duplicated:
+ ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR));
+ break;
+ case NameSelectionError::Empty:
+ ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR));
+ break;
+ }
+ };
+
+ auto ShowNewKindErrors = [&]() -> void {
+ if (mACNewKind == Template::InvalidKind) {
+ ImGui::ErrorMessage(I18N_TEXT("Invalid template type", L10N_TEMPLATE_INVALID_TYPE_ERROR));
+ }
+ };
+
+ auto IsInputValid = [&]() -> bool {
+ return mACNewNameError == NameSelectionError::None &&
+ mACNewKind != Template::InvalidKind;
+ };
+
+ auto ResetState = [&]() -> void {
+ mACNewName.clear();
+ mACNewKind = Template::InvalidKind;
+ ValidateNewName();
+ };
+
+ if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) {
+ ValidateNewName();
+ }
+
+ if (ImGui::BeginCombo(I18N_TEXT("Type", L10N_TYPE), Template::FormatKind(mACNewKind))) {
+ for (int i = 0; i < Template::KindCount; ++i) {
+ auto kind = static_cast<Template::Kind>(i);
+ if (ImGui::Selectable(Template::FormatKind(kind), mACNewKind == kind)) {
+ mACNewKind = kind;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ShowNewNameErrors();
+ ShowNewKindErrors();
+
+ if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) {
+ ImGui::CloseCurrentPopup();
+
+ Create(SavedAsset{
+ .Name = mACNewName,
+ .Payload = static_cast<uint64_t>(mACNewKind),
+ });
+ ResetState();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void TemplateAssetList::DisplayDetailsTable(ListState& state) const
+{
+ ImGui::BeginTable("AssetDetailsTable", 2, ImGuiTableFlags_Borders);
+
+ ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME));
+ ImGui::TableSetupColumn(I18N_TEXT("Type", L10N_TYPE));
+ ImGui::TableHeadersRow();
+
+ for (auto& asset : this->GetAssets()) {
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) {
+ state.SelectedAsset = &asset;
+ }
+
+ ImGui::TableNextColumn();
+ auto kind = static_cast<Template::Kind>(asset.Payload);
+ ImGui::TextUnformatted(Template::FormatKind(kind));
+ }
+
+ ImGui::EndTable();
+}
diff --git a/app/source/Cplt/Model/Template/Template_RTTI.cpp b/app/source/Cplt/Model/Template/Template_RTTI.cpp
new file mode 100644
index 0000000..a96680b
--- /dev/null
+++ b/app/source/Cplt/Model/Template/Template_RTTI.cpp
@@ -0,0 +1,29 @@
+#include "Template.hpp"
+
+#include <Cplt/Model/Template/TableTemplate.hpp>
+#include <Cplt/Utils/I18n.hpp>
+
+const char* Template::FormatKind(Kind kind)
+{
+ switch (kind) {
+ case KD_Table: return I18N_TEXT("Table template", L10N_TEMPLATE_TABLE);
+
+ case InvalidKind: break;
+ }
+ return "";
+}
+
+std::unique_ptr<Template> Template::CreateByKind(Kind kind)
+{
+ switch (kind) {
+ case KD_Table: return std::make_unique<TableTemplate>();
+
+ case InvalidKind: break;
+ }
+ return nullptr;
+}
+
+bool Template::IsInstance(const Template* tmpl)
+{
+ return true;
+}
diff --git a/app/source/Cplt/Model/Template/fwd.hpp b/app/source/Cplt/Model/Template/fwd.hpp
new file mode 100644
index 0000000..8378871
--- /dev/null
+++ b/app/source/Cplt/Model/Template/fwd.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+// TableTemplate.hpp
+class TableCell;
+class TableArrayGroup;
+class TableInstantiationParameters;
+class TableTemplate;
+
+// Template.hpp
+class Template;
+class TemplateAssetList;