diff options
Diffstat (limited to 'app/source/Cplt/Model/Template')
-rw-r--r-- | app/source/Cplt/Model/Template/TableTemplate.cpp | 591 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/TableTemplate.hpp | 223 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/TableTemplateIterator.cpp | 52 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/TableTemplateIterator.hpp | 35 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/Template.hpp | 68 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/Template_Main.cpp | 214 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/Template_RTTI.cpp | 29 | ||||
-rw-r--r-- | app/source/Cplt/Model/Template/fwd.hpp | 11 |
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; |