diff --git a/src/basketscenemodel.cpp b/src/basketscenemodel.cpp index a42d9ae..fff61a9 100644 --- a/src/basketscenemodel.cpp +++ b/src/basketscenemodel.cpp @@ -1,293 +1,301 @@ /* * SPDX-FileCopyrightText: 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)) { + 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" }, - { GeometryRole, "geometry" }, { 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: // To ease debugging return item->toolTipInfo(); case BasketSceneModel::ContentRole: return QVariant::fromValue(note->content()); case BasketSceneModel::TypeRole: return note->content()->type(); - case BasketSceneModel::GeometryRole: - return item->bounds(); case BasketSceneModel::GroupRole: return note->isGroup(); + 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)); } 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::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 3399267..ef297d9 100644 --- a/src/basketscenemodel.h +++ b/src/basketscenemodel.h @@ -1,68 +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 AdditionalRoles { - ContentRole = Qt::UserRole + 1, - TypeRole, - GeometryRole, - GroupRole, - AddressRole // String representation of the internal NotePtr address + ContentRole = Qt::UserRole + 1, // NoteContent * : the internal NoteContent + TypeRole, // NoteContent::Type: the type of the NoteContent + GroupRole, // bool: whether the Note is a group + 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 + // 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 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 83dd297..2e027bb 100644 --- a/src/noteitem.cpp +++ b/src/noteitem.cpp @@ -1,163 +1,184 @@ /* * SPDX-FileCopyrightText: 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #include "basketscenemodel.h" 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) : m_parent(nullptr) , m_note(note) { } NoteItem::~NoteItem() { 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 { 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()); } 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"); } return QIcon::fromTheme(iconNameForType(m_note->content()->type())); } QRect NoteItem::bounds() const { if (!m_note) { return {}; } return QRect( m_note->x(), m_note->y(), m_note->width(), m_note->height() ); } +NoteItem::EditInfo NoteItem::editInformation() const +{ + return EditInfo { + m_note->addedDate(), + m_note->lastModificationDate() + }; +} + QString NoteItem::formatAddress(void *ptr) { return QString::number(reinterpret_cast(ptr), 16); } QString NoteItem::address() const { + if (!m_note) { + return QStringLiteral("root"); + } return formatAddress(m_note); } QString NoteItem::toolTipInfo() const { - const QString fullAddress = QStringLiteral("%1 (@ <%2>[%3])") - .arg(formatAddress(m_note)) - .arg(m_parent ? m_parent->address() : QStringLiteral("root")) - .arg(row()); + QStringList toolTip; + + // fullAddress and position within parent (debug) + toolTip << QStringLiteral("%1 (@ <%2>[%3])") + .arg(formatAddress(m_note)) + .arg(m_parent->address()) + .arg(row()); + // geometry const QRect geometry = bounds(); - const QString geometryString = QStringLiteral("(%1,%2) (%3x%4)") - .arg(geometry.x()).arg(geometry.y()) - .arg(geometry.width()).arg(geometry.height()); + 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 fullAddress + "\n" + geometryString; + return toolTip.join(QStringLiteral("\n")); } diff --git a/src/noteitem.h b/src/noteitem.h index 59d9e6c..26bd7d8 100644 --- a/src/noteitem.h +++ b/src/noteitem.h @@ -1,55 +1,65 @@ /* * 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 *) 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: + struct EditInfo + { + QDateTime created; + QDateTime modified; + }; + public: explicit NoteItem(NotePtr note); ~NoteItem(); 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); NotePtr note() const; void setNote(NotePtr note); QString displayText() const; QIcon decoration() const; QRect bounds() const; + EditInfo editInformation() const; // Find and debug Notes by its pointer address static QString formatAddress(void *ptr); QString address() const; QString toolTipInfo() const; private: NoteItem *m_parent; QVector m_children; NotePtr m_note; }; +Q_DECLARE_METATYPE(NoteContent *) +Q_DECLARE_METATYPE(NoteItem::EditInfo)