diff --git a/src/basketscenemodel.cpp b/src/basketscenemodel.cpp index efe427f..5efadb2 100644 --- a/src/basketscenemodel.cpp +++ b/src/basketscenemodel.cpp @@ -1,290 +1,308 @@ /* - * SPDX-FileCopyrightText: 2020 by Ismael Asensio + * SPDX-FileCopyrightText: (C) 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #include "basketscenemodel.h" #include BasketSceneModel::BasketSceneModel(QObject *parent) : QAbstractItemModel(parent) - , m_root(new NoteItem(nullptr)) + , m_root(new NoteItem()) { + qRegisterMetaType(); + qRegisterMetaType(); } BasketSceneModel::~BasketSceneModel() { delete m_root; } int BasketSceneModel::rowCount(const QModelIndex &parent) const { return itemAtIndex(parent)->childrenCount(); } int BasketSceneModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent) return 1; } QModelIndex BasketSceneModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return QModelIndex(); } NoteItem *item = itemAtIndex(parent)->children().at(row); if (item) { return createIndex(row, column, item); } return QModelIndex(); } QModelIndex BasketSceneModel::parent(const QModelIndex& index) const { if (!index.isValid()) return QModelIndex(); NoteItem *parentItem = itemAtIndex(index)->parent(); return indexOfItem(parentItem); } QVariant BasketSceneModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section) if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return QStringLiteral("BasketSceneModel"); return QVariant(); } QHash BasketSceneModel::roleNames() const { static const auto roles = QHash { { ContentRole, "content" }, { TypeRole, "type" }, - { GroupRole, "isGroup" }, + { GeometryRole, "geometry" }, + { EditionRole, "edition" }, { AddressRole, "address" } }.unite(QAbstractItemModel::roleNames()); return roles; } QVariant BasketSceneModel::data(const QModelIndex &index, int role) const { if (!checkIndex(index, CheckIndexOption::IndexIsValid)) { return QVariant(); } const NoteItem *item = itemAtIndex(index); - const NotePtr note = item->note(); switch (role) { case Qt::DisplayRole: return item->displayText(); case Qt::DecorationRole: return item->decoration(); - case Qt::ToolTipRole: - return item->fullAddress(); // To ease debugging + case Qt::ToolTipRole: // To ease debugging + return item->toolTipInfo(); case BasketSceneModel::ContentRole: - return QVariant::fromValue(note->content()); + return QVariant::fromValue(item->content()); case BasketSceneModel::TypeRole: - return note->content()->type(); - case BasketSceneModel::GroupRole: - return note->isGroup(); + return item->type(); + case BasketSceneModel::GeometryRole: + return item->bounds(); + case BasketSceneModel::EditionRole: { + QVariant info; + info.setValue(item->editInformation()); + return info; + } case BasketSceneModel::AddressRole: return item->address(); } return QVariant(); } -void BasketSceneModel::clear() -{ - qDeleteAll(m_root->children()); -} - NoteItem *BasketSceneModel::itemAtIndex(const QModelIndex &index) const { if (!index.isValid()) { return m_root; } return static_cast(index.internalPointer()); } QModelIndex BasketSceneModel::indexOfItem(NoteItem *item) const { if (!item || item == m_root) { return QModelIndex(); } return createIndex(item->row(), 0, item); } QModelIndex BasketSceneModel::findNote(NotePtr note) const { if (!note || m_root->childrenCount() == 0) { return QModelIndex(); } const QModelIndexList matchResult = match(indexOfItem(m_root->children().at(0)), BasketSceneModel::AddressRole, NoteItem::formatAddress(note), 1, Qt::MatchExactly | Qt::MatchWrap | Qt::MatchRecursive); if (matchResult.isEmpty()) { return QModelIndex(); } return matchResult[0]; } bool BasketSceneModel::insertRows(int row, int count, const QModelIndex& parent) { if(!checkIndex(parent)) { return false; } beginInsertRows(parent, row, row + count - 1); for (int i = 0; i < count; i++) { - itemAtIndex(parent)->insertAt(row + i, new NoteItem(nullptr)); + itemAtIndex(parent)->insertAt(row + i, new NoteItem()); } endInsertRows(); return true; } bool BasketSceneModel::removeRows(int row, int count, const QModelIndex& parent) { if(!checkIndex(parent)) { return false; } beginRemoveRows(parent, row, row + count - 1); for (int i = 0; i < count; i++) { itemAtIndex(parent)->removeAt(row); } endRemoveRows(); return true; } bool BasketSceneModel::moveRows(const QModelIndex& sourceParent, int sourceRow, int count, const QModelIndex& destinationParent, int destinationChild) { const bool isMoveDown = (destinationParent == sourceParent && destinationChild > sourceRow); if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count -1, destinationParent, (isMoveDown) ? destinationChild + 1 : destinationChild)) { return false; } for (int i = 0; i < count; i++) { const int oldRow = (isMoveDown) ? sourceRow : sourceRow + i; itemAtIndex(destinationParent)->insertAt(destinationChild + i, itemAtIndex(sourceParent)->takeAt(oldRow)); } endMoveRows(); return true; } +void BasketSceneModel::clear() +{ + beginResetModel(); + qDeleteAll(m_root->children()); + endResetModel(); +} + +void BasketSceneModel::loadFromXML(const QDomElement &node) +{ + beginResetModel(); + qDeleteAll(m_root->children()); + m_root->loadFromXMLNode(node); + endResetModel(); +} + + void BasketSceneModel::insertNote(NotePtr note, NotePtr parentNote, int row) { if (!note) { return; } const QModelIndex parentIndex = findNote(parentNote); if (!checkIndex(parentIndex)) { return; } if (row < 0 || row >= rowCount(parentIndex)) { // row == -1 : insert at the end row = rowCount(parentIndex); } qDebug() << "BasketSceneModel::insertNote " << NoteItem::formatAddress(note) << " at " << itemAtIndex(parentIndex)->address() << "[" << row << "]"; // Protection against adding the same Note * twice (same pointer) // This should not be necessary once the old code in BasketScene has been cleaned-up if (findNote(note).isValid()) { QModelIndex prevIndex = findNote(note); qDebug() << " · Note " << itemAtIndex(prevIndex)->address() << " was alreadyPresent at " << itemAtIndex(parentIndex)->address() << "[" << prevIndex.row() << "]" << ". Moving it instead to new location"; moveNoteTo(note, parentNote, row); return; } if (insertRow(row, parentIndex)) { itemAtIndex(parentIndex)->children().at(row)->setNote(note); connect(note, &QObject::destroyed, this, [=](QObject *note) { BasketSceneModel::removeNote(static_cast(note)); }); } } void BasketSceneModel::removeNote(NotePtr note) { if (!note) { return; } const QModelIndex index = findNote(note); const QModelIndex parentIndex = index.parent(); qDebug() << "BasketSceneModel::removeNote " << itemAtIndex(index)->address(); removeRow(index.row(), parentIndex); } void BasketSceneModel::moveNoteTo(NotePtr note, NotePtr parentNote, int row) { if (!note) { return; } const QModelIndex currentIndex = findNote(note); const QModelIndex srcParentIndex = currentIndex.parent(); const QModelIndex destParentIndex = findNote(parentNote); if (!checkIndex(currentIndex, CheckIndexOption::IndexIsValid)) { return; } if (row < 0 || row >= rowCount(destParentIndex)) { row = rowCount(destParentIndex); // row == -1 : insert at the end } qDebug() << "BasketSceneModel::moveNote " << itemAtIndex(currentIndex)->address() << "\n from: " << itemAtIndex(srcParentIndex)->address() << "[" << currentIndex.row() << "]" << "\n to: " << itemAtIndex(destParentIndex)->address() << "[" << row << "]"; moveRow(srcParentIndex, currentIndex.row(), destParentIndex, row); } void BasketSceneModel::collapseItem(const QModelIndex &index) { if (hasChildren(index)) { itemAtIndex(index)->children().at(0)->note()->tryFoldParent(); } } void BasketSceneModel::expandItem(const QModelIndex &index) { if (hasChildren(index)) { itemAtIndex(index)->children().at(0)->note()->tryExpandParent(); } } void BasketSceneModel::changeSelection(const QItemSelection& selected, const QItemSelection& deselected) { for (const auto index : deselected.indexes()) { itemAtIndex(index)->note()->setSelected(false); } for (const auto index : selected.indexes()) { itemAtIndex(index)->note()->setSelected(true); } } diff --git a/src/basketscenemodel.h b/src/basketscenemodel.h index 997e5bd..cbe5c2e 100644 --- a/src/basketscenemodel.h +++ b/src/basketscenemodel.h @@ -1,67 +1,69 @@ /* * SPDX-FileCopyrightText: 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "basket_export.h" #include "noteitem.h" #include #include class BASKET_EXPORT BasketSceneModel : public QAbstractItemModel { Q_OBJECT public: - enum NoteRoles { - ContentRole = Qt::UserRole + 1, - TypeRole, - GroupRole, - AddressRole // String representation of the internal NotePtr address + enum AdditionalRoles { + ContentRole = Qt::UserRole + 1, // NoteContent * : the internal NoteContent + TypeRole, // NoteContent::Type: the type of the NoteContent + GeometryRole, // QRect: the bounds of the Note + EditionRole, // NoteItem::EditInfo: the edition information about the Note + AddressRole // QString: representation of the internal NotePtr address }; public: explicit BasketSceneModel(QObject *parent = nullptr); ~BasketSceneModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QHash roleNames() const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; // bool setData(const QModelIndex &index, const QVariant &value, int role) override; - // Implemented model operations bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) override; - // Specific methods to call from current code at BasketScene + void clear(); + void loadFromXML(const QDomElement &node); + + // Specific methods to call from BasketScene (temporary) void insertNote(NotePtr note, NotePtr parentNote, int row = -1); void removeNote(NotePtr note); void moveNoteTo(NotePtr note, NotePtr parentNote, int row = -1); - void clear(); public slots: - // Interaction between the model and BasketScene, mainly for demo and debuggint purposes + // Interaction between the model and BasketScene, mainly for demo and debugging purposes void expandItem(const QModelIndex &index); void collapseItem(const QModelIndex &index); void changeSelection(const QItemSelection &selected, const QItemSelection &deselected); private: NoteItem *itemAtIndex(const QModelIndex &index) const; QModelIndex indexOfItem(NoteItem *item) const; QModelIndex findNote(NotePtr note) const; private: NoteItem *m_root; }; diff --git a/src/noteitem.cpp b/src/noteitem.cpp index 92b5317..669a600 100644 --- a/src/noteitem.cpp +++ b/src/noteitem.cpp @@ -1,143 +1,281 @@ /* - * SPDX-FileCopyrightText: 2020 by Ismael Asensio + * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût + * SPDX-FileCopyrightText: (C) 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #include "basketscenemodel.h" +#include "notefactory.h" +#include "xmlwork.h" + +#include + namespace { QString iconNameForType(NoteType::Id type) { switch (type) { case NoteType::Group: return "package"; case NoteType::Text: return "text-x-plain"; case NoteType::Html: return "text-html"; case NoteType::Image: return "folder-pictures-symbolic"; case NoteType::Animation: return "folder-video-symbolic"; case NoteType::Sound: return "folder-music-symbolic"; case NoteType::File: return "application-x-zerosize"; case NoteType::Link: return "edit-link"; case NoteType::CrossReference: return "basket"; case NoteType::Launcher: return "run-build"; case NoteType::Color: return "color-profile"; case NoteType::Unknown: return "application-octet-stream"; } return QString(); } } -NoteItem::NoteItem(Note* note) +NoteItem::NoteItem() : m_parent(nullptr) - , m_note(note) + , m_note(nullptr) + , m_helperNote(new Note()) { } NoteItem::~NoteItem() { + delete m_helperNote; qDeleteAll(m_children); } + int NoteItem::row() const { if (!m_parent) { return 0; } return m_parent->m_children.indexOf(const_cast(this)); } NoteItem *NoteItem::parent() const { return m_parent; } void NoteItem::setParent(NoteItem *parent) { m_parent = parent; } QVector NoteItem::children() const { return m_children; } int NoteItem::childrenCount() const { return m_children.count(); } void NoteItem::insertAt(int row, NoteItem *item) { if (!item) return; item->setParent(this); if (row >= 0 && row < m_children.count() ) { m_children.insert(row, item); } else { m_children.append(item); } } void NoteItem::removeAt(int row) { if (row < 0 || row >= m_children.count()) { return; } delete m_children[row]; m_children.remove(row); } NoteItem *NoteItem::takeAt(int row) { if (row < 0 || row >= m_children.count()) { return nullptr; } return m_children.takeAt(row); } NotePtr NoteItem::note() const { + if (!m_note) { + return m_helperNote; + } return m_note; } void NoteItem::setNote(NotePtr note) { m_note = note; } + QString NoteItem::displayText() const { - if (!m_note) { return QStringLiteral("NULL NOTE"); } - if (m_note->isGroup()) { return QStringLiteral("GROUP"); } - return m_note->toText(QString()); + if (!note()) { return QStringLiteral("NULL NOTE"); } + if (type() == NoteType::Group) { return QStringLiteral("GROUP"); } + return content()->toText(QString()); } QIcon NoteItem::decoration() const { - if (!m_note) { return QIcon::fromTheme("data-error"); } - if (m_note->isGroup()) { return QIcon::fromTheme("package"); } - if (!m_note->content()) { return QIcon::fromTheme("empty"); } + if (!note()) { return QIcon::fromTheme("data-error"); } + if (type() == NoteType::Group) { return QIcon::fromTheme("package"); } + if (!content()) { return QIcon::fromTheme("empty"); } - return QIcon::fromTheme(iconNameForType(m_note->content()->type())); + return QIcon::fromTheme(iconNameForType(content()->type())); +} + +NoteContent *NoteItem::content() const +{ + if (!note()) { + return nullptr; + } + return note()->content(); +} + +NoteType::Id NoteItem::type() const +{ + if (!note()) { + return NoteType::Unknown; + } + if (!note()->content()) { + return NoteType::Group; + } + return note()->content()->type(); +} + +QRect NoteItem::bounds() const +{ + return QRect( + note()->x(), + note()->y(), + note()->width(), + note()->height() + ); +} + +NoteItem::EditInfo NoteItem::editInformation() const +{ + return EditInfo { + note()->addedDate(), + note()->lastModificationDate() + }; } QString NoteItem::formatAddress(void *ptr) { return QString::number(reinterpret_cast(ptr), 16); } QString NoteItem::address() const { - return formatAddress(m_note); + if (!note()) { + return QStringLiteral("root"); + } + return formatAddress(note()); +} + +QString NoteItem::toolTipInfo() const +{ + QStringList toolTip; + + // fullAddress and position within parent (debug) + toolTip << QStringLiteral("%1 (@ <%2>[%3])") + .arg(formatAddress(note())) + .arg(m_parent->address()) + .arg(row()); + + // type + toolTip << QStringLiteral("Type: %1").arg(content() ? content()->typeName() : "Group"); + + // geometry + const QRect geometry = bounds(); + toolTip << QStringLiteral("x:%1 y:%2 w:%3 h:%4") + .arg(geometry.x()).arg(geometry.y()) + .arg(geometry.width()).arg(geometry.height()); + + // edition information + const EditInfo info = editInformation(); + toolTip << QStringLiteral("created: %1\nmodified: %2") + .arg(info.created.toString()) + .arg(info.modified.toString()); + + return toolTip.join(QStringLiteral("\n")); } -QString NoteItem::fullAddress() const + +void NoteItem::loadFromXMLNode(const QDomElement& node) { - return QStringLiteral("%1 (@ <%2>[%3])").arg(formatAddress(m_note)) - .arg(m_parent ? m_parent->address() : QStringLiteral("root")) - .arg(row()); + for (QDomNode n = node.firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + if (e.isNull()) { + continue; + } + + NoteItem *noteItem = new NoteItem(); + noteItem->setParent(this); + m_children.append(noteItem); + + NotePtr note = noteItem->note(); // Helper Note object + + if (e.tagName() == "group") { + // Node is a group. Recursively load from this element + noteItem->loadFromXMLNode(e); + } else if (e.tagName() == "note" || e.tagName() == "item") { // "item" is to keep compatible with 0.6.0 Alpha 1 (required?) + // Load note content +/* IT CRASHES. Too dependant on QObject hierarchy? + NoteFactory::loadNode(XMLWork::getElement(e, "content"), + e.attribute("type"), + note, + true); //lazyload +*/ + } + + // Load dates + if (e.hasAttribute("added")) { + note->setAddedDate(QDateTime::fromString(e.attribute("added"), Qt::ISODate)); + } + if (e.hasAttribute("lastModification")) { + note->setLastModificationDate(QDateTime::fromString(e.attribute("lastModification"), Qt::ISODate)); + } + // Free Note Properties: + if (note->isFree()) { + int x = e.attribute("x").toInt(); + int y = e.attribute("y").toInt(); + note->setX(x < 0 ? 0 : x); + note->setY(y < 0 ? 0 : y); + } + // Resizeable Note Properties: + if (note->hasResizer() || note->isColumn()) { + note->setGroupWidth(e.attribute("width", "200").toInt()); + } + // Group Properties: + if (note->isGroup() && !note->isColumn() && XMLWork::trueOrFalse(e.attribute("folded", "false"))) { + note->toggleFolded(); + } + // Tags: + if (note->content()) { + const QString tagsString = XMLWork::getElementText(e, QStringLiteral("tags"), QString()); + const QStringList tagsId = tagsString.split(';'); + for (const QString &tagId : tagsId) { + State *state = Tag::stateForId(tagId); + if (state) { + note->addState(state, /*orReplace=*/true); + } + } + } + } } diff --git a/src/noteitem.h b/src/noteitem.h index 94dda1d..55ba138 100644 --- a/src/noteitem.h +++ b/src/noteitem.h @@ -1,53 +1,81 @@ /* * SPDX-FileCopyrightText: 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "note.h" #include "notecontent.h" +#include -Q_DECLARE_METATYPE(NoteContent *) + +class QDomElement; typedef Note * NotePtr; /** NoteItem: Container that stores a Note object within a tree model * Eventually implement the managing functionallity here and use directly the `NoteContent` object */ class NoteItem { public: - explicit NoteItem(NotePtr note); + struct EditInfo + { + QDateTime created; + QDateTime modified; + }; + +public: + explicit NoteItem(); ~NoteItem(); + // Tree structure int row() const; NoteItem *parent() const; void setParent(NoteItem *parent); QVector children() const; int childrenCount() const; void insertAt(int row, NoteItem *item); void removeAt(int row); NoteItem *takeAt(int row); + // Accesors to Note object (compatibility with current code) NotePtr note() const; void setNote(NotePtr note); + // NoteItem property getters QString displayText() const; QIcon decoration() const; + NoteContent *content() const; + NoteType::Id type() const; + QRect bounds() const; + EditInfo editInformation() const; + + // Recursive loader from an XML node + void loadFromXMLNode(const QDomElement &node); // Find and debug Notes by its pointer address - QString address() const; - QString fullAddress() const; static QString formatAddress(void *ptr); + QString address() const; + + QString toolTipInfo() const; private: NoteItem *m_parent; QVector m_children; +// TODO: Remove m_note and store the information here NotePtr m_note; + NotePtr m_helperNote; // Dummy note to help with the code transition. +// NoteContent *m_content; +// QRect m_bounds; +// EditInfo m_editInfo; +// QVector m_tags; }; +Q_DECLARE_METATYPE(NoteContent *) +Q_DECLARE_METATYPE(NoteItem::EditInfo)