diff --git a/src/basketscenemodel.cpp b/src/basketscenemodel.cpp index 5efadb2..6ebc968 100644 --- a/src/basketscenemodel.cpp +++ b/src/basketscenemodel.cpp @@ -1,308 +1,313 @@ /* * SPDX-FileCopyrightText: (C) 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #include "basketscenemodel.h" +#include "basketscene.h" #include BasketSceneModel::BasketSceneModel(QObject *parent) : QAbstractItemModel(parent) - , m_root(new NoteItem()) + , m_root(nullptr) { + + NoteItem::setBasketParent(qobject_cast(parent)); + 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" }, { 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); 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(item->content()); case BasketSceneModel::TypeRole: 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(); } 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()); } 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 cbe5c2e..91c1b41 100644 --- a/src/basketscenemodel.h +++ b/src/basketscenemodel.h @@ -1,69 +1,72 @@ /* * 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 BasketScene; + + class BASKET_EXPORT BasketSceneModel : public QAbstractItemModel { Q_OBJECT public: 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; 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; 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); public slots: // 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/note.cpp b/src/note.cpp index d1a586c..ae853c1 100644 --- a/src/note.cpp +++ b/src/note.cpp @@ -1,2524 +1,2527 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "note.h" #include #include #include #include //For KGLobal::locale( #include #include #include #include #include #include #include #include // sqrt() and pow() functions #include // rand() function #include "basketscene.h" #include "debugwindow.h" #include "filter.h" #include "notefactory.h" // For NoteFactory::filteredURL() #include "noteselection.h" #include "settings.h" #include "tag.h" #include "tools.h" /** class Note: */ #define FOR_EACH_CHILD(childVar) for (Note *childVar = firstChild(); childVar; childVar = childVar->next()) class NotePrivate { public: NotePrivate() : prev(nullptr) , next(nullptr) , width(-1) , height(Note::MIN_HEIGHT) { } Note *prev; Note *next; qreal width; qreal height; }; qreal Note::NOTE_MARGIN = 2; qreal Note::INSERTION_HEIGHT = 5; qreal Note::EXPANDER_WIDTH = 9; qreal Note::EXPANDER_HEIGHT = 9; qreal Note::GROUP_WIDTH = 2 * NOTE_MARGIN + EXPANDER_WIDTH; qreal Note::HANDLE_WIDTH = GROUP_WIDTH; qreal Note::RESIZER_WIDTH = GROUP_WIDTH; qreal Note::TAG_ARROW_WIDTH = 5; qreal Note::EMBLEM_SIZE = 16; qreal Note::MIN_HEIGHT = 2 * NOTE_MARGIN + EMBLEM_SIZE; Note::Note(BasketScene *parent) : d(new NotePrivate) , m_groupWidth(250) , m_isFolded(false) , m_firstChild(nullptr) , m_parentNote(nullptr) , m_basket(parent) , m_content(nullptr) , m_addedDate(QDateTime::currentDateTime()) , m_lastModificationDate(QDateTime::currentDateTime()) , m_computedAreas(false) , m_onTop(false) , m_hovered(false) , m_hoveredZone(Note::None) , m_focused(false) , m_selected(false) , m_wasInLastSelectionRect(false) , m_computedState() , m_emblemsCount(0) , m_haveInvisibleTags(false) , m_matching(true) { setHeight(MIN_HEIGHT); if (m_basket) { m_basket->addItem(this); } } Note::~Note() { if (m_basket) { if (m_content && m_content->graphicsItem()) { m_basket->removeItem(m_content->graphicsItem()); } m_basket->removeItem(this); } delete m_content; deleteChilds(); } void Note::setNext(Note *next) { d->next = next; } Note *Note::next() const { return d->next; } void Note::setPrev(Note *prev) { d->prev = prev; } Note *Note::prev() const { return d->prev; } qreal Note::bottom() const { return y() + height() - 1; } void Note::setParentBasket(BasketScene *basket) { if (m_basket) m_basket->removeItem(this); m_basket = basket; if (m_basket) m_basket->addItem(this); } QString Note::addedStringDate() { return m_addedDate.toString(); } QString Note::lastModificationStringDate() { return m_lastModificationDate.toString(); } QString Note::toText(const QString &cuttedFullPath) { if (content()) { // Convert note to text: QString text = content()->toText(cuttedFullPath); // If we should not export tags with the text, return immediately: if (!Settings::exportTextTags()) return text; // Compute the text equivalent of the tag states: QString firstLine; QString otherLines; for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { if (!(*it)->textEquivalent().isEmpty()) { firstLine += (*it)->textEquivalent() + ' '; if ((*it)->onAllTextLines()) otherLines += (*it)->textEquivalent() + ' '; } } // Merge the texts: if (firstLine.isEmpty()) return text; if (otherLines.isEmpty()) return firstLine + text; QStringList lines = text.split('\n'); QString result = firstLine + lines[0] + (lines.count() > 1 ? "\n" : QString()); for (int i = 1 /*Skip the first line*/; i < lines.count(); ++i) result += otherLines + lines[i] + (i < lines.count() - 1 ? "\n" : QString()); return result; } else return QString(); } bool Note::computeMatching(const FilterData &data) { // Groups are always matching: if (!content()) return true; // If we were editing this note and there is a save operation in the middle, then do not hide it suddenly: if (basket()->editedNote() == this) return true; bool matching; // First match tags (they are fast to compute): switch (data.tagFilterType) { default: case FilterData::DontCareTagsFilter: matching = true; break; case FilterData::NotTaggedFilter: matching = m_states.count() <= 0; break; case FilterData::TaggedFilter: matching = m_states.count() > 0; break; case FilterData::TagFilter: matching = hasTag(data.tag); break; case FilterData::StateFilter: matching = hasState(data.state); break; } // Don't try to match the content text if we are not matching now (the filter is of 'AND' type) or if we shouldn't try to match the string: if (matching && !data.string.isEmpty()) matching = content()->match(data); return matching; } int Note::newFilter(const FilterData &data) { bool wasMatching = matching(); m_matching = computeMatching(data); setOnTop(wasMatching && matching()); if (!matching()) { setSelected(false); hide(); } else if (!wasMatching) { show(); } int countMatches = (content() && matching() ? 1 : 0); FOR_EACH_CHILD(child) { countMatches += child->newFilter(data); } return countMatches; } void Note::deleteSelectedNotes(bool deleteFilesToo, QSet *notesToBeDeleted) { if (content()) { if (isSelected()) { basket()->unplugNote(this); if (deleteFilesToo && content()->useFile()) { Tools::deleteRecursively(fullPath()); // basket()->deleteFiles(fullPath()); // Also delete the folder if it's a folder } if (notesToBeDeleted) { notesToBeDeleted->insert(this); } } return; } bool isColumn = this->isColumn(); Note *child = firstChild(); Note *next; while (child) { next = child->next(); // If we delete 'child' on the next line, child->next() will be 0! child->deleteSelectedNotes(deleteFilesToo, notesToBeDeleted); child = next; } // if it remains at least two notes, the group must not be deleted if (!isColumn && !(firstChild() && firstChild()->next())) { if (notesToBeDeleted) { notesToBeDeleted->insert(this); } } } int Note::count() { if (content()) return 1; int count = 0; FOR_EACH_CHILD(child) { count += child->count(); } return count; } int Note::countDirectChilds() { int count = 0; FOR_EACH_CHILD(child) { ++count; } return count; } QString Note::fullPath() { if (content()) return basket()->fullPath() + content()->fileName(); else return QString(); } /*void Note::update() { update(0,0,boundingRect().width,boundingRect().height); }*/ void Note::setFocused(bool focused) { if (m_focused == focused) return; m_focused = focused; unbufferize(); update(); // FIXME: ??? } void Note::setSelected(bool selected) { if (isGroup()) selected = false; // A group cannot be selected! if (m_selected == selected) return; if (!selected && basket()->editedNote() == this) { // basket()->closeEditor(); return; // To avoid a bug that would count 2 less selected notes instead of 1 less! Because m_selected is modified only below. } if (selected) basket()->addSelectedNote(); else basket()->removeSelectedNote(); m_selected = selected; unbufferize(); update(); // FIXME: ??? } void Note::resetWasInLastSelectionRect() { m_wasInLastSelectionRect = false; FOR_EACH_CHILD(child) { child->resetWasInLastSelectionRect(); } } void Note::finishLazyLoad() { if (content()) content()->finishLazyLoad(); FOR_EACH_CHILD(child) { child->finishLazyLoad(); } } void Note::selectIn(const QRectF &rect, bool invertSelection, bool unselectOthers /*= true*/) { // QRect myRect(x(), y(), width(), height()); // bool intersects = myRect.intersects(rect); // Only intersects with visible areas. // If the note is not visible, the user don't think it will be selected while selecting the note(s) that hide this, so act like the user think: bool intersects = false; for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &r = *it; if (r.intersects(rect)) { intersects = true; break; } } bool toSelect = intersects || (!unselectOthers && isSelected()); if (invertSelection) { toSelect = (m_wasInLastSelectionRect == intersects) ? isSelected() : !isSelected(); } setSelected(toSelect); m_wasInLastSelectionRect = intersects; Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->selectIn(rect, invertSelection, unselectOthers); else child->setSelectedRecursively(false); child = child->next(); first = false; } } bool Note::allSelected() { if (isGroup()) { Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) if (!child->allSelected()) return false; ; child = child->next(); first = false; } return true; } else return isSelected(); } void Note::setSelectedRecursively(bool selected) { setSelected(selected && matching()); FOR_EACH_CHILD(child) { child->setSelectedRecursively(selected); } } void Note::invertSelectionRecursively() { if (content()) setSelected(!isSelected() && matching()); FOR_EACH_CHILD(child) { child->invertSelectionRecursively(); } } void Note::unselectAllBut(Note *toSelect) { if (this == toSelect) setSelectedRecursively(true); else { setSelected(false); Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->unselectAllBut(toSelect); else child->setSelectedRecursively(false); child = child->next(); first = false; } } } void Note::invertSelectionOf(Note *toSelect) { if (this == toSelect) setSelectedRecursively(!isSelected()); else { Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->invertSelectionOf(toSelect); child = child->next(); first = false; } } } Note *Note::theSelectedNote() { if (!isGroup() && isSelected()) return this; Note *selectedOne; Note *child = firstChild(); while (child) { selectedOne = child->theSelectedNote(); if (selectedOne) return selectedOne; child = child->next(); } return nullptr; } NoteSelection *Note::selectedNotes() { if (content()) { if (isSelected()) return new NoteSelection(this); else return nullptr; } NoteSelection *selection = new NoteSelection(this); FOR_EACH_CHILD(child) { selection->append(child->selectedNotes()); } if (selection->firstChild) { if (selection->firstChild->next) return selection; else { // If 'selection' is a group with only one content, return directly that content: NoteSelection *reducedSelection = selection->firstChild; // delete selection; // TODO: Cut all connections of 'selection' before deleting it! for (NoteSelection *node = reducedSelection; node; node = node->next) node->parent = nullptr; return reducedSelection; } } else { delete selection; return nullptr; } } bool Note::isAfter(Note *note) { if (this == nullptr || note == nullptr) return true; Note *next = this; while (next) { if (next == note) return false; next = next->nextInStack(); } return true; } bool Note::containsNote(Note *note) { // if (this == note) // return true; while (note) if (note == this) return true; else note = note->parentNote(); // FOR_EACH_CHILD (child) // if (child->containsNote(note)) // return true; return false; } Note *Note::firstRealChild() { Note *child = m_firstChild; while (child) { if (!child->isGroup() /*&& child->matching()*/) return child; child = child->firstChild(); } // Empty group: return nullptr; } Note *Note::lastRealChild() { Note *child = lastChild(); while (child) { if (child->content()) return child; Note *possibleChild = child->lastRealChild(); if (possibleChild && possibleChild->content()) return possibleChild; child = child->prev(); } return nullptr; } Note *Note::lastChild() { Note *child = m_firstChild; while (child && child->next()) child = child->next(); return child; } Note *Note::lastSibling() { Note *last = this; while (last && last->next()) last = last->next(); return last; } qreal Note::yExpander() { Note *child = firstRealChild(); if (child && !child->isShown()) child = child->nextShownInStack(); // FIXME: Restrict scope to 'this' if (child) return (child->boundingRect().height() - EXPANDER_HEIGHT) / 2; else // Groups always have at least 2 notes, except for columns which can have no child (but should exists anyway): return 0; } bool Note::isFree() const { return parentNote() == nullptr && basket() && basket()->isFreeLayout(); } bool Note::isColumn() const { return parentNote() == nullptr && basket() && basket()->isColumnsLayout(); } bool Note::hasResizer() const { // "isFree" || "isColumn but not the last" return parentNote() == nullptr && ((basket() && basket()->isFreeLayout()) || d->next != nullptr); } qreal Note::resizerHeight() const { return (isColumn() ? basket()->sceneRect().height() : d->height); } void Note::setHoveredZone(Zone zone) // TODO: Remove setHovered(bool) and assume it is hovered if zone != None !!!!!!! { if (m_hoveredZone != zone) { if (content()) content()->setHoveredZone(m_hoveredZone, zone); m_hoveredZone = zone; unbufferize(); } } Note::Zone Note::zoneAt(const QPointF &pos, bool toAdd) { // Keep the resizer highlighted when resizong, even if the cursor is over another note: if (basket()->resizingNote() == this) return Resizer; // When dropping/pasting something on a column resizer, add it at the bottom of the column, and don't group it with the whole column: if (toAdd && isColumn() && hasResizer()) { qreal right = rightLimit() - x(); if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= 0) && (pos.y() < resizerHeight())) // Code copied from below return BottomColumn; } // Below a column: if (isColumn()) { if (pos.y() >= height() && pos.x() < rightLimit() - x()) return BottomColumn; } // If toAdd, return only TopInsert, TopGroup, BottomInsert or BottomGroup // (by spanning those areas in 4 equal rectangles in the note): if (toAdd) { if (!isFree() && !Settings::groupOnInsertionLine()) { if (pos.y() < height() / 2) return TopInsert; else return BottomInsert; } if (isColumn() && pos.y() >= height()) return BottomGroup; if (pos.y() < height() / 2) if (pos.x() < width() / 2 && !isFree()) return TopInsert; else return TopGroup; else if (pos.x() < width() / 2 && !isFree()) return BottomInsert; else return BottomGroup; } // If in the resizer: if (hasResizer()) { qreal right = rightLimit() - x(); if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= 0) && (pos.y() < resizerHeight())) return Resizer; } // If isGroup, return only Group, GroupExpander, TopInsert or BottomInsert: if (isGroup()) { if (pos.y() < INSERTION_HEIGHT) { if (isFree()) return TopGroup; else return TopInsert; } if (pos.y() >= height() - INSERTION_HEIGHT) { if (isFree()) return BottomGroup; else return BottomInsert; } if (pos.x() >= NOTE_MARGIN && pos.x() < NOTE_MARGIN + EXPANDER_WIDTH) { qreal yExp = yExpander(); if (pos.y() >= yExp && pos.y() < yExp + EXPANDER_HEIGHT) return GroupExpander; } if (pos.x() < width()) return Group; else return Note::None; } // Else, it's a normal note: if (pos.x() < HANDLE_WIDTH) return Handle; if (pos.y() < INSERTION_HEIGHT) { if ((!isFree() && !Settings::groupOnInsertionLine()) || (pos.x() < width() / 2 && !isFree())) return TopInsert; else return TopGroup; } if (pos.y() >= height() - INSERTION_HEIGHT) { if ((!isFree() && !Settings::groupOnInsertionLine()) || (pos.x() < width() / 2 && !isFree())) return BottomInsert; else return BottomGroup; } for (int i = 0; i < m_emblemsCount; i++) { if (pos.x() >= HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * i && pos.x() < HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * i + NOTE_MARGIN + EMBLEM_SIZE) return (Zone)(Emblem0 + i); } if (pos.x() < HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * m_emblemsCount + NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN) return TagsArrow; if (!linkAt(pos).isEmpty()) return Link; int customZone = content()->zoneAt(pos - QPointF(contentX(), NOTE_MARGIN)); if (customZone) return (Note::Zone)customZone; return Content; } QString Note::linkAt(const QPointF &pos) { QString link = m_content->linkAt(pos - QPointF(contentX(), NOTE_MARGIN)); if (link.isEmpty() || link.startsWith(QLatin1String("basket://"))) return link; else return NoteFactory::filteredURL(QUrl::fromUserInput(link)).toDisplayString(); // KURIFilter::self()->filteredURI(link); } qreal Note::contentX() const { return HANDLE_WIDTH + NOTE_MARGIN + (EMBLEM_SIZE + NOTE_MARGIN) * m_emblemsCount + TAG_ARROW_WIDTH + NOTE_MARGIN; } QRectF Note::zoneRect(Note::Zone zone, const QPointF &pos) { if (zone >= Emblem0) return QRect(HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * (zone - Emblem0), INSERTION_HEIGHT, NOTE_MARGIN + EMBLEM_SIZE, height() - 2 * INSERTION_HEIGHT); qreal yExp; qreal right; qreal xGroup = (isFree() ? (isGroup() ? 0 : GROUP_WIDTH) : width() / 2); QRectF rect; qreal insertSplit = (Settings::groupOnInsertionLine() ? 2 : 1); switch (zone) { case Note::Handle: return QRectF(0, 0, HANDLE_WIDTH, d->height); case Note::Group: yExp = yExpander(); if (pos.y() < yExp) { return QRectF(0, INSERTION_HEIGHT, d->width, yExp - INSERTION_HEIGHT); } if (pos.y() > yExp + EXPANDER_HEIGHT) { return QRectF(0, yExp + EXPANDER_HEIGHT, d->width, d->height - yExp - EXPANDER_HEIGHT - INSERTION_HEIGHT); } if (pos.x() < NOTE_MARGIN) { return QRectF(0, 0, NOTE_MARGIN, d->height); } else { return QRectF(d->width - NOTE_MARGIN, 0, NOTE_MARGIN, d->height); } case Note::TagsArrow: return QRectF(HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * m_emblemsCount, INSERTION_HEIGHT, NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN, d->height - 2 * INSERTION_HEIGHT); case Note::Custom0: case Note::Content: rect = content()->zoneRect(zone, pos - QPointF(contentX(), NOTE_MARGIN)); rect.translate(contentX(), NOTE_MARGIN); return rect.intersected(QRectF(contentX(), INSERTION_HEIGHT, d->width - contentX(), d->height - 2 * INSERTION_HEIGHT)); // Only IN contentRect case Note::GroupExpander: return QRectF(NOTE_MARGIN, yExpander(), EXPANDER_WIDTH, EXPANDER_HEIGHT); case Note::Resizer: right = rightLimit(); return QRectF(right - x(), 0, RESIZER_WIDTH, resizerHeight()); case Note::Link: case Note::TopInsert: if (isGroup()) return QRectF(0, 0, d->width, INSERTION_HEIGHT); else return QRectF(HANDLE_WIDTH, 0, d->width / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); case Note::TopGroup: return QRectF(xGroup, 0, d->width - xGroup, INSERTION_HEIGHT); case Note::BottomInsert: if (isGroup()) return QRectF(0, d->height - INSERTION_HEIGHT, d->width, INSERTION_HEIGHT); else return QRectF(HANDLE_WIDTH, d->height - INSERTION_HEIGHT, d->width / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); case Note::BottomGroup: return QRectF(xGroup, d->height - INSERTION_HEIGHT, d->width - xGroup, INSERTION_HEIGHT); case Note::BottomColumn: return QRectF(0, d->height, rightLimit() - x(), basket()->sceneRect().height() - d->height); case Note::None: return QRectF(/*0, 0, -1, -1*/); default: return QRectF(/*0, 0, -1, -1*/); } } Qt::CursorShape Note::cursorFromZone(Zone zone) const { switch (zone) { case Note::Handle: case Note::Group: return Qt::SizeAllCursor; break; case Note::Resizer: if (isColumn()) { return Qt::SplitHCursor; } else { return Qt::SizeHorCursor; } break; case Note::Custom0: return m_content->cursorFromZone(zone); break; case Note::Link: case Note::TagsArrow: case Note::GroupExpander: return Qt::PointingHandCursor; break; case Note::Content: return Qt::IBeamCursor; break; case Note::TopInsert: case Note::TopGroup: case Note::BottomInsert: case Note::BottomGroup: case Note::BottomColumn: return Qt::CrossCursor; break; case Note::None: return Qt::ArrowCursor; break; default: State *state = stateForEmblemNumber(zone - Emblem0); if (state && state->parentTag()->states().count() > 1) return Qt::PointingHandCursor; else return Qt::ArrowCursor; } } qreal Note::height() const { return d->height; } void Note::setHeight(qreal height) { setInitialHeight(height); } void Note::setInitialHeight(qreal height) { prepareGeometryChange(); d->height = height; } void Note::unsetWidth() { prepareGeometryChange(); d->width = 0; unbufferize(); FOR_EACH_CHILD(child) child->unsetWidth(); } qreal Note::width() const { return (isGroup() ? (isColumn() ? 0 : GROUP_WIDTH) : d->width); } void Note::requestRelayout() { prepareGeometryChange(); d->width = 0; unbufferize(); basket()->relayoutNotes(); // TODO: A signal that will relayout ONCE and DELAYED if called several times } void Note::setWidth(qreal width) // TODO: inline ? { if (d->width != width) setWidthForceRelayout(width); } void Note::setWidthForceRelayout(qreal width) { prepareGeometryChange(); unbufferize(); d->width = (width < minWidth() ? minWidth() : width); int contentWidth = width - contentX() - NOTE_MARGIN; if (m_content) { ///// FIXME: is this OK? if (contentWidth < 1) contentWidth = 1; if (contentWidth < m_content->minWidth()) contentWidth = m_content->minWidth(); setHeight(m_content->setWidthAndGetHeight(contentWidth /* < 1 ? 1 : contentWidth*/) + 2 * NOTE_MARGIN); if (d->height < 3 * INSERTION_HEIGHT) // Assure a minimal size... setHeight(3 * INSERTION_HEIGHT); } } qreal Note::minWidth() const { if (m_content) return contentX() + m_content->minWidth() + NOTE_MARGIN; else return GROUP_WIDTH; ///// FIXME: is this OK? } qreal Note::minRight() { if (isGroup()) { qreal right = x() + width(); Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) right = qMax(right, child->minRight()); child = child->next(); first = false; } if (isColumn()) { qreal minColumnRight = x() + 2 * HANDLE_WIDTH; if (right < minColumnRight) return minColumnRight; } return right; } else return x() + minWidth(); } bool Note::toggleFolded() { // Close the editor if it was editing a note that we are about to hide after collapsing: if (!m_isFolded && basket() && basket()->isDuringEdit()) { if (containsNote(basket()->editedNote()) && firstRealChild() != basket()->editedNote()) basket()->closeEditor(); } // Important to close the editor FIRST, because else, the last edited note would not show during folding animation (don't ask me why ;-) ): m_isFolded = !m_isFolded; unbufferize(); return true; } Note *Note::noteAt(QPointF pos) { if (matching() && hasResizer()) { int right = rightLimit(); // TODO: This code is duplicated 3 times: !!!! if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= y()) && (pos.y() < y() + resizerHeight())) { if (!m_computedAreas) recomputeAreas(); for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &rect = *it; if (rect.contains(pos.x(), pos.y())) return this; } } } if (isGroup()) { if ((pos.x() >= x()) && (pos.x() < x() + width()) && (pos.y() >= y()) && (pos.y() < y() + d->height)) { if (!m_computedAreas) recomputeAreas(); for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &rect = *it; if (rect.contains(pos.x(), pos.y())) return this; } return nullptr; } Note *child = firstChild(); Note *found; bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) { found = child->noteAt(pos); if (found) return found; } child = child->next(); first = false; } } else if (matching() && pos.y() >= y() && pos.y() < y() + d->height && pos.x() >= x() && pos.x() < x() + d->width) { if (!m_computedAreas) recomputeAreas(); for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &rect = *it; if (rect.contains(pos.x(), pos.y())) return this; } return nullptr; } return nullptr; } QRectF Note::boundingRect() const { if (hasResizer()) { return QRectF(0, 0, rightLimit() - x() + RESIZER_WIDTH, resizerHeight()); } return QRectF(0, 0, width(), height()); } QRectF Note::resizerRect() { return QRectF(rightLimit(), y(), RESIZER_WIDTH, resizerHeight()); } bool Note::showSubNotes() { return !m_isFolded || basket()->isFiltering(); } void Note::relayoutAt(qreal ax, qreal ay) { if (!matching()) return; m_computedAreas = false; m_areas.clear(); // Don't relayout free notes one under the other, because by definition they are freely positioned! if (isFree()) { ax = x(); ay = y(); // If it's a column, it always have the same "fixed" position (no animation): } else if (isColumn()) { ax = (prev() ? prev()->rightLimit() + RESIZER_WIDTH : 0); ay = 0; setX(ax); setY(ay); // But relayout others vertically if they are inside such primary groups or if it is a "normal" basket: } else { setX(ax); setY(ay); } // Then, relayout sub-notes (only the first, if the group is folded) and so, assign an height to the group: if (isGroup()) { qreal h = 0; Note *child = firstChild(); bool first = true; while (child) { if (child->matching() && (!m_isFolded || first || basket()->isFiltering())) { // Don't use showSubNotes() but use !m_isFolded because we don't want a relayout for the animated collapsing notes child->relayoutAt(ax + width(), ay + h); h += child->height(); if (!child->isVisible()) child->show(); } else { // In case the user collapse a group, then move it and then expand it: child->setXRecursively(x() + width()); // notes SHOULD have a good X coordinate, and not the old one! if (child->isVisible()) child->hideRecursively(); } // For future animation when re-match, but on bottom of already matched notes! // Find parent primary note and set the Y to THAT y: // if (!child->matching()) // child->setY(parentPrimaryNote()->y()); child = child->next(); first = false; } if (height() != h || d->height != h) { unbufferize(); /*if (animate) addAnimation(0, 0, h - height()); else {*/ setHeight(h); unbufferize(); //} } } else { // If rightLimit is exceeded, set the top-level right limit!!! // and NEED RELAYOUT setWidth(finalRightLimit() - x()); } // Set the basket area limits (but not for child notes: no need, because they will look for their parent note): if (!parentNote()) { if (basket()->tmpWidth < finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0)) basket()->tmpWidth = finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0); if (basket()->tmpHeight < y() + height()) basket()->tmpHeight = y() + height(); // However, if the note exceed the allowed size, let it! : } else if (!isGroup()) { if (basket()->tmpWidth < x() + width() + (hasResizer() ? RESIZER_WIDTH : 0)) basket()->tmpWidth = x() + width() + (hasResizer() ? RESIZER_WIDTH : 0); if (basket()->tmpHeight < y() + height()) basket()->tmpHeight = y() + height(); } } void Note::setXRecursively(qreal x) { setX(x); FOR_EACH_CHILD(child) child->setXRecursively(x + width()); } void Note::setYRecursively(qreal y) { setY(y); FOR_EACH_CHILD(child) child->setYRecursively(y); } void Note::hideRecursively() { hide(); FOR_EACH_CHILD(child) child->hideRecursively(); } void Note::setGroupWidth(qreal width) { m_groupWidth = width; } qreal Note::groupWidth() const { if (hasResizer()) return m_groupWidth; else return rightLimit() - x(); } qreal Note::rightLimit() const { if (isColumn() && d->next == nullptr) // The last column return qMax((x() + minWidth()), (qreal)basket()->graphicsView()->viewport()->width()); else if (parentNote()) return parentNote()->rightLimit(); else return x() + m_groupWidth; } qreal Note::finalRightLimit() const { if (isColumn() && d->next == nullptr) // The last column return qMax(x() + minWidth(), (qreal)basket()->graphicsView()->viewport()->width()); else if (parentNote()) return parentNote()->finalRightLimit(); else return x() + m_groupWidth; } void Note::drawExpander(QPainter *painter, qreal x, qreal y, const QColor &background, bool expand, BasketScene *basket) { QStyleOption opt; opt.state = (expand ? QStyle::State_On : QStyle::State_Off); opt.rect = QRect(x, y, 9, 9); opt.palette = basket->palette(); opt.palette.setColor(QPalette::Base, background); painter->fillRect(opt.rect, background); QStyle *style = basket->style(); if (!expand) { style->drawPrimitive(QStyle::PE_IndicatorArrowDown, &opt, painter, basket->graphicsView()->viewport()); } else { style->drawPrimitive(QStyle::PE_IndicatorArrowRight, &opt, painter, basket->graphicsView()->viewport()); } } QColor expanderBackground(qreal height, qreal y, const QColor &foreground) { // We will divide height per two, subtract one and use that below a division bar: // To avoid division by zero error, height should be bigger than 3. // And to avoid y errors or if y is on the borders, we return the border color: the background color. if (height <= 3 || y <= 0 || y >= height - 1) return foreground; const QColor dark = foreground.darker(110); // 1/1.1 of brightness const QColor light = foreground.lighter(150); // 50% brighter qreal h1, h2, s1, s2, v1, v2; int ng; if (y <= (height - 2) / 2) { light.getHsvF(&h1, &s1, &v1); dark.getHsvF(&h2, &s2, &v2); ng = (height - 2) / 2; y -= 1; } else { dark.getHsvF(&h1, &s1, &v1); foreground.getHsvF(&h2, &s2, &v2); ng = (height - 2) - (height - 2) / 2; y -= 1 + (height - 2) / 2; } return QColor::fromHsvF(h1 + ((h2 - h1) * y) / (ng - 1), s1 + ((s2 - s1) * y) / (ng - 1), v1 + ((v2 - v1) * y) / (ng - 1)); } void Note::drawHandle(QPainter *painter, qreal x, qreal y, qreal width, qreal height, const QColor &background, const QColor &foreground, const QColor &lightForeground) { const QPen backgroundPen(background); const QPen foregroundPen(foreground); // Draw the surrounding rectangle: painter->setPen(foregroundPen); painter->drawLine(0, 0, width - 1, 0); painter->drawLine(0, 0, 0, height - 1); painter->drawLine(0, height - 1, width - 1, height - 1); // Draw the gradients: painter->fillRect(1 + x, 1 + y, width - 2, height - 2, lightForeground); // Round the top corner with background color: painter->setPen(backgroundPen); painter->drawLine(0, 0, 0, 3); painter->drawLine(1, 0, 3, 0); painter->drawPoint(1, 1); // Round the bottom corner with background color: painter->drawLine(0, height - 1, 0, height - 4); painter->drawLine(1, height - 1, 3, height - 1); painter->drawPoint(1, height - 2); // Surrounding line of the rounded top-left corner: painter->setPen(foregroundPen); painter->drawLine(1, 2, 1, 3); painter->drawLine(2, 1, 3, 1); // Surrounding line of the rounded bottom-left corner: painter->drawLine(1, height - 3, 1, height - 4); painter->drawLine(2, height - 2, 3, height - 2); // Anti-aliased rounded top corner (1/2): painter->setPen(lightForeground); painter->drawPoint(0, 3); painter->drawPoint(3, 0); painter->drawPoint(2, 2); // Anti-aliased rounded bottom corner: painter->drawPoint(0, height - 4); painter->drawPoint(3, height - 1); painter->drawPoint(2, height - 3); // Draw the grips: const qreal middleHeight = (height - 1) / 2; const qreal middleWidth = width / 2; painter->fillRect(middleWidth - 2, middleHeight, 2, 2, foreground); painter->fillRect(middleWidth + 2, middleHeight, 2, 2, foreground); painter->fillRect(middleWidth - 2, middleHeight - 4, 2, 2, foreground); painter->fillRect(middleWidth + 2, middleHeight - 4, 2, 2, foreground); painter->fillRect(middleWidth - 2, middleHeight + 4, 2, 2, foreground); painter->fillRect(middleWidth + 2, middleHeight + 4, 2, 2, foreground); } void Note::drawResizer(QPainter *painter, qreal x, qreal y, qreal width, qreal height, const QColor &background, const QColor &foreground, bool rounded) { const QPen backgroundPen(background); const QPen foregroundPen(foreground); const QColor lightForeground = Tools::mixColor(background, foreground, 2); // Draw the surrounding rectangle: painter->setPen(foregroundPen); painter->fillRect(0, 0, width, height, lightForeground); painter->drawLine(0, 0, width - 2, 0); painter->drawLine(0, height - 1, width - 2, height - 1); painter->drawLine(width - 1, 2, width - 1, height - 2); if (isColumn()) { painter->drawLine(0, 2, 0, height - 2); } if (rounded) { // Round the top corner with background color: painter->setPen(backgroundPen); painter->drawLine(width - 1, 0, width - 3, 0); painter->drawLine(width - 1, 1, width - 1, 2); painter->drawPoint(width - 2, 1); // Round the bottom corner with background color: painter->drawLine(width - 1, height - 1, width - 1, height - 4); painter->drawLine(width - 2, height - 1, width - 4, height - 1); painter->drawPoint(width - 2, height - 2); // Surrounding line of the rounded top-left corner: painter->setPen(foregroundPen); painter->drawLine(width - 2, 2, width - 2, 3); painter->drawLine(width - 3, 1, width - 4, 1); // Surrounding line of the rounded bottom-left corner: painter->drawLine(width - 2, height - 3, width - 2, height - 4); painter->drawLine(width - 3, height - 2, width - 4, height - 2); // Anti-aliased rounded top corner (1/2): painter->setPen(Tools::mixColor(foreground, background, 2)); painter->drawPoint(width - 1, 3); painter->drawPoint(width - 4, 0); // Anti-aliased rounded bottom corner: painter->drawPoint(width - 1, height - 4); painter->drawPoint(width - 4, height - 1); // Anti-aliased rounded top corner (2/2): painter->setPen(foreground); painter->drawPoint(width - 3, 2); // Anti-aliased rounded bottom corner (2/2): painter->drawPoint(width - 3, height - 3); } // Draw the arrows: qreal xArrow = 2; qreal hMargin = 9; int countArrows = (height >= hMargin * 4 + 6 * 3 ? 3 : (height >= hMargin * 3 + 6 * 2 ? 2 : 1)); for (int i = 0; i < countArrows; ++i) { qreal yArrow; switch (countArrows) { default: case 1: yArrow = (height - 6) / 2; break; case 2: yArrow = (i == 1 ? hMargin : height - hMargin - 6); break; case 3: yArrow = (i == 1 ? hMargin : (i == 2 ? (height - 6) / 2 : height - hMargin - 6)); break; } /// Dark color: painter->setPen(foreground); // Left arrow: painter->drawLine(xArrow, yArrow + 2, xArrow + 2, yArrow); painter->drawLine(xArrow, yArrow + 2, xArrow + 2, yArrow + 4); // Right arrow: painter->drawLine(width - 1 - xArrow, yArrow + 2, width - 1 - xArrow - 2, yArrow); painter->drawLine(width - 1 - xArrow, yArrow + 2, width - 1 - xArrow - 2, yArrow + 4); /// Light color: painter->setPen(background); // Left arrow: painter->drawLine(xArrow, yArrow + 2 + 1, xArrow + 2, yArrow + 1); painter->drawLine(xArrow, yArrow + 2 + 1, xArrow + 2, yArrow + 4 + 1); // Right arrow: painter->drawLine(width - 1 - xArrow, yArrow + 2 + 1, width - 1 - xArrow - 2, yArrow + 1); painter->drawLine(width - 1 - xArrow, yArrow + 2 + 1, width - 1 - xArrow - 2, yArrow + 4 + 1); } } void Note::drawInactiveResizer(QPainter *painter, qreal x, qreal y, qreal height, const QColor &background, bool column) { // If background color is too dark, we compute a lighter color instead of a darker: QColor darkBgColor = (Tools::tooDark(background) ? background.lighter(120) : background.darker(105)); painter->fillRect(x, y, RESIZER_WIDTH, height, Tools::mixColor(background, darkBgColor, 2)); } QPalette Note::palette() const { return (m_basket ? m_basket->palette() : qApp->palette()); } /* type: 1: topLeft * 2: bottomLeft * 3: topRight * 4: bottomRight * 5: fourCorners * 6: noteInsideAndOutsideCorners * (x,y) relate to the painter origin * (width,height) only used for 5:fourCorners type */ void Note::drawRoundings(QPainter *painter, qreal x, qreal y, int type, qreal width, qreal height) { qreal right; switch (type) { case 1: x += this->x(); y += this->y(); basket()->blendBackground(*painter, QRectF(x, y, 4, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 1, 2, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 3, 1, 1), this->x(), this->y()); break; case 2: x += this->x(); y += this->y(); basket()->blendBackground(*painter, QRectF(x, y - 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 1, 2, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 2, 4, 1), this->x(), this->y()); break; case 3: right = rightLimit(); x += right; y += this->y(); basket()->blendBackground(*painter, QRectF(x - 1, y, 4, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 1, y + 1, 2, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 2, y + 2, 1, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 2, y + 3, 1, 1), right, this->y()); break; case 4: right = rightLimit(); x += right; y += this->y(); basket()->blendBackground(*painter, QRectF(x + 2, y - 1, 1, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 2, y, 1, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 1, y + 1, 2, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x - 1, y + 2, 4, 1), right, this->y()); break; case 5: // First make sure the corners are white (depending on the widget style): painter->setPen(basket()->backgroundColor()); painter->drawPoint(x, y); painter->drawPoint(x + width - 1, y); painter->drawPoint(x + width - 1, y + height - 1); painter->drawPoint(x, y + height - 1); // And then blend corners: x += this->x(); y += this->y(); basket()->blendBackground(*painter, QRectF(x, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 1, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + height - 1, 1, 1), this->x(), this->y()); break; case 6: x += this->x(); y += this->y(); // if (!isSelected()) { // Inside left corners: basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH + 1, y + 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH, y + 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH + 1, y + height - 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH, y + height - 3, 1, 1), this->x(), this->y()); // Inside right corners: basket()->blendBackground(*painter, QRectF(x + width - 4, y + 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 3, y + 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 4, y + height - 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 3, y + height - 3, 1, 1), this->x(), this->y()); //} // Outside right corners: basket()->blendBackground(*painter, QRectF(x + width - 1, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); break; } } /// Blank Spaces Drawing: void Note::setOnTop(bool onTop) { setZValue(onTop ? 100 : 0); m_onTop = onTop; Note *note = firstChild(); while (note) { note->setOnTop(onTop); note = note->next(); } } void substractRectOnAreas(const QRectF &rectToSubstract, QList &areas, bool andRemove) { for (int i = 0; i < areas.size();) { QRectF &rect = areas[i]; // Split the rectangle if it intersects with rectToSubstract: if (rect.intersects(rectToSubstract)) { // Create the top rectangle: if (rectToSubstract.top() > rect.top()) { areas.insert(i++, QRectF(rect.left(), rect.top(), rect.width(), rectToSubstract.top() - rect.top())); rect.setTop(rectToSubstract.top()); } // Create the bottom rectangle: if (rectToSubstract.bottom() < rect.bottom()) { areas.insert(i++, QRectF(rect.left(), rectToSubstract.bottom(), rect.width(), rect.bottom() - rectToSubstract.bottom())); rect.setBottom(rectToSubstract.bottom()); } // Create the left rectangle: if (rectToSubstract.left() > rect.left()) { areas.insert(i++, QRectF(rect.left(), rect.top(), rectToSubstract.left() - rect.left(), rect.height())); rect.setLeft(rectToSubstract.left()); } // Create the right rectangle: if (rectToSubstract.right() < rect.right()) { areas.insert(i++, QRectF(rectToSubstract.right(), rect.top(), rect.right() - rectToSubstract.right(), rect.height())); rect.setRight(rectToSubstract.right()); } // Remove the rectangle if it's entirely contained: if (andRemove && rectToSubstract.contains(rect)) areas.removeAt(i); else ++i; } else ++i; } } void Note::recomputeAreas() { // Initialize the areas with the note rectangle(s): m_areas.clear(); m_areas.append(visibleRect()); if (hasResizer()) m_areas.append(resizerRect()); // Cut the areas where other notes are on top of this note: Note *note = basket()->firstNote(); bool noteIsAfterThis = false; while (note) { noteIsAfterThis = recomputeAreas(note, noteIsAfterThis); note = note->next(); } } bool Note::recomputeAreas(Note *note, bool noteIsAfterThis) { if (note == this) noteIsAfterThis = true; // Only compute overlapping of notes AFTER this, or ON TOP this: // else if ( note->matching() && noteIsAfterThis && (!isOnTop() || (isOnTop() && note->isOnTop())) || (!isOnTop() && note->isOnTop()) ) { else if (note->matching() && noteIsAfterThis && ((!(isOnTop() || isEditing()) || ((isOnTop() || isEditing()) && (note->isOnTop() || note->isEditing()))) || (!(isOnTop() || isEditing()) && (note->isOnTop() || note->isEditing())))) { // if (!(isSelected() && !note->isSelected())) { // FIXME: FIXME: FIXME: FIXME: This last condition was added LATE, so we should look if it's ALWAYS good: substractRectOnAreas(note->visibleRect(), m_areas, true); if (note->hasResizer()) substractRectOnAreas(note->resizerRect(), m_areas, true); //} } if (note->isGroup()) { Note *child = note->firstChild(); bool first = true; while (child) { if ((note->showSubNotes() || first) && note->matching()) noteIsAfterThis = recomputeAreas(child, noteIsAfterThis); child = child->next(); first = false; } } return noteIsAfterThis; } bool Note::isEditing() { return basket()->editedNote() == this; } /* Drawing policy: * ============== * - Draw the note on a pixmap and then draw the pixmap on screen is faster and * flicker-free, rather than drawing directly on screen * - The next time the pixmap can be directly redrawn on screen without * (relatively low, for small texts) time-consuming text-drawing * - To keep memory footprint low, we can destruct the bufferPixmap because * redrawing it offscreen and applying it onscreen is nearly as fast as just * drawing the pixmap onscreen * - But as drawing the pixmap offscreen is little time consuming we can keep * last visible notes buffered and then the redraw of the entire window is * INSTANTANEOUS * - We keep buffered note/group draws BUT NOT the resizer: such objects are * small and fast to draw, so we don't complexify code for that */ void Note::draw(QPainter *painter, const QRectF & /*clipRect*/) { if (!matching()) return; /** Compute visible areas: */ if (!m_computedAreas) recomputeAreas(); if (m_areas.isEmpty()) return; /** Directly draw pixmap on screen if it is already buffered: */ if (isBufferized()) { drawBufferOnScreen(painter, m_bufferedPixmap); return; } /** If pixmap is Null (size 0), no point in painting: **/ if (!width() || !height()) return; /** Initialise buffer painter: */ m_bufferedPixmap = QPixmap(width(), height()); - Q_ASSERT(!m_bufferedPixmap.isNull()); + if(m_bufferedPixmap.isNull()) { + return; + } + QPainter painter2(&m_bufferedPixmap); /** Initialise colors: */ QColor baseColor(basket()->backgroundColor()); QColor highColor(palette().color(QPalette::Highlight)); QColor midColor = Tools::mixColor(baseColor, highColor, 2); /** Initialise brushes and pens: */ QBrush baseBrush(baseColor); QBrush highBrush(highColor); QPen basePen(baseColor); QPen highPen(highColor); QPen midPen(midColor); /** Figure out the state of the note: */ bool hovered = m_hovered && m_hoveredZone != TopInsert && m_hoveredZone != BottomInsert && m_hoveredZone != Resizer; /** And then draw the group: */ if (isGroup()) { // Draw background or handle: if (hovered) { drawHandle(&painter2, 0, 0, width(), height(), baseColor, highColor, midColor); drawRoundings(&painter2, 0, 0, /*type=*/1); drawRoundings(&painter2, 0, height() - 3, /*type=*/2); } else { painter2.fillRect(0, 0, width(), height(), baseBrush); basket()->blendBackground(painter2, boundingRect().translated(x(), y()), -1, -1, /*opaque=*/true); } // Draw expander: qreal yExp = yExpander(); drawExpander(&painter2, NOTE_MARGIN, yExp, hovered ? midColor : baseColor, m_isFolded, basket()); // Draw expander rounded edges: if (hovered) { QColor color1 = expanderBackground(height(), yExp, highColor); QColor color2 = expanderBackground(height(), yExp + EXPANDER_HEIGHT - 1, highColor); painter2.setPen(color1); painter2.drawPoint(NOTE_MARGIN, yExp); painter2.drawPoint(NOTE_MARGIN + 9 - 1, yExp); painter2.setPen(color2); painter2.drawPoint(NOTE_MARGIN, yExp + 9 - 1); painter2.drawPoint(NOTE_MARGIN + 9 - 1, yExp + 9 - 1); } else drawRoundings(&painter2, NOTE_MARGIN, yExp, /*type=*/5, 9, 9); // Draw on screen: painter2.end(); drawBufferOnScreen(painter, m_bufferedPixmap); return; } /** Or draw the note: */ // What are the background colors: QColor background; if (m_computedState.backgroundColor().isValid()) { background = m_computedState.backgroundColor(); } else { background = basket()->backgroundColor(); } if (!hovered && !isSelected()) { // Draw background: painter2.fillRect(0, 0, width(), height(), background); basket()->blendBackground(painter2, boundingRect().translated(x(), y())); } else { // Draw selection background: painter2.fillRect(0, 0, width(), height(), midColor); // Top/Bottom lines: painter2.setPen(highPen); painter2.drawLine(0, height() - 1, width(), height() - 1); painter2.drawLine(0, 0, width(), 0); // The handle: drawHandle(&painter2, 0, 0, HANDLE_WIDTH, height(), baseColor, highColor, midColor); drawRoundings(&painter2, 0, 0, /*type=*/1); drawRoundings(&painter2, 0, height() - 3, /*type=*/2); painter2.setPen(midColor); drawRoundings(&painter2, 0, 0, /*type=*/6, width(), height()); } // Draw the Emblems: qreal yIcon = (height() - EMBLEM_SIZE) / 2; qreal xIcon = HANDLE_WIDTH + NOTE_MARGIN; for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { if (!(*it)->emblem().isEmpty()) { QPixmap stateEmblem = KIconLoader::global()->loadIcon((*it)->emblem(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, false); painter2.drawPixmap(xIcon, yIcon, stateEmblem); xIcon += NOTE_MARGIN + EMBLEM_SIZE; } } // Determine the colors (for the richText drawing) and the text color (for the tags arrow too): QPalette notePalette(basket()->palette()); notePalette.setColor(QPalette::Text, (m_computedState.textColor().isValid() ? m_computedState.textColor() : basket()->textColor())); notePalette.setColor(QPalette::Background, background); if (isSelected()) notePalette.setColor(QPalette::Text, palette().color(QPalette::HighlightedText)); // Draw the Tags Arrow: if (hovered) { QColor textColor = notePalette.color(QPalette::Text); QColor light = Tools::mixColor(textColor, background); QColor mid = Tools::mixColor(textColor, light); painter2.setPen(light); // QPen(basket()->colorGroup().darker().lighter(150))); painter2.drawLine(xIcon, yIcon + 6, xIcon + 4, yIcon + 6); painter2.setPen(mid); // QPen(basket()->colorGroup().darker())); painter2.drawLine(xIcon + 1, yIcon + 7, xIcon + 3, yIcon + 7); painter2.setPen(textColor); // QPen(basket()->colorGroup().foreground())); painter2.drawPoint(xIcon + 2, yIcon + 8); } else if (m_haveInvisibleTags) { painter2.setPen(notePalette.color(QPalette::Text) /*QPen(basket()->colorGroup().foreground())*/); painter2.drawPoint(xIcon, yIcon + 7); painter2.drawPoint(xIcon + 2, yIcon + 7); painter2.drawPoint(xIcon + 4, yIcon + 7); } // Draw on screen: painter2.end(); drawBufferOnScreen(painter, m_bufferedPixmap); } void Note::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { if (!m_basket->isLoaded()) return; if (boundingRect().width() <= 0.1 || boundingRect().height() <= 0.1) return; draw(painter, boundingRect()); if (hasResizer()) { qreal right = rightLimit() - x(); QRectF resizerRect(0, 0, RESIZER_WIDTH, resizerHeight()); // Prepare to draw the resizer: QPixmap pixmap(RESIZER_WIDTH, resizerHeight()); QPainter painter2(&pixmap); // Draw gradient or resizer: if ((m_hovered && m_hoveredZone == Resizer) || ((m_hovered || isSelected()) && !isColumn())) { QColor baseColor(basket()->backgroundColor()); QColor highColor(palette().color(QPalette::Highlight)); drawResizer(&painter2, 0, 0, RESIZER_WIDTH, resizerHeight(), baseColor, highColor, /*rounded=*/!isColumn()); if (!isColumn()) { drawRoundings(&painter2, RESIZER_WIDTH - 3, 0, /*type=*/3); drawRoundings(&painter2, RESIZER_WIDTH - 3, resizerHeight() - 3, /*type=*/4); } } else { drawInactiveResizer(&painter2, /*x=*/0, /*y=*/0, /*height=*/resizerHeight(), basket()->backgroundColor(), isColumn()); resizerRect.translate(rightLimit(), y()); basket()->blendBackground(painter2, resizerRect); } // Draw on screen: painter2.end(); painter->drawPixmap(right, 0, pixmap); } } void Note::drawBufferOnScreen(QPainter *painter, const QPixmap &contentPixmap) { for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF rect = (*it).translated(-x(), -y()); if (rect.x() >= width()) // It's a rect of the resizer, don't draw it! continue; painter->drawPixmap(rect.x(), rect.y(), contentPixmap, rect.x(), rect.y(), rect.width(), rect.height()); } } void Note::setContent(NoteContent *content) { m_content = content; } /*const */ State::List &Note::states() const { return (State::List &)m_states; } void Note::addState(State *state, bool orReplace) { if (!content()) return; Tag *tag = state->parentTag(); State::List::iterator itStates = m_states.begin(); // Browse all tags, see if the note has it, increment itSates if yes, and then insert the state at this position... // For each existing tags: for (Tag::List::iterator it = Tag::all.begin(); it != Tag::all.end(); ++it) { // If the current tag isn't the one to assign or the current one on the note, go to the next tag: if (*it != tag && itStates != m_states.end() && *it != (*itStates)->parentTag()) continue; // We found the tag to insert: if (*it == tag) { // And the note already have the tag: if (itStates != m_states.end() && *it == (*itStates)->parentTag()) { // We replace the state if wanted: if (orReplace) { itStates = m_states.insert(itStates, state); ++itStates; m_states.erase(itStates); recomputeStyle(); } } else { m_states.insert(itStates, state); recomputeStyle(); } return; } // The note has this tag: if (itStates != m_states.end() && *it == (*itStates)->parentTag()) ++itStates; } } QFont Note::font() { return m_computedState.font(basket()->QGraphicsScene::font()); } QColor Note::backgroundColor() { if (m_computedState.backgroundColor().isValid()) return m_computedState.backgroundColor(); else return basket()->backgroundColor(); } QColor Note::textColor() { if (m_computedState.textColor().isValid()) return m_computedState.textColor(); else return basket()->textColor(); } void Note::recomputeStyle() { State::merge(m_states, &m_computedState, &m_emblemsCount, &m_haveInvisibleTags, basket()->backgroundColor()); // unsetWidth(); if (content()) { if (content()->graphicsItem()) content()->graphicsItem()->setPos(contentX(), NOTE_MARGIN); content()->fontChanged(); } // requestRelayout(); // TODO! } void Note::recomputeAllStyles() { if (content()) // We do the merge ourself, without calling recomputeStyle(), so there is no infinite recursion: // State::merge(m_states, &m_computedState, &m_emblemsCount, &m_haveInvisibleTags, basket()->backgroundColor()); recomputeStyle(); else if (isGroup()) FOR_EACH_CHILD(child) child->recomputeAllStyles(); } bool Note::removedStates(const QList &deletedStates) { bool modifiedBasket = false; if (!states().isEmpty()) { for (QList::const_iterator it = deletedStates.begin(); it != deletedStates.end(); ++it) if (hasState(*it)) { removeState(*it); modifiedBasket = true; } } FOR_EACH_CHILD(child) if (child->removedStates(deletedStates)) modifiedBasket = true; return modifiedBasket; } void Note::addTag(Tag *tag) { addState(tag->states().first(), /*but do not replace:*/ false); } void Note::removeState(State *state) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if (*it == state) { m_states.erase(it); recomputeStyle(); return; } } void Note::removeTag(Tag *tag) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if ((*it)->parentTag() == tag) { m_states.erase(it); recomputeStyle(); return; } } void Note::removeAllTags() { m_states.clear(); recomputeStyle(); } void Note::addTagToSelectedNotes(Tag *tag) { if (content() && isSelected()) addTag(tag); FOR_EACH_CHILD(child) child->addTagToSelectedNotes(tag); } void Note::removeTagFromSelectedNotes(Tag *tag) { if (content() && isSelected()) { if (hasTag(tag)) setWidth(0); removeTag(tag); } FOR_EACH_CHILD(child) child->removeTagFromSelectedNotes(tag); } void Note::removeAllTagsFromSelectedNotes() { if (content() && isSelected()) { if (m_states.count() > 0) setWidth(0); removeAllTags(); } FOR_EACH_CHILD(child) child->removeAllTagsFromSelectedNotes(); } void Note::addStateToSelectedNotes(State *state, bool orReplace) { if (content() && isSelected()) addState(state, orReplace); FOR_EACH_CHILD(child) child->addStateToSelectedNotes(state, orReplace); // TODO: BasketScene::addStateToSelectedNotes() does not have orReplace } void Note::changeStateOfSelectedNotes(State *state) { if (content() && isSelected() && hasTag(state->parentTag())) addState(state); FOR_EACH_CHILD(child) child->changeStateOfSelectedNotes(state); } bool Note::selectedNotesHaveTags() { if (content() && isSelected() && m_states.count() > 0) return true; FOR_EACH_CHILD(child) if (child->selectedNotesHaveTags()) return true; return false; } bool Note::hasState(State *state) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if (*it == state) return true; return false; } bool Note::hasTag(Tag *tag) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if ((*it)->parentTag() == tag) return true; return false; } State *Note::stateOfTag(Tag *tag) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if ((*it)->parentTag() == tag) return *it; return nullptr; } bool Note::allowCrossReferences() { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if (!(*it)->allowCrossReferences()) return false; return true; } State *Note::stateForEmblemNumber(int number) const { int i = -1; for (State::List::const_iterator it = m_states.begin(); it != m_states.end(); ++it) if (!(*it)->emblem().isEmpty()) { ++i; if (i == number) return *it; } return nullptr; } bool Note::stateForTagFromSelectedNotes(Tag *tag, State **state) { if (content() && isSelected()) { // What state is the tag on this note? State *stateOfTag = this->stateOfTag(tag); // This tag is not assigned to this note, the action will assign it, then: if (stateOfTag == nullptr) *state = nullptr; else { // Take the LOWEST state of all the selected notes: // Say the two selected notes have the state "Done" and "To Do". // The first note set *state to "Done". // When reaching the second note, we should recognize "To Do" is first in the tag states, then take it // Because pressing the tag shortcut key should first change state before removing the tag! if (*state == nullptr) *state = stateOfTag; else { bool stateIsFirst = true; for (State *nextState = stateOfTag->nextState(); nextState; nextState = nextState->nextState(/*cycle=*/false)) if (nextState == *state) stateIsFirst = false; if (!stateIsFirst) *state = stateOfTag; } } return true; // We encountered a selected note } bool encounteredSelectedNote = false; FOR_EACH_CHILD(child) { bool encountered = child->stateForTagFromSelectedNotes(tag, state); if (encountered && *state == nullptr) return true; if (encountered) encounteredSelectedNote = true; } return encounteredSelectedNote; } void Note::inheritTagsOf(Note *note) { if (!note || !content()) return; for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) if ((*it)->parentTag() && (*it)->parentTag()->inheritedBySiblings()) addTag((*it)->parentTag()); } void Note::unbufferizeAll() { unbufferize(); if (isGroup()) { Note *child = firstChild(); while (child) { child->unbufferizeAll(); child = child->next(); } } } QRectF Note::visibleRect() { QList areas; areas.append(QRectF(x(), y(), width(), height())); // When we are folding a parent group, if this note is bigger than the first real note of the group, cut the top of this: /*Note *parent = parentNote(); while (parent) { if (parent->expandingOrCollapsing()) substractRectOnAreas(QRect(x(), parent->y() - height(), width(), height()), areas, true); parent = parent->parentNote(); }*/ if (areas.count() > 0) return areas.first(); else return QRectF(); } void Note::recomputeBlankRects(QList &blankAreas) { if (!matching()) return; // visibleRect() instead of rect() because if we are folding/expanding a smaller parent group, then some part is hidden! // But anyway, a resizer is always a primary note and is never hidden by a parent group, so no visibleResizerRect() method! substractRectOnAreas(visibleRect(), blankAreas, true); if (hasResizer()) substractRectOnAreas(resizerRect(), blankAreas, true); if (isGroup()) { Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->recomputeBlankRects(blankAreas); child = child->next(); first = false; } } } void Note::linkLookChanged() { if (isGroup()) { Note *child = firstChild(); while (child) { child->linkLookChanged(); child = child->next(); } } else content()->linkLookChanged(); } Note *Note::noteForFullPath(const QString &path) { if (content() && fullPath() == path) return this; Note *child = firstChild(); Note *found; while (child) { found = child->noteForFullPath(path); if (found) return found; child = child->next(); } return nullptr; } void Note::listUsedTags(QList &list) { for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { Tag *tag = (*it)->parentTag(); if (!list.contains(tag)) list.append(tag); } FOR_EACH_CHILD(child) child->listUsedTags(list); } void Note::usedStates(QList &states) { if (content()) for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) if (!states.contains(*it)) states.append(*it); FOR_EACH_CHILD(child) child->usedStates(states); } Note *Note::nextInStack() { // First, search in the children: if (firstChild()) { if (firstChild()->content()) return firstChild(); else return firstChild()->nextInStack(); } // Then, in the next: if (next()) { if (next()->content()) return next(); else return next()->nextInStack(); } // And finally, in the parent: Note *note = parentNote(); while (note) if (note->next()) if (note->next()->content()) return note->next(); else return note->next()->nextInStack(); else note = note->parentNote(); // Not found: return nullptr; } Note *Note::prevInStack() { // First, search in the previous: if (prev() && prev()->content()) return prev(); // Else, it's a group, get the last item in that group: if (prev()) { Note *note = prev()->lastRealChild(); if (note) return note; } if (parentNote()) return parentNote()->prevInStack(); else return nullptr; } Note *Note::nextShownInStack() { Note *next = nextInStack(); while (next && !next->isShown()) next = next->nextInStack(); return next; } Note *Note::prevShownInStack() { Note *prev = prevInStack(); while (prev && !prev->isShown()) prev = prev->prevInStack(); return prev; } bool Note::isShown() { // First, the easy one: groups are always shown: if (isGroup()) return true; // And another easy part: non-matching notes are hidden: if (!matching()) return false; if (basket()->isFiltering()) // And isMatching() because of the line above! return true; // So, here we go to the complex case: if the note is inside a collapsed group: Note *group = parentNote(); Note *child = this; while (group) { if (group->isFolded() && group->firstChild() != child) return false; child = group; group = group->parentNote(); } return true; } void Note::debug() { qDebug() << "Note@" << (quint64)this; if (!this) { qDebug(); return; } if (isColumn()) qDebug() << ": Column"; else if (isGroup()) qDebug() << ": Group"; else qDebug() << ": Content[" << content()->lowerTypeName() << "]: " << toText(QString()); qDebug(); } Note *Note::firstSelected() { if (isSelected()) return this; Note *first = nullptr; FOR_EACH_CHILD(child) { first = child->firstSelected(); if (first) break; } return first; } Note *Note::lastSelected() { if (isSelected()) return this; Note *last = nullptr, *tmp = nullptr; FOR_EACH_CHILD(child) { tmp = child->lastSelected(); if (tmp) last = tmp; } return last; } Note *Note::selectedGroup() { if (isGroup() && allSelected() && count() == basket()->countSelecteds()) return this; FOR_EACH_CHILD(child) { Note *selectedGroup = child->selectedGroup(); if (selectedGroup) return selectedGroup; } return nullptr; } void Note::groupIn(Note *group) { if (this == group) return; if (allSelected() && !isColumn()) { basket()->unplugNote(this); basket()->insertNote(this, group, Note::BottomColumn); } else { Note *next; Note *child = firstChild(); while (child) { next = child->next(); child->groupIn(group); child = next; } } } bool Note::tryExpandParent() { Note *parent = parentNote(); Note *child = this; while (parent) { if (parent->firstChild() != child) return false; if (parent->isColumn()) return false; if (parent->isFolded()) { parent->toggleFolded(); basket()->relayoutNotes(); return true; } child = parent; parent = parent->parentNote(); } return false; } bool Note::tryFoldParent() // TODO: withCtrl ? withShift ? { Note *parent = parentNote(); Note *child = this; while (parent) { if (parent->firstChild() != child) return false; if (parent->isColumn()) return false; if (!parent->isFolded()) { parent->toggleFolded(); basket()->relayoutNotes(); return true; } child = parent; parent = parent->parentNote(); } return false; } qreal Note::distanceOnLeftRight(Note *note, int side) { if (side == BasketScene::RIGHT_SIDE) { // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (x() > note->x() || finalRightLimit() > note->finalRightLimit()) return -1; } else { /*LEFT_SIDE:*/ // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (x() < note->x() || finalRightLimit() < note->finalRightLimit()) return -1; } if (x() == note->x() && finalRightLimit() == note->finalRightLimit()) return -1; qreal thisCenterX = x() + (side == BasketScene::LEFT_SIDE ? width() : /*RIGHT_SIDE:*/ 0); qreal thisCenterY = y() + height() / 2; qreal noteCenterX = note->x() + note->width() / 2; qreal noteCenterY = note->y() + note->height() / 2; if (thisCenterY > note->bottom()) noteCenterY = note->bottom(); else if (thisCenterY < note->y()) noteCenterY = note->y(); else noteCenterY = thisCenterY; qreal angle = 0; if (noteCenterX - thisCenterX != 0) angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); if (angle < 0) angle = -angle; return sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle; } qreal Note::distanceOnTopBottom(Note *note, int side) { if (side == BasketScene::BOTTOM_SIDE) { // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (y() > note->y() || bottom() > note->bottom()) return -1; } else { /*TOP_SIDE:*/ // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (y() < note->y() || bottom() < note->bottom()) return -1; } if (y() == note->y() && bottom() == note->bottom()) return -1; qreal thisCenterX = x() + width() / 2; qreal thisCenterY = y() + (side == BasketScene::TOP_SIDE ? height() : /*BOTTOM_SIDE:*/ 0); qreal noteCenterX = note->x() + note->width() / 2; qreal noteCenterY = note->y() + note->height() / 2; if (thisCenterX > note->finalRightLimit()) noteCenterX = note->finalRightLimit(); else if (thisCenterX < note->x()) noteCenterX = note->x(); else noteCenterX = thisCenterX; qreal angle = 0; if (noteCenterX - thisCenterX != 0) angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); if (angle < 0) angle = -angle; return sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle; } Note *Note::parentPrimaryNote() { Note *primary = this; while (primary->parentNote()) primary = primary->parentNote(); return primary; } void Note::deleteChilds() { Note *child = firstChild(); while (child) { Note *tmp = child->next(); delete child; child = tmp; } } bool Note::saveAgain() { bool result = true; if (content()) { if (!content()->saveToFile()) result = false; } FOR_EACH_CHILD(child) { if (!child->saveAgain()) result = false; } if (!result) { DEBUG_WIN << QString("Note::saveAgain returned false for %1:%2").arg((content() != nullptr) ? content()->typeName() : "null", toText(QString())); } return result; } bool Note::convertTexts() { bool convertedNotes = false; if (content() && content()->lowerTypeName() == "text") { QString text = ((TextContent *)content())->text(); QString html = "" + Tools::textToHTMLWithoutP(text) + ""; basket()->saveToFile(fullPath(), html); setContent(new HtmlContent(this, content()->fileName())); convertedNotes = true; } FOR_EACH_CHILD(child) if (child->convertTexts()) convertedNotes = true; return convertedNotes; } /* vim: set et sts=4 sw=4 ts=16 tw=78 : */ /* kate: indent-width 4; replace-tabs on; */ diff --git a/src/noteitem.cpp b/src/noteitem.cpp index 669a600..f70c001 100644 --- a/src/noteitem.cpp +++ b/src/noteitem.cpp @@ -1,281 +1,285 @@ /* * 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(); } } +BasketScene *NoteItem::s_basket; NoteItem::NoteItem() : m_parent(nullptr) , m_note(nullptr) - , m_helperNote(new Note()) + , m_helperNote(new Note(s_basket)) { } 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; } +void NoteItem::setBasketParent(BasketScene* basket) +{ + s_basket = basket; +} + QString NoteItem::displayText() const { if (!note()) { return QStringLiteral("NULL NOTE"); } if (type() == NoteType::Group) { return QStringLiteral("GROUP"); } return content()->toText(QString()); } QIcon NoteItem::decoration() const { 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(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 { 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")); } void NoteItem::loadFromXMLNode(const QDomElement& node) { 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 55ba138..e35503d 100644 --- a/src/noteitem.h +++ b/src/noteitem.h @@ -1,81 +1,86 @@ /* * SPDX-FileCopyrightText: 2020 by Ismael Asensio * SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "note.h" #include "notecontent.h" #include 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: struct EditInfo { QDateTime created; QDateTime modified; }; public: explicit NoteItem(); + explicit NoteItem(BasketScene *basket); ~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) + // Accesors for compatibility with current code NotePtr note() const; void setNote(NotePtr note); + static void setBasketParent(BasketScene *basket); // 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 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. + NotePtr m_helperNote; // Dummy note to help with the code transition. + static BasketScene *s_basket; // Stored to set a parent to the notes (and avoid crashes) + // NoteContent *m_content; // QRect m_bounds; // EditInfo m_editInfo; // QVector m_tags; }; Q_DECLARE_METATYPE(NoteContent *) Q_DECLARE_METATYPE(NoteItem::EditInfo)