diff options
author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-10 00:18:25 +0000 |
---|---|---|
committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-10 00:18:25 +0000 |
commit | f21e5792b5084f5d008bf46f6316030c6dfb31e5 (patch) | |
tree | d51583b36aa1672bac78d98a682cdc330df27e4d /src/note.cpp | |
download | basket-f21e5792b5084f5d008bf46f6316030c6dfb31e5.tar.gz basket-f21e5792b5084f5d008bf46f6316030c6dfb31e5.zip |
Add author-abandoned basket application
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/basket@1072339 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'src/note.cpp')
-rw-r--r-- | src/note.cpp | 2890 |
1 files changed, 2890 insertions, 0 deletions
diff --git a/src/note.cpp b/src/note.cpp new file mode 100644 index 0000000..fd1fb34 --- /dev/null +++ b/src/note.cpp @@ -0,0 +1,2890 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * slaout@linux62.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qpainter.h> +#include <kglobalsettings.h> +#include <qstyle.h> +#include <kapplication.h> +#include <kstyle.h> +#include <qcursor.h> +#include <kiconloader.h> +#include <kpixmapeffect.h> +#include <kpixmap.h> +#include <kglobal.h> +#include <klocale.h> +#include <kurifilter.h> +#include <qfile.h> + +#include <stdlib.h> // rand() function +#include <math.h> // sqrt() and pow() functions + +#include <iostream> + +#ifdef None +#undef None +#endif + +#include "basket.h" +#include "tag.h" +#include "note.h" +#include "tools.h" +#include "settings.h" +#include "notefactory.h" // For NoteFactory::filteredURL() + +/** class Note: */ + +#define FOR_EACH_CHILD(childVar) \ + for (Note *childVar = firstChild(); childVar; childVar = childVar->next()) + +// TODO: +#define FOR_EACH_VISIBLE_CHILD(childVar) \ + for (...) + +int Note::NOTE_MARGIN = 2; +int Note::INSERTION_HEIGHT = 5; +int Note::EXPANDER_WIDTH = 9; +int Note::EXPANDER_HEIGHT = 9; +int Note::GROUP_WIDTH = 2*NOTE_MARGIN + EXPANDER_WIDTH; +int Note::HANDLE_WIDTH = GROUP_WIDTH; +int Note::RESIZER_WIDTH = GROUP_WIDTH; +int Note::TAG_ARROW_WIDTH = 5; +int Note::EMBLEM_SIZE = 16; +int Note::MIN_HEIGHT = 2*NOTE_MARGIN + EMBLEM_SIZE; + +Note::Note(Basket *parent) + : m_prev(0), m_next(0), + m_x(0), m_y(-1), m_width(-1), m_height(-1), + m_groupWidth(250), + m_isFolded(false), m_firstChild(0L), m_parentNote(0), + m_basket(parent), m_content(0), m_addedDate(QDateTime::currentDateTime()), m_lastModificationDate(QDateTime::currentDateTime()), + m_computedAreas(false), m_onTop(false), + m_deltaX(0), m_deltaY(0), m_deltaHeight(0), m_collapseFinished(true), m_expandingFinished(true), + 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) +{ +} + +Note::~Note() +{ + delete m_content; + deleteChilds(); +} + +QString Note::addedStringDate() +{ + return KGlobal::locale()->formatDateTime(m_addedDate); +} + +QString Note::lastModificationStringDate() +{ + return KGlobal::locale()->formatDateTime(m_lastModificationDate); +} + +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 immediatly: + 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 = QStringList::split('\n', text, /*allowEmptyEntries=*/true); + QString result = firstLine + lines[0] + (lines.count() > 1 ? "\n" : ""); + for (uint i = 1/*Skip the first line*/; i < lines.count(); ++i) + result += otherLines + lines[i] + (i < lines.count() - 1 ? "\n" : ""); + return result; + } else + return ""; +} + +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 suddently: + 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); + + int countMatches = (content() && matching() ? 1 : 0); + + FOR_EACH_CHILD (child) + countMatches += child->newFilter(data); + + return countMatches; +} + +void Note::deleteSelectedNotes(bool deleteFilesToo) +{ + if (content() && isSelected()) { + basket()->unplugNote(this); + if (deleteFilesToo && content() && content()->useFile()) + Tools::deleteRecursively(fullPath());//basket()->deleteFiles(fullPath()); // Also delete the folder if it's a folder + //delete this; + return; + } + + 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); + child = next; + } +} + +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 ""; +} + +void Note::update() +{ + basket()->updateNote(this); +} + +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 QRect &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 (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &r = *it; + if (r.intersects(rect)) { + intersects = true; + break; + } + } + + bool toSelect = intersects || (!unselectOthers && isSelected()); + if (invertSelection) { + if (m_wasInLastSelectionRect == intersects) + toSelect = isSelected(); + else if (intersects xor m_wasInLastSelectionRect) + toSelect = !isSelected();// xor intersects; + } + 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->setSelectedRecursivly(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::setSelectedRecursivly(bool selected) +{ + setSelected(selected && matching()); + + FOR_EACH_CHILD (child) + child->setSelectedRecursivly(selected); +} + +void Note::invertSelectionRecursivly() +{ + if (content()) + setSelected(!isSelected() && matching()); + + FOR_EACH_CHILD (child) + child->invertSelectionRecursivly(); +} + +void Note::unselectAllBut(Note *toSelect) +{ + if (this == toSelect) + setSelectedRecursivly(true); + else { + setSelected(false); + + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->unselectAllBut(toSelect); + else + child->setSelectedRecursivly(false); + child = child->next(); + first = false; + } + } +} + +void Note::invertSelectionOf(Note *toSelect) +{ + if (this == toSelect) + setSelectedRecursivly(!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 0; +} + +NoteSelection* Note::selectedNotes() +{ + if (content()) + if (isSelected()) + return new NoteSelection(this); + else + return 0; + + 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 groupe with only one content, return directly that content: + NoteSelection *reducedSelection = selection->firstChild; +// delete selection; // TODO: Cut all connexions of 'selection' before deleting it! + for (NoteSelection *node = reducedSelection; node; node = node->next) + node->parent = 0; + return reducedSelection; + } + } else { + delete selection; + return 0; + } +} + +bool Note::isAfter(Note *note) +{ + if (this == 0 || note == 0) + return true; + + Note *next = this; + while (next) { + if (next == note) + return false; + next = next->nextInStack(); + } + return true; +} + +bool Note::contains(Note *note) +{ +// if (this == note) +// return true; + + while (note) + if (note == this) + return true; + else + note = note->parentNote(); + +// FOR_EACH_CHILD (child) +// if (child->contains(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 0; +} + +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 0; +} + +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; +} + +int Note::yExpander() +{ + Note *child = firstRealChild(); + if (child && !child->isShown()) + child = child->nextShownInStack(); // FIXME: Restrict scope to 'this' + + if (child) + return (child->height() - EXPANDER_HEIGHT) / 2 + !(child->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() +{ + return parentNote() == 0 && basket()->isFreeLayout(); +} + +bool Note::isColumn() +{ + return parentNote() == 0 && basket()->isColumnsLayout(); +} + +bool Note::hasResizer() +{ + // "isFree" || "isColmun but not the last" + return parentNote() == 0 && (basket()->isFreeLayout() || m_next != 0L); +} + +int Note::resizerHeight() +{ + return (isColumn() ? basket()->contentsHeight() : 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 QPoint &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 whith the whole column: + if (toAdd && isColumn() && hasResizer()) { + int 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()) + return (pos.y() < height() / 2 ? TopInsert : 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()) { + int 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) + return (isFree() ? TopGroup : TopInsert); + if (pos.y() >= height() - INSERTION_HEIGHT) + return (isFree() ? BottomGroup : BottomInsert); + + if (pos.x() >= NOTE_MARGIN && pos.x() < NOTE_MARGIN + EXPANDER_WIDTH) { + int 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 - QPoint(contentX(), NOTE_MARGIN)); + if (customZone) + return (Note::Zone)customZone; + + return Content; +} + +QString Note::linkAt(const QPoint &pos) +{ + QString link = m_content->linkAt(pos - QPoint(contentX(), NOTE_MARGIN)); + if (link.isEmpty()) + return link; + else + return NoteFactory::filteredURL(KURL(link)).prettyURL();//KURIFilter::self()->filteredURI(link); +} + +int Note::contentX() +{ + return HANDLE_WIDTH + NOTE_MARGIN + (EMBLEM_SIZE+NOTE_MARGIN)*m_emblemsCount + TAG_ARROW_WIDTH + NOTE_MARGIN; +} + +QRect Note::zoneRect(Note::Zone zone, const QPoint &pos) +{ + if (zone >= Emblem0) + return QRect(HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*(zone-Emblem0), + INSERTION_HEIGHT, + NOTE_MARGIN + EMBLEM_SIZE, + height() - 2*INSERTION_HEIGHT); + + int yExp; + int right; + int xGroup = (isFree() ? (isGroup() ? 0 : GROUP_WIDTH) : width() / 2); + QRect rect; + int insertSplit = (Settings::groupOnInsertionLine() ? 2 : 1); + switch (zone) { + case Note::Handle: return QRect(0, 0, HANDLE_WIDTH, height()); + case Note::Group: + yExp = yExpander(); + if (pos.y() < yExp) return QRect(0, INSERTION_HEIGHT, width(), yExp - INSERTION_HEIGHT); + if (pos.y() > yExp + EXPANDER_HEIGHT) return QRect(0, yExp + EXPANDER_HEIGHT, width(), height() - yExp - EXPANDER_HEIGHT - INSERTION_HEIGHT); + if (pos.x() < NOTE_MARGIN) return QRect(0, 0, NOTE_MARGIN, height()); + else return QRect(width() - NOTE_MARGIN, 0, NOTE_MARGIN, height()); + case Note::TagsArrow: return QRect(HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*m_emblemsCount, + INSERTION_HEIGHT, + NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN, + height() - 2*INSERTION_HEIGHT); + case Note::Custom0: + case Note::Content: rect = content()->zoneRect(zone, pos - QPoint(contentX(), NOTE_MARGIN)); + rect.moveBy(contentX(), NOTE_MARGIN); + return rect.intersect( QRect(contentX(), INSERTION_HEIGHT, width() - contentX(), height() - 2*INSERTION_HEIGHT) ); // Only IN contentRect + case Note::GroupExpander: return QRect(NOTE_MARGIN, yExpander(), EXPANDER_WIDTH, EXPANDER_HEIGHT); + case Note::Resizer: right = rightLimit(); + return QRect(right - x(), 0, RESIZER_WIDTH, resizerHeight()); + case Note::Link: + case Note::TopInsert: if (isGroup()) return QRect(0, 0, width(), INSERTION_HEIGHT); + else return QRect(HANDLE_WIDTH, 0, width() / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); + case Note::TopGroup: return QRect(xGroup, 0, width() - xGroup, INSERTION_HEIGHT); + case Note::BottomInsert: if (isGroup()) return QRect(0, height() - INSERTION_HEIGHT, width(), INSERTION_HEIGHT); + else return QRect(HANDLE_WIDTH, height() - INSERTION_HEIGHT, width() / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); + case Note::BottomGroup: return QRect(xGroup, height() - INSERTION_HEIGHT, width() - xGroup, INSERTION_HEIGHT); + case Note::BottomColumn: return QRect(0, height(), rightLimit() - x(), basket()->contentsHeight() - height()); + case Note::None: return QRect(/*0, 0, -1, -1*/); + default: return QRect(/*0, 0, -1, -1*/); + } +} + +void Note::setCursor(Zone zone) +{ + switch (zone) { + case Note::Handle: + case Note::Group: basket()->viewport()->setCursor(Qt::SizeAllCursor); break; + case Note::Resizer: if (isColumn()) + basket()->viewport()->setCursor(Qt::SplitHCursor); + else + basket()->viewport()->setCursor(Qt::SizeHorCursor); break; + + case Note::Custom0: content()->setCursor(basket()->viewport(), zone); break; + + case Note::Link: + case Note::TagsArrow: + case Note::GroupExpander: basket()->viewport()->setCursor(Qt::PointingHandCursor); break; + + case Note::Content: basket()->viewport()->setCursor(Qt::IbeamCursor); break; + + case Note::TopInsert: + case Note::TopGroup: + case Note::BottomInsert: + case Note::BottomGroup: + case Note::BottomColumn: basket()->viewport()->setCursor(Qt::CrossCursor); break; + case Note::None: basket()->viewport()->unsetCursor(); break; + default: + State *state = stateForEmblemNumber(zone - Emblem0); + if (state && state->parentTag()->states().count() > 1) + basket()->viewport()->setCursor(Qt::PointingHandCursor); + else + basket()->viewport()->unsetCursor(); + } +} + +void Note::addAnimation(int deltaX, int deltaY, int deltaHeight) +{ + // Don't process animation that make the note stay in place! + if (deltaX == 0 && deltaY == 0 && deltaHeight == 0) + return; + + // If it was not animated previsouly, make it animated: + if (m_deltaX == 0 && m_deltaY == 0 && m_deltaHeight == 0) + basket()->addAnimatedNote(this); + + // Configure the animation: + m_deltaX += deltaX; + m_deltaY += deltaY; + m_deltaHeight += deltaHeight; +} + +void Note::setFinalPosition(int x, int y) +{ + addAnimation(x - finalX(), y - finalY()); +} + +void Note::initAnimationLoad() +{ + int x, y; + switch (rand() % 4) { + case 0: // Put it on top: + x = basket()->contentsX() + rand() % basket()->contentsWidth(); + y = -height(); + break; + case 1: // Put it on bottom: + x = basket()->contentsX() + rand() % basket()->contentsWidth(); + y = basket()->contentsY() + basket()->visibleHeight(); + break; + case 2: // Put it on left: + x = -width() - (hasResizer() ? Note::RESIZER_WIDTH : 0); + y = basket()->contentsY() + rand() % basket()->visibleHeight(); + break; + case 3: // Put it on right: + default: // In the case of... + x = basket()->contentsX() + basket()->visibleWidth(); + y = basket()->contentsY() + rand() % basket()->visibleHeight(); + break; + } + cancelAnimation(); + addAnimation(finalX() - x, finalY() - y); + setX(x); + setY(y); + + if (isGroup()) { + const int viewHeight = basket()->contentsY() + basket()->visibleHeight(); + Note *child = firstChild(); + bool first = true; + while (child) { + if (child->finalY() < viewHeight) { + if ((showSubNotes() || first) && child->matching()) + child->initAnimationLoad(); + } else + break; // 'child' are not a free notes (because child of at least one note, 'this'), so 'child' is ordered vertically. + child = child->next(); + first = false; + } + } +} + + +bool Note::advance() +{ + // Animate X: + if (m_deltaX != 0) { + int deltaX = m_deltaX / 3; + if (deltaX == 0) + deltaX = (m_deltaX > 0 ? 1 : -1); + setX(m_x + deltaX); + m_deltaX -= deltaX; + } + + // Animate Y: + if (m_deltaY != 0) { + int deltaY = m_deltaY / 3; + if (deltaY == 0) + deltaY = (m_deltaY > 0 ? 1 : -1); + setY(m_y + deltaY); + m_deltaY -= deltaY; + } + + // Animate Height: + if (m_deltaHeight != 0) { + int deltaHeight = m_deltaHeight / 3; + if (deltaHeight == 0) + deltaHeight = (m_deltaHeight > 0 ? 1 : -1); + m_height += deltaHeight; + unbufferize(); + m_deltaHeight -= deltaHeight; + } + + if (m_deltaHeight == 0) { + m_collapseFinished = true; + m_expandingFinished = true; + } + + // Return true if the animation is finished: + return (m_deltaX == 0 && m_deltaY == 0 && m_deltaHeight == 0); +} + +void Note::unsetWidth() +{ + m_width = 0; + unbufferize(); + + FOR_EACH_CHILD (child) + child->unsetWidth(); +} + +void Note::requestRelayout() +{ + m_width = 0; + unbufferize(); + basket()->relayoutNotes(true); // TODO: A signal that will relayout ONCE and DELAYED if called several times +} + +void Note::setWidth(int width) // TODO: inline ? +{ + if (m_width != width) + setWidthForceRelayout(width); +} + +void Note::setWidthForceRelayout(int width) +{ + unbufferize(); + m_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(); + m_height = m_content->setWidthAndGetHeight(contentWidth/* < 1 ? 1 : contentWidth*/) + 2 * NOTE_MARGIN; + if (m_height < 3 * INSERTION_HEIGHT) // Assure a minimal size... + m_height = 3 * INSERTION_HEIGHT; + } +} + +int Note::minWidth() +{ + if (m_content) + return contentX() + m_content->minWidth() + NOTE_MARGIN; + else + return GROUP_WIDTH; ///// FIXME: is this OK? +} + +int Note::minRight() +{ + if (isGroup()) { + int right = finalX() + 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()) { + int minColumnRight = finalX() + 2*HANDLE_WIDTH; + if (right < minColumnRight) + return minColumnRight; + } + return right; + } else + return finalX() + minWidth(); +} + +void Note::setX(int x) +{ + if (m_x == x) + return; + + if (isBufferized() && basket()->hasBackgroundImage()) { + // Unbufferize only if the background change: + if (basket()->isTiledBackground()) + unbufferize(); + else { + int bgw = basket()->backgroundPixmap()->width(); + if (m_x >= bgw && x < bgw) // Was not in the background image and is now inside it: + unbufferize(); + else if (m_x < bgw) // Was in the background image and is now at another position of the background image or is now outside: + unbufferize(); + } + } + + m_x = x; +} + +void Note::setY(int y) +{ + if (m_y == y) + return; + + if (isBufferized() && basket()->hasBackgroundImage()) { + // Unbufferize only if the background change: + if (basket()->isTiledBackground()) + unbufferize(); + else { + int bgh = basket()->backgroundPixmap()->height(); + if (m_y >= bgh && y < bgh) // Was not in the background image and is now inside it: + unbufferize(); + else if (m_y < bgh) // Was in the background image and is now at another position of the background image or is now outside: + unbufferize(); + } + } + + m_y = y; +} + + +void Note::toggleFolded(bool animate) +{ + // Close the editor if it was editing a note that we are about to hide after collapsing: + if (!m_isFolded && basket() && basket()->isDuringEdit()) { + if (contains(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(); + + if (animate) { + // We animate collapsing (so sub-notes fluidly go under the first note) + // We don't animate expanding: we place sub-notes directly under the first note (and the next relayout will animate the expanding) + // But if user quickly collapsed and then expand (while the collapsing animation isn't finished), we animate anyway + bool animateSetUnder = (m_isFolded || !m_collapseFinished); +// std::cout << "fold:" << m_isFolded << " collapseFinished:" << m_collapseFinished << " animateSetUnder:" << animateSetUnder << std::endl; + + if (m_isFolded) + m_collapseFinished = false; + else + m_expandingFinished = false; + + Note* note = firstChild(); + if (note) { + note->setOnTop(true); + while ( (note = note->next()) ) { // Don't process the first child: it is OK + note->setRecursivelyUnder(/*firstRealChild*/firstChild(), animateSetUnder); + note->setOnTop(false); + } + } + } + + //if (basket()->focusedNote() && !basket()->focusedNote()->isShown()) { + if (basket()->isLoaded()) { + basket()->setFocusedNote(firstRealChild()); + basket()->m_startOfShiftSelectionNote = firstRealChild(); + } + + if (basket()->isLoaded() && !m_isFolded) { + //basket()->setFocusedNote(this); + basket()->relayoutNotes(true); + basket()->ensureNoteVisible(this); + } + + basket()->save(); // FIXME: SHOULD WE ALWAYS SAVE ???????? +} + +void Note::setRecursivelyUnder(Note *under, bool animate) +{ + int y = /*finalHeight() > under->finalHeight() ? under->finalY() :*/ under->finalBottom() - finalHeight() + 1; + if (animate) + setFinalPosition(finalX(), y); + else { + setY(y); + cancelAnimation(); + } + + if (isGroup()) + FOR_EACH_CHILD (child) + child->setRecursivelyUnder(under, animate); +} + + +Note* Note::noteAt(int x, int y) +{ + if (matching() && hasResizer()) { + int right = rightLimit(); + // TODO: This code is dupliacted 3 times: !!!! + if ((x >= right) && (x < right + RESIZER_WIDTH) && (y >= m_y) && (y < m_y + resizerHeight())) { + if ( ! m_computedAreas ) + recomputeAreas(); + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.contains(x, y)) + return this; + } + } + } + + if (isGroup()) { + if ((x >= m_x) && (x < m_x + width()) && (y >= m_y) && (y < m_y + m_height)) { + if ( ! m_computedAreas ) + recomputeAreas(); + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.contains(x, y)) + return this; + } + return NULL; + } + Note *child = firstChild(); + Note *found; + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) { + found = child->noteAt(x, y); + if (found) + return found; + } + child = child->next(); + first = false; + } + } else if (matching() && y >= m_y && y < m_y + m_height && x >= m_x && x < m_x + m_width) { + if ( ! m_computedAreas ) + recomputeAreas(); + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.contains(x, y)) + return this; + } + return NULL; + } + + return NULL; +} + +QRect Note::rect() +{ + return QRect(x(), y(), width(), height()); +} + +QRect Note::resizerRect() +{ + return QRect(rightLimit(), y(), RESIZER_WIDTH, resizerHeight()); +} + + +bool Note::showSubNotes() +{ + return !m_isFolded || !m_collapseFinished || basket()->isFiltering(); +} + +void Note::relayoutAt(int x, int y, bool animate) +{ + if (!matching()) + return; + + m_computedAreas = false; + m_areas.clear(); + + // Don't relayout free notes one under the other, because by definition they are freely positionned! + if (isFree()) { + x = finalX(); + y = finalY(); + // If it's a column, it always have the same "fixed" position (no animation): + } else if (isColumn()) { + x = (prev() ? prev()->rightLimit() + RESIZER_WIDTH : 0); + y = 0; + cancelAnimation(); + setX(x); + setY(y); + // But relayout others vertically if they are inside such primary groups or if it is a "normal" basket: + } else { + if (animate) + setFinalPosition(x, y); + else { + cancelAnimation(); + setX(x); + setY(y); + } + } + + // Then, relayout sub-notes (only the first, if the group is folded) and so, assign an height to the group: + if (isGroup()) { + int 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(x + width(), y+h, animate); + h += child->finalHeight(); + } else // In case the user collapse a group, then move it and then expand it: + child->setXRecursivly(x + width()); // notes SHOULD have a good X coordonate, and not the old one! + // 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 (finalHeight() != h || m_height != h) { + unbufferize(); + if (animate) + addAnimation(0, 0, h - finalHeight()); + else { + m_height = h; + unbufferize(); + } + } + } else { + setWidth(finalRightLimit() - x); + // If rightLimit is excedded, set the top-level right limit!!! + // and NEED RELAYOUT + } + + // Set the basket area limits (but not for child notes: no need, because they will look for theire parent note): + if (!parentNote()) { + if (basket()->tmpWidth < finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0)) + basket()->tmpWidth = finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0); + if (basket()->tmpHeight < finalY() + finalHeight()) + basket()->tmpHeight = finalY() + finalHeight(); + // However, if the note exceed the allowed size, let it! : + } else if (!isGroup()) { + if (basket()->tmpWidth < finalX() + width()) + basket()->tmpWidth = finalX() + width(); + if (basket()->tmpHeight < finalY() + finalHeight()) + basket()->tmpHeight = finalY() + finalHeight(); + } +} + +void Note::setXRecursivly(int x) +{ + m_deltaX = 0; + setX(x); + + FOR_EACH_CHILD (child) + child->setXRecursivly(x + width()); +} + +void Note::setYRecursivly(int y) +{ + m_deltaY = 0; + setY(y); + + FOR_EACH_CHILD (child) + child->setYRecursivly(y); +} + +void Note::setGroupWidth(int width) +{ + m_groupWidth = width; +} + +int Note::groupWidth() +{ + if (hasResizer()) + return m_groupWidth; + else + return rightLimit() - x(); +} + +int Note::rightLimit() +{ + if (isColumn() && m_next == 0L) // The last column + return QMAX(x() + minWidth(), basket()->visibleWidth()); + else if (parentNote()) + return parentNote()->rightLimit(); + else + return m_x + m_groupWidth; +} + +int Note::finalRightLimit() +{ + if (isColumn() && m_next == 0L) // The last column + return QMAX(finalX() + minWidth(), basket()->visibleWidth()); + else if (parentNote()) + return parentNote()->finalRightLimit(); + else + return finalX() + m_groupWidth; +} + +/* + * This code is derivated from drawMetalGradient() from the Qt documentation: + */ +void drawGradient( QPainter *p, const QColor &colorTop, const QColor & colorBottom, + int x, int y, int w, int h, + bool sunken, bool horz, bool flat ) /*const*/ +{ + QColor highlight(colorBottom); + QColor subh1(colorTop); + QColor subh2(colorTop); + + QColor topgrad(colorTop); + QColor botgrad(colorBottom); + + + if ( flat && !sunken ) + p->fillRect(x, y, w, h, colorTop); + else { + int i = 0; + int x1 = x; + int y1 = y; + int x2 = x + w - 1; + int y2 = y + h - 1; + if ( horz ) + x2 = x2; + else + y2 = y2; + +#define DRAWLINE if (horz) \ + p->drawLine( x1, y1+i, x2, y1+i ); \ + else \ + p->drawLine( x1+i, y1, x1+i, y2 ); \ + i++; + + // Gradient: + int ng = (horz ? h : w); // how many lines for the gradient? + + int h1, h2, s1, s2, v1, v2; + if ( !sunken ) { + topgrad.hsv( &h1, &s1, &v1 ); + botgrad.hsv( &h2, &s2, &v2 ); + } else { + botgrad.hsv( &h1, &s1, &v1 ); + topgrad.hsv( &h2, &s2, &v2 ); + } + + if ( ng > 1 ) { + for ( int j =0; j < ng; j++ ) { + p->setPen( QColor( h1 + ((h2-h1)*j)/(ng-1), + s1 + ((s2-s1)*j)/(ng-1), + v1 + ((v2-v1)*j)/(ng-1), QColor::Hsv ) ); + DRAWLINE; + } + } else if ( ng == 1 ) { + p->setPen( QColor((h1+h2)/2, (s1+s2)/2, (v1+v2)/2, QColor::Hsv) ); + DRAWLINE; + } + } +} + +void Note::drawExpander(QPainter *painter, int x, int y, const QColor &background, bool expand, Basket *basket) +{ + // If the current style is a KStyle, use it to draw the expander (plus or minus): + if (dynamic_cast<KStyle*>(&(kapp->style())) != NULL) { + // Set the 4 rounded corners background to background color: + QColorGroup cg(basket->colorGroup()); + cg.setColor(QColorGroup::Base, background); + + // Fill the inside of the expander in white, typically: + QBrush brush(KGlobalSettings::baseColor()); + painter->fillRect(x, y, 9, 9, brush); + + // Draw it: + ((KStyle&)(kapp->style())).drawKStylePrimitive( KStyle::KPE_ListViewExpander, + painter, + basket->viewport(), + QRect(x, y, 9, 9), + cg, + (expand ? QStyle::Style_On : QStyle::Style_Off) ); + // Else, QStyle does not provide easy way to do so (if it's doable at all...) + // So, I'm drawing it myself my immitating Plastik (pretty style)... + // After all, the note/group handles are all non-QStyle aware so that doesn't matter if the expander is a custom one too. + } else { + int width = EXPANDER_WIDTH; + int height = EXPANDER_HEIGHT; + const QColorGroup &cg = basket->colorGroup(); + + // Fill white area: + painter->fillRect(x + 1, y + 1, width - 2, height - 2, cg.base()); + // Draw contour lines: + painter->setPen(cg.dark()); + painter->drawLine(x + 2, y, x + width - 3, y); + painter->drawLine(x + 2, y + height - 1, x + width - 3, y + height - 1); + painter->drawLine(x, y + 2, x, y + height - 3); + painter->drawLine(x + width - 1, y + 2, x + width - 1, y + height - 3); + // Draw edge points: + painter->drawPoint(x + 1, y + 1); + painter->drawPoint(x + width - 2, y + 1); + painter->drawPoint(x + 1, y + height - 2); + painter->drawPoint(x + width - 2, y + height - 2); + // Draw anti-aliased points: + painter->setPen(Tools::mixColor(cg.dark(), background)); + painter->drawPoint(x + 1, y); + painter->drawPoint(x + width - 2, y); + painter->drawPoint(x, y + 1); + painter->drawPoint(x + width - 1, y + 1); + painter->drawPoint(x, y + height - 2); + painter->drawPoint(x + width - 1, y + height - 2); + painter->drawPoint(x + 1, y + height - 1); + painter->drawPoint(x + width - 2, y + height - 1); + // Draw plus / minus: + painter->setPen(cg.text()); + painter->drawLine(x + 2, y + height / 2, x + width - 3, y + height / 2); + if (expand) + painter->drawLine(x + width / 2, y + 2, x + width / 2, y + height - 3); + } +} + +QColor expanderBackground(int height, int y, const QColor &foreground) +{ + // We will divide height per two, substract 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; + + QColor dark = foreground.dark(110); // 1/1.1 of brightness + QColor light = foreground.light(150); // 50% brighter + + int h1, h2, s1, s2, v1, v2; + int ng; + if (y <= (height-2)/2) { + light.hsv( &h1, &s1, &v1 ); + dark.hsv( &h2, &s2, &v2 ); + ng = (height-2)/2; + y -= 1; + } else { + dark.hsv( &h1, &s1, &v1 ); + foreground.hsv( &h2, &s2, &v2 ); + ng = (height-2)-(height-2)/2; + y -= 1 + (height-2)/2; + } + return QColor( h1 + ((h2-h1)*y)/(ng-1), + s1 + ((s2-s1)*y)/(ng-1), + v1 + ((v2-v1)*y)/(ng-1), QColor::Hsv ); +} + +void Note::drawHandle(QPainter *painter, int x, int y, int width, int height, const QColor &background, const QColor &foreground) +{ + QPen backgroundPen(background); + QPen foregroundPen(foreground); + + QColor dark = foreground.dark(110); // 1/1.1 of brightness + QColor light = foreground.light(150); // 50% brighter + + // Draw the surrounding rectangle: + painter->setPen(foregroundPen); + painter->drawLine(0, 0, width - 1, 0); + painter->drawLine(0, 0, 0, height - 1); + painter->drawLine(width - 1, 0, width - 1, height - 1); + painter->drawLine(0, height - 1, width - 1, height - 1); + + // Draw the gradients: + drawGradient( painter, light, dark, 1 + x, 1 + y, width-2, (height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + drawGradient( painter, dark, foreground, 1 + x, 1 + y + (height-2)/2, width-2, (height-2)-(height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + + // 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); + + // Anti-aliased rounded top corner (1/2): + painter->setPen(Tools::mixColor(foreground, background)); + painter->drawPoint(0, 3); + painter->drawPoint(3, 0); + // Anti-aliased rounded bottom corner: + painter->drawPoint(0, height - 4); + painter->drawPoint(3, height - 1); + // Anti-aliased rounded top corner (2/2): + painter->setPen(Tools::mixColor(foreground, light)); + painter->drawPoint(2, 2); + + // Draw the grips: + int xGrips = 4; + int marginedHeight = (height * 80 / 100); // 10% empty on top, and 10% empty on bottom, so 20% of the height should be empty of any grip, and 80% should be in the grips + int nbGrips = (marginedHeight - 3) / 6; + if (nbGrips < 2) + nbGrips = 2; + int yGrips = (height + 1 - nbGrips * 6 - 3) / 2; // +1 to avoid rounding errors, -nbGrips*6-3 the size of the grips + QColor darker = foreground.dark(130); + QColor lighter = foreground.light(130); + for (int i = 0; i < nbGrips; ++i) { + /// Dark color: + painter->setPen(darker); + // Top-left point: + painter->drawPoint(xGrips, yGrips); + painter->drawPoint(xGrips + 1, yGrips); + painter->drawPoint(xGrips, yGrips + 1); + // Bottom-right point: + painter->drawPoint(xGrips + 4, yGrips + 3); + painter->drawPoint(xGrips + 5, yGrips + 3); + painter->drawPoint(xGrips + 4, yGrips + 4); + /// Light color: + painter->setPen(lighter); + // Top-left point: + painter->drawPoint(xGrips + 1, yGrips + 1); + // Bottom-right point: + painter->drawPoint(xGrips + 5, yGrips + 4); + yGrips += 6; + } + // The remaining point: + painter->setPen(darker); + painter->drawPoint(xGrips, yGrips); + painter->drawPoint(xGrips + 1, yGrips); + painter->drawPoint(xGrips, yGrips + 1); + painter->setPen(lighter); + painter->drawPoint(xGrips + 1, yGrips + 1); +} + +void Note::drawResizer(QPainter *painter, int x, int y, int width, int height, const QColor &background, const QColor &foreground, bool rounded) +{ + QPen backgroundPen(background); + QPen foregroundPen(foreground); + + QColor dark = foreground.dark(110); // 1/1.1 of brightness + QColor light = foreground.light(150); // 50% brighter + QColor midLight = foreground.light(105); // 5% brighter + + // Draw the surrounding rectangle: + painter->setPen(foregroundPen); + painter->drawRect(0, 0, width, height); + + // Draw the gradients: + drawGradient( painter, light, dark, 1 + x, 1 + y, width-2, (height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + drawGradient( painter, dark, foreground, 1 + x, 1 + y + (height-2)/2, width-2, (height-2)-(height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + + 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); + + // Anti-aliased rounded top corner (1/2): + painter->setPen(Tools::mixColor(foreground, background)); + 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(Tools::mixColor(foreground, light)); + painter->drawPoint(width - 3, 2); + } + + // Draw the arows: + int xArrow = 2; + int hMargin = 9; + int countArrows = (height >= hMargin*4 + 6*3 ? 3 : (height >= hMargin*3 + 6*2 ? 2 : 1)); + QColor darker = foreground.dark(130); + QColor lighter = foreground.light(130); + for (int i = 0; i < countArrows; ++i) { + int 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(darker); + // 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(lighter); + // 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, int x, int y, int 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.light(120) : background.dark(105)); + if (column) { + int halfWidth = RESIZER_WIDTH / 2; + drawGradient(painter, darkBgColor, background, x, y, halfWidth, height, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + drawGradient(painter, background, darkBgColor, halfWidth, y, RESIZER_WIDTH - halfWidth, height, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + } else + drawGradient(painter, darkBgColor, background, x, y, RESIZER_WIDTH, height, /*sunken=*/false, /*horz=*/false, /*flat=*/false ); +} + + +#include <qimage.h> +#include <kimageeffect.h> + +/* 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, int x, int y, int type, int width, int height) +{ + int right; + + switch (type) { + case 1: + x += this->x(); + y += this->y(); + basket()->blendBackground(*painter, QRect(x, y, 4, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 1, 2, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 3, 1, 1), this->x(), this->y()); + break; + case 2: + x += this->x(); + y += this->y(); + basket()->blendBackground(*painter, QRect(x, y - 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 1, 2, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 2, 4, 1), this->x(), this->y()); + break; + case 3: + right = rightLimit(); + x += right; + y += this->y(); + basket()->blendBackground(*painter, QRect(x - 1, y, 4, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 1, y + 1, 2, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 2, y + 2, 1, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 2, y + 3, 1, 1), right, this->y()); + break; + case 4: + right = rightLimit(); + x += right; + y += this->y(); + basket()->blendBackground(*painter, QRect(x + 2, y - 1, 1, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 2, y, 1, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 1, y + 1, 2, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(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, QRect(x, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 1, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(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, QRect(x + HANDLE_WIDTH + 1, y + 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH, y + 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH + 1, y + height - 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH, y + height - 3, 1, 1), this->x(), this->y()); + // Inside right corners: + basket()->blendBackground(*painter, QRect(x + width - 4, y + 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 3, y + 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 4, y + height - 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 3, y + height - 3, 1, 1), this->x(), this->y()); + //} + // Outside right corners: + basket()->blendBackground(*painter, QRect(x + width - 1, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); + break; + } +} + +/// Blank Spaces Drawing: + +void Note::setOnTop(bool onTop) +{ + m_onTop = onTop; + + Note *note = firstChild(); + while (note) { + note->setOnTop(onTop); + note = note->next(); + } +} + +void substractRectOnAreas(const QRect &rectToSubstract, QValueList<QRect> &areas, bool andRemove) +{ + for (QValueList<QRect>::iterator it = areas.begin(); it != areas.end(); ) { + QRect &rect = *it; + // Split the rectangle if it intersects with rectToSubstract: + if (rect.intersects(rectToSubstract)) { + // Create the top rectangle: + if (rectToSubstract.top() > rect.top()) { + areas.insert(it, QRect(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(it, QRect(rect.left(), rectToSubstract.bottom() + 1, rect.width(), rect.bottom() - rectToSubstract.bottom())); + rect.setBottom(rectToSubstract.bottom()); + } + // Create the left rectangle: + if (rectToSubstract.left() > rect.left()) { + areas.insert(it, QRect(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(it, QRect(rectToSubstract.right() + 1, 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)) + it = areas.remove(it); + else + ++it; + } else + ++it; + } +} + +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; +} + +void Note::getGradientColors(const QColor &originalBackground, QColor *colorTop, QColor *colorBottom) +{ + bool wasTooDark = Tools::tooDark(originalBackground); + if (wasTooDark) { + *colorTop = originalBackground; + *colorBottom = originalBackground.light(120); + } else { + *colorTop = originalBackground.dark(105); + *colorBottom = originalBackground; + } +} + +/* 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 bufferized 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 QRect &clipRect) +{ + if (!matching()) + return; + + /** Paint childs: */ + if (isGroup()) { + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->draw(painter, clipRect); + child = child->next(); + first = false; + } + } + + QRect myRect(x(), y(), width(), height()); + /** Paint the resizer if needed: */ + if (hasResizer()) { + int right = rightLimit(); + QRect resizerRect(right, y(), RESIZER_WIDTH, resizerHeight()); + if (resizerRect.intersects(clipRect)) { + // Prepare to draw the resizer: + QPixmap pixmap(RESIZER_WIDTH, resizerHeight()); + QPainter painter2(&pixmap); + // Draw gradient or resizer: + if (m_hovered && m_hoveredZone == Resizer) { + QColor baseColor(basket()->backgroundColor()); + QColor highColor(KGlobalSettings::highlightColor()); + 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()); + basket()->blendBackground(painter2, resizerRect); + } + // Draw inserter: + if (basket()->inserterShown() && resizerRect.intersects(basket()->inserterRect())) + basket()->drawInserter(painter2, right, y()); + // Draw selection rect: + if (basket()->isSelecting() && resizerRect.intersects(basket()->selectionRect())) { + QRect selectionRect = basket()->selectionRect(); + selectionRect.moveBy(-right, -y()); + QRect selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); + if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { + QColor insideColor = basket()->selectionRectInsideColor(); + QColor darkInsideColor(insideColor.dark(105)); + painter2.setClipRect(selectionRectInside); + if (isColumn()) { + int halfWidth = RESIZER_WIDTH / 2; + drawGradient(&painter2, darkInsideColor, insideColor, 0, 0, halfWidth, resizerHeight(), /*sunken=*/false, /*horz=*/false, /*flat=*/false ); + drawGradient(&painter2, insideColor, darkInsideColor, halfWidth, 0, RESIZER_WIDTH-halfWidth, resizerHeight(), /*sunken=*/false, /*horz=*/false, /*flat=*/false ); + } else + drawGradient( &painter2, darkInsideColor, insideColor, 0, 0, RESIZER_WIDTH, resizerHeight(), /*sunken=*/false, /*horz=*/false, /*flat=*/false ); + painter2.setClipping(false); + selectionRectInside.moveBy(right, y()); + basket()->blendBackground(painter2, selectionRectInside, right, y(), false); + } + painter2.setPen(KGlobalSettings::highlightColor().dark()); + painter2.drawRect(selectionRect); + painter2.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), basket()->backgroundColor())); + painter2.drawPoint(selectionRect.topLeft()); + painter2.drawPoint(selectionRect.topRight()); + painter2.drawPoint(selectionRect.bottomLeft()); + painter2.drawPoint(selectionRect.bottomRight()); + } + // Draw on screen: + painter2.end(); + /** Compute visible areas: */ + if ( ! m_computedAreas ) + recomputeAreas(); + if (m_areas.isEmpty()) + return; + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + painter->drawPixmap(rect.x(), rect.y(), pixmap, rect.x() - right, rect.y() - y(), rect.width(), rect.height()); + } + } + } + + /** Then, draw the note/group ONLY if needed: */ + if ( ! myRect.intersects(clipRect) ) + 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; + } + + /** Initialise buffer painter: */ + m_bufferedPixmap.resize(width(), height()); + QPainter painter2(&m_bufferedPixmap); + + /** Initialise colors: */ + QColor baseColor(basket()->backgroundColor()); + QColor highColor(KGlobalSettings::highlightColor()); + QColor midColor = Tools::mixColor(baseColor, highColor); + + /** Initialise brushs 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); + drawRoundings(&painter2, 0, 0, /*type=*/1); + drawRoundings(&painter2, 0, height() - 3, /*type=*/2); + } else { + painter2.fillRect(0, 0, width(), height(), baseBrush); + basket()->blendBackground(painter2, myRect, -1, -1, /*opaque=*/true); + } + + // Draw expander: + int yExp = yExpander(); + drawExpander(&painter2, NOTE_MARGIN, yExp, (hovered ? expanderBackground(height(), yExp+EXPANDER_HEIGHT/2, highColor) : 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 = basket()->backgroundColor(); + if (isSelected()) + if (m_computedState.backgroundColor().isValid()) + background = Tools::mixColor(Tools::mixColor(m_computedState.backgroundColor(), KGlobalSettings::highlightColor()), KGlobalSettings::highlightColor()); + else + background = KGlobalSettings::highlightColor(); + else if (m_computedState.backgroundColor().isValid()) + background = m_computedState.backgroundColor(); + QColor bgColor; + QColor darkBgColor; + getGradientColors(background, &darkBgColor, &bgColor); + // Draw background (color, gradient and pixmap): + drawGradient( &painter2, bgColor, darkBgColor, 0, 0, width(), height(), /*sunken=*/!hovered, /*horz=*/true, /*flat=*/false ); + if (!hovered) { + painter2.setPen(Tools::mixColor(bgColor, darkBgColor)); + painter2.drawLine(0, height() - 1, width(), height() - 1); + } + basket()->blendBackground(painter2, myRect); + + if (hovered) { + // 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); + drawRoundings(&painter2, 0, 0, /*type=*/1); + drawRoundings(&painter2, 0, height() - 3, /*type=*/2); + // Round handle-right-side border: + painter2.setPen(highPen); + painter2.drawPoint(HANDLE_WIDTH, 1); + painter2.drawPoint(HANDLE_WIDTH, height() - 2); + // Light handle top-right round corner: + painter2.setPen(QPen(highColor.light(150))); + painter2.drawPoint(HANDLE_WIDTH - 1, 1); + // Handle anti-aliased rounded handle-right-side corners: + QColor insideMidColor = Tools::mixColor(bgColor, highColor); + painter2.setPen(insideMidColor); + // Left inside round corners: + painter2.drawPoint(HANDLE_WIDTH + 1, 1); + painter2.drawPoint(HANDLE_WIDTH, 2); + painter2.drawPoint(HANDLE_WIDTH + 1, height() - 2); + painter2.drawPoint(HANDLE_WIDTH, height() - 3); + // Right inside round corners: + painter2.drawPoint(width() - 4, 1); + painter2.drawPoint(width() - 3, 2); + painter2.drawPoint(width() - 4, height() - 2); + painter2.drawPoint(width() - 3, height() - 3); + // Right rounded edge: + painter2.setPen(highPen); + painter2.fillRect(width() - 2, 0, 2, height(), highBrush); + painter2.drawPoint(width() - 3, 1); + painter2.drawPoint(width() - 3, height() - 2); + // Right anti-aliased rounded edge: + painter2.setPen(midPen); + painter2.drawPoint(width() - 1, 0); + painter2.drawPoint(width() - 1, height() - 1); + // Blend background pixmap: + drawRoundings(&painter2, 0, 0, /*type=*/6, width(), height()); + } + + if (isFocused()) { + QRect focusRect(HANDLE_WIDTH, NOTE_MARGIN - 1, width() - HANDLE_WIDTH - 2, height() - 2*NOTE_MARGIN + 2); + painter2.drawWinFocusRect(focusRect); + } + + // Draw the Emblems: + int yIcon = (height() - EMBLEM_SIZE) / 2; + int xIcon = HANDLE_WIDTH + NOTE_MARGIN; + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { + if (!(*it)->emblem().isEmpty()) { + QPixmap stateEmblem = kapp->iconLoader()->loadIcon((*it)->emblem(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, 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): + QColorGroup cg(basket()->colorGroup()); + cg.setColor(QColorGroup::Text, (m_computedState.textColor().isValid() ? m_computedState.textColor() : basket()->textColor()) ); + cg.setColor(QColorGroup::Background, bgColor); + if (isSelected()) + cg.setColor(QColorGroup::Text, KGlobalSettings::highlightedTextColor()); + + // Draw the Tags Arrow: + if (hovered) { + QColor textColor = cg.color(QColorGroup::Text); + QColor light = Tools::mixColor(textColor, bgColor); + QColor mid = Tools::mixColor(textColor, light); + painter2.setPen(light);//QPen(basket()->colorGroup().dark().light(150))); + painter2.drawLine(xIcon, yIcon + 6, xIcon + 4, yIcon + 6); + painter2.setPen(mid);//QPen(basket()->colorGroup().dark())); + 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(cg.color(QColorGroup::Text)/*QPen(basket()->colorGroup().foreground())*/); + painter2.drawPoint(xIcon, yIcon + 7); + painter2.drawPoint(xIcon + 2, yIcon + 7); + painter2.drawPoint(xIcon + 4, yIcon + 7); + } + + // Draw content: + // Optimization: do not draw text notes because it is time consuming and should be done nearly at each tetx modification. + if (basket()->editedNote() != this || basket()->editedNote()->content()->type() != NoteType::Html) { + painter2.translate(contentX(), NOTE_MARGIN); + painter2.setFont( m_computedState.font(painter2.font()) ); + m_content->paint(&painter2, width() - contentX() - NOTE_MARGIN, height() - 2*NOTE_MARGIN, cg, !m_computedState.textColor().isValid(), isSelected(), hovered); + } + + // Draw on screen: + painter2.end(); + drawBufferOnScreen(painter, m_bufferedPixmap); +} + +void Note::drawBufferOnScreen(QPainter *painter, const QPixmap &contentPixmap) +{ + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.x() >= x() + width()) // It's a rect of the resizer, don't draw it! + continue; + // If the inserter is above the note, draw it, BUT NOT in the buffer pixmap, + // we copy the rectangle in a new pixmap, apply the inserter and then draw this new pixmap on screen: + if ( (basket()->inserterShown() && rect.intersects(basket()->inserterRect())) || + (basket()->isSelecting() && rect.intersects(basket()->selectionRect())) ) { + QPixmap pixmap3(rect.width(), rect.height()); + QPainter painter3(&pixmap3); + painter3.drawPixmap(0, 0, contentPixmap, rect.x() - x(), rect.y() - y(), rect.width(), rect.height()); + // Draw inserter: + if (basket()->inserterShown() && rect.intersects(basket()->inserterRect())) + basket()->drawInserter(painter3, rect.x(), rect.y()); + // Draw selection rect: + if (basket()->isSelecting() && rect.intersects(basket()->selectionRect())) { + QRect selectionRect = basket()->selectionRect(); + selectionRect.moveBy(-rect.x(), -rect.y()); + + QRect selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); + if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { + bufferizeSelectionPixmap(); + selectionRectInside.moveBy(rect.x(), rect.y()); + QRect rectToPaint = rect.intersect(selectionRectInside); + rectToPaint.moveBy(-x(), -y()); + painter3.drawPixmap(rectToPaint.topLeft() + QPoint(x(), y()) - rect.topLeft(), m_bufferedSelectionPixmap, rectToPaint); + //blendBackground(painter2, selectionRectInside, rect.x(), rect.y(), true, &m_selectedBackgroundPixmap); + } + + painter3.setPen(KGlobalSettings::highlightColor().dark()); + painter3.drawRect(selectionRect); + if (isGroup()) + painter3.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), basket()->backgroundColor())); + else { + // What are the background colors: + QColor bgColor = basket()->backgroundColor(); + if (isSelected()) + bgColor = (m_computedState.backgroundColor().isValid() ? Tools::mixColor(Tools::mixColor(m_computedState.backgroundColor(), KGlobalSettings::highlightColor()), KGlobalSettings::highlightColor()) : KGlobalSettings::highlightColor()); + else if (m_computedState.backgroundColor().isValid()) + bgColor = m_computedState.backgroundColor(); + painter3.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), bgColor)); + } + painter3.drawPoint(selectionRect.topLeft()); + painter3.drawPoint(selectionRect.topRight()); + painter3.drawPoint(selectionRect.bottomLeft()); + painter3.drawPoint(selectionRect.bottomRight()); + } + painter3.end(); + painter->drawPixmap(rect.x(), rect.y(), pixmap3); + // Else, draw the rect pixmap directly on screen: + } else + painter->drawPixmap(rect.x(), rect.y(), contentPixmap, rect.x() - x(), rect.y() - 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.remove(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()->QScrollView::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()) + 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 QValueList<State*> &deletedStates) +{ + bool modifiedBasket = false; + + if (!states().isEmpty()) { + for (QValueList<State*>::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.remove(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.remove(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: Basket::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 0; +} + +State* Note::stateForEmblemNumber(int number) +{ + int i = -1; + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) + if (!(*it)->emblem().isEmpty()) { + ++i; + if (i == number) + return *it; + } + return 0; +} + +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 == 0) + *state = 0; + 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 == 0) + *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 == 0) + 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(); + } + } +} + +void Note::bufferizeSelectionPixmap() +{ + if (m_bufferedSelectionPixmap.isNull()) { + QColor insideColor = KGlobalSettings::highlightColor(); + KPixmap kpixmap(m_bufferedPixmap); + m_bufferedSelectionPixmap = KPixmapEffect::fade(kpixmap, 0.25, insideColor); + } +} + +QRect Note::visibleRect() +{ + QValueList<QRect> areas; + areas.append(rect()); + + // 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[0]; + else + return QRect(); +} + +void Note::recomputeBlankRects(QValueList<QRect> &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 0; +} + +void Note::listUsedTags(QValueList<Tag*> &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(QValueList<State*> &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 childs: + 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 0; +} + +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 0; +} + +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 complexe 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() +{ + std::cout << "Note@" << (Q_UINT64)this; + if (!this) { + std::cout << std::endl; + return; + } + + if (isColumn()) + std::cout << ": Column"; + else if (isGroup()) + std::cout << ": Group"; + else + std::cout << ": Content[" << content()->lowerTypeName() << "]: " << toText(""); + std::cout << std::endl; +} + +Note* Note::firstSelected() +{ + if (isSelected()) + return this; + + Note *first = 0; + FOR_EACH_CHILD (child) { + first = child->firstSelected(); + if (first) + break; + } + return first; +} + +Note* Note::lastSelected() +{ + if (isSelected()) + return this; + + Note *last = 0, *tmp = 0; + 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 0; +} + +void Note::groupIn(Note *group) +{ + if (this == group) + return; + + if (allSelected() && !isColumn()) { + basket()->unplugNote(this); + basket()->insertNote(this, group, Note::BottomColumn, QPoint(), /*animateNewPosition=*/true); + } 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(true); + basket()->relayoutNotes(true); + 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(true); + basket()->relayoutNotes(true); + return true; + } + child = parent; + parent = parent->parentNote(); + } + return false; +} + + +int Note::distanceOnLeftRight(Note *note, int side) +{ + if (side == Basket::RIGHT_SIDE) { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalX() > note->finalX() || 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 (finalX() < note->finalX() || finalRightLimit() < note->finalRightLimit()) + return -1; + } + if (finalX() == note->finalX() && finalRightLimit() == note->finalRightLimit()) + return -1; + + float thisCenterX = finalX() + (side == Basket::LEFT_SIDE ? width() : /*RIGHT_SIDE:*/ 0); + float thisCenterY = finalY() + finalHeight() / 2; + float noteCenterX = note->finalX() + note->width() / 2; + float noteCenterY = note->finalY() + note->finalHeight() / 2; + + if (thisCenterY > note->finalBottom()) + noteCenterY = note->finalBottom(); + else if (thisCenterY < note->finalY()) + noteCenterY = note->finalY(); + else + noteCenterY = thisCenterY; + + float angle = 0; + if (noteCenterX - thisCenterX != 0) + angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); + if (angle < 0) + angle = -angle; + + return int(sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle); +} + +int Note::distanceOnTopBottom(Note *note, int side) +{ + if (side == Basket::BOTTOM_SIDE) { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalY() > note->finalY() || finalBottom() > note->finalBottom()) + return -1; + } else /*TOP_SIDE:*/ { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalY() < note->finalY() || finalBottom() < note->finalBottom()) + return -1; + } + if (finalY() == note->finalY() && finalBottom() == note->finalBottom()) + return -1; + + float thisCenterX = finalX() + width() / 2; + float thisCenterY = finalY() + (side == Basket::TOP_SIDE ? finalHeight() : /*BOTTOM_SIDE:*/ 0); + float noteCenterX = note->finalX() + note->width() / 2; + float noteCenterY = note->finalY() + note->finalHeight() / 2; + + if (thisCenterX > note->finalRightLimit()) + noteCenterX = note->finalRightLimit(); + else if (thisCenterX < note->finalX()) + noteCenterX = note->finalX(); + else + noteCenterX = thisCenterX; + + float angle = 0; + if (noteCenterX - thisCenterX != 0) + angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); + if (angle < 0) + angle = -angle; + + return int(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() +{ + if(content()) + { + if(!content()->saveToFile()) + return false; + } + FOR_EACH_CHILD (child) + { + if(!child->saveAgain()) + return false; + } + return true; +} + +bool Note::convertTexts() +{ + bool convertedNotes = false; + + if (content() && content()->lowerTypeName() == "text") { + QString text = ((TextContent*)content())->text(); + QString html = "<html><head><meta name=\"qrichtext\" content=\"1\" /></head><body>" + Tools::textToHTMLWithoutP(text) + "</body></html>"; + basket()->saveToFile(fullPath(), html, /*isLocalEncoding=*/true); + setContent( new HtmlContent(this, content()->fileName()) ); + convertedNotes = true; + } + + FOR_EACH_CHILD (child) + if (child->convertTexts()) + convertedNotes = true; + + return convertedNotes; +} + +#if 0 + +/** Note */ + +QString Note::toHtml(const QString &imageName) +{ + switch (m_type) { + case Text: + /*return "<font color=" + backgroundColor().name() + + font().name() + font + ">" + + Tools::textToHTMLWithoutP(text()) + "</font>";*/ + return Tools::textToHTMLWithoutP(text()); + case Html: + return Tools::htmlToParagraph(html()); + case Image: + case Animation: + { + if ( (m_type == Image && pixmap() == 0L) || + (m_type == Animation && movie() == 0L) ) { + QMimeSourceFactory::defaultFactory()->setData(imageName, 0L); + return i18n("(Image)"); // Image or animation not yet loaded!! + } + + QImage image; + if (m_type == Image) + image = pixmap()->convertToImage(); + else + image = movie()->framePixmap().convertToImage(); + image = image.smoothScale(200, 150, QImage::ScaleMin); + QPixmap pixmap = QPixmap(image); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, pixmap); + return "<img src=" + imageName + ">"; /// + +/* // FIXME: movie isn't loaded yet: CRASH! + return i18n("(Image)"); + // Not executed, because don't work: + QImage image; + if (m_type == Image) + image = pixmap()->convertToImage(); + else + image = movie()->framePixmap().convertToImage(); + image = image.smoothScale(200, 150, QImage::ScaleMin); + QPixmap pixmap = QPixmap(image); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, pixmap); + return "<img src=" + imageName + ">"; /// + */ //TODO?: QMimeSourceFactory::defaultFactory()->setData(imageName, 0L); + } + case Sound: + case File: + { + /// FIXME: Since fullPath() doesn't exist yet, the icon rely on the extension. + /// Bad if there isn't one or if it's a wrong one. + /*QPixmap icon = DesktopIcon( + NoteFactory::iconForURL(fullPath()), + (m_type == Sound ? LinkLook::soundLook : LinkLook::fileLook)->iconSize()); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, icon); + return "<img src=" + imageName + "> " + fileName(); */ /// + return m_linkLabel->toHtml(imageName); + } + case Link: + { + QString link = m_linkLabel->toHtml(imageName); + if (!autoTitle() && title() != NoteFactory::titleForURL(url().prettyURL())) + link += "<br><i>" + url().prettyURL() + "</i>"; /// + return link; + } + case Launcher: + { + return m_linkLabel->toHtml(imageName); + //KService service(fullPath()); // service.icon() + //return service.name() + "<br><i>" + service.exec() + "</i>"; /// + } + case Color: + return "<b><font color=" + color().name() + ">" + color().name() + "</font></b>"; + case Unknown: + return text(); + } + return QString(); +} + +#endif // #if 0 |