Loading editor/documentmanager.cpp +32 −0 Original line number Diff line number Diff line Loading @@ -825,3 +825,35 @@ QString DocumentManager::publishTarget() const { return _publishTarget; } void DocumentManager::setPublishTarget(const QString &target) { _publishTarget = target; } QString DocumentManager::publishParams() const { return _publishParams; } void DocumentManager::setPublishParams(const QString ¶msJson) { _publishParams = paramsJson; } EditPlugin *DocumentManager::findParentOf(const EditPlugin *child) const { if (!child || !_docRoot) return nullptr; // Check if child is a top-level sibling (parent = virtual root = nullptr) const EditPlugin *el = _docRoot; while (el) { if (el == child) return nullptr; el = el->nextElement(); } // Recursive search through the tree std::function<EditPlugin*(EditPlugin*)> search = [&](EditPlugin *node) -> EditPlugin* { const EditPlugin *c = node->getChildElement(); while (c) { if (c == child) return node; // Search deeper auto *found = search(const_cast<EditPlugin*>(c)); if (found) return found; c = c->nextElement(); } return nullptr; }; el = _docRoot; while (el) { auto *found = search(const_cast<EditPlugin*>(el)); if (found) return found; el = el->nextElement(); } return nullptr; } editor/documentmanager.h +4 −0 Original line number Diff line number Diff line Loading @@ -88,6 +88,10 @@ public: QString publishParams() const; void setPublishParams(const QString ¶msJson); /* Tree access for DocumentTreeModel */ blogi::webedit::EditPlugin *docRoot() const { return _docRoot; } blogi::webedit::EditPlugin *findParentOf(const blogi::webedit::EditPlugin *child) const; private: void clearDocument(); void loadWidgetsFromXml(tinyxml2::XMLElement *xmlEl, Loading editor/documenttreemodel.cpp 0 → 100644 +201 −0 Original line number Diff line number Diff line /******************************************************************************* * Copyright (c) 2026, Jan Koester jan.koester@gmx.net * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * Neither the name of the <organization> nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ #include "documenttreemodel.h" #include "documentmanager.h" /* Undefine Qt's 'slots' macro before including plugin.h */ #ifdef slots #undef slots #endif #include "plugin.h" using blogi::webedit::EditPlugin; DocumentTreeModel::DocumentTreeModel(DocumentManager *doc, QObject *parent) : QAbstractItemModel(parent), _doc(doc) { } QHash<int, QByteArray> DocumentTreeModel::roleNames() const { QHash<int, QByteArray> roles; roles[NameRole] = "name"; roles[UuidRole] = "uuid"; roles[TypeIdRole] = "typeId"; roles[HasChildrenRole] = "hasChildren"; roles[Qt::DisplayRole] = "display"; return roles; } QModelIndex DocumentTreeModel::index(int row, int column, const QModelIndex &parent) const { if (column != 0) return QModelIndex(); EditPlugin *parentPlugin = pluginFromIndex(parent); // Get the Nth child const EditPlugin *child = nullptr; if (!parentPlugin) { // Top-level: walk the docRoot linked list child = _doc->docRoot(); } else { child = parentPlugin->getChildElement(); } for (int i = 0; i < row && child; ++i) child = child->nextElement(); if (!child) return QModelIndex(); return createIndex(row, 0, const_cast<EditPlugin*>(child)); } QModelIndex DocumentTreeModel::parent(const QModelIndex &child) const { if (!child.isValid()) return QModelIndex(); auto *childPlugin = static_cast<EditPlugin*>(child.internalPointer()); if (!childPlugin) return QModelIndex(); EditPlugin *par = _doc->findParentOf(childPlugin); if (!par) return QModelIndex(); // top-level node // Find row of parent in its own parent's children int row = childRow(par); return createIndex(row, 0, par); } int DocumentTreeModel::rowCount(const QModelIndex &parent) const { const EditPlugin *node = pluginFromIndex(parent); const EditPlugin *child = nullptr; if (!node) { // Top-level count child = _doc->docRoot(); } else { child = node->getChildElement(); } int count = 0; while (child) { ++count; child = child->nextElement(); } return count; } int DocumentTreeModel::columnCount(const QModelIndex &) const { return 1; } QVariant DocumentTreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto *plugin = static_cast<EditPlugin*>(index.internalPointer()); if (!plugin) return QVariant(); switch (role) { case Qt::DisplayRole: case NameRole: return QString::fromStdString(plugin->getName()); case UuidRole: return QString::fromStdString(plugin->getInstanceId().c_str()); case TypeIdRole: return QString::fromStdString(plugin->getTypeId().c_str()); case HasChildrenRole: return plugin->getChildElement() != nullptr; default: return QVariant(); } } void DocumentTreeModel::notifyFullReset() { beginResetModel(); endResetModel(); } void DocumentTreeModel::notifyNodeChanged(const QString &uuid) { // Find the node and emit dataChanged for it QModelIndex idx = indexForPlugin(nullptr); // For simplicity, do a full reset for now — still much cheaper than // recreating hundreds of QML Loader items Q_UNUSED(uuid) beginResetModel(); endResetModel(); } /* ---- Private helpers ---- */ EditPlugin *DocumentTreeModel::pluginFromIndex(const QModelIndex &index) const { if (!index.isValid()) return nullptr; return static_cast<EditPlugin*>(index.internalPointer()); } QModelIndex DocumentTreeModel::indexForPlugin(const EditPlugin *plugin, const QModelIndex &parent) const { if (!plugin) return QModelIndex(); int rows = rowCount(parent); for (int i = 0; i < rows; ++i) { QModelIndex idx = index(i, 0, parent); if (idx.internalPointer() == plugin) return idx; // Recurse into children QModelIndex found = indexForPlugin(plugin, idx); if (found.isValid()) return found; } return QModelIndex(); } EditPlugin *DocumentTreeModel::parentPlugin(const EditPlugin *child) const { return _doc->findParentOf(child); } int DocumentTreeModel::childRow(const EditPlugin *child) const { if (!child) return 0; EditPlugin *par = _doc->findParentOf(child); const EditPlugin *sibling = nullptr; if (!par) { sibling = _doc->docRoot(); } else { sibling = par->getChildElement(); } int row = 0; while (sibling && sibling != child) { ++row; sibling = sibling->nextElement(); } return row; } editor/documenttreemodel.h 0 → 100644 +45 −0 Original line number Diff line number Diff line #pragma once #include <QAbstractItemModel> #include <QModelIndex> #include <QVariant> #include <QString> /* Forward-declare EditPlugin without pulling in netplus headers */ namespace blogi { namespace webedit { class EditPlugin; } } class DocumentManager; class DocumentTreeModel : public QAbstractItemModel { Q_OBJECT public: enum Roles { NameRole = Qt::UserRole + 1, UuidRole, TypeIdRole, HasChildrenRole }; Q_ENUM(Roles) explicit DocumentTreeModel(DocumentManager *doc, QObject *parent = nullptr); /* QAbstractItemModel interface */ QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &child) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash<int, QByteArray> roleNames() const override; /* Notify model of structural changes */ void notifyFullReset(); void notifyNodeChanged(const QString &uuid); private: blogi::webedit::EditPlugin *pluginFromIndex(const QModelIndex &index) const; QModelIndex indexForPlugin(const blogi::webedit::EditPlugin *plugin, const QModelIndex &parent = QModelIndex()) const; blogi::webedit::EditPlugin *parentPlugin(const blogi::webedit::EditPlugin *child) const; int childRow(const blogi::webedit::EditPlugin *child) const; DocumentManager *_doc; }; editor/localapi.cpp +7 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ #include "localapi.h" #include "documentmanager.h" #include "publishclient.h" #include "documenttreemodel.h" #include <QFileDialog> #include <QFile> Loading @@ -46,12 +47,18 @@ LocalApi::LocalApi(DocumentManager *doc, PublishClient *pub, QObject *parent) : QObject(parent), _doc(doc), _pub(pub) { _fsModel = new GitFileSystemModel(this); _treeModel = new DocumentTreeModel(doc, this); connect(this, &LocalApi::documentChanged, _treeModel, &DocumentTreeModel::notifyFullReset); } GitFileSystemModel* LocalApi::fileSystemModel() { return _fsModel; } DocumentTreeModel* LocalApi::documentTreeModel() { return _treeModel; } void LocalApi::setFileSystemRoot(const QString &path) { _fsModel->setRootPath(path); } Loading Loading
editor/documentmanager.cpp +32 −0 Original line number Diff line number Diff line Loading @@ -825,3 +825,35 @@ QString DocumentManager::publishTarget() const { return _publishTarget; } void DocumentManager::setPublishTarget(const QString &target) { _publishTarget = target; } QString DocumentManager::publishParams() const { return _publishParams; } void DocumentManager::setPublishParams(const QString ¶msJson) { _publishParams = paramsJson; } EditPlugin *DocumentManager::findParentOf(const EditPlugin *child) const { if (!child || !_docRoot) return nullptr; // Check if child is a top-level sibling (parent = virtual root = nullptr) const EditPlugin *el = _docRoot; while (el) { if (el == child) return nullptr; el = el->nextElement(); } // Recursive search through the tree std::function<EditPlugin*(EditPlugin*)> search = [&](EditPlugin *node) -> EditPlugin* { const EditPlugin *c = node->getChildElement(); while (c) { if (c == child) return node; // Search deeper auto *found = search(const_cast<EditPlugin*>(c)); if (found) return found; c = c->nextElement(); } return nullptr; }; el = _docRoot; while (el) { auto *found = search(const_cast<EditPlugin*>(el)); if (found) return found; el = el->nextElement(); } return nullptr; }
editor/documentmanager.h +4 −0 Original line number Diff line number Diff line Loading @@ -88,6 +88,10 @@ public: QString publishParams() const; void setPublishParams(const QString ¶msJson); /* Tree access for DocumentTreeModel */ blogi::webedit::EditPlugin *docRoot() const { return _docRoot; } blogi::webedit::EditPlugin *findParentOf(const blogi::webedit::EditPlugin *child) const; private: void clearDocument(); void loadWidgetsFromXml(tinyxml2::XMLElement *xmlEl, Loading
editor/documenttreemodel.cpp 0 → 100644 +201 −0 Original line number Diff line number Diff line /******************************************************************************* * Copyright (c) 2026, Jan Koester jan.koester@gmx.net * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * Neither the name of the <organization> nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ #include "documenttreemodel.h" #include "documentmanager.h" /* Undefine Qt's 'slots' macro before including plugin.h */ #ifdef slots #undef slots #endif #include "plugin.h" using blogi::webedit::EditPlugin; DocumentTreeModel::DocumentTreeModel(DocumentManager *doc, QObject *parent) : QAbstractItemModel(parent), _doc(doc) { } QHash<int, QByteArray> DocumentTreeModel::roleNames() const { QHash<int, QByteArray> roles; roles[NameRole] = "name"; roles[UuidRole] = "uuid"; roles[TypeIdRole] = "typeId"; roles[HasChildrenRole] = "hasChildren"; roles[Qt::DisplayRole] = "display"; return roles; } QModelIndex DocumentTreeModel::index(int row, int column, const QModelIndex &parent) const { if (column != 0) return QModelIndex(); EditPlugin *parentPlugin = pluginFromIndex(parent); // Get the Nth child const EditPlugin *child = nullptr; if (!parentPlugin) { // Top-level: walk the docRoot linked list child = _doc->docRoot(); } else { child = parentPlugin->getChildElement(); } for (int i = 0; i < row && child; ++i) child = child->nextElement(); if (!child) return QModelIndex(); return createIndex(row, 0, const_cast<EditPlugin*>(child)); } QModelIndex DocumentTreeModel::parent(const QModelIndex &child) const { if (!child.isValid()) return QModelIndex(); auto *childPlugin = static_cast<EditPlugin*>(child.internalPointer()); if (!childPlugin) return QModelIndex(); EditPlugin *par = _doc->findParentOf(childPlugin); if (!par) return QModelIndex(); // top-level node // Find row of parent in its own parent's children int row = childRow(par); return createIndex(row, 0, par); } int DocumentTreeModel::rowCount(const QModelIndex &parent) const { const EditPlugin *node = pluginFromIndex(parent); const EditPlugin *child = nullptr; if (!node) { // Top-level count child = _doc->docRoot(); } else { child = node->getChildElement(); } int count = 0; while (child) { ++count; child = child->nextElement(); } return count; } int DocumentTreeModel::columnCount(const QModelIndex &) const { return 1; } QVariant DocumentTreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto *plugin = static_cast<EditPlugin*>(index.internalPointer()); if (!plugin) return QVariant(); switch (role) { case Qt::DisplayRole: case NameRole: return QString::fromStdString(plugin->getName()); case UuidRole: return QString::fromStdString(plugin->getInstanceId().c_str()); case TypeIdRole: return QString::fromStdString(plugin->getTypeId().c_str()); case HasChildrenRole: return plugin->getChildElement() != nullptr; default: return QVariant(); } } void DocumentTreeModel::notifyFullReset() { beginResetModel(); endResetModel(); } void DocumentTreeModel::notifyNodeChanged(const QString &uuid) { // Find the node and emit dataChanged for it QModelIndex idx = indexForPlugin(nullptr); // For simplicity, do a full reset for now — still much cheaper than // recreating hundreds of QML Loader items Q_UNUSED(uuid) beginResetModel(); endResetModel(); } /* ---- Private helpers ---- */ EditPlugin *DocumentTreeModel::pluginFromIndex(const QModelIndex &index) const { if (!index.isValid()) return nullptr; return static_cast<EditPlugin*>(index.internalPointer()); } QModelIndex DocumentTreeModel::indexForPlugin(const EditPlugin *plugin, const QModelIndex &parent) const { if (!plugin) return QModelIndex(); int rows = rowCount(parent); for (int i = 0; i < rows; ++i) { QModelIndex idx = index(i, 0, parent); if (idx.internalPointer() == plugin) return idx; // Recurse into children QModelIndex found = indexForPlugin(plugin, idx); if (found.isValid()) return found; } return QModelIndex(); } EditPlugin *DocumentTreeModel::parentPlugin(const EditPlugin *child) const { return _doc->findParentOf(child); } int DocumentTreeModel::childRow(const EditPlugin *child) const { if (!child) return 0; EditPlugin *par = _doc->findParentOf(child); const EditPlugin *sibling = nullptr; if (!par) { sibling = _doc->docRoot(); } else { sibling = par->getChildElement(); } int row = 0; while (sibling && sibling != child) { ++row; sibling = sibling->nextElement(); } return row; }
editor/documenttreemodel.h 0 → 100644 +45 −0 Original line number Diff line number Diff line #pragma once #include <QAbstractItemModel> #include <QModelIndex> #include <QVariant> #include <QString> /* Forward-declare EditPlugin without pulling in netplus headers */ namespace blogi { namespace webedit { class EditPlugin; } } class DocumentManager; class DocumentTreeModel : public QAbstractItemModel { Q_OBJECT public: enum Roles { NameRole = Qt::UserRole + 1, UuidRole, TypeIdRole, HasChildrenRole }; Q_ENUM(Roles) explicit DocumentTreeModel(DocumentManager *doc, QObject *parent = nullptr); /* QAbstractItemModel interface */ QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &child) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash<int, QByteArray> roleNames() const override; /* Notify model of structural changes */ void notifyFullReset(); void notifyNodeChanged(const QString &uuid); private: blogi::webedit::EditPlugin *pluginFromIndex(const QModelIndex &index) const; QModelIndex indexForPlugin(const blogi::webedit::EditPlugin *plugin, const QModelIndex &parent = QModelIndex()) const; blogi::webedit::EditPlugin *parentPlugin(const blogi::webedit::EditPlugin *child) const; int childRow(const blogi::webedit::EditPlugin *child) const; DocumentManager *_doc; };
editor/localapi.cpp +7 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ #include "localapi.h" #include "documentmanager.h" #include "publishclient.h" #include "documenttreemodel.h" #include <QFileDialog> #include <QFile> Loading @@ -46,12 +47,18 @@ LocalApi::LocalApi(DocumentManager *doc, PublishClient *pub, QObject *parent) : QObject(parent), _doc(doc), _pub(pub) { _fsModel = new GitFileSystemModel(this); _treeModel = new DocumentTreeModel(doc, this); connect(this, &LocalApi::documentChanged, _treeModel, &DocumentTreeModel::notifyFullReset); } GitFileSystemModel* LocalApi::fileSystemModel() { return _fsModel; } DocumentTreeModel* LocalApi::documentTreeModel() { return _treeModel; } void LocalApi::setFileSystemRoot(const QString &path) { _fsModel->setRootPath(path); } Loading