Tue, 28 Jun 2022 12:29:38 +0300
Make settings editor scrollable
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013 - 2020 Teemu Piippo * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include <QPixmap> #include "src/model.h" constexpr unsigned int gcd(unsigned int a, unsigned int b) { while (a != b) { if (b > a) { b -= a; } else if (a > b) { a -= b; } } return a; } static_assert(gcd(16, 15) == 1); static_assert(gcd(16, 4) == 4); static_assert(gcd(272, 192) == 16); static constexpr const char* circularPrimitiveTypeString(const CircularPrimitive& circ) { return circularPrimitiveStems[circ.type]; } static QString circularPrimitiveFilePath(const CircularPrimitive& circ) { QString result; if (circ.fraction.divisions != 16) { result += QString::number(circ.fraction.divisions) + QStringLiteral("\\"); } const unsigned int factor = gcd(circ.fraction.segments, circ.fraction.divisions); unsigned int num = circ.fraction.segments / factor; unsigned int denom = circ.fraction.divisions / factor; if (denom < 4) { num *= 4 / denom; denom = 4; } result += QStringLiteral("%1-%2").arg(num).arg(denom); result += QString::fromLatin1(circularPrimitiveTypeString(circ)); result += QStringLiteral(".dat"); return result; } static const char* iconPathForElement(const ModelElement& element) { return std::visit(overloaded{ [](const Colored<SubfileReference>&) { return ":/icons/linetype-subfile.png"; }, [](const Colored<LineSegment>&) { return ":/icons/linetype-edgeline.png"; }, [](const Colored<Triangle>&) { return ":/icons/linetype-triangle.png"; }, [](const Colored<Quadrilateral>&) { return ":/icons/linetype-quadrilateral.png"; }, [](const Colored<ConditionalEdge>&) { return ":/icons/linetype-conditionaledge.png"; }, [](const Colored<CircularPrimitive>&) { return ":/icons/linetype-circularprimitive.png"; }, [](const Comment&) { return ":/icons/chatbubble-ellipses-outline.png"; }, [](const Empty&) { return ""; }, [](const ParseError&) { return ":/icons/linetype-errorline.png"; }, }, element); } static QPixmap iconForElement(const ModelElement& element) { // We avoid processing the same image over and over again by storing it // in a static constant. However, we need one per each possible type // of ModelElement, so we put the pixmap constant inside a templated lambda, // which gets instiated once for each type of ModelElement. return std::visit([](auto&& element){ static const QPixmap pixmap = QPixmap::fromImage( QImage{iconPathForElement(element)} .scaledToHeight(24, Qt::SmoothTransformation) ); return pixmap; }, element); } QString modelElementToString(const ModelElement &element) { return std::visit(overloaded{ [](const Colored<SubfileReference>& ref) { return QStringLiteral("1 %1 %2 %3") .arg(ref.color.index) .arg(transformToString(ref.transformation)) .arg(ref.name); }, [](const Colored<LineSegment>& seg) { return QStringLiteral("2 %1 %2 %3") .arg(seg.color.index) .arg(vertexToString(seg.p1)) .arg(vertexToString(seg.p2)); }, [](const Colored<Triangle>& triangle) { return QStringLiteral("3 %1 %2 %3 %4") .arg(triangle.color.index) .arg(vertexToString(triangle.p1)) .arg(vertexToString(triangle.p2)) .arg(vertexToString(triangle.p3)); }, [](const Colored<Quadrilateral>& quad) { return QStringLiteral("4 %1 %2 %3 %4 %5") .arg(quad.color.index) .arg(vertexToString(quad.p1)) .arg(vertexToString(quad.p2)) .arg(vertexToString(quad.p3)) .arg(vertexToString(quad.p4)); }, [](const Colored<ConditionalEdge>& cedge) { return QStringLiteral("5 %1 %2 %3 %4 %5") .arg(cedge.color.index) .arg(vertexToString(cedge.p1)) .arg(vertexToString(cedge.p2)) .arg(vertexToString(cedge.c1)) .arg(vertexToString(cedge.c2)); }, [](const Colored<CircularPrimitive>& circ) { return QStringLiteral("1 %1 %2 %3") .arg(circ.color.index) .arg(transformToString(circ.transformation)) .arg(circularPrimitiveFilePath(circ)); }, [](const Comment& comment) { return "0 " + comment.text; }, [](const Empty&) { return QStringLiteral(""); }, [](const ParseError& parseError) { return parseError.code; }, }, element); } Model::Model(QObject *parent) : QAbstractListModel{parent} { } Model::~Model() { } ModelId Model::append(const ModelElement &value) { const std::size_t position = this->size(); const ModelId id = this->runningId; this->runningId.value += 1; const int row = narrow<int>(signed_cast(this->size())); Q_EMIT this->beginInsertRows({}, row, row); this->body.push_back({value, id}); this->positions[id] = position; Q_EMIT this->endInsertRows(); return id; } const ModelElement &Model::at(std::size_t position) const { return this->body[position].data; } ModelId Model::idAt(std::size_t position) const { return this->body[position].id; } void Model::assignAt(std::size_t position, const ModelElement &element) { this->body[position].data = element; const QModelIndex index = this->index(narrow<int>(signed_cast(position))); Q_EMIT this->dataChanged(index, index); } std::optional<std::size_t> Model::find(ModelId id) const { return pointerToOptional(findInMap(this->positions, id)); } template<typename K, typename V> void removeFromMap(std::map<K, V>& map, const K& key) { const auto it = map.find(key); if (it != map.end()) { map.erase(it); } } void Model::remove(const std::size_t index) { if (index < this->body.size()) { const int row = narrow<int>(signed_cast(index)); Q_EMIT this->beginRemoveRows({}, row, row); removeFromMap(this->positions, this->body[index].id); this->body.erase(this->body.begin() + row); for (std::size_t i = index; i < this->body.size(); ++i) { this->positions[this->body[i].id] = i; } Q_EMIT this->endRemoveRows(); } } int Model::rowCount(const QModelIndex &) const { return narrow<int>(signed_cast(this->size())); } QVariant Model::data(const QModelIndex &index, int role) const { const std::size_t i = unsigned_cast(index.row()); const ModelElement& element = this->body[i].data; switch(role) { case Qt::DecorationRole: return iconForElement(element); case Qt::DisplayRole: return modelElementToString(element); /* case Qt::ForegroundRole: return object->textRepresentationForeground(); case Qt::BackgroundRole: return object->textRepresentationBackground(); case Qt::FontRole: return object->textRepresentationFont(); */ default: return {}; } } const ModelElement &Model::operator[](std::size_t index) const { return this->body[index].data; } std::size_t Model::size() const { return this->body.size(); } void save(const Model &model, QIODevice *device) { QTextStream out{device}; for (std::size_t i = 0; i < model.size(); ++i) { out << modelElementToString(model[i]) << "\r\n"; } } /** * @brief Sets the path to the model * @param path New path to use */ void updateHeaderNameField(Model& model, const QString &name) { // Update the "Name: 1234.dat" comment if (model.size() >= 2) { if (const Comment* nameObject = std::get_if<Comment>(&model[1])) { if (nameObject->text.startsWith("Name: ")) { model[1] = Comment{"Name: " + name}; } } } }