diff options
Diffstat (limited to 'src/basket.cpp')
-rw-r--r-- | src/basket.cpp | 5734 |
1 files changed, 5734 insertions, 0 deletions
diff --git a/src/basket.cpp b/src/basket.cpp new file mode 100644 index 0000000..95a3614 --- /dev/null +++ b/src/basket.cpp @@ -0,0 +1,5734 @@ +/*************************************************************************** + * 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 <qdragobject.h> +#include <qdom.h> +#include <qpainter.h> +#include <qstyle.h> +#include <kstyle.h> +#include <qtooltip.h> +#include <qlistview.h> +#include <qcursor.h> +#include <qsimplerichtext.h> +#include <qpushbutton.h> +#include <ktextedit.h> +#include <qpoint.h> +#include <qstringlist.h> +#include <kapplication.h> +#include <kglobalsettings.h> +#include <kopenwith.h> +#include <kservice.h> +#include <klocale.h> +#include <kglobalaccel.h> +#include <qdir.h> +#include <qfile.h> +#include <qfileinfo.h> +#include <kfiledialog.h> +#include <kaboutdata.h> +#include <klineedit.h> +#include <ksavefile.h> +#include <kdebug.h> +#include <qvbox.h> + +#include <unistd.h> // For sleep() + +#include <kpopupmenu.h> +#include <kiconloader.h> +#include <krun.h> + +#include <qtoolbar.h> +#include <qclipboard.h> + +#include <kmessagebox.h> +#include <qinputdialog.h> + +#include <qlayout.h> + +#include <stdlib.h> // rand() function +#include <qdatetime.h> // seed for rand() + +#include "basket.h" +#include "note.h" +#include "notedrag.h" +#include "notefactory.h" +#include "noteedit.h" +#include "tagsedit.h" +#include "xmlwork.h" +#include "global.h" +#include "backgroundmanager.h" +#include "settings.h" +#include "tools.h" +#include "debugwindow.h" +#include "exporterdialog.h" +#include "config.h" +#include "popupmenu.h" +#ifdef HAVE_LIBGPGME +#include "kgpgme.h" +#endif + +#include <iostream> + +/** Class NoteSelection: */ + +NoteSelection* NoteSelection::nextStacked() +{ + // First, search in the childs: + if (firstChild) + if (firstChild->note && firstChild->note->content()) + return firstChild; + else + return firstChild->nextStacked(); + + // Then, in the next: + if (next) + if (next->note && next->note->content()) + return next; + else + return next->nextStacked(); + + // And finally, in the parent: + NoteSelection *node = parent; + while (node) + if (node->next) + if (node->next->note && node->next->note->content()) + return node->next; + else + return node->next->nextStacked(); + else + node = node->parent; + + // Not found: + return 0; +} + +NoteSelection* NoteSelection::firstStacked() +{ + if (!this) + return 0; + + if (note && note->content()) + return this; + else + return nextStacked(); +} + +void NoteSelection::append(NoteSelection *node) +{ + if (!this || !node) + return; + + if (firstChild) { + NoteSelection *last = firstChild; + while (last->next) + last = last->next; + last->next = node; + } else + firstChild = node; + + while (node) { + node->parent = this; + node = node->next; + } +} + +int NoteSelection::count() +{ + if (!this) + return 0; + + int count = 0; + + for (NoteSelection *node = this; node; node = node->next) + if (node->note && node->note->content()) + ++count; + else + count += node->firstChild->count(); + + return count; +} + +QValueList<Note*> NoteSelection::parentGroups() +{ + QValueList<Note*> groups; + + // For each note: + for (NoteSelection *node = firstStacked(); node; node = node->nextStacked()) + // For each parent groups of the note: + for (Note *note = node->note->parentNote(); note; note = note->parentNote()) + // Add it (if it was not already in the list): + if (!note->isColumn() && !groups.contains(note)) + groups.append(note); + + return groups; +} + +/** Class DecoratedBasket: */ + +DecoratedBasket::DecoratedBasket(QWidget *parent, const QString &folderName, const char *name, WFlags fl) + : QWidget(parent, name, fl) +{ + m_layout = new QVBoxLayout(this); + m_filter = new FilterBar(this); + m_basket = new Basket(this, folderName); + m_layout->addWidget(m_basket); + setFilterBarPosition(Settings::filterOnTop()); + + m_filter->setShown(true); + m_basket->setFocus(); // To avoid the filter bar have focus on load + + connect( m_filter, SIGNAL(newFilter(const FilterData&)), m_basket, SLOT(newFilter(const FilterData&)) ); + connect( m_filter, SIGNAL(escapePressed()), m_basket, SLOT(cancelFilter()) ); + connect( m_filter, SIGNAL(returnPressed()), m_basket, SLOT(validateFilter()) ); + + connect( m_basket, SIGNAL(postMessage(const QString&)), Global::bnpView, SLOT(postStatusbarMessage(const QString&)) ); + connect( m_basket, SIGNAL(setStatusBarText(const QString&)), Global::bnpView, SLOT(setStatusBarHint(const QString&)) ); + connect( m_basket, SIGNAL(resetStatusBarText()), Global::bnpView, SLOT(updateStatusBarHint()) ); +} + +DecoratedBasket::~DecoratedBasket() +{ +} + +void DecoratedBasket::setFilterBarPosition(bool onTop) +{ + m_layout->remove(m_filter); + if (onTop) { + m_layout->insertWidget(0, m_filter); + setTabOrder(this/*(QWidget*)parent()*/, m_filter); + setTabOrder(m_filter, m_basket); + setTabOrder(m_basket, (QWidget*)parent()); + } else { + m_layout->addWidget(m_filter); + setTabOrder(this/*(QWidget*)parent()*/, m_basket); + setTabOrder(m_basket, m_filter); + setTabOrder(m_filter, (QWidget*)parent()); + } +} + +void DecoratedBasket::setFilterBarShown(bool show, bool switchFocus) +{ +// m_basket->setShowFilterBar(true);//show); +// m_basket->save(); + // In this order (m_basket and then m_filter) because setShown(false) + // will call resetFilter() that will update actions, and then check the + // Ctrl+F action whereas it should be unchecked + // FIXME: It's very uggly all those things + m_filter->setShown(true);//show); + if (show) { + if (switchFocus) + m_filter->setEditFocus(); + } else if (m_filter->hasEditFocus()) + m_basket->setFocus(); +} + +void DecoratedBasket::resetFilter() +{ + m_filter->reset(); +} + +/** Class TransparentWidget */ + +TransparentWidget::TransparentWidget(Basket *basket) + : QWidget(basket->viewport(), "", Qt::WNoAutoErase), m_basket(basket) +{ + setFocusPolicy(QWidget::NoFocus); + setWFlags(Qt::WNoAutoErase); + setMouseTracking(true); // To receive mouseMoveEvents + + basket->viewport()->installEventFilter(this); +} + +/*void TransparentWidget::reparent(QWidget *parent, WFlags f, const QPoint &p, bool showIt) +{ + QWidget::reparent(parent, Qt::WNoAutoErase, p, showIt); +}*/ + +void TransparentWidget::setPosition(int x, int y) +{ + m_x = x; + m_y = y; +} + +void TransparentWidget::paintEvent(QPaintEvent*event) +{ + QWidget::paintEvent(event); + QPainter painter(this); + +// painter.save(); + + painter.translate(-m_x, -m_y); + m_basket->drawContents(&painter, m_x, m_y, width(), height()); + +// painter.restore(); +// painter.setPen(Qt::blue); +// painter.drawRect(0, 0, width(), height()); +} + +void TransparentWidget::mouseMoveEvent(QMouseEvent *event) +{ + QMouseEvent *translated = new QMouseEvent(QEvent::MouseMove, event->pos() + QPoint(m_x, m_y), event->button(), event->state()); + m_basket->contentsMouseMoveEvent(translated); + delete translated; +} + +bool TransparentWidget::eventFilter(QObject */*object*/, QEvent *event) +{ + // If the parent basket viewport has changed, we should change too: + if (event->type() == QEvent::Paint) + update(); + + return false; // Event not consumed, in every cases (because it's only a notification)! +} + +/** Class Basket: */ + +const int Basket::FRAME_DELAY = 50/*1500*/; // Delay between two animation "frames" in milliseconds + +/* + * Convenient function (defined in note.cpp !): + */ +void drawGradient( QPainter *p, const QColor &colorTop, const QColor & colorBottom, + int x, int y, int w, int h, + bool sunken, bool horz, bool flat ); + +/* + * Defined in note.cpp: + */ +extern void substractRectOnAreas(const QRect &rectToSubstract, QValueList<QRect> &areas, bool andRemove = true); + +void debugZone(int zone) +{ + QString s; + switch (zone) { + case Note::Handle: s = "Handle"; break; + case Note::Group: s = "Group"; break; + case Note::TagsArrow: s = "TagsArrow"; break; + case Note::Custom0: s = "Custom0"; break; + case Note::GroupExpander: s = "GroupExpander"; break; + case Note::Content: s = "Content"; break; + case Note::Link: s = "Link"; break; + case Note::TopInsert: s = "TopInsert"; break; + case Note::TopGroup: s = "TopGroup"; break; + case Note::BottomInsert: s = "BottomInsert"; break; + case Note::BottomGroup: s = "BottomGroup"; break; + case Note::BottomColumn: s = "BottomColumn"; break; + case Note::None: s = "None"; break; + default: + if (zone == Note::Emblem0) + s = "Emblem0"; + else + s = "Emblem0+" + QString::number(zone - Note::Emblem0); + break; + } + std::cout << s << std::endl; +} + +#define FOR_EACH_NOTE(noteVar) \ + for (Note *noteVar = firstNote(); noteVar; noteVar = noteVar->next()) + +void Basket::prependNoteIn(Note *note, Note *in) +{ + if (!note) + // No note to prepend: + return; + + if (in) { + // The normal case: + preparePlug(note); + + Note *last = note->lastSibling(); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(in); +// note->setPrev(0L); + last->setNext(in->firstChild()); + + if (in->firstChild()) + in->firstChild()->setPrev(last); + + in->setFirstChild(note); + + if (m_loaded) + signalCountsChanged(); + } else + // Prepend it directly in the basket: + appendNoteBefore(note, firstNote()); +} + +void Basket::appendNoteIn(Note *note, Note *in) +{ + if (!note) + // No note to append: + return; + + if (in) { + // The normal case: + preparePlug(note); + +// Note *last = note->lastSibling(); + Note *lastChild = in->lastChild(); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(in); + note->setPrev(lastChild); +// last->setNext(0L); + + if (!in->firstChild()) + in->setFirstChild(note); + + if (lastChild) + lastChild->setNext(note); + + if (m_loaded) + signalCountsChanged(); + } else + // Prepend it directly in the basket: + appendNoteAfter(note, lastNote()); +} + +void Basket::appendNoteAfter(Note *note, Note *after) +{ + if (!note) + // No note to append: + return; + + if (!after) + // By default, insert after the last note: + after = lastNote(); + + if (m_loaded && after && !after->isFree() && !after->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(after); + +// if (!alreadyInBasket) + preparePlug(note); + + Note *last = note->lastSibling(); + if (after) { + // The normal case: + for (Note *n = note; n; n = n->next()) + n->setParentNote(after->parentNote()); + note->setPrev(after); + last->setNext(after->next()); + after->setNext(note); + if (last->next()) + last->next()->setPrev(last); + } else { + // There is no note in the basket: + for (Note *n = note; n; n = n->next()) + n->setParentNote(0); + m_firstNote = note; +// note->setPrev(0); +// last->setNext(0); + } + +// if (!alreadyInBasket) + if (m_loaded) + signalCountsChanged(); +} + +void Basket::appendNoteBefore(Note *note, Note *before) +{ + if (!note) + // No note to append: + return; + + if (!before) + // By default, insert before the first note: + before = firstNote(); + + if (m_loaded && before && !before->isFree() && !before->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(before); + + preparePlug(note); + + Note *last = note->lastSibling(); + if (before) { + // The normal case: + for (Note *n = note; n; n = n->next()) + n->setParentNote(before->parentNote()); + note->setPrev(before->prev()); + last->setNext(before); + before->setPrev(last); + if (note->prev()) + note->prev()->setNext(note); + else { + if (note->parentNote()) + note->parentNote()->setFirstChild(note); + else + m_firstNote = note; + } + } else { + // There is no note in the basket: + for (Note *n = note; n; n = n->next()) + n->setParentNote(0); + m_firstNote = note; +// note->setPrev(0); +// last->setNext(0); + } + + if (m_loaded) + signalCountsChanged(); +} + +DecoratedBasket* Basket::decoration() +{ + return (DecoratedBasket*)parent(); +} + +void Basket::preparePlug(Note *note) +{ + // Select only the new notes, compute the new notes count and the new number of found notes: + if (m_loaded) + unselectAll(); + int count = 0; + int founds = 0; + Note *last = 0; + for (Note *n = note; n; n = n->next()) { + if (m_loaded) + n->setSelectedRecursivly(true); // Notes should have a parent basket (and they have, so that's OK). + count += n->count(); + founds += n->newFilter(decoration()->filterData()); + last = n; + } + m_count += count; + m_countFounds += founds; + + // Focus the last inserted note: + if (m_loaded && last) { + setFocusedNote(last); + m_startOfShiftSelectionNote = (last->isGroup() ? last->lastRealChild() : last); + } + + // If some notes don't match (are hidden), tell it to the user: + if (m_loaded && founds < count) { + if (count == 1) postMessage( i18n("The new note does not match the filter and is hidden.") ); + else if (founds == count - 1) postMessage( i18n("A new note does not match the filter and is hidden.") ); + else if (founds > 0) postMessage( i18n("Some new notes do not match the filter and are hidden.") ); + else postMessage( i18n("The new notes do not match the filter and are hidden.") ); + } +} + +void Basket::unplugNote(Note *note) +{ + // If there is nothing to do... + if (!note) + return; + +// if (!willBeReplugged) { + note->setSelectedRecursivly(false); // To removeSelectedNote() and decrease the selectedsCount. + m_count -= note->count(); + m_countFounds -= note->newFilter(decoration()->filterData()); + signalCountsChanged(); +// } + + // If it was the first note, change the first note: + if (m_firstNote == note) + m_firstNote = note->next(); + + // Change previous and next notes: + if (note->prev()) + note->prev()->setNext(note->next()); + if (note->next()) + note->next()->setPrev(note->prev()); + + if (note->parentNote()) { + // If it was the first note of a group, change the first note of the group: + if (note->parentNote()->firstChild() == note) + note->parentNote()->setFirstChild( note->next() ); + + if (!note->parentNote()->isColumn()) { + // Ungroup if still 0 note inside parent group: + if ( ! note->parentNote()->firstChild() ) + unplugNote(note->parentNote()); // TODO delete + + // Ungroup if still 1 note inside parent group: + else if ( ! note->parentNote()->firstChild()->next() ) + ungroupNote(note->parentNote()); + } + } + + note->setParentNote(0); + note->setPrev(0); + note->setNext(0); + +// recomputeBlankRects(); // FIXME: called too much time. It's here because when dragging and moving a note to another basket and then go back to the original basket, the note is deleted but the note rect is not painter anymore. +} + +void Basket::ungroupNote(Note *group) +{ + Note *note = group->firstChild(); + Note *lastGroupedNote = group; + Note *nextNote; + + // Move all notes after the group (not before, to avoid to change m_firstNote or group->m_firstChild): + while (note) { + nextNote = note->next(); + + if (lastGroupedNote->next()) + lastGroupedNote->next()->setPrev(note); + note->setNext(lastGroupedNote->next()); + lastGroupedNote->setNext(note); + note->setParentNote(group->parentNote()); + note->setPrev(lastGroupedNote); + + note->setGroupWidth(group->groupWidth() - Note::GROUP_WIDTH); + lastGroupedNote = note; + note = nextNote; + } + + // Unplug the group: + group->setFirstChild(0); + unplugNote(group); // TODO: delete + + relayoutNotes(true); +} + +void Basket::groupNoteBefore(Note *note, Note *with) +{ + if (!note || !with) + // No note to group or nowhere to group it: + return; + +// if (m_loaded && before && !with->isFree() && !with->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(with); + + preparePlug(note); + + Note *last = note->lastSibling(); + + Note *group = new Note(this); + group->setPrev(with->prev()); + group->setNext(with->next()); + group->setX(with->x()); + group->setY(with->y()); + if (with->parentNote() && with->parentNote()->firstChild() == with) + with->parentNote()->setFirstChild(group); + else if (m_firstNote == with) + m_firstNote = group; + group->setParentNote(with->parentNote()); + group->setFirstChild(note); + group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); + + if (with->prev()) + with->prev()->setNext(group); + if (with->next()) + with->next()->setPrev(group); + with->setParentNote(group); + with->setPrev(last); + with->setNext(0L); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(group); +// note->setPrev(0L); + last->setNext(with); + + if (m_loaded) + signalCountsChanged(); +} + +void Basket::groupNoteAfter(Note *note, Note *with) +{ + if (!note || !with) + // No note to group or nowhere to group it: + return; + +// if (m_loaded && before && !with->isFree() && !with->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(with); + + preparePlug(note); + +// Note *last = note->lastSibling(); + + Note *group = new Note(this); + group->setPrev(with->prev()); + group->setNext(with->next()); + group->setX(with->x()); + group->setY(with->y()); + if (with->parentNote() && with->parentNote()->firstChild() == with) + with->parentNote()->setFirstChild(group); + else if (m_firstNote == with) + m_firstNote = group; + group->setParentNote(with->parentNote()); + group->setFirstChild(with); + group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); + + if (with->prev()) + with->prev()->setNext(group); + if (with->next()) + with->next()->setPrev(group); + with->setParentNote(group); + with->setPrev(0L); + with->setNext(note); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(group); + note->setPrev(with); +// last->setNext(0L); + + if (m_loaded) + signalCountsChanged(); +} + +void Basket::loadNotes(const QDomElement ¬es, Note *parent) +{ + Note *note; + for (QDomNode n = notes.firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + if (e.isNull()) // Cannot handle that! + continue; + note = 0; + // Load a Group: + if (e.tagName() == "group") { + note = new Note(this); // 1. Create the group... + loadNotes(e, note); // 3. ... And populate it with child notes. + int noteCount = note->count(); + if (noteCount > 0 || (parent == 0 && !isFreeLayout())) { // But don't remove columns! + appendNoteIn(note, parent); // 2. ... Insert it... FIXME: Initially, the if() the insrtion was the step 2. Was it on purpose? + // The notes in the group are counted two times (it's why appendNoteIn() was called before loadNotes): + m_count -= noteCount;// TODO: Recompute note count every time noteCount() is emitted! + m_countFounds -= noteCount; + } + } + // Load a Content-Based Note: + if (e.tagName() == "note" || e.tagName() == "item") { // Keep compatible with 0.6.0 Alpha 1 + note = new Note(this); // Create the note... + NoteFactory__loadNode(XMLWork::getElement(e, "content"), e.attribute("type"), note, /*lazyLoad=*/m_finishLoadOnFirstShow); // ... Populate it with content... + if (e.attribute("type") == "text") + m_shouldConvertPlainTextNotes = true; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! + appendNoteIn(note, parent); // ... And insert it. + // 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)); + } + // If we successfully loaded a note: + if (note) { + // 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(false); + // Tags: + if (note->content()) { + QString tagsString = XMLWork::getElementText(e, "tags", ""); + QStringList tagsId = QStringList::split(";", tagsString); + for (QStringList::iterator it = tagsId.begin(); it != tagsId.end(); ++it) { + State *state = Tag::stateForId(*it); + if (state) + note->addState(state, /*orReplace=*/true); + } + } + } +// kapp->processEvents(); + } +} + +void Basket::saveNotes(QDomDocument &document, QDomElement &element, Note *parent) +{ + Note *note = (parent ? parent->firstChild() : firstNote()); + while (note) { + // Create Element: + QDomElement noteElement = document.createElement(note->isGroup() ? "group" : "note"); + element.appendChild(noteElement); + // Free Note Properties: + if (note->isFree()) { + noteElement.setAttribute("x", note->finalX()); + noteElement.setAttribute("y", note->finalY()); + } + // Resizeable Note Properties: + if (note->hasResizer()) + noteElement.setAttribute("width", note->groupWidth()); + // Group Properties: + if (note->isGroup() && !note->isColumn()) + noteElement.setAttribute("folded", XMLWork::trueOrFalse(note->isFolded())); + // Save Content: + if (note->content()) { + // Save Dates: + noteElement.setAttribute("added", note->addedDate().toString(Qt::ISODate) ); + noteElement.setAttribute("lastModification", note->lastModificationDate().toString(Qt::ISODate)); + // Save Content: + noteElement.setAttribute("type", note->content()->lowerTypeName()); + QDomElement content = document.createElement("content"); + noteElement.appendChild(content); + note->content()->saveToNode(document, content); + // Save Tags: + if (note->states().count() > 0) { + QString tags; + for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) + tags += (tags.isEmpty() ? "" : ";") + (*it)->id(); + XMLWork::addElement(document, noteElement, "tags", tags); + } + } else + // Save Child Notes: + saveNotes(document, noteElement, note); + // Go to the Next One: + note = note->next(); + } +} + +void Basket::loadProperties(const QDomElement &properties) +{ + // Compute Default Values for When Loading the Properties: + QString defaultBackgroundColor = (backgroundColorSetting().isValid() ? backgroundColorSetting().name() : ""); + QString defaultTextColor = (textColorSetting().isValid() ? textColorSetting().name() : ""); + + // Load the Properties: + QString icon = XMLWork::getElementText(properties, "icon", this->icon() ); + QString name = XMLWork::getElementText(properties, "name", basketName() ); + + QDomElement appearance = XMLWork::getElement(properties, "appearance"); + // In 0.6.0-Alpha versions, there was a typo error: "backround" instead of "background" + QString backgroundImage = appearance.attribute( "backgroundImage", appearance.attribute( "backroundImage", backgroundImageName() ) ); + QString backgroundColorString = appearance.attribute( "backgroundColor", appearance.attribute( "backroundColor", defaultBackgroundColor ) ); + QString textColorString = appearance.attribute( "textColor", defaultTextColor ); + QColor backgroundColor = (backgroundColorString.isEmpty() ? QColor() : QColor(backgroundColorString)); + QColor textColor = (textColorString.isEmpty() ? QColor() : QColor(textColorString) ); + + QDomElement disposition = XMLWork::getElement(properties, "disposition"); + bool free = XMLWork::trueOrFalse( disposition.attribute( "free", XMLWork::trueOrFalse(isFreeLayout()) ) ); + int columnCount = disposition.attribute( "columnCount", QString::number(this->columnsCount()) ).toInt(); + bool mindMap = XMLWork::trueOrFalse( disposition.attribute( "mindMap", XMLWork::trueOrFalse(isMindMap()) ) ); + + QDomElement shortcut = XMLWork::getElement(properties, "shortcut"); + QString actionStrings[] = { "show", "globalShow", "globalSwitch" }; + KShortcut combination = KShortcut( shortcut.attribute( "combination", m_action->shortcut().toStringInternal() ) ); + QString actionString = shortcut.attribute( "action" ); + int action = shortcutAction(); + if (actionString == actionStrings[0]) action = 0; + if (actionString == actionStrings[1]) action = 1; + if (actionString == actionStrings[2]) action = 2; + + QDomElement protection = XMLWork::getElement(properties, "protection"); + m_encryptionType = protection.attribute( "type" ).toInt(); + m_encryptionKey = protection.attribute( "key" ); + + // Apply the Properties: + setDisposition((free ? (mindMap ? 2 : 1) : 0), columnCount); + setShortcut(combination, action); + setAppearance(icon, name, backgroundImage, backgroundColor, textColor); // Will emit propertiesChanged(this) +} + +void Basket::saveProperties(QDomDocument &document, QDomElement &properties) +{ + XMLWork::addElement( document, properties, "name", basketName() ); + XMLWork::addElement( document, properties, "icon", icon() ); + + QDomElement appearance = document.createElement("appearance"); + properties.appendChild(appearance); + appearance.setAttribute( "backgroundImage", backgroundImageName() ); + appearance.setAttribute( "backgroundColor", backgroundColorSetting().isValid() ? backgroundColorSetting().name() : "" ); + appearance.setAttribute( "textColor", textColorSetting().isValid() ? textColorSetting().name() : "" ); + + QDomElement disposition = document.createElement("disposition"); + properties.appendChild(disposition); + disposition.setAttribute( "free", XMLWork::trueOrFalse(isFreeLayout()) ); + disposition.setAttribute( "columnCount", QString::number(columnsCount()) ); + disposition.setAttribute( "mindMap", XMLWork::trueOrFalse(isMindMap()) ); + + QDomElement shortcut = document.createElement("shortcut"); + properties.appendChild(shortcut); + QString actionStrings[] = { "show", "globalShow", "globalSwitch" }; + shortcut.setAttribute( "combination", m_action->shortcut().toStringInternal() ); + shortcut.setAttribute( "action", actionStrings[shortcutAction()] ); + + QDomElement protection = document.createElement("protection"); + properties.appendChild(protection); + protection.setAttribute( "type", m_encryptionType ); + protection.setAttribute( "key", m_encryptionKey ); +} + +void Basket::subscribeBackgroundImages() +{ + if (!m_backgroundImageName.isEmpty()) { + Global::backgroundManager->subscribe(m_backgroundImageName); + Global::backgroundManager->subscribe(m_backgroundImageName, this->backgroundColor()); + Global::backgroundManager->subscribe(m_backgroundImageName, selectionRectInsideColor()); + m_backgroundPixmap = Global::backgroundManager->pixmap(m_backgroundImageName); + m_opaqueBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, this->backgroundColor()); + m_selectedBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, selectionRectInsideColor()); + m_backgroundTiled = Global::backgroundManager->tiled(m_backgroundImageName); + } +} + +void Basket::unsubscribeBackgroundImages() +{ + if (hasBackgroundImage()) { + Global::backgroundManager->unsubscribe(m_backgroundImageName); + Global::backgroundManager->unsubscribe(m_backgroundImageName, this->backgroundColor()); + Global::backgroundManager->unsubscribe(m_backgroundImageName, selectionRectInsideColor()); + m_backgroundPixmap = 0; + m_opaqueBackgroundPixmap = 0; + m_selectedBackgroundPixmap = 0; + } +} + +void Basket::setAppearance(const QString &icon, const QString &name, const QString &backgroundImage, const QColor &backgroundColor, const QColor &textColor) +{ + unsubscribeBackgroundImages(); + + m_icon = icon; + m_basketName = name; + m_backgroundImageName = backgroundImage; + m_backgroundColorSetting = backgroundColor; + m_textColorSetting = textColor; + + m_action->setText("BASKET SHORTCUT: " + name); + + // Basket should ALWAYS have an icon (the "basket" icon by default): + QPixmap iconTest = kapp->iconLoader()->loadIcon(m_icon, KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/true); + if (iconTest.isNull()) + m_icon = "basket"; + + // We don't request the background images if it's not loaded yet (to make the application startup fast). + // When the basket is loading (because requested by the user: he/she want to access it) + // it load the properties, subscribe to (and then load) the images, update the "Loading..." message with the image, + // load all the notes and it's done! + if (m_loadingLaunched) + subscribeBackgroundImages(); + + recomputeAllStyles(); // If a note have a tag with the same background color as the basket one, then display a "..." + recomputeBlankRects(); // See the drawing of blank areas in Basket::drawContents() + unbufferizeAll(); + updateContents(); + + if (isDuringEdit() && m_editor->widget()) { + m_editor->widget()->setPaletteBackgroundColor( m_editor->note()->backgroundColor() ); + m_editor->widget()->setPaletteForegroundColor( m_editor->note()->textColor() ); + } + + emit propertiesChanged(this); +} + +void Basket::setDisposition(int disposition, int columnCount) +{ + static const int COLUMNS_LAYOUT = 0; + static const int FREE_LAYOUT = 1; + static const int MINDMAPS_LAYOUT = 2; + + int currentDisposition = (isFreeLayout() ? (isMindMap() ? MINDMAPS_LAYOUT : FREE_LAYOUT) : COLUMNS_LAYOUT); + + if (currentDisposition == COLUMNS_LAYOUT && disposition == COLUMNS_LAYOUT) { + if (firstNote() && columnCount > m_columnsCount) { + // Insert each new columns: + for (int i = m_columnsCount; i < columnCount; ++i) { + Note *newColumn = new Note(this); + insertNote(newColumn, /*clicked=*/lastNote(), /*zone=*/Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + } + } else if (firstNote() && columnCount < m_columnsCount) { + Note *column = firstNote(); + Note *cuttedNotes = 0; + for (int i = 1; i <= m_columnsCount; ++i) { + Note *columnToRemove = column; + column = column->next(); + if (i > columnCount) { + // Remove the columns that are too much: + unplugNote(columnToRemove); + // "Cut" the content in the columns to be deleted: + if (columnToRemove->firstChild()) { + for (Note *it = columnToRemove->firstChild(); it; it = it->next()) + it->setParentNote(0); + if (!cuttedNotes) + cuttedNotes = columnToRemove->firstChild(); + else { + Note *lastCuttedNote = cuttedNotes; + while (lastCuttedNote->next()) + lastCuttedNote = lastCuttedNote->next(); + lastCuttedNote->setNext(columnToRemove->firstChild()); + columnToRemove->firstChild()->setPrev(lastCuttedNote); + } + columnToRemove->setFirstChild(0); + } + } + } + // Paste the content in the last column: + if (cuttedNotes) + insertNote(cuttedNotes, /*clicked=*/lastNote(), /*zone=*/Note::BottomColumn, QPoint(), /*animateNewPosition=*/true); + unselectAll(); + } + if (columnCount != m_columnsCount) { + m_columnsCount = (columnCount <= 0 ? 1 : columnCount); + equalizeColumnSizes(); // Will relayoutNotes() + } + } else if (currentDisposition == COLUMNS_LAYOUT && (disposition == FREE_LAYOUT || disposition == MINDMAPS_LAYOUT)) { + Note *column = firstNote(); + m_columnsCount = 0; // Now, so relayoutNotes() will not relayout the free notes as if they were columns! + while (column) { + // Move all childs on the first level: + Note *nextColumn = column->next(); + ungroupNote(column); + column = nextColumn; + } + unselectAll(); + m_mindMap = (disposition == MINDMAPS_LAYOUT); + relayoutNotes(true); + } else if ((currentDisposition == FREE_LAYOUT || currentDisposition == MINDMAPS_LAYOUT) && disposition == COLUMNS_LAYOUT) { + if (firstNote()) { + // TODO: Reorder notes! + // Remove all notes (but keep a reference to them, we're not crazy ;-) ): + Note *notes = m_firstNote; + m_firstNote = 0; + m_count = 0; + m_countFounds = 0; + // Insert the number of columns that is needed: + Note *lastInsertedColumn = 0; + for (int i = 0; i < columnCount; ++i) { + Note *column = new Note(this); + if (lastInsertedColumn) + insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + else + m_firstNote = column; + lastInsertedColumn = column; + } + // Reinsert the old notes in the first column: + insertNote(notes, /*clicked=*/firstNote(), /*zone=*/Note::BottomColumn, QPoint(), /*animateNewPosition=*/true); + unselectAll(); + } else { + // Insert the number of columns that is needed: + Note *lastInsertedColumn = 0; + for (int i = 0; i < columnCount; ++i) { + Note *column = new Note(this); + if (lastInsertedColumn) + insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + else + m_firstNote = column; + lastInsertedColumn = column; + } + } + m_columnsCount = (columnCount <= 0 ? 1 : columnCount); + equalizeColumnSizes(); // Will relayoutNotes() + } +} + +void Basket::equalizeColumnSizes() +{ + if (!firstNote()) + return; + + // Necessary to know the available space; + relayoutNotes(true); + + int availableSpace = visibleWidth(); + int columnWidth = (visibleWidth() - (columnsCount()-1)*Note::GROUP_WIDTH) / columnsCount(); + int columnCount = columnsCount(); + Note *column = firstNote(); + while (column) { + int minGroupWidth = column->minRight() - column->x(); + if (minGroupWidth > columnWidth) { + availableSpace -= minGroupWidth; + --columnCount; + } + column = column->next(); + } + columnWidth = (availableSpace - (columnsCount()-1)*Note::GROUP_WIDTH) / columnCount; + + column = firstNote(); + while (column) { + int minGroupWidth = column->minRight() - column->x(); + if (minGroupWidth > columnWidth) + column->setGroupWidth(minGroupWidth); + else + column->setGroupWidth(columnWidth); + column = column->next(); + } + + relayoutNotes(true); +} + +void Basket::enableActions() +{ + Global::bnpView->enableActions(); + setFocusPolicy(isLocked() ? QWidget::NoFocus : QWidget::StrongFocus); + if (isLocked()) + viewport()->setCursor(Qt::ArrowCursor); // When locking, the cursor stays the last form it was +} + +bool Basket::save() +{ + if (!m_loaded) + return false; + + DEBUG_WIN << "Basket[" + folderName() + "]: Saving..."; + + // Create Document: + QDomDocument document(/*doctype=*/"basket"); + QDomElement root = document.createElement("basket"); + document.appendChild(root); + + // Create Properties Element and Populate It: + QDomElement properties = document.createElement("properties"); + saveProperties(document, properties); + root.appendChild(properties); + + // Create Notes Element and Populate It: + QDomElement notes = document.createElement("notes"); + saveNotes(document, notes, 0); + root.appendChild(notes); + + // Write to Disk: + if(!saveToFile(fullPath() + ".basket", "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + document.toString())) + { + DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to save</font>!"; + return false; + } + + Global::bnpView->setUnsavedStatus(false); + return true; +} + +void Basket::aboutToBeActivated() +{ + if (m_finishLoadOnFirstShow) { + FOR_EACH_NOTE (note) + note->finishLazyLoad(); + + //relayoutNotes(/*animate=*/false); + setFocusedNote(0); // So that during the focusInEvent that will come shortly, the FIRST note is focused. + + if (Settings::playAnimations() && !decoration()->filterBar()->filterData().isFiltering && Global::bnpView->currentBasket() == this) // No animation when filtering all! + animateLoad();//QTimer::singleShot( 0, this, SLOT(animateLoad()) ); + + m_finishLoadOnFirstShow = false; + } +} + +void Basket::load() +{ + // Load only once: + if (m_loadingLaunched) + return; + m_loadingLaunched = true; + +// StopWatch::start(10); + + DEBUG_WIN << "Basket[" + folderName() + "]: Loading..."; + QDomDocument *doc = 0; + QString content; + +// StopWatch::start(0); + + if (loadFromFile(fullPath() + ".basket", &content)) { + doc = new QDomDocument("basket"); + if ( ! doc->setContent(content) ) { + DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to parse XML</font>!"; + delete doc; + doc = 0; + } + } + if(isEncrypted()) + DEBUG_WIN << "Basket is encrypted."; + if ( ! doc) { + DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to load</font>!"; + m_loadingLaunched = false; + if (isEncrypted()) + m_locked = true; + Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar + return; + } + m_locked = false; + + QDomElement docElem = doc->documentElement(); + QDomElement properties = XMLWork::getElement(docElem, "properties"); + + loadProperties(properties); // Since we are loading, this time the background image will also be loaded! + // Now that the background image is loaded and subscribed, we display it during the load process: + delete doc; + updateContents(); +// kapp->processEvents(); + + //BEGIN Compatibility with 0.6.0 Pre-Alpha versions: + QDomElement notes = XMLWork::getElement(docElem, "notes"); + if (notes.isNull()) + notes = XMLWork::getElement(docElem, "items"); + m_watcher->stopScan(); + m_shouldConvertPlainTextNotes = false; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! +// StopWatch::check(0); + +// StopWatch::start(1); + m_finishLoadOnFirstShow = (Global::bnpView->currentBasket() != this); + loadNotes(notes, 0L); +// StopWatch::check(1); +// StopWatch::start(2); + + if (m_shouldConvertPlainTextNotes) + convertTexts(); + m_watcher->startScan(); + //loadNotes(XMLWork::getElement(docElem, "notes"), 0L); + //END + +// StopWatch::check(0); + + signalCountsChanged(); + if (isColumnsLayout()) { + // Count the number of columns: + int columnsCount = 0; + Note *column = firstNote(); + while (column) { + ++columnsCount; + column = column->next(); + } + m_columnsCount = columnsCount; + } + + relayoutNotes(false); + + // On application start, the current basket is not focused yet, so the focus rectangle is not shown when calling focusANote(): + if (Global::bnpView->currentBasket() == this) + setFocus(); + focusANote(); + + if (Settings::playAnimations() && !decoration()->filterBar()->filterData().isFiltering && Global::bnpView->currentBasket() == this) // No animation when filtering all! + animateLoad();//QTimer::singleShot( 0, this, SLOT(animateLoad()) ); + else + m_loaded = true; + enableActions(); +// StopWatch::check(2); + +// StopWatch::check(10); +} + +void Basket::filterAgain(bool andEnsureVisible/* = true*/) +{ + newFilter(decoration()->filterData(), andEnsureVisible); +} + +void Basket::filterAgainDelayed() +{ + QTimer::singleShot( 0, this, SLOT(filterAgain()) ); +} + +void Basket::newFilter(const FilterData &data, bool andEnsureVisible/* = true*/) +{ + if (!isLoaded()) + return; + +//StopWatch::start(20); + + m_countFounds = 0; + for (Note *note = firstNote(); note; note = note->next()) + m_countFounds += note->newFilter(data); + + relayoutNotes(true); + signalCountsChanged(); + + if (hasFocus()) // if (!hasFocus()), focusANote() will be called at focusInEvent() + focusANote(); // so, we avoid de-focus a note if it will be re-shown soon + if (andEnsureVisible && m_focusedNote != 0L) + ensureNoteVisible(m_focusedNote); + + Global::bnpView->setFiltering(data.isFiltering); + +//StopWatch::check(20); +} + +void Basket::cancelFilter() +{ + decoration()->filterBar()->reset(); + validateFilter(); +} + +void Basket::validateFilter() +{ + if (isDuringEdit()) + m_editor->widget()->setFocus(); + else + setFocus(); +} + +bool Basket::isFiltering() +{ + return decoration()->filterBar()->filterData().isFiltering; +} + + + +QString Basket::fullPath() +{ + return Global::basketsFolder() + folderName(); +} + +QString Basket::fullPathForFileName(const QString &fileName) +{ + return fullPath() + fileName; +} + +/*static*/ QString Basket::fullPathForFolderName(const QString &folderName) +{ + return Global::basketsFolder() + folderName; +} + + +void Basket::setShortcut(KShortcut shortcut, int action) +{ + if(!Global::globalAccel) + return; + QString sAction = "global_basket_activate_" + folderName(); + Global::globalAccel->remove(sAction); + Global::globalAccel->updateConnections(); + + m_action->setShortcut(shortcut); + m_shortcutAction = action; + + if (action > 0) + Global::globalAccel->insert(sAction, m_action->text(), /*whatsThis=*/"", m_action->shortcut(), KShortcut(), this, SLOT(activatedShortcut()), /*configurable=*/false); + Global::globalAccel->updateConnections(); +} + +void Basket::activatedShortcut() +{ + Global::bnpView->setCurrentBasket(this); + + if (m_shortcutAction == 1) + Global::bnpView->setActive(true); +} + +void Basket::signalCountsChanged() +{ + if (!m_timerCountsChanged.isActive()) + m_timerCountsChanged.start(0/*ms*/, /*singleShot=*/true); +} + +void Basket::countsChangedTimeOut() +{ + emit countsChanged(this); +} + + +Basket::Basket(QWidget *parent, const QString &folderName) + : QScrollView(parent), + QToolTip(viewport()), + m_noActionOnMouseRelease(false), m_ignoreCloseEditorOnNextMouseRelease(false), m_pressPos(-100, -100), m_canDrag(false), + m_firstNote(0), m_columnsCount(1), m_mindMap(false), m_resizingNote(0L), m_pickedResizer(0), m_movingNote(0L), m_pickedHandle(0, 0), + m_clickedToInsert(0), m_zoneToInsert(0), m_posToInsert(-1, -1), + m_isInsertPopupMenu(false), + m_loaded(false), m_loadingLaunched(false), m_locked(false), m_decryptBox(0), m_button(0), m_encryptionType(NoEncryption), +#ifdef HAVE_LIBGPGME + m_gpg(0), +#endif + m_backgroundPixmap(0), m_opaqueBackgroundPixmap(0), m_selectedBackgroundPixmap(0), + m_action(0), m_shortcutAction(0), + m_hoveredNote(0), m_hoveredZone(Note::None), m_lockedHovering(false), m_underMouse(false), + m_inserterRect(), m_inserterShown(false), m_inserterSplit(true), m_inserterTop(false), m_inserterGroup(false), + m_isSelecting(false), m_selectionStarted(false), + m_count(0), m_countFounds(0), m_countSelecteds(0), + m_folderName(folderName), + m_editor(0), m_leftEditorBorder(0), m_rightEditorBorder(0), m_redirectEditActions(false), m_editorWidth(-1), m_editorHeight(-1), + m_doNotCloseEditor(false), m_editParagraph(0), m_editIndex(0), + m_isDuringDrag(false), m_draggedNotes(), + m_focusedNote(0), m_startOfShiftSelectionNote(0), + m_finishLoadOnFirstShow(false), m_relayoutOnNextShow(false) +{ + QString sAction = "local_basket_activate_" + folderName; + m_action = new KAction("FAKE TEXT", "FAKE ICON", KShortcut(), this, SLOT(activatedShortcut()), Global::bnpView->actionCollection(), sAction); + m_action->setShortcutConfigurable(false); // We do it in the basket properties dialog (and keep it in sync with the global one) + + if (!m_folderName.endsWith("/")) + m_folderName += "/"; + + setFocusPolicy(QWidget::StrongFocus); + setWFlags(Qt::WNoAutoErase); + setDragAutoScroll(true); + + // By default, there is no corner widget: we set one for the corner area to be painted! + // If we don't set one and there are two scrollbars present, slowly resizing up the window show graphical glitches in that area! + m_cornerWidget = new QWidget(this); + setCornerWidget(m_cornerWidget); + + viewport()->setAcceptDrops(true); + viewport()->setMouseTracking(true); + viewport()->setBackgroundMode(NoBackground); // Do not clear the widget before paintEvent() because we always draw every pixels (faster and flicker-free) + + // File Watcher: + m_watcher = new KDirWatch(this); + connect( m_watcher, SIGNAL(dirty(const QString&)), this, SLOT(watchedFileModified(const QString&)) ); + connect( m_watcher, SIGNAL(deleted(const QString&)), this, SLOT(watchedFileDeleted(const QString&)) ); + connect( &m_watcherTimer, SIGNAL(timeout()), this, SLOT(updateModifiedNotes()) ); + + // Various Connections: + connect( &m_animationTimer, SIGNAL(timeout()), this, SLOT(animateObjects()) ); + connect( &m_autoScrollSelectionTimer, SIGNAL(timeout()), this, SLOT(doAutoScrollSelection()) ); + connect( &m_timerCountsChanged, SIGNAL(timeout()), this, SLOT(countsChangedTimeOut()) ); + connect( &m_inactivityAutoSaveTimer, SIGNAL(timeout()), this, SLOT(inactivityAutoSaveTimeout()) ); + connect( &m_inactivityAutoLockTimer, SIGNAL(timeout()), this, SLOT(inactivityAutoLockTimeout()) ); + connect( this, SIGNAL(contentsMoving(int, int)), this, SLOT(contentsMoved()) ); +#ifdef HAVE_LIBGPGME + m_gpg = new KGpgMe(); +#endif + m_locked = isFileEncrypted(); +} + +void Basket::contentsMoved() +{ + // This slot is called BEFORE the content move, so we delay the hover effects: + QTimer::singleShot(0, this, SLOT(doHoverEffects())); +} + +void Basket::enterEvent(QEvent *) +{ + m_underMouse = true; + doHoverEffects(); +} + +void Basket::leaveEvent(QEvent *) +{ + m_underMouse = false; + doHoverEffects(); + + if (m_lockedHovering) + return; + + removeInserter(); + if (m_hoveredNote) { + m_hoveredNote->setHovered(false); + m_hoveredNote->setHoveredZone(Note::None); + updateNote(m_hoveredNote); + } + m_hoveredNote = 0; +} + +void Basket::setFocusIfNotInPopupMenu() +{ + if (!kapp->activePopupWidget()) + if (isDuringEdit()) + m_editor->widget()->setFocus(); + else + setFocus(); +} + +void Basket::contentsMousePressEvent(QMouseEvent *event) +{ + // If user click the basket, focus it! + // The focus is delayed because if the click results in showing a popup menu, + // the interface flicker by showing the focused rectangle (as the basket gets focus) + // and immediatly removing it (because the popup menu now have focus). + if (!isDuringEdit()) + QTimer::singleShot(0, this, SLOT(setFocusIfNotInPopupMenu())); + + // Convenient variables: + bool controlPressed = event->stateAfter() & Qt::ControlButton; + bool shiftPressed = event->stateAfter() & Qt::ShiftButton; + + // Do nothing if we disabled the click some milliseconds sooner. + // For instance when a popup menu has been closed with click, we should not do action: + if (event->button() == Qt::LeftButton && (kapp->activePopupWidget() || m_lastDisableClick.msecsTo(QTime::currentTime()) <= 80)) { + doHoverEffects(); + m_noActionOnMouseRelease = true; + // But we allow to select: + // The code is the same as at the bottom of this method: + if (event->button() == Qt::LeftButton) { + m_selectionStarted = true; + m_selectionBeginPoint = event->pos(); + m_selectionInvert = controlPressed || shiftPressed; + } + return; + } + + // Figure out what is the clicked note and zone: + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ) : Note::None); + + // Popup Tags menu: + if (zone == Note::TagsArrow && !controlPressed && !shiftPressed && event->button() != Qt::MidButton) { + if (!clicked->isSelected()) + unselectAllBut(clicked); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = clicked; + m_noActionOnMouseRelease = true; + popupTagsMenu(clicked); + return; + } + + if (event->button() == Qt::LeftButton) { + // Prepare to allow drag and drop when moving mouse further: + if ( (zone == Note::Handle || zone == Note::Group) || + (clicked && clicked->isSelected() && + (zone == Note::TagsArrow || zone == Note::Custom0 || zone == Note::Content || zone == Note::Link /**/ || zone >= Note::Emblem0 /**/)) ) { + if (!shiftPressed && !controlPressed) { + m_pressPos = event->pos(); // TODO: Allow to drag emblems to assign them to other notes. Then don't allow drag at Emblem0!! + m_canDrag = true; + // Saving where we were editing, because during a drag, the mouse can fly over the text edit and move the cursor position: + if (m_editor && m_editor->textEdit()) { + QTextEdit *editor = m_editor->textEdit(); + editor->getCursorPosition(&m_editParagraph, &m_editIndex); + } + } + } + + // Initializing Resizer move: + if (zone == Note::Resizer) { + m_resizingNote = clicked; + m_pickedResizer = event->pos().x() - clicked->rightLimit(); + m_noActionOnMouseRelease = true; + m_lockedHovering = true; + return; + } + + // Select note(s): + if (zone == Note::Handle || zone == Note::Group || (zone == Note::GroupExpander && (controlPressed || shiftPressed))) { + Note *end = clicked; + if (clicked->isGroup() && shiftPressed) { + if (clicked->contains(m_startOfShiftSelectionNote)) { + m_startOfShiftSelectionNote = clicked->firstRealChild(); + end = clicked->lastRealChild(); + } else if (clicked->firstRealChild()->isAfter(m_startOfShiftSelectionNote)) + end = clicked->lastRealChild(); + else + end = clicked->firstRealChild(); + } + if (controlPressed && shiftPressed) + selectRange(m_startOfShiftSelectionNote, end, /*unselectOthers=*/false); + else if (shiftPressed) + selectRange(m_startOfShiftSelectionNote, end); + else if (controlPressed) + clicked->setSelectedRecursivly(!clicked->allSelected()); + else if (!clicked->allSelected()) + unselectAllBut(clicked); + setFocusedNote(end); /// /// /// + m_startOfShiftSelectionNote = (end->isGroup() ? end->firstRealChild() : end); + //m_noActionOnMouseRelease = false; + m_noActionOnMouseRelease = true; + return; + } + // MOVED TO RELEASE EVENT: + /* else if (clicked && zone != Note::None && zone != Note::BottomColumn && zone != Note::Resizer && (controlPressed || shiftPressed)) { + if (controlPressed && shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked, / *unselectOthers=* /false); + else if (shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked); + else if (controlPressed) + clicked->setSelectedRecursivly(!clicked->allSelected()); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); + m_noActionOnMouseRelease = true; + return; + }*/ + + // Initializing Note move: +/* if ((zone == Note::Group || zone == Note::Handle) && clicked->isFree()) { + m_movingNote = clicked; + m_pickedHandle = QPoint(event->pos().x() - clicked->x(), event->pos().y() - clicked->y()); + m_noActionOnMouseRelease = true; + m_lockedHovering = true; + return; + } +*/ + + // Folding/Unfolding group: + if (zone == Note::GroupExpander) { + clicked->toggleFolded(Settings::playAnimations()); + relayoutNotes(true); + m_noActionOnMouseRelease = true; + return; + } + } + + // Popup menu for tag emblems: + if (event->button() == Qt::RightButton && zone >= Note::Emblem0) { + if (!clicked->isSelected()) + unselectAllBut(clicked); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = clicked; + popupEmblemMenu(clicked, zone - Note::Emblem0); + m_noActionOnMouseRelease = true; + return; + } + + // Insertion Popup Menu: + if ( (event->button() == Qt::RightButton) && + ((!clicked && isFreeLayout()) || + (clicked && (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn))) ) { + unselectAll(); + m_clickedToInsert = clicked; + m_zoneToInsert = zone; + m_posToInsert = event->pos(); + KPopupMenu* menu = (KPopupMenu*)(Global::bnpView->popupMenu("insert_popup")); + if (!menu->title(/*id=*/120).isEmpty()) // If we already added a title, remove it because it would be kept and then added several times: + menu->removeItem(/*id=*/120); + menu->insertTitle((zone == Note::TopGroup || zone == Note::BottomGroup ? i18n("The verb (Group New Note)", "Group") : i18n("The verb (Insert New Note)", "Insert")), /*id=*/120, /*index=*/0); + setInsertPopupMenu(); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(delayedCancelInsertPopupMenu()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(hideInsertPopupMenu()) ); + doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! + m_lockedHovering = true; + menu->exec(QCursor::pos()); + m_noActionOnMouseRelease = true; + return; + } + + // Note Context Menu: + if (event->button() == Qt::RightButton && clicked && !clicked->isColumn() && zone != Note::Resizer) { + if (!clicked->isSelected()) + unselectAllBut(clicked); + setFocusedNote(clicked); /// /// /// + if (editedNote() == clicked) { + closeEditor(); + clicked->setSelected(true); + } + m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); + QPopupMenu* menu = Global::bnpView->popupMenu("note_popup"); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! + m_lockedHovering = true; + menu->exec(QCursor::pos()); + m_noActionOnMouseRelease = true; + return; + } + + // Paste selection under cursor (but not "create new primary note under cursor" because this is on moveRelease): + if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { + if ((Settings::middleAction() != 0) && (event->state() == Qt::ShiftButton)) { + m_clickedToInsert = clicked; + m_zoneToInsert = zone; + m_posToInsert = event->pos(); + closeEditor(); + removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, + NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed + switch (Settings::middleAction()) { + case 1: + m_isInsertPopupMenu = true; + pasteNote(); + break; + case 2: type = NoteType::Image; break; + case 3: type = NoteType::Link; break; + case 4: type = NoteType::Launcher; break; + default: + m_noActionOnMouseRelease = false; + return; // Other options should be done on mouse release (to avoid mouse release to cancel them!) +/* case 5: type = NoteType::Color; break; + case 6: + Global::bnpView->grabScreenshot(); + break; + case 7: + Global::bnpView->slotColorFromScreen(); + break; + case 8: + Global::bnpView->insertWizard(3); // loadFromFile + break; + case 9: + Global::bnpView->insertWizard(1); // importKMenuLauncher + break; + case 10: + Global::bnpView->insertWizard(2); // importIcon + break; +*/ } + if (type != 0) { + m_ignoreCloseEditorOnNextMouseRelease = true; + Global::bnpView->insertEmpty(type); + } + } else { + if (clicked) + zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), true ); + closeEditor(); + clickedToInsert(event, clicked, zone); + save(); + } + m_noActionOnMouseRelease = true; + return; + } + + // Finally, no action has been done durint pressEvent, so an action can be done on releaseEvent: + m_noActionOnMouseRelease = false; + + /* Selection scenario: + * On contentsMousePressEvent, put m_selectionStarted to true and init Begin and End selection point. + * On contentsMouseMoveEvent, if m_selectionStarted, update End selection point, update selection rect, + * and if it's larger, switching to m_isSelecting mode: we can draw the selection rectangle. + */ + // Prepare selection: + if (event->button() == Qt::LeftButton) { + m_selectionStarted = true; + m_selectionBeginPoint = event->pos(); + // We usualy invert the selection with the Ctrl key, but some environements (like GNOME or The Gimp) do it with the Shift key. + // Since the Shift key has no specific usage, we allow to invert selection ALSO with Shift for Gimp people + m_selectionInvert = controlPressed || shiftPressed; + } +} + +void Basket::delayedCancelInsertPopupMenu() +{ + QTimer::singleShot( 0, this, SLOT(cancelInsertPopupMenu()) ); +} + +void Basket::contentsContextMenuEvent(QContextMenuEvent *event) +{ + if (event->reason() == QContextMenuEvent::Keyboard) { + if (countFounds/*countShown*/() == 0) { // TODO: Count shown!! + QRect basketRect( mapToGlobal(QPoint(0,0)), size() ); + QPopupMenu *menu = Global::bnpView->popupMenu("insert_popup"); + setInsertPopupMenu(); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(delayedCancelInsertPopupMenu()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + removeInserter(); + m_lockedHovering = true; + PopupMenu::execAtRectCenter(*menu, basketRect); // Popup at center or the basket + } else { + if ( ! m_focusedNote->isSelected() ) + unselectAllBut(m_focusedNote); + setFocusedNote(m_focusedNote); /// /// /// + m_startOfShiftSelectionNote = (m_focusedNote->isGroup() ? m_focusedNote->firstRealChild() : m_focusedNote); + // Popup at bottom (or top) of the focused note, if visible : + QPopupMenu *menu = Global::bnpView->popupMenu("note_popup"); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + doHoverEffects(m_focusedNote, Note::Content); // In the case where another popup menu was open, we should do that manually! + m_lockedHovering = true; + PopupMenu::execAtRectBottom(*menu, noteVisibleRect(m_focusedNote), true); + } + } +} + +QRect Basket::noteVisibleRect(Note *note) +{ + QRect rect( contentsToViewport(QPoint(note->x(), note->y())), QSize(note->width(),note->height()) ); + QPoint basketPoint = mapToGlobal(QPoint(0,0)); + rect.moveTopLeft( rect.topLeft() + basketPoint + QPoint(frameWidth(), frameWidth()) ); + + // Now, rect contain the global note rectangle on the screen. + // We have to clip it by the basket widget : + if (rect.bottom() > basketPoint.y() + visibleHeight() + 1) { // Bottom too... bottom + rect.setBottom( basketPoint.y() + visibleHeight() + 1); + if (rect.height() <= 0) // Have at least one visible pixel of height + rect.setTop(rect.bottom()); + } + if (rect.top() < basketPoint.y() + frameWidth()) { // Top too... top + rect.setTop( basketPoint.y() + frameWidth()); + if (rect.height() <= 0) + rect.setBottom(rect.top()); + } + if (rect.right() > basketPoint.x() + visibleWidth() + 1) { // Right too... right + rect.setRight( basketPoint.x() + visibleWidth() + 1); + if (rect.width() <= 0) // Have at least one visible pixel of width + rect.setLeft(rect.right()); + } + if (rect.left() < basketPoint.x() + frameWidth()) { // Left too... left + rect.setLeft( basketPoint.x() + frameWidth()); + if (rect.width() <= 0) + rect.setRight(rect.left()); + } + + return rect; +} + +void Basket::disableNextClick() +{ + m_lastDisableClick = QTime::currentTime(); +} + +void Basket::recomputeAllStyles() +{ + FOR_EACH_NOTE (note) + note->recomputeAllStyles(); +} + +void Basket::removedStates(const QValueList<State*> &deletedStates) +{ + bool modifiedBasket = false; + + FOR_EACH_NOTE (note) + if (note->removedStates(deletedStates)) + modifiedBasket = true; + + if (modifiedBasket) + save(); +} + +void Basket::insertNote(Note *note, Note *clicked, int zone, const QPoint &pos, bool animateNewPosition) +{ + if (!note) { + std::cout << "Wanted to insert NO note" << std::endl; + return; + } + + if (clicked && zone == Note::BottomColumn) { + // When inserting at the bottom of a column, it's obvious the new note SHOULD inherit tags. + // We ensure that by changing the insertion point after the last note of the column: + Note *last = clicked->lastChild(); + if (last) { + clicked = last; + zone = Note::BottomInsert; + } + } + + /// Insertion at the bottom of a column: + if (clicked && zone == Note::BottomColumn) { + note->setWidth(clicked->rightLimit() - clicked->x()); + Note *lastChild = clicked->lastChild(); + if (!animateNewPosition || !Settings::playAnimations()) + for (Note *n = note; n; n = n->next()) { + n->setXRecursivly(clicked->x()); + n->setYRecursivly((lastChild ? lastChild : clicked)->bottom() + 1); + } + appendNoteIn(note, clicked); + + /// Insertion relative to a note (top/bottom, insert/group): + } else if (clicked) { + note->setWidth(clicked->width()); + if (!animateNewPosition || !Settings::playAnimations()) + for (Note *n = note; n; n = n->next()) { + if (zone == Note::TopGroup || zone == Note::BottomGroup) + n->setXRecursivly(clicked->x() + Note::GROUP_WIDTH); + else + n->setXRecursivly(clicked->x()); + if (zone == Note::TopInsert || zone == Note::TopGroup) + n->setYRecursivly(clicked->y()); + else + n->setYRecursivly(clicked->bottom() + 1); + } + + if (zone == Note::TopInsert) { appendNoteBefore(note, clicked); } + else if (zone == Note::BottomInsert) { appendNoteAfter(note, clicked); } + else if (zone == Note::TopGroup) { groupNoteBefore(note, clicked); } + else if (zone == Note::BottomGroup) { groupNoteAfter(note, clicked); } + + /// Free insertion: + } else if (isFreeLayout()) { + // Group if note have siblings: + if (note->next()) { + Note *group = new Note(this); + for (Note *n = note; n; n = n->next()) + n->setParentNote(group); + group->setFirstChild(note); + note = group; + } + // Insert at cursor position: + const int initialWidth = 250; + note->setWidth(note->isGroup() ? Note::GROUP_WIDTH : initialWidth); + if (note->isGroup() && note->firstChild()) + note->setInitialHeight(note->firstChild()->height()); + //note->setGroupWidth(initialWidth); + if (animateNewPosition && Settings::playAnimations()) + note->setFinalPosition(pos.x(), pos.y()); + else { + note->setXRecursivly(pos.x()); + note->setYRecursivly(pos.y()); + } + appendNoteAfter(note, lastNote()); + } + + relayoutNotes(true); +} + +void Basket::clickedToInsert(QMouseEvent *event, Note *clicked, /*Note::Zone*/int zone) +{ + Note *note; + if (event->button() == Qt::MidButton) + note = NoteFactory::dropNote(KApplication::clipboard()->data(QClipboard::Selection), this); + else + note = NoteFactory::createNoteText("", this); + + if (!note) + return; + + insertNote(note, clicked, zone, event->pos(), /*animateNewPosition=*/false); + +// ensureNoteVisible(lastInsertedNote()); // TODO: in insertNote() + + if (event->button() != Qt::MidButton) { + removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. + closeEditor(); + noteEdit(note, /*justAdded=*/true); + } +} + +void Basket::contentsDragEnterEvent(QDragEnterEvent *event) +{ + m_isDuringDrag = true; + Global::bnpView->updateStatusBarHint(); + if (NoteDrag::basketOf(event) == this) + m_draggedNotes = NoteDrag::notesOf(event); +} + +void Basket::contentsDragMoveEvent(QDragMoveEvent *event) +{ +// m_isDuringDrag = true; + +// if (isLocked()) +// return; + +// FIXME: viewportToContents does NOT work !!! +// QPoint pos = viewportToContents(event->pos()); +// QPoint pos( event->pos().x() + contentsX(), event->pos().y() + contentsY() ); + +// if (insertAtCursorPos()) +// computeInsertPlace(pos); + doHoverEffects(event->pos()); + +// showFrameInsertTo(); + if (isFreeLayout() || noteAt(event->pos().x(), event->pos().y())) // Cursor before rightLimit() or hovering the dragged source notes + acceptDropEvent(event); + else { + event->acceptAction(false); + event->accept(false); + } + +/* Note *hoveredNote = noteAt(event->pos().x(), event->pos().y()); + if ( (isColumnsLayout() && !hoveredNote) || (draggedNotes().contains(hoveredNote)) ) { + event->acceptAction(false); + event->accept(false); + } else + acceptDropEvent(event);*/ + + // A workarround since QScrollView::dragAutoScroll seem to have no effect : +// ensureVisible(event->pos().x() + contentsX(), event->pos().y() + contentsY(), 30, 30); +// QScrollView::dragMoveEvent(event); +} + +void Basket::contentsDragLeaveEvent(QDragLeaveEvent*) +{ +// resetInsertTo(); + m_isDuringDrag = false; + m_draggedNotes.clear(); + m_noActionOnMouseRelease = true; + emit resetStatusBarText(); + doHoverEffects(); +} + +void Basket::contentsDropEvent(QDropEvent *event) +{ + QPoint pos = event->pos(); + std::cout << "Contents Drop Event at position " << pos.x() << ":" << pos.y() << std::endl; + + m_isDuringDrag = false; + emit resetStatusBarText(); + +// if (isLocked()) +// return; + + // Do NOT check the bottom&right borders. + // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), + // the note is first removed, and relayoutNotes() compute the new height that is smaller + // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! + // Should, of course, not return 0: + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + + if (NoteFactory::movingNotesInTheSameBasket(event, this, event->action()) && event->action() == QDropEvent::Move) { + m_doNotCloseEditor = true; + } + + Note *note = NoteFactory::dropNote( event, this, true, event->action(), dynamic_cast<Note*>(event->source()) ); + + if (note) { + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), /*toAdd=*/true ) : Note::None); + bool animateNewPosition = NoteFactory::movingNotesInTheSameBasket(event, this, event->action()); + if (animateNewPosition) { + FOR_EACH_NOTE (n) + n->setOnTop(false); + // FOR_EACH_NOTE_IN_CHUNK(note) + for (Note *n = note; n; n = n->next()) + n->setOnTop(true); + } + + insertNote(note, clicked, zone, event->pos(), animateNewPosition); + + // If moved a note on bottom, contentsHeight has been disminished, then view scrolled up, and we should re-scroll the view down: + ensureNoteVisible(note); + +// if (event->button() != Qt::MidButton) { +// removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. +// } + +// resetInsertTo(); +// doHoverEffects(); called by insertNote() + save(); + } + + m_draggedNotes.clear(); + + m_doNotCloseEditor = false; + // When starting the drag, we saved where we were editing. + // This is because during a drag, the mouse can fly over the text edit and move the cursor position, and even HIDE the cursor. + // So we re-show the cursor, and re-position it at the right place: + if (m_editor && m_editor->textEdit()) { + QTextEdit *editor = m_editor->textEdit(); + editor->setCursorPosition(m_editParagraph, m_editIndex); + } +} + +// handles dropping of a note to basket that is not shown +// (usually through its entry in the basket list) +void Basket::blindDrop(QDropEvent* event) +{ + if (!m_isInsertPopupMenu && redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->paste(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->paste(); + } else { + if (!isLoaded()) { + Global::bnpView->showPassiveLoading(this); + load(); + } + closeEditor(); + unselectAll(); + Note *note = NoteFactory::dropNote( event, this, true, event->action(), + dynamic_cast<Note*>(event->source()) ); + if (note) { + insertCreatedNote(note); + //unselectAllBut(note); + if (Settings::usePassivePopup()) + Global::bnpView->showPassiveDropped(i18n("Dropped to basket <i>%1</i>")); + } + } + save(); +} + +void Basket::insertEmptyNote(int type) +{ + if (!isLoaded()) + load(); + if (isDuringEdit()) + closeEditor(); + Note *note = NoteFactory::createEmptyNote((NoteType::Id)type, this); + insertCreatedNote(note/*, / *edit=* /true*/); + noteEdit(note, /*justAdded=*/true); +} + +void Basket::insertWizard(int type) +{ + saveInsertionData(); + Note *note = 0; + switch (type) { + default: + case 1: note = NoteFactory::importKMenuLauncher(this); break; + case 2: note = NoteFactory::importIcon(this); break; + case 3: note = NoteFactory::importFileContent(this); break; + } + if (!note) + return; + restoreInsertionData(); + insertCreatedNote(note); + unselectAllBut(note); + resetInsertionData(); +} + +void Basket::insertColor(const QColor &color) +{ + Note *note = NoteFactory::createNoteColor(color, this); + restoreInsertionData(); + insertCreatedNote(note); + unselectAllBut(note); + resetInsertionData(); +} + +void Basket::insertImage(const QPixmap &image) +{ + Note *note = NoteFactory::createNoteImage(image, this); + restoreInsertionData(); + insertCreatedNote(note); + unselectAllBut(note); + resetInsertionData(); +} + +void Basket::pasteNote(QClipboard::Mode mode) +{ + if (!m_isInsertPopupMenu && redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->paste(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->paste(); + } else { + if (!isLoaded()) { + Global::bnpView->showPassiveLoading(this); + load(); + } + closeEditor(); + unselectAll(); + Note *note = NoteFactory::dropNote(KApplication::clipboard()->data(mode), this); + if (note) { + insertCreatedNote(note); + //unselectAllBut(note); + } + } +} + +void Basket::insertCreatedNote(Note *note) +{ + // Get the insertion data if the user clicked inside the basket: + Note *clicked = m_clickedToInsert; + int zone = m_zoneToInsert; + QPoint pos = m_posToInsert; + + // If it isn't the case, use the default position: + if (!clicked && (pos.x() < 0 || pos.y() < 0)) { + // Insert right after the focused note: + focusANote(); + if (m_focusedNote) { + clicked = m_focusedNote; + zone = (m_focusedNote->isFree() ? Note::BottomGroup : Note::BottomInsert); + pos = QPoint(m_focusedNote->x(), m_focusedNote->finalBottom()); + // Insert at the end of the last column: + } else if (isColumnsLayout()) { + Note *column = /*(Settings::newNotesPlace == 0 ?*/ firstNote() /*: lastNote())*/; + /*if (Settings::newNotesPlace == 0 && column->firstChild()) { // On Top, if at least one child in the column + clicked = column->firstChild(); + zone = Note::TopInsert; + } else { // On Bottom*/ + clicked = column; + zone = Note::BottomColumn; + /*}*/ + // Insert at free position: + } else { + pos = QPoint(0, 0); + } + } + + insertNote(note, clicked, zone, pos); +// ensureNoteVisible(lastInsertedNote()); + removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. +// resetInsertTo(); + save(); +} + +void Basket::saveInsertionData() +{ + m_savedClickedToInsert = m_clickedToInsert; + m_savedZoneToInsert = m_zoneToInsert; + m_savedPosToInsert = m_posToInsert; +} + +void Basket::restoreInsertionData() +{ + m_clickedToInsert = m_savedClickedToInsert; + m_zoneToInsert = m_savedZoneToInsert; + m_posToInsert = m_savedPosToInsert; +} + +void Basket::resetInsertionData() +{ + m_clickedToInsert = 0; + m_zoneToInsert = 0; + m_posToInsert = QPoint(-1, -1); +} + +void Basket::hideInsertPopupMenu() +{ + QTimer::singleShot( 50/*ms*/, this, SLOT(timeoutHideInsertPopupMenu()) ); +} + +void Basket::timeoutHideInsertPopupMenu() +{ + resetInsertionData(); +} + +void Basket::acceptDropEvent(QDropEvent *event, bool preCond) +{ + // FIXME: Should not accept all actions! Or not all actions (link not supported?!) + event->acceptAction(preCond && 1); + event->accept(preCond); +} + +void Basket::contentsMouseReleaseEvent(QMouseEvent *event) +{ + // Now disallow drag: + m_canDrag = false; + + // Cancel Resizer move: + if (m_resizingNote) { + m_resizingNote = 0; + m_pickedResizer = 0; + m_lockedHovering = false; + doHoverEffects(); + save(); + } + + // Cancel Note move: +/* if (m_movingNote) { + m_movingNote = 0; + m_pickedHandle = QPoint(0, 0); + m_lockedHovering = false; + //doHoverEffects(); + save(); + } +*/ + + // Cancel Selection rectangle: + if (m_isSelecting) { + m_isSelecting = false; + stopAutoScrollSelection(); + resetWasInLastSelectionRect(); + doHoverEffects(); + updateContents(m_selectionRect); + } + m_selectionStarted = false; + + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ) : Note::None); + if ((zone == Note::Handle || zone == Note::Group) && editedNote() && editedNote() == clicked) { + if (m_ignoreCloseEditorOnNextMouseRelease) + m_ignoreCloseEditorOnNextMouseRelease = false; + else { + bool editedNoteStillThere = closeEditor(); + if (editedNoteStillThere) + //clicked->setSelected(true); + unselectAllBut(clicked); + } + } + + + if (event->stateAfter() == 0 && (zone == Note::Group || zone == Note::Handle)) { + closeEditor(); + unselectAllBut(clicked); + } + + + // Do nothing if an action has already been made during mousePressEvent, + // or if user made a selection and canceled it by regressing to a very small rectangle. + if (m_noActionOnMouseRelease) + return; + // We immediatly set it to true, to avoid actions set on mouseRelease if NO mousePress event has been triggered. + // This is the case when a popup menu is shown, and user click to the basket area to close it: + // the menu then receive the mousePress event and the basket area ONLY receive the mouseRelease event. + // Obviously, nothing should be done in this case: + m_noActionOnMouseRelease = true; + + + + if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { + if ((Settings::middleAction() != 0) && (event->stateAfter() == Qt::ShiftButton)) { + m_clickedToInsert = clicked; + m_zoneToInsert = zone; + m_posToInsert = event->pos(); + closeEditor(); + removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, + NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed + switch (Settings::middleAction()) { + case 5: type = NoteType::Color; break; + case 6: + Global::bnpView->grabScreenshot(); + return; + case 7: + Global::bnpView->slotColorFromScreen(); + return; + case 8: + Global::bnpView->insertWizard(3); // loadFromFile + return; + case 9: + Global::bnpView->insertWizard(1); // importKMenuLauncher + return; + case 10: + Global::bnpView->insertWizard(2); // importIcon + return; + } + if (type != 0) { + m_ignoreCloseEditorOnNextMouseRelease = true; + Global::bnpView->insertEmpty(type); + return; + } + } + } + +// Note *clicked = noteAt(event->pos().x(), event->pos().y()); + if ( ! clicked ) { + if (isFreeLayout() && event->button() == Qt::LeftButton) { + clickedToInsert(event); + save(); + } + return; + } +// Note::Zone zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ); + + // Convenient variables: + bool controlPressed = event->stateAfter() & Qt::ControlButton; + bool shiftPressed = event->stateAfter() & Qt::ShiftButton; + + if (clicked && zone != Note::None && zone != Note::BottomColumn && zone != Note::Resizer && (controlPressed || shiftPressed)) { + if (controlPressed && shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked, /*unselectOthers=*/false); + else if (shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked); + else if (controlPressed) + clicked->setSelectedRecursivly(!clicked->allSelected()); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); + m_noActionOnMouseRelease = true; + return; + } + + // Switch tag states: + if (zone >= Note::Emblem0) { + if (event->button() == Qt::LeftButton) { + int icons = -1; + for (State::List::iterator it = clicked->states().begin(); it != clicked->states().end(); ++it) { + if ( ! (*it)->emblem().isEmpty() ) + icons++; + if (icons == zone - Note::Emblem0) { + State *state = (*it)->nextState(); + if (!state) + return; + it = clicked->states().insert(it, state); + ++it; + clicked->states().remove(it); + clicked->recomputeStyle(); + clicked->unbufferize(); + updateNote(clicked); + updateEditorAppearance(); + filterAgain(); + save(); + break; + } + } + return; + }/* else if (event->button() == Qt::RightButton) { + popupEmblemMenu(clicked, zone - Note::Emblem0); + return; + }*/ + } + + // Insert note or past clipboard: + QString text; +// Note *note; + QString link; + //int zone = zone; + if (event->button() == Qt::MidButton && zone == Note::Resizer) + return; //zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), true ); + if (event->button() == Qt::RightButton && (clicked->isColumn() || zone == Note::Resizer)) + return; + if (clicked->isGroup() && zone == Note::None) + return; + switch (zone) { + case Note::Handle: + case Note::Group: + // We select note on mousePress if it was unselected or Ctrl is pressed. + // But the user can want to drag select_s_ notes, so it the note is selected, we only select it alone on mouseRelease: + if (event->stateAfter() == 0) { + std::cout << "EXEC" << std::endl; + if ( !(event->stateAfter() & Qt::ControlButton) && clicked->allSelected()) + unselectAllBut(clicked); + if (zone == Note::Handle && isDuringEdit() && editedNote() == clicked) { + closeEditor(); + clicked->setSelected(true); + } + } + break; + + case Note::Custom0: + //unselectAllBut(clicked); + setFocusedNote(clicked); + noteOpen(clicked); + break; + + case Note::GroupExpander: + case Note::TagsArrow: + break; + + case Note::Link: + link = clicked->linkAt(event->pos() - QPoint(clicked->x(), clicked->y())); + if ( ! link.isEmpty() ) { + if (link == "basket-internal-remove-basket") { + // TODO: ask confirmation: "Do you really want to delete the welcome baskets?\n You can re-add them at any time in the Help menu." + Global::bnpView->doBasketDeletion(this); + } else if (link == "basket-internal-import") { + QPopupMenu *menu = Global::bnpView->popupMenu("fileimport"); + menu->exec(event->globalPos()); + } else { + KRun *run = new KRun(link); // open the URL. + run->setAutoDelete(true); + } + break; + } // If there is no link, edit note content + case Note::Content: + closeEditor(); + unselectAllBut(clicked); + noteEdit(clicked, /*justAdded=*/false, event->pos()); + break; + + case Note::TopInsert: + case Note::TopGroup: + case Note::BottomInsert: + case Note::BottomGroup: + case Note::BottomColumn: + clickedToInsert(event, clicked, zone); + save(); + break; + + case Note::None: + default: + KMessageBox::information(viewport(), + i18n("This message should never appear. If it does, this program is buggy! " + "Please report the bug to the developer.")); + break; + } +} + +void Basket::contentsMouseDoubleClickEvent(QMouseEvent *event) +{ + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ) : Note::None); + + if (event->button() == Qt::LeftButton && (zone == Note::Group || zone == Note::Handle)) { + doCopy(CopyToSelection); + m_noActionOnMouseRelease = true; + } else + contentsMousePressEvent(event); +} + +void Basket::contentsMouseMoveEvent(QMouseEvent *event) +{ + // Drag the notes: + if (m_canDrag && (m_pressPos - event->pos()).manhattanLength() > KApplication::startDragDistance()) { + m_canDrag = false; + m_isSelecting = false; // Don't draw selection rectangle ater drag! + m_selectionStarted = false; + NoteSelection *selection = selectedNotes(); + if (selection->firstStacked()) { + QDragObject *d = NoteDrag::dragObject(selection, /*cutting=*/false, /*source=*/this); // d will be deleted by QT + /*bool shouldRemove = */d->drag(); +// delete selection; + + // Never delete because URL is dragged and the file must be available for the extern appliation +// if (shouldRemove && d->target() == 0) // If target is another application that request to remove the note +// emit wantDelete(this); + } + return; + } + + // Moving a Resizer: + if (m_resizingNote) { + int groupWidth = event->pos().x() - m_resizingNote->x() - m_pickedResizer; + int minRight = m_resizingNote->minRight(); + int maxRight = 100 * contentsWidth(); // A big enough value (+infinity) for free layouts. + Note *nextColumn = m_resizingNote->next(); + if (m_resizingNote->isColumn()) { + if (nextColumn) + maxRight = nextColumn->x() + nextColumn->rightLimit() - nextColumn->minRight() - Note::RESIZER_WIDTH; + else + maxRight = contentsWidth(); + } + if (groupWidth > maxRight - m_resizingNote->x()) + groupWidth = maxRight - m_resizingNote->x(); + if (groupWidth < minRight - m_resizingNote->x()) + groupWidth = minRight - m_resizingNote->x(); + int delta = groupWidth - m_resizingNote->groupWidth(); + m_resizingNote->setGroupWidth(groupWidth); + // If resizing columns: + if (m_resizingNote->isColumn()) { + Note *column = m_resizingNote; + if ( (column = column->next()) ) { + // Next columns should not have them X coordinate animated, because it would flicker: + column->setXRecursivly(column->x() + delta); + // And the resizer should resize the TWO sibling columns, and not push the other columns on th right: + column->setGroupWidth(column->groupWidth() - delta); + } + } + relayoutNotes(true); + } + + // Moving a Note: +/* if (m_movingNote) { + int x = event->pos().x() - m_pickedHandle.x(); + int y = event->pos().y() - m_pickedHandle.y(); + if (x < 0) x = 0; + if (y < 0) y = 0; + m_movingNote->setX(x); + m_movingNote->setY(y); + m_movingNote->relayoutAt(x, y, / *animate=* /false); + relayoutNotes(true); + } +*/ + + // Dragging the selection rectangle: + if (m_selectionStarted) + doAutoScrollSelection(); + + doHoverEffects(event->pos()); +} + +void Basket::doAutoScrollSelection() +{ + static const int AUTO_SCROLL_MARGIN = 50; // pixels + static const int AUTO_SCROLL_DELAY = 100; // milliseconds + + QPoint pos = viewport()->mapFromGlobal(QCursor::pos()); + + // Do the selection: + + if (m_isSelecting) + updateContents(m_selectionRect); + + m_selectionEndPoint = viewportToContents(pos); + m_selectionRect = QRect(m_selectionBeginPoint, m_selectionEndPoint).normalize(); + if (m_selectionRect.left() < 0) m_selectionRect.setLeft(0); + if (m_selectionRect.top() < 0) m_selectionRect.setTop(0); + if (m_selectionRect.right() >= contentsWidth()) m_selectionRect.setRight(contentsWidth() - 1); + if (m_selectionRect.bottom() >= contentsHeight()) m_selectionRect.setBottom(contentsHeight() - 1); + + if ( (m_selectionBeginPoint - m_selectionEndPoint).manhattanLength() > QApplication::startDragDistance() ) { + m_isSelecting = true; + selectNotesIn(m_selectionRect, m_selectionInvert); + updateContents(m_selectionRect); + m_noActionOnMouseRelease = true; + } else { + // If the user was selecting but cancel by making the rectangle too small, cancel it really!!! + if (m_isSelecting) { + if (m_selectionInvert) + selectNotesIn(QRect(), m_selectionInvert); + else + unselectAllBut(0); // TODO: unselectAll(); + } + if (m_isSelecting) + resetWasInLastSelectionRect(); + m_isSelecting = false; + stopAutoScrollSelection(); + return; + } + + // Do the auto-scrolling: + // FIXME: It's still flickering + + QRect insideRect(AUTO_SCROLL_MARGIN, AUTO_SCROLL_MARGIN, visibleWidth() - 2*AUTO_SCROLL_MARGIN, visibleHeight() - 2*AUTO_SCROLL_MARGIN); + + int dx = 0; + int dy = 0; + + if (pos.y() < AUTO_SCROLL_MARGIN) + dy = pos.y() - AUTO_SCROLL_MARGIN; + else if (pos.y() > visibleHeight() - AUTO_SCROLL_MARGIN) + dy = pos.y() - visibleHeight() + AUTO_SCROLL_MARGIN; + + if (pos.x() < AUTO_SCROLL_MARGIN) + dx = pos.x() - AUTO_SCROLL_MARGIN; + else if (pos.x() > visibleWidth() - AUTO_SCROLL_MARGIN) + dx = pos.x() - visibleWidth() + AUTO_SCROLL_MARGIN; + + if (dx || dy) { + kapp->sendPostedEvents(); // Do the repaints, because the scrolling will make the area to repaint to be wrong + scrollBy(dx, dy); + if (!m_autoScrollSelectionTimer.isActive()) + m_autoScrollSelectionTimer.start(AUTO_SCROLL_DELAY); + } else + stopAutoScrollSelection(); +} + +void Basket::stopAutoScrollSelection() +{ + m_autoScrollSelectionTimer.stop(); +} + +void Basket::resetWasInLastSelectionRect() +{ + Note *note = m_firstNote; + while (note) { + note->resetWasInLastSelectionRect(); + note = note->next(); + } +} + +void Basket::selectAll() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->selectAll(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->selectAll(); + } else { + // First select all in the group, then in the parent group... + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + while (parent) { + if (!parent->allSelected()) { + parent->setSelectedRecursivly(true); + return; + } + child = parent; + parent = parent->parentNote(); + } + // Then, select all: + FOR_EACH_NOTE (note) + note->setSelectedRecursivly(true); + } +} + +void Basket::unselectAll() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) { + m_editor->textEdit()->removeSelection(); + selectionChangedInEditor(); // THIS IS NOT EMITED BY Qt!!! + } else if (m_editor->lineEdit()) + m_editor->lineEdit()->deselect(); + } else { + if (countSelecteds() > 0) // Optimisation + FOR_EACH_NOTE (note) + note->setSelectedRecursivly(false); + } +} + +void Basket::invertSelection() +{ + FOR_EACH_NOTE (note) + note->invertSelectionRecursivly(); +} + +void Basket::unselectAllBut(Note *toSelect) +{ + FOR_EACH_NOTE (note) + note->unselectAllBut(toSelect); +} + +void Basket::invertSelectionOf(Note *toSelect) +{ + FOR_EACH_NOTE (note) + note->invertSelectionOf(toSelect); +} + +void Basket::selectNotesIn(const QRect &rect, bool invertSelection, bool unselectOthers /*= true*/) +{ + FOR_EACH_NOTE (note) + note->selectIn(rect, invertSelection, unselectOthers); +} + +void Basket::doHoverEffects() +{ + doHoverEffects( viewportToContents( viewport()->mapFromGlobal(QCursor::pos()) ) ); +} + +void Basket::doHoverEffects(Note *note, Note::Zone zone, const QPoint &pos) +{ + // Inform the old and new hovered note (if any): + Note *oldHoveredNote = m_hoveredNote; + if (note != m_hoveredNote) { + if (m_hoveredNote) { + m_hoveredNote->setHovered(false); + m_hoveredNote->setHoveredZone(Note::None); + updateNote(m_hoveredNote); + } + m_hoveredNote = note; + if (note) + note->setHovered(true); + } + + // If we are hovering a note, compute which zone is hovered and inform the note: + if (m_hoveredNote) { + if (zone != m_hoveredZone || oldHoveredNote != m_hoveredNote) { + m_hoveredZone = zone; + m_hoveredNote->setCursor(zone); + updateNote(m_hoveredNote); + } + m_hoveredNote->setHoveredZone(zone); + // If we are hovering an insert line zone, update this thing: + if (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn) + placeInserter(m_hoveredNote, zone); + else + removeInserter(); + // If we are hovering an embedded link in a rich text element, show the destination in the statusbar: + if (zone == Note::Link) + emit setStatusBarText(m_hoveredNote->linkAt( pos - QPoint(m_hoveredNote->x(), m_hoveredNote->y()) )); + else if (m_hoveredNote->content()) + emit setStatusBarText(m_hoveredNote->content()->statusBarMessage(m_hoveredZone));//resetStatusBarText(); + // If we aren't hovering a note, reset all: + } else { + if (isFreeLayout() && !isSelecting()) + viewport()->setCursor(Qt::CrossCursor); + else + viewport()->unsetCursor(); + m_hoveredZone = Note::None; + removeInserter(); + emit resetStatusBarText(); + } +} + +void Basket::doHoverEffects(const QPoint &pos) +{ +// if (isDuringEdit()) +// viewport()->unsetCursor(); + + // Do we have the right to do hover effects? + if ( ! m_loaded || m_lockedHovering) + return; + + // enterEvent() (mouse enter in the widget) set m_underMouse to true, and leaveEvent() make it false. + // But some times the enterEvent() is not trigerred: eg. when dragging the scrollbar: + // Ending the drag INSIDE the basket area will make NO hoverEffects() because m_underMouse is false. + // User need to leave the area and re-enter it to get effects. + // This hack solve that by dismissing the m_underMouse variable: + bool underMouse = Global::bnpView->currentBasket() == this && QRect(contentsX(), contentsY(), visibleWidth(), visibleHeight()).contains(pos); + + // Don't do hover effects when a popup menu is opened. + // Primarily because the basket area will only receive mouseEnterEvent and mouveLeaveEvent. + // It willn't be noticed of mouseMoveEvent, which would result in a apparently broken application state: + if (kapp->activePopupWidget()) + underMouse = false; + + // Compute which note is hovered: + Note *note = (m_isSelecting || !underMouse ? 0 : noteAt(pos.x(), pos.y())); + Note::Zone zone = (note ? note->zoneAt( pos - QPoint(note->x(), note->y()), isDuringDrag() ) : Note::None); + + // Inform the old and new hovered note (if any) and update the areas: + doHoverEffects(note, zone, pos); +} + +void Basket::mouseEnteredEditorWidget() +{ + if (!m_lockedHovering && !kapp->activePopupWidget()) + doHoverEffects(editedNote(), Note::Content, QPoint()); +} + +void Basket::removeInserter() +{ + if (m_inserterShown) { // Do not hide (and then update/repaint the view) if it is already hidden! + m_inserterShown = false; + updateContents(m_inserterRect); + } +} + +void Basket::placeInserter(Note *note, int zone) +{ + // Remove the inserter: + if (!note) { + removeInserter(); + return; + } + + // Update the old position: + if (inserterShown()) + updateContents(m_inserterRect); + // Some comodities: + m_inserterShown = true; + m_inserterTop = (zone == Note::TopGroup || zone == Note::TopInsert); + m_inserterGroup = (zone == Note::TopGroup || zone == Note::BottomGroup); + // X and width: + int groupIndent = (note->isGroup() ? note->width() : Note::HANDLE_WIDTH); + int x = note->x(); + int width = (note->isGroup() ? note->rightLimit() - note->x() : note->width()); + if (m_inserterGroup) { + x += groupIndent; + width -= groupIndent; + } + m_inserterSplit = (Settings::groupOnInsertionLine() && note && !note->isGroup() && !note->isFree() && !note->isColumn()); +// if (note->isGroup()) +// width = note->rightLimit() - note->x() - (m_inserterGroup ? groupIndent : 0); + // Y: + int y = note->y() - (m_inserterGroup && m_inserterTop ? 1 : 3); + if (!m_inserterTop) + y += (note->isColumn() ? note->finalHeight() : note->height()); + // Assigning result: + m_inserterRect = QRect(x, y, width, 6 - (m_inserterGroup ? 2 : 0)); + // Update the new position: + updateContents(m_inserterRect); +} + +inline void drawLineByRect(QPainter &painter, int x, int y, int width, int height) +{ + painter.drawLine(x, y, x + width - 1, y + height - 1); +} + +void Basket::drawInserter(QPainter &painter, int xPainter, int yPainter) +{ + if (!m_inserterShown) + return; + + QRect rect = m_inserterRect; // For shorter code-lines when drawing! + rect.moveBy(-xPainter, -yPainter); + int lineY = (m_inserterGroup && m_inserterTop ? 0 : 2); + int roundY = (m_inserterGroup && m_inserterTop ? 0 : 1); + + QColor dark = KApplication::palette().active().dark(); + QColor light = dark.light().light(); + if (m_inserterGroup && Settings::groupOnInsertionLine()) + light = Tools::mixColor(light, KGlobalSettings::highlightColor()); + painter.setPen(dark); + // The horizontal line: + //painter.drawRect( rect.x(), rect.y() + lineY, rect.width(), 2); + int width = rect.width() - 4; + drawGradient(&painter, dark, light, rect.x() + 2, rect.y() + lineY, width/2, 2, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + drawGradient(&painter, light, dark, rect.x() + 2 + width/2, rect.y() + lineY, width - width/2, 2, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + // The left-most and right-most edges (biggest vertical lines): + drawLineByRect(painter, rect.x(), rect.y(), 1, (m_inserterGroup ? 4 : 6)); + drawLineByRect(painter, rect.x() + rect.width() - 1, rect.y(), 1, (m_inserterGroup ? 4 : 6)); + // The left and right mid vertical lines: + drawLineByRect(painter, rect.x() + 1, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); + drawLineByRect(painter, rect.x() + rect.width() - 2, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); + // Draw the split as a feedback to know where is the limit between insert and group: + if (m_inserterSplit) { + int noteWidth = rect.width() + (m_inserterGroup ? Note::HANDLE_WIDTH : 0); + int xSplit = rect.x() - (m_inserterGroup ? Note::HANDLE_WIDTH : 0) + noteWidth / 2; + painter.setPen(Tools::mixColor(dark, light)); + painter.drawRect(xSplit - 2, rect.y() + lineY, 4, 2); + painter.setPen(dark); + painter.drawRect(xSplit - 1, rect.y() + lineY, 2, 2); + } +} + +void Basket::maybeTip(const QPoint &pos) +{ + if ( !m_loaded || !Settings::showNotesToolTip() ) + return; + + QString message; + QRect rect; + + QPoint contentPos = viewportToContents(pos); + Note *note = noteAt(contentPos.x(), contentPos.y()); + + if (!note && isFreeLayout()) { + message = i18n("Insert note here\nRight click for more options"); + QRect itRect; + for (QValueList<QRect>::iterator it = m_blankAreas.begin(); it != m_blankAreas.end(); ++it) { + itRect = QRect(0, 0, visibleWidth(), visibleHeight()).intersect(*it); + if (itRect.contains(contentPos)) { + rect = itRect; + rect.moveLeft(rect.left() - contentsX()); + rect.moveTop( rect.top() - contentsY()); + break; + } + } + } else { + if (!note) + return; + + Note::Zone zone = note->zoneAt( contentPos - QPoint(note->x(), note->y()) ); + switch (zone) { + case Note::Resizer: message = (note->isColumn() ? + i18n("Resize those columns") : + (note->isGroup() ? + i18n("Resize this group") : + i18n("Resize this note"))); break; + case Note::Handle: message = i18n("Select or move this note"); break; + case Note::Group: message = i18n("Select or move this group"); break; + case Note::TagsArrow: message = i18n("Assign or remove tags from this note"); + if (note->states().count() > 0) { + message = "<qt><nobr>" + message + "</nobr><br>" + i18n("<b>Assigned Tags</b>: %1"); + QString tagsString = ""; + for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) { + QString tagName = "<nobr>" + Tools::textToHTMLWithoutP((*it)->fullName()) + "</nobr>"; + if (tagsString.isEmpty()) + tagsString = tagName; + else + tagsString = i18n("%1, %2").arg(tagsString, tagName); + } + message = message.arg(tagsString); + } + break; + case Note::Custom0: message = note->content()->zoneTip(zone); break; //"Open this link/Open this file/Open this sound file/Launch this application" + case Note::GroupExpander: message = (note->isFolded() ? + i18n("Expand this group") : + i18n("Collapse this group")); break; + case Note::Link: + case Note::Content: message = note->content()->editToolTipText(); break; + case Note::TopInsert: + case Note::BottomInsert: message = i18n("Insert note here\nRight click for more options"); break; + case Note::TopGroup: message = i18n("Group note with the one below\nRight click for more options"); break; + case Note::BottomGroup: message = i18n("Group note with the one above\nRight click for more options"); break; + case Note::BottomColumn: message = i18n("Insert note here\nRight click for more options"); break; + case Note::None: message = "** Zone NONE: internal error **"; break; + default: + if (zone >= Note::Emblem0) + message = note->stateForEmblemNumber(zone - Note::Emblem0)->fullName(); + else + message = ""; + break; + } + + if (zone == Note::Content || zone == Note::Link || zone == Note::Custom0) { + QStringList keys; + QStringList values; + + note->content()->toolTipInfos(&keys, &values); + keys.append(i18n("Added")); + keys.append(i18n("Last Modification")); + values.append(note->addedStringDate()); + values.append(note->lastModificationStringDate()); + + message = "<qt><nobr>" + message; + QStringList::iterator key; + QStringList::iterator value; + for (key = keys.begin(), value = values.begin(); key != keys.end() && value != values.end(); ++key, ++value) + message += "<br>" + i18n("of the form 'key: value'", "<b>%1</b>: %2").arg(*key, *value); + message += "</nobr></qt>"; + } else if (m_inserterSplit && (zone == Note::TopInsert || zone == Note::BottomInsert)) + message += "\n" + i18n("Click on the right to group instead of insert"); + else if (m_inserterSplit && (zone == Note::TopGroup || zone == Note::BottomGroup)) + message += "\n" + i18n("Click on the left to insert instead of group"); + + rect = note->zoneRect( zone, contentPos - QPoint(note->x(), note->y()) ); + + rect.moveLeft(rect.left() - contentsX()); + rect.moveTop( rect.top() - contentsY()); + + rect.moveLeft(rect.left() + note->x()); + rect.moveTop( rect.top() + note->y()); + } + + tip(rect, message); +} + +Note* Basket::lastNote() +{ + Note *note = firstNote(); + while (note && note->next()) + note = note->next(); + return note; +} + +void Basket::deleteNotes() +{ + Note *note = m_firstNote; + + while (note){ + Note *tmp = note->next(); + delete note; + note = tmp; + } + m_firstNote = 0; + m_resizingNote = 0; + m_movingNote = 0; + m_focusedNote = 0; + m_startOfShiftSelectionNote = 0; + m_tagPopupNote = 0; + m_clickedToInsert = 0; + m_savedClickedToInsert = 0; + m_hoveredNote = 0; + m_count = 0; + m_countFounds = 0; + m_countSelecteds = 0; + + emit resetStatusBarText(); + emit countsChanged(this); +} + +Note* Basket::noteAt(int x, int y) +{ +//NO: +// // Do NOT check the bottom&right borders. +// // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), +// // the note is first removed, and relayoutNotes() compute the new height that is smaller +// // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! +// // Should, of course, not return 0: + if (x < 0 || x > contentsWidth() || y < 0 || y > contentsHeight()) + return 0; + + // When resizing a note/group, keep it highlighted: + if (m_resizingNote) + return m_resizingNote; + + // Search and return the hovered note: + Note *note = m_firstNote; + Note *possibleNote; + while (note) { + possibleNote = note->noteAt(x, y); + if (possibleNote) { + if (draggedNotes().contains(possibleNote)) + return 0; + else + return possibleNote; + } + note = note->next(); + } + + // If the basket is layouted in columns, return one of the columns to be able to add notes in them: + if (isColumnsLayout()) { + Note *column = m_firstNote; + while (column) { + if (x >= column->x() && x < column->rightLimit()) + return column; + column = column->next(); + } + } + + // Nothing found, no note is hovered: + return NULL; +} + +Basket::~Basket() +{ + //delete m_action; + if(m_decryptBox) + delete m_decryptBox; +#ifdef HAVE_LIBGPGME + delete m_gpg; +#endif + deleteNotes(); +} + +void Basket::viewportResizeEvent(QResizeEvent *event) +{ + relayoutNotes(true); + //cornerWidget()->setShown(horizontalScrollBar()->isShown() && verticalScrollBar()->isShown()); + if (horizontalScrollBar()->isShown() && verticalScrollBar()->isShown()) { + if (!cornerWidget()) + setCornerWidget(m_cornerWidget); + } else { + if (cornerWidget()) + setCornerWidget(0); + } +// if (isDuringEdit()) +// ensureNoteVisible(editedNote()); + QScrollView::viewportResizeEvent(event); +} + +void Basket::animateLoad() +{ + const int viewHeight = contentsY() + visibleHeight(); + + QTime t = QTime::currentTime(); // Set random seed + srand(t.hour()*12 + t.minute()*60 + t.second()*60); + + Note *note = firstNote(); + while (note) { + if ((note->finalY() < viewHeight) && note->matching()) + note->initAnimationLoad(); + note = note->next(); + } + + m_loaded = true; +} + +QColor Basket::selectionRectInsideColor() +{ + return Tools::mixColor(Tools::mixColor(backgroundColor(), KGlobalSettings::highlightColor()), backgroundColor()); +} + +QColor alphaBlendColors(const QColor &bgColor, const QColor &fgColor, const int a) +{ + // normal button... + QRgb rgb = bgColor.rgb(); + QRgb rgb_b = fgColor.rgb(); + int alpha = a; + if (alpha>255) alpha = 255; + if (alpha<0) alpha = 0; + int inv_alpha = 255 - alpha; + QColor result = QColor( qRgb(qRed(rgb_b)*inv_alpha/255 + qRed(rgb)*alpha/255, + qGreen(rgb_b)*inv_alpha/255 + qGreen(rgb)*alpha/255, + qBlue(rgb_b)*inv_alpha/255 + qBlue(rgb)*alpha/255) ); + + return result; +} + +void Basket::unlock() +{ + QTimer::singleShot( 0, this, SLOT(load()) ); +} + +void Basket::inactivityAutoLockTimeout() +{ + lock(); +} + +void Basket::drawContents(QPainter *painter, int clipX, int clipY, int clipWidth, int clipHeight) +{ + // Start the load the first time the basket is shown: + if (!m_loadingLaunched) + { + if(!m_locked) + QTimer::singleShot( 0, this, SLOT(load()) ); + else { + Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar + } + } + + QBrush brush(backgroundColor()); // FIXME: share it for all the basket? + QRect clipRect(clipX, clipY, clipWidth, clipHeight); + + if(m_locked) + { + if(!m_decryptBox) + { + m_decryptBox = new QFrame( this, "m_decryptBox" ); + m_decryptBox->setFrameShape( QFrame::StyledPanel ); + m_decryptBox->setFrameShadow( QFrame::Plain ); + m_decryptBox->setLineWidth( 1 ); + + QGridLayout* layout = new QGridLayout( m_decryptBox, 1, 1, 11, 6, "decryptBoxLayout"); + +#ifdef HAVE_LIBGPGME + m_button = new QPushButton( m_decryptBox, "button" ); + m_button->setText( i18n( "&Unlock" ) ); + layout->addWidget( m_button, 1, 2 ); + connect( m_button, SIGNAL( clicked() ), this, SLOT( unlock() ) ); +#endif + QLabel* label = new QLabel( m_decryptBox, "label" ); + QString text = "<b>" + i18n("Password protected basket.") + "</b><br/>"; +#ifdef HAVE_LIBGPGME + label->setText( text + i18n("Press Unlock to access it.") ); +#else + label->setText( text + i18n("Encryption is not supported by<br/>this version of %1.").arg(kapp->aboutData()->programName()) ); +#endif + label->setAlignment( int( QLabel::AlignTop ) ); + layout->addMultiCellWidget( label, 0, 0, 1, 2 ); + QLabel* pixmap = new QLabel( m_decryptBox, "pixmap" ); + pixmap->setPixmap( KGlobal::iconLoader()->loadIcon("encrypted", KIcon::NoGroup, KIcon::SizeHuge) ); + layout->addMultiCellWidget( pixmap, 0, 1, 0, 0 ); + + QSpacerItem* spacer = new QSpacerItem( 40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum ); + layout->addItem( spacer, 1, 1 ); + + label = new QLabel("<small>" + + i18n("To make baskets stay unlocked, change the automatic<br>" + "locking duration in the application settings.") + "</small>", + m_decryptBox); + //label->setFixedWidth(label->sizeHint().width() / 2); + label->setAlignment( int( QLabel::AlignTop ) ); + layout->addMultiCellWidget( label, 2,2,0,2 ); + + m_decryptBox->resize(layout->sizeHint()); + } + if(m_decryptBox->isHidden()) + { + m_decryptBox->show(); + } +#ifdef HAVE_LIBGPGME + m_button->setFocus(); +#endif + m_decryptBox->move((visibleWidth() - m_decryptBox->width()) / 2, + (visibleHeight() - m_decryptBox->height()) / 2); + } + else + { + if(m_decryptBox && m_decryptBox->isShown()) + m_decryptBox->hide(); + } + + // Draw notes (but not when it's not loaded or locked yet): + Note *note = ((m_loaded || m_locked) ? m_firstNote : 0); + while (note) { + note->draw(painter, clipRect); + note = note->next(); + } + enableActions(); + + // Draw loading message: + if (!m_loaded) { + QPixmap pixmap(visibleWidth(), visibleHeight()); // TODO: Clip it to asked size only! + QPainter painter2(&pixmap); + QSimpleRichText rt(QString("<center>%1</center>").arg(i18n("Loading...")), QScrollView::font()); + rt.setWidth(visibleWidth()); + int hrt = rt.height(); + painter2.fillRect(0, 0, visibleWidth(), visibleHeight(), brush); + blendBackground(painter2, QRect(0, 0, visibleWidth(), visibleHeight()), -1, -1, /*opaque=*/true); + QColorGroup cg = colorGroup(); + cg.setColor(QColorGroup::Text, textColor()); + rt.draw(&painter2, 0, (visibleHeight() - hrt) / 2, QRect(), cg); + painter2.end(); + painter->drawPixmap(0, 0, pixmap); + return; // TODO: Clip to the wanted rectangle + } + + // We will draw blank areas below. + // For each rectangle to be draw there is three ways to do so: + // - The rectangle is full of the background COLOR => we fill a rect directly on screen + // - The rectangle is full of the background PIXMAP => we draw it directly on screen (we draw m_opaqueBackgroundPixmap that is not transparent) + // - The rectangle contains the resizer => We draw it on an offscreen buffer and then paint the buffer on screen + // If the background image is not tiled, we know that recomputeBlankRects() broken rects so that they are full of either background pixmap or color, but not a mix. + + // Draw blank areas (see the last preparation above): + QPixmap pixmap; + QPainter painter2; + QRect rect; + for (QValueList<QRect>::iterator it = m_blankAreas.begin(); it != m_blankAreas.end(); ++it) { + rect = clipRect.intersect(*it); + if (rect.width() > 0 && rect.height() > 0) { + // If there is an inserter to draw, draw the image off screen, + // apply the inserter and then draw the image on screen: + if ( (inserterShown() && rect.intersects(inserterRect())) || (m_isSelecting && rect.intersects(m_selectionRect)) ) { + pixmap.resize(rect.width(), rect.height()); + painter2.begin(&pixmap); + painter2.fillRect(0, 0, rect.width(), rect.height(), backgroundColor()); + blendBackground(painter2, rect, -1, -1, /*opaque=*/true); + // Draw inserter: + if (inserterShown() && rect.intersects(inserterRect())) + drawInserter(painter2, rect.x(), rect.y()); + // Draw selection rect: + if (m_isSelecting && rect.intersects(m_selectionRect)) { + QRect selectionRect = m_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) { + QColor insideColor = selectionRectInsideColor(); + painter2.fillRect(selectionRectInside, insideColor); + selectionRectInside.moveBy(rect.x(), rect.y()); + blendBackground(painter2, selectionRectInside, rect.x(), rect.y(), true, /*&*/m_selectedBackgroundPixmap); + } + painter2.setPen(KGlobalSettings::highlightColor().dark()); + painter2.drawRect(selectionRect); + painter2.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), backgroundColor())); + painter2.drawPoint(selectionRect.topLeft()); + painter2.drawPoint(selectionRect.topRight()); + painter2.drawPoint(selectionRect.bottomLeft()); + painter2.drawPoint(selectionRect.bottomRight()); + } + painter2.end(); + painter->drawPixmap(rect.x(), rect.y(), pixmap); + // If it's only a blank rectangle to draw, draw it directly on screen (faster!!!): + } else if ( ! hasBackgroundImage() ) { + painter->fillRect(rect, backgroundColor()); + // It's either a background pixmap to draw or a background color to fill: + } else { + if (isTiledBackground() || (rect.x() < backgroundPixmap()->width() && rect.y() < backgroundPixmap()->height())) + blendBackground(*painter, rect, 0, 0, /*opaque=*/true); + else + painter->fillRect(rect, backgroundColor()); + } + } + } +} + +/* rect(x,y,width,height)==(xBackgroundToDraw,yBackgroundToDraw,widthToDraw,heightToDraw) + */ +void Basket::blendBackground(QPainter &painter, const QRect &rect, int xPainter, int yPainter, bool opaque, QPixmap *bg) +{ + if (xPainter == -1 && yPainter == -1) { + xPainter = rect.x(); + yPainter = rect.y(); + } + + if (hasBackgroundImage()) { + const QPixmap *bgPixmap = (bg ? /* * */bg : (opaque ? m_opaqueBackgroundPixmap : m_backgroundPixmap)); + if (isTiledBackground()) + painter.drawTiledPixmap(rect.x() - xPainter, rect.y() - yPainter, rect.width(), rect.height(), *bgPixmap, rect.x(), rect.y()); + else + painter.drawPixmap(rect.x() - xPainter, rect.y() - yPainter, *bgPixmap, rect.x(), rect.y(), rect.width(), rect.height()); + } +} + +void Basket::recomputeBlankRects() +{ + m_blankAreas.clear(); + m_blankAreas.append( QRect(0, 0, contentsWidth(), contentsHeight()) ); + + FOR_EACH_NOTE (note) + note->recomputeBlankRects(m_blankAreas); + + // See the drawing of blank areas in Basket::drawContents() + if (hasBackgroundImage() && ! isTiledBackground()) + substractRectOnAreas( QRect(0, 0, backgroundPixmap()->width(), backgroundPixmap()->height()), m_blankAreas, false ); +} + +void Basket::addAnimatedNote(Note *note) +{ + if (m_animatedNotes.isEmpty()) { + m_animationTimer.start(FRAME_DELAY); + m_lastFrameTime = QTime::currentTime(); + } + + m_animatedNotes.append(note); +} + +void Basket::unsetNotesWidth() +{ + Note *note = m_firstNote; + while (note) { + note->unsetWidth(); + note = note->next(); + } +} + +void Basket::relayoutNotes(bool animate) +{ + if (Global::bnpView->currentBasket() != this) + return; // Optimize load time, and basket will be relaid out when activated, anyway + + if (!Settings::playAnimations()) + animate = false; + + if (!animate) { + m_animatedNotes.clear(); + m_animationTimer.stop(); + } + + int h = 0; + tmpWidth = 0; + tmpHeight = 0; + Note *note = m_firstNote; + while (note) { + if (note->matching()) { + note->relayoutAt(0, h, animate); + if (note->hasResizer()) { + int minGroupWidth = note->minRight() - note->finalX(); + if (note->groupWidth() < minGroupWidth) { + note->setGroupWidth(minGroupWidth); + relayoutNotes(animate); // Redo the thing, but this time it should not recurse + return; + } + } + h += note->finalHeight(); + } + note = note->next(); + } + + if (isFreeLayout()) + tmpHeight += 100; + else + tmpHeight += 15; + + resizeContents( QMAX(tmpWidth, visibleWidth()), QMAX(tmpHeight, visibleHeight()) ); + recomputeBlankRects(); + placeEditor(); + doHoverEffects(); + updateContents(); +} + +void Basket::updateNote(Note *note) +{ + updateContents(note->rect()); + if (note->hasResizer()) + updateContents(note->resizerRect()); +} + +void Basket::animateObjects() +{ + QValueList<Note*>::iterator it; + for (it = m_animatedNotes.begin(); it != m_animatedNotes.end(); ) +// if ((*it)->y() >= contentsY() && (*it)->bottom() <= contentsY() + contentsWidth()) +// updateNote(*it); + if ((*it)->advance()) + it = m_animatedNotes.remove(it); + else { +// if ((*it)->y() >= contentsY() && (*it)->bottom() <= contentsY() + contentsWidth()) +// updateNote(*it); + ++it; + } + + if (m_animatedNotes.isEmpty()) { + // Stop animation timer: + m_animationTimer.stop(); + // Reset all onTop notes: + Note* note = m_firstNote; + while (note) { + note->setOnTop(false); + note = note->next(); + } + } + + if (m_focusedNote) + ensureNoteVisible(m_focusedNote); + + // We refresh content if it was the last frame, + // or if the drawing of the last frame was not too long. + if (!m_animationTimer.isActive() || (m_lastFrameTime.msecsTo(QTime::currentTime()) < FRAME_DELAY*11/10)) { // *11/10 == *1.1 : We keep a 0.1 security margin + m_lastFrameTime = m_lastFrameTime.addMSecs(FRAME_DELAY); // because timers are not accurate and can trigger late + //m_lastFrameTime = QTime::currentTime(); +//std::cout << ">>" << m_lastFrameTime.toString("hh:mm:ss.zzz") << std::endl; + if (m_underMouse) + doHoverEffects(); + recomputeBlankRects(); + //relayoutNotes(true); // In case an animated note was to the contents view boundaries, resize the view! + updateContents(); + // If the drawing of the last frame was too long, we skip the drawing of the current and do the next one: + } else { + m_lastFrameTime = m_lastFrameTime.addMSecs(FRAME_DELAY); +//std::cout << "+=" << m_lastFrameTime.toString("hh:mm:ss.zzz") << std::endl; + animateObjects(); + } + + doHoverEffects(); + placeEditor(); + +/* int delta = m_deltaY / 3; + if (delta == 0 && m_deltaY != 0) + delta = (m_deltaY > 0 ? 1 : -1); + m_deltaY -= delta; + resizeContents(contentsWidth(), contentsHeight() + delta); //m_lastNote->y() + m_lastNote->height() +*/ +} + +void Basket::popupEmblemMenu(Note *note, int emblemNumber) +{ + m_tagPopupNote = note; + State *state = note->stateForEmblemNumber(emblemNumber); + State *nextState = state->nextState(/*cycle=*/false); + Tag *tag = state->parentTag(); + m_tagPopup = tag; + + QKeySequence sequence = tag->shortcut().operator QKeySequence(); + bool sequenceOnDelete = (nextState == 0 && !tag->shortcut().isNull()); + + KPopupMenu menu(this); + if (tag->countStates() == 1) { + menu.insertTitle(/*SmallIcon(state->icon()), */tag->name()); + menu.insertItem( SmallIconSet("editdelete"), i18n("&Remove"), 1 ); + menu.insertItem( SmallIconSet("configure"), i18n("&Customize..."), 2 ); + menu.insertSeparator(); + menu.insertItem( SmallIconSet("filter"), i18n("&Filter by this Tag"), 3 ); + } else { + menu.insertTitle(tag->name()); + QValueList<State*>::iterator it; + State *currentState; + + int i = 10; + for (it = tag->states().begin(); it != tag->states().end(); ++it) { + currentState = *it; + QKeySequence sequence; + if (currentState == nextState && !tag->shortcut().isNull()) + sequence = tag->shortcut().operator QKeySequence(); + menu.insertItem(StateMenuItem::radioButtonIconSet(state == currentState, menu.colorGroup()), new StateMenuItem(currentState, sequence, false), i ); + if (currentState == nextState && !tag->shortcut().isNull()) + menu.setAccel(sequence, i); + ++i; + } + menu.insertSeparator(); + menu.insertItem( new IndentedMenuItem(i18n("&Remove"), "editdelete", (sequenceOnDelete ? sequence : QKeySequence())), 1 ); + menu.insertItem( new IndentedMenuItem(i18n("&Customize..."), "configure"), 2 ); + menu.insertSeparator(); + menu.insertItem( new IndentedMenuItem(i18n("&Filter by this Tag"), "filter"), 3 ); + menu.insertItem( new IndentedMenuItem(i18n("Filter by this &State"), "filter"), 4 ); + } + if (sequenceOnDelete) + menu.setAccel(sequence, 1); + + connect( &menu, SIGNAL(activated(int)), this, SLOT(toggledStateInMenu(int)) ); + connect( &menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( &menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + + m_lockedHovering = true; + menu.exec(QCursor::pos()); +} + +void Basket::toggledStateInMenu(int id) +{ + if (id == 1) { + removeTagFromSelectedNotes(m_tagPopup); + //m_tagPopupNote->removeTag(m_tagPopup); + //m_tagPopupNote->setWidth(0); // To force a new layout computation + updateEditorAppearance(); + filterAgain(); + save(); + return; + } + if (id == 2) { // Customize this State: + TagsEditDialog dialog(this, m_tagPopupNote->stateOfTag(m_tagPopup)); + dialog.exec(); + return; + } + if (id == 3) { // Filter by this Tag + decoration()->filterBar()->filterTag(m_tagPopup); + return; + } + if (id == 4) { // Filter by this State + decoration()->filterBar()->filterState(m_tagPopupNote->stateOfTag(m_tagPopup)); + return; + } + + /*addStateToSelectedNotes*/changeStateOfSelectedNotes(m_tagPopup->states()[id - 10] /*, orReplace=true*/); + //m_tagPopupNote->addState(m_tagPopup->states()[id - 10], true); + filterAgain(); + save(); +} + +State* Basket::stateForTagFromSelectedNotes(Tag *tag) +{ + State *state = 0; + + FOR_EACH_NOTE (note) + if (note->stateForTagFromSelectedNotes(tag, &state) && state == 0) + return 0; + return state; +} + +void Basket::activatedTagShortcut(Tag *tag) +{ + // Compute the next state to set: + State *state = stateForTagFromSelectedNotes(tag); + if (state) + state = state->nextState(/*cycle=*/false); + else + state = tag->states().first(); + + // Set or unset it: + if (state) { + FOR_EACH_NOTE (note) + note->addStateToSelectedNotes(state, /*orReplace=*/true); + updateEditorAppearance(); + } else + removeTagFromSelectedNotes(tag); + + filterAgain(); + save(); +} + +void Basket::popupTagsMenu(Note *note) +{ + m_tagPopupNote = note; + + KPopupMenu menu(this); + menu.insertTitle(i18n("Tags")); +// QValueList<Tag*>::iterator it; +// Tag *currentTag; +// State *currentState; +// int i = 10; +// for (it = Tag::all.begin(); it != Tag::all.end(); ++it) { +// // Current tag and first state of it: +// currentTag = *it; +// currentState = currentTag->states().first(); +// QKeySequence sequence; +// if (!currentTag->shortcut().isNull()) +// sequence = currentTag->shortcut().operator QKeySequence(); +// menu.insertItem(StateMenuItem::checkBoxIconSet(note->hasTag(currentTag), menu.colorGroup()), new StateMenuItem(currentState, sequence, true), i ); +// if (!currentTag->shortcut().isNull()) +// menu.setAccel(sequence, i); +// ++i; +// } +// +// menu.insertSeparator(); +// // menu.insertItem( /*SmallIconSet("editdelete"),*/ "&Assign New Tag...", 1 ); +// //id = menu.insertItem( SmallIconSet("editdelete"), "&Remove All", -2 ); +// //if (note->states().isEmpty()) +// // menu.setItemEnabled(id, false); +// // menu.insertItem( SmallIconSet("configure"), "&Customize...", 3 ); +// menu.insertItem( new IndentedMenuItem(i18n("&Assign New Tag...")), 1 ); +// menu.insertItem( new IndentedMenuItem(i18n("&Remove All"), "editdelete"), 2 ); +// menu.insertItem( new IndentedMenuItem(i18n("&Customize..."), "configure"), 3 ); +// +// if (!selectedNotesHaveTags())//note->states().isEmpty()) +// menu.setItemEnabled(2, false); +// +// connect( &menu, SIGNAL(activated(int)), this, SLOT(toggledTagInMenu(int)) ); +// connect( &menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); +// connect( &menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + + Global::bnpView->populateTagsMenu(menu, note); + + m_lockedHovering = true; + menu.exec(QCursor::pos()); +} + +void Basket::unlockHovering() +{ + m_lockedHovering = false; + doHoverEffects(); +} + +void Basket::toggledTagInMenu(int id) +{ + if (id == 1) { // Assign new Tag... + TagsEditDialog dialog(this, /*stateToEdit=*/0, /*addNewTag=*/true); + dialog.exec(); + if (!dialog.addedStates().isEmpty()) { + State::List states = dialog.addedStates(); + for (State::List::iterator itState = states.begin(); itState != states.end(); ++itState) + FOR_EACH_NOTE (note) + note->addStateToSelectedNotes(*itState); + updateEditorAppearance(); + filterAgain(); + save(); + } + return; + } + if (id == 2) { // Remove All + removeAllTagsFromSelectedNotes(); + filterAgain(); + save(); + return; + } + if (id == 3) { // Customize... + TagsEditDialog dialog(this); + dialog.exec(); + return; + } + + Tag *tag = Tag::all[id - 10]; + if (!tag) + return; + + if (m_tagPopupNote->hasTag(tag)) + removeTagFromSelectedNotes(tag); + else + addTagToSelectedNotes(tag); + m_tagPopupNote->setWidth(0); // To force a new layout computation + filterAgain(); + save(); +} + +void Basket::addTagToSelectedNotes(Tag *tag) +{ + FOR_EACH_NOTE (note) + note->addTagToSelectedNotes(tag); + updateEditorAppearance(); +} + +void Basket::removeTagFromSelectedNotes(Tag *tag) +{ + FOR_EACH_NOTE (note) + note->removeTagFromSelectedNotes(tag); + updateEditorAppearance(); +} + +void Basket::addStateToSelectedNotes(State *state) +{ + FOR_EACH_NOTE (note) + note->addStateToSelectedNotes(state); + updateEditorAppearance(); +} + +void Basket::updateEditorAppearance() +{ + if (isDuringEdit() && m_editor->widget()) { + m_editor->widget()->setFont(m_editor->note()->font()); + m_editor->widget()->setPaletteBackgroundColor(m_editor->note()->backgroundColor()); + m_editor->widget()->setPaletteForegroundColor(m_editor->note()->textColor()); + + // Uggly Hack arround Qt bugs: placeCursor() don't call any signal: + HtmlEditor *htmlEditor = dynamic_cast<HtmlEditor*>(m_editor); + if (htmlEditor) { + int para, index; + m_editor->textEdit()->getCursorPosition(¶, &index); + if (para == 0 && index == 0) { + m_editor->textEdit()->moveCursor(QTextEdit::MoveForward, /*select=*/false); + m_editor->textEdit()->moveCursor(QTextEdit::MoveBackward, /*select=*/false); + } else { + m_editor->textEdit()->moveCursor(QTextEdit::MoveBackward, /*select=*/false); + m_editor->textEdit()->moveCursor(QTextEdit::MoveForward, /*select=*/false); + } + htmlEditor->cursorPositionChanged(); // Does not work anyway :-( (when clicking on a red bold text, the toolbar still show black normal text) + } + } +} + +void Basket::editorPropertiesChanged() +{ + if (isDuringEdit() && m_editor->note()->content()->type() == NoteType::Html) { + m_editor->textEdit()->setAutoFormatting(Settings::autoBullet() ? QTextEdit::AutoAll : QTextEdit::AutoNone); + } +} + +void Basket::changeStateOfSelectedNotes(State *state) +{ + FOR_EACH_NOTE (note) + note->changeStateOfSelectedNotes(state); + updateEditorAppearance(); +} + +void Basket::removeAllTagsFromSelectedNotes() +{ + FOR_EACH_NOTE (note) + note->removeAllTagsFromSelectedNotes(); + updateEditorAppearance(); +} + +bool Basket::selectedNotesHaveTags() +{ + FOR_EACH_NOTE (note) + if (note->selectedNotesHaveTags()) + return true; + return false; +} + +QColor Basket::backgroundColor() +{ + if (m_backgroundColorSetting.isValid()) + return m_backgroundColorSetting; + else + return KGlobalSettings::baseColor(); +} + +QColor Basket::textColor() +{ + if (m_textColorSetting.isValid()) + return m_textColorSetting; + else + return KGlobalSettings::textColor(); +} + +void Basket::unbufferizeAll() +{ + FOR_EACH_NOTE (note) + note->unbufferizeAll(); +} + +Note* Basket::editedNote() +{ + if (m_editor) + return m_editor->note(); + else + return 0; +} + +bool Basket::hasTextInEditor() +{ + if (!isDuringEdit() || !redirectEditActions()) + return false; + + if (m_editor->textEdit()) + return ! m_editor->textEdit()->text().isEmpty(); + else if (m_editor->lineEdit()) + return ! m_editor->lineEdit()->text().isEmpty(); + else + return false; +} + +bool Basket::hasSelectedTextInEditor() +{ + if (!isDuringEdit() || !redirectEditActions()) + return false; + + if (m_editor->textEdit()) { + // The following line does NOT work if one letter is selected and the user press Shift+Left or Shift+Right to unselect than letter: + // Qt misteriously tell us there is an invisible selection!! + //return m_editor->textEdit()->hasSelectedText(); + return !m_editor->textEdit()->selectedText().isEmpty(); + } else if (m_editor->lineEdit()) + return m_editor->lineEdit()->hasSelectedText(); + else + return false; +} + +bool Basket::selectedAllTextInEditor() +{ + if (!isDuringEdit() || !redirectEditActions()) + return false; + + if (m_editor->textEdit()) + return m_editor->textEdit()->text().isEmpty() || m_editor->textEdit()->text() == m_editor->textEdit()->selectedText(); + else if (m_editor->lineEdit()) + return m_editor->lineEdit()->text().isEmpty() || m_editor->lineEdit()->text() == m_editor->lineEdit()->selectedText(); + else + return false; +} + +void Basket::selectionChangedInEditor() +{ + Global::bnpView->notesStateChanged(); +} + +void Basket::contentChangedInEditor() +{ + // Do not wait 3 seconds, because we need the note to expand as needed (if a line is too wider... the note should grow wider): + if (m_editor->textEdit()) + m_editor->autoSave(/*toFileToo=*/false); +// else { + if (m_inactivityAutoSaveTimer.isActive()) + m_inactivityAutoSaveTimer.stop(); + m_inactivityAutoSaveTimer.start(3 * 1000, /*singleShot=*/true); + Global::bnpView->setUnsavedStatus(true); +// } +} + +void Basket::inactivityAutoSaveTimeout() +{ + if (m_editor) + m_editor->autoSave(/*toFileToo=*/true); +} + +void Basket::placeEditorAndEnsureVisible() +{ + placeEditor(/*andEnsureVisible=*/true); +} + +void Basket::placeEditor(bool /*andEnsureVisible*/ /*= false*/) +{ + if (!isDuringEdit()) + return; + + QFrame *editorQFrame = dynamic_cast<QFrame*>(m_editor->widget()); + KTextEdit *textEdit = m_editor->textEdit(); +// QLineEdit *lineEdit = m_editor->lineEdit(); + Note *note = m_editor->note(); + + int frameWidth = (editorQFrame ? editorQFrame->frameWidth() : 0); + int x = note->x() + note->contentX() + note->content()->xEditorIndent() - frameWidth; + int y; + int maxHeight = QMAX(visibleHeight(), contentsHeight()); + int height, width; + + if (textEdit) { + x -= 4; + // Need to do it 2 times, because it's wrong overwise + // (sometimes, width depends on height, and sometimes, height depends on with): + for (int i = 0; i < 2; i++) { + // FIXME: CRASH: Select all text, press Del or [<--] and editor->removeSelectedText() is called: + // editor->sync() CRASH!! + // editor->sync(); + y = note->y() + Note::NOTE_MARGIN - frameWidth; + height = textEdit->contentsHeight() + 2*frameWidth; +// height = /*QMAX(*/height/*, note->height())*/; +// height = QMIN(height, visibleHeight()); + width = note->x() + note->width() - x + 1;// /*note->x() + note->width()*/note->rightLimit() - x + 2*frameWidth + 1; +//width=QMAX(width,textEdit->contentsWidth()+2*frameWidth); + if (y + height > maxHeight) + y = maxHeight - height; + textEdit->setFixedSize(width, height); + } + } else { + height = note->height() - 2*Note::NOTE_MARGIN + 2*frameWidth; + width = note->x() + note->width() - x;//note->rightLimit() - x + 2*frameWidth; + m_editor->widget()->setFixedSize(width, height); + x -= 1; + y = note->y() + Note::NOTE_MARGIN - frameWidth; + } + if ((m_editorWidth > 0 && m_editorWidth != width) || (m_editorHeight > 0 && m_editorHeight != height)) { + m_editorWidth = width; // Avoid infinite recursion!!! + m_editorHeight = height; + m_editor->autoSave(/*toFileToo=*/true); + } + m_editorWidth = width; + m_editorHeight = height; + addChild(m_editor->widget(), x, y); + m_editorX = x; + m_editorY = y; + + m_leftEditorBorder->setFixedSize( (m_editor->textEdit() ? 3 : 0), height); +// m_leftEditorBorder->raise(); + addChild(m_leftEditorBorder, x, y ); + m_leftEditorBorder->setPosition( x, y ); + + m_rightEditorBorder->setFixedSize(3, height); +// m_rightEditorBorder->raise(); +// addChild(m_rightEditorBorder, note->rightLimit() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); +// m_rightEditorBorder->setPosition( note->rightLimit() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); + addChild(m_rightEditorBorder, note->x() + note->width() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); + m_rightEditorBorder->setPosition( note->x() + note->width() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); + +// if (andEnsureVisible) +// ensureNoteVisible(note); +} + +#include <iostream> +#include <private/qrichtext_p.h> +void Basket::editorCursorPositionChanged() +{ + if (!isDuringEdit()) + return; + + FocusedTextEdit *textEdit = (FocusedTextEdit*) m_editor->textEdit(); + const QTextCursor *cursor = textEdit->textCursor(); +// std::cout << cursor->x() << ";" << cursor->y() << " " +// << cursor->globalX() << ";" << cursor->globalY() << " " +// << cursor->offsetX() << ";" << cursor->offsetY() << ";" << std::endl; + + ensureVisible(m_editorX + cursor->globalX(), m_editorY + cursor->globalY(), 50, 50); +} + +void Basket::closeEditorDelayed() +{ + setFocus(); + QTimer::singleShot( 0, this, SLOT(closeEditor()) ); +} + +bool Basket::closeEditor() +{ + if (!isDuringEdit()) + return true; + + if (m_doNotCloseEditor) + return true; + + if (m_redirectEditActions) { + disconnect( m_editor->widget(), SIGNAL(selectionChanged()), this, SLOT(selectionChangedInEditor()) ); + if (m_editor->textEdit()) { + disconnect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(selectionChangedInEditor()) ); + disconnect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(contentChangedInEditor()) ); + } else if (m_editor->lineEdit()) { + disconnect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(selectionChangedInEditor()) ); + disconnect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(contentChangedInEditor()) ); + } + } + m_editor->widget()->disconnect(); + m_editor->widget()->hide(); + m_editor->validate(); + + delete m_leftEditorBorder; + delete m_rightEditorBorder; + m_leftEditorBorder = 0; + m_rightEditorBorder = 0; + + Note *note = m_editor->note(); + note->setWidth(0); // For relayoutNotes() to succeed to take care of the change + + // Delete the editor BEFORE unselecting the note because unselecting the note would trigger closeEditor() recursivly: + bool isEmpty = m_editor->isEmpty(); + delete m_editor; + m_editor = 0; + m_redirectEditActions = false; + m_editorWidth = -1; + m_editorHeight = -1; + m_inactivityAutoSaveTimer.stop(); + + // Delete the note if it is now empty: + if (isEmpty) { + focusANonSelectedNoteAboveOrThenBelow(); + note->setSelected(true); + note->deleteSelectedNotes(); + save(); + note = 0; + } + + unlockHovering(); + filterAgain(/*andEnsureVisible=*/false); + +// Does not work: +// if (Settings::playAnimations()) +// note->setOnTop(true); // So if it grew, do not obscure it temporarily while the notes below it are moving + + if (note) + note->setSelected(false);//unselectAll(); + doHoverEffects(); +// save(); + + Global::bnpView->m_actEditNote->setEnabled( !isLocked() && countSelecteds() == 1 /*&& !isDuringEdit()*/ ); + + emit resetStatusBarText(); // Remove the "Editing. ... to validate." text. + + //if (kapp->activeWindow() == Global::mainContainer) + + // Set focus to the basket, unless the user pressed a letter key in the filter bar and the currently edited note came hidden, then editing closed: + if (!decoration()->filterBar()->lineEdit()->hasFocus()) + setFocus(); + + // Return true if the note is still there: + return (note != 0); +} + +void Basket::closeBasket() +{ + closeEditor(); + unbufferizeAll(); // Keep the memory footprint low + if (isEncrypted()) { + if (Settings::enableReLockTimeout()) { + int seconds = Settings::reLockTimeoutMinutes() * 60; + m_inactivityAutoLockTimer.start(seconds * 1000, /*singleShot=*/true); + } + } +} + +void Basket::openBasket() +{ + if (m_inactivityAutoLockTimer.isActive()) + m_inactivityAutoLockTimer.stop(); +} + +Note* Basket::theSelectedNote() +{ + if (countSelecteds() != 1) { + std::cout << "NO SELECTED NOTE !!!!" << std::endl; + return 0; + } + + Note *selectedOne; + FOR_EACH_NOTE (note) { + selectedOne = note->theSelectedNote(); + if (selectedOne) + return selectedOne; + } + + std::cout << "One selected note, BUT NOT FOUND !!!!" << std::endl; + + return 0; +} + +void debugSel(NoteSelection* sel, int n = 0) +{ + for (NoteSelection *node = sel; node; node = node->next) { + for (int i = 0; i < n; i++) + std::cout << "-"; + std::cout << (node->firstChild ? "Group" : node->note->content()->toText("")) << std::endl; + if (node->firstChild) + debugSel(node->firstChild, n+1); + } +} + +NoteSelection* Basket::selectedNotes() +{ + NoteSelection selection; + + FOR_EACH_NOTE (note) + selection.append(note->selectedNotes()); + + if (!selection.firstChild) + return 0; + + for (NoteSelection *node = selection.firstChild; node; node = node->next) + node->parent = 0; + + // If the top-most groups are columns, export only childs of those groups + // (because user is not consciencious that columns are groups, and don't care: it's not what she want): + if (selection.firstChild->note->isColumn()) { + NoteSelection tmpSelection; + NoteSelection *nextNode; + NoteSelection *nextSubNode; + for (NoteSelection *node = selection.firstChild; node; node = nextNode) { + nextNode = node->next; + if (node->note->isColumn()) { + for (NoteSelection *subNode = node->firstChild; subNode; subNode = nextSubNode) { + nextSubNode = subNode->next; + tmpSelection.append(subNode); + subNode->parent = 0; + subNode->next = 0; + } + } else { + tmpSelection.append(node); + node->parent = 0; + node->next = 0; + } + } +// debugSel(tmpSelection.firstChild); + return tmpSelection.firstChild; + } else { +// debugSel(selection.firstChild); + return selection.firstChild; + } +} + +void Basket::showEditedNoteWhileFiltering() +{ + if (m_editor) { + Note *note = m_editor->note(); + filterAgain(); + note->setSelected(true); + relayoutNotes(false); + note->setX(note->finalX()); + note->setY(note->finalY()); + filterAgainDelayed(); + } +} + +void Basket::noteEdit(Note *note, bool justAdded, const QPoint &clickedPoint) // TODO: Remove the first parameter!!! +{ + if (!note) + note = theSelectedNote(); // TODO: Or pick the focused note! + if (!note) + return; + + if (isDuringEdit()) { + closeEditor(); // Validate the noteeditors in KLineEdit that does not intercept Enter key press (and edit is triggered with Enter too... Can conflict) + return; + } + + if (note != m_focusedNote) { + setFocusedNote(note); + m_startOfShiftSelectionNote = note; + } + + if (justAdded && isFiltering()) { + QTimer::singleShot( 0, this, SLOT(showEditedNoteWhileFiltering()) ); + } + + doHoverEffects(note, Note::Content); // Be sure (in the case Edit was triggered by menu or Enter key...): better feedback! + //m_lockedHovering = true; + + //m_editorWidget = note->content()->launchEdit(this); + NoteEditor *editor = NoteEditor::editNoteContent(note->content(), this); + + if (editor->widget()) { + m_editor = editor; + m_leftEditorBorder = new TransparentWidget(this); + m_rightEditorBorder = new TransparentWidget(this); + m_editor->widget()->reparent(viewport(), QPoint(0,0), true); + m_leftEditorBorder->reparent(viewport(), QPoint(0,0), true); + m_rightEditorBorder->reparent(viewport(), QPoint(0,0), true); + addChild(m_editor->widget(), 0, 0); + placeEditorAndEnsureVisible(); // placeEditor(); // FIXME: After? + m_redirectEditActions = m_editor->lineEdit() || m_editor->textEdit(); + if (m_redirectEditActions) { + connect( m_editor->widget(), SIGNAL(selectionChanged()), this, SLOT(selectionChangedInEditor()) ); + // In case there is NO text, "Select All" is disabled. But if the user press a key the there is now a text: + // selection has not changed but "Select All" should be re-enabled: + if (m_editor->textEdit()) { + connect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(selectionChangedInEditor()) ); + connect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(contentChangedInEditor()) ); + } else if (m_editor->lineEdit()) { + connect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(selectionChangedInEditor()) ); + connect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(contentChangedInEditor()) ); + } + } + m_editor->widget()->show(); + //m_editor->widget()->raise(); + m_editor->widget()->setFocus(); + connect( m_editor, SIGNAL(askValidation()), this, SLOT(closeEditorDelayed()) ); + connect( m_editor, SIGNAL(mouseEnteredEditorWidget()), this, SLOT(mouseEnteredEditorWidget()) ); + if (m_editor->textEdit()) { + connect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(placeEditorAndEnsureVisible()) ); + if (clickedPoint != QPoint()) { + QPoint pos(clickedPoint.x() - note->x() - note->contentX() + m_editor->textEdit()->frameWidth() + 4 - m_editor->textEdit()->frameWidth(), + clickedPoint.y() - note->y() - m_editor->textEdit()->frameWidth()); + // Do it right before the kapp->processEvents() to not have the cursor to quickly flicker at end (and sometimes stay at end AND where clicked): + m_editor->textEdit()->moveCursor(KTextEdit::MoveHome, false); + m_editor->textEdit()->ensureCursorVisible(); + m_editor->textEdit()->placeCursor(pos); + updateEditorAppearance(); + } + } +// kapp->processEvents(); // Show the editor toolbar before ensuring the note is visible + ensureNoteVisible(note); // because toolbar can create a new line and then partially hide the note + m_editor->widget()->setFocus(); // When clicking in the basket, a QTimer::singleShot(0, ...) focus the basket! So we focus the the widget after kapp->processEvents() + emit resetStatusBarText(); // Display "Editing. ... to validate." + } else { + // Delete the note user have canceled the addition: + if ((justAdded && editor->canceled()) || editor->isEmpty() /*) && editor->note()->states().count() <= 0*/) { + focusANonSelectedNoteAboveOrThenBelow(); + editor->note()->setSelected(true); + editor->note()->deleteSelectedNotes(); + save(); + } + delete editor; + unlockHovering(); + filterAgain(); + unselectAll(); + } + Global::bnpView->m_actEditNote->setEnabled(false); +} + +void Basket::noteDelete() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->del(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->del(); + return; + } + + if (countSelecteds() <= 0) + return; + int really = KMessageBox::Yes; + if (Settings::confirmNoteDeletion()) + really = KMessageBox::questionYesNo( this, + i18n("<qt>Do you really want to delete this note?</qt>", + "<qt>Do you really want to delete those <b>%n</b> notes?</qt>", + countSelecteds()), + i18n("Delete Note", "Delete Notes", countSelecteds()) +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + , KStdGuiItem::del(), KStdGuiItem::cancel()); +#else + ); +#endif + if (really == KMessageBox::No) + return; + + noteDeleteWithoutConfirmation(); +} + +void Basket::focusANonSelectedNoteBelow(bool inSameColumn) +{ + // First focus another unselected one below it...: + if (m_focusedNote && m_focusedNote->isSelected()) { + Note *next = m_focusedNote->nextShownInStack(); + while (next && next->isSelected()) + next = next->nextShownInStack(); + if (next) { + if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == next->parentPrimaryNote()) { + setFocusedNote(next); + m_startOfShiftSelectionNote = next; + } + } + } +} + +void Basket::focusANonSelectedNoteAbove(bool inSameColumn) +{ + // ... Or above it: + if (m_focusedNote && m_focusedNote->isSelected()) { + Note *prev = m_focusedNote->prevShownInStack(); + while (prev && prev->isSelected()) + prev = prev->prevShownInStack(); + if (prev) { + if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == prev->parentPrimaryNote()) { + setFocusedNote(prev); + m_startOfShiftSelectionNote = prev; + } + } + } +} + +void Basket::focusANonSelectedNoteBelowOrThenAbove() +{ + focusANonSelectedNoteBelow(/*inSameColumn=*/true); + focusANonSelectedNoteAbove(/*inSameColumn=*/true); + focusANonSelectedNoteBelow(/*inSameColumn=*/false); + focusANonSelectedNoteAbove(/*inSameColumn=*/false); +} + +void Basket::focusANonSelectedNoteAboveOrThenBelow() +{ + focusANonSelectedNoteAbove(/*inSameColumn=*/true); + focusANonSelectedNoteBelow(/*inSameColumn=*/true); + focusANonSelectedNoteAbove(/*inSameColumn=*/false); + focusANonSelectedNoteBelow(/*inSameColumn=*/false); +} + +void Basket::noteDeleteWithoutConfirmation(bool deleteFilesToo) +{ + // If the currently focused note is selected, it will be deleted. + focusANonSelectedNoteBelowOrThenAbove(); + + // Do the deletion: + Note *note = firstNote(); + Note *next; + while (note) { + next = note->next(); // If we delete 'note' on the next line, note->next() will be 0! + note->deleteSelectedNotes(deleteFilesToo); + note = next; + } + + relayoutNotes(true); // FIXME: filterAgain()? + save(); +} + +void Basket::doCopy(CopyMode copyMode) +{ + QClipboard *cb = KApplication::clipboard(); + QClipboard::Mode mode = (copyMode == CopyToSelection ? QClipboard::Selection : QClipboard::Clipboard); + + NoteSelection *selection = selectedNotes(); + int countCopied = countSelecteds(); + if (selection->firstStacked()) { + QDragObject *d = NoteDrag::dragObject(selection, copyMode == CutToClipboard, /*source=*/0); // d will be deleted by QT +// /*bool shouldRemove = */d->drag(); +// delete selection; + cb->setData(d, mode); // NoteMultipleDrag will be deleted by QT +// if (copyMode == CutToClipboard && !note->useFile()) // If useFile(), NoteDrag::dragObject() will delete it TODO +// note->slotDelete(); + + if (copyMode == CutToClipboard) + noteDeleteWithoutConfirmation(/*deleteFilesToo=*/false); + + switch (copyMode) { + default: + case CopyToClipboard: emit postMessage(i18n("Copied note to clipboard.", "Copied notes to clipboard.", countCopied)); break; + case CutToClipboard: emit postMessage(i18n("Cut note to clipboard.", "Cut notes to clipboard.", countCopied)); break; + case CopyToSelection: emit postMessage(i18n("Copied note to selection.", "Copied notes to selection.", countCopied)); break; + } + } +} + +void Basket::noteCopy() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->copy(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->copy(); + } else + doCopy(CopyToClipboard); +} + +void Basket::noteCut() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->cut(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->cut(); + } else + doCopy(CutToClipboard); +} + +void Basket::noteOpen(Note *note) +{ + /* + GetSelectedNotes + NoSelectedNote || Count == 0 ? return + AllTheSameType ? + Get { url, message(count) } + */ + + // TODO: Open ALL selected notes! + if (!note) + note = theSelectedNote(); + if (!note) + return; + + KURL url = note->content()->urlToOpen(/*with=*/false); + QString message = note->content()->messageWhenOpenning(NoteContent::OpenOne /*NoteContent::OpenSeveral*/); + if (url.isEmpty()) { + if (message.isEmpty()) + emit postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); + else { + int result = KMessageBox::warningContinueCancel(this, message, /*caption=*/QString::null, KGuiItem(i18n("&Edit"), "edit")); + if (result == KMessageBox::Continue) + noteEdit(note); + } + } else { + emit postMessage(message); // "Openning link target..." / "Launching application..." / "Openning note file..." + // Finally do the opening job: + QString customCommand = note->content()->customOpenCommand(); + if (customCommand.isEmpty()) { + KRun *run = new KRun(url); + run->setAutoDelete(true); + } else + KRun::run(customCommand, url); + } +} + +/** Code from bool KRun::displayOpenWithDialog(const KURL::List& lst, bool tempFiles) + * It does not allow to set a text, so I ripped it to do that: + */ +bool KRun__displayOpenWithDialog(const KURL::List& lst, bool tempFiles, const QString &text) +{ + if (kapp && !kapp->authorizeKAction("openwith")) { + KMessageBox::sorry(0L, i18n("You are not authorized to open this file.")); // TODO: Better message, i18n freeze :-( + return false; + } + KOpenWithDlg l(lst, text, QString::null, 0L); + if (l.exec()) { + KService::Ptr service = l.service(); + if (!!service) + return KRun::run(*service, lst, tempFiles); + //kdDebug(250) << "No service set, running " << l.text() << endl; + return KRun::run(l.text(), lst); // TODO handle tempFiles + } + return false; +} + +void Basket::noteOpenWith(Note *note) +{ + if (!note) + note = theSelectedNote(); + if (!note) + return; + + KURL url = note->content()->urlToOpen(/*with=*/true); + QString message = note->content()->messageWhenOpenning(NoteContent::OpenOneWith /*NoteContent::OpenSeveralWith*/); + QString text = note->content()->messageWhenOpenning(NoteContent::OpenOneWithDialog /*NoteContent::OpenSeveralWithDialog*/); + if (url.isEmpty()) + emit postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); + else if (KRun__displayOpenWithDialog(url, false, text)) + emit postMessage(message); // "Openning link target with..." / "Openning note file with..." +} + +void Basket::noteSaveAs() +{ +// if (!note) +// note = theSelectedNote(); + Note *note = theSelectedNote(); + if (!note) + return; + + KURL url = note->content()->urlToOpen(/*with=*/false); + if (url.isEmpty()) + return; + + QString fileName = KFileDialog::getSaveFileName(url.fileName(), note->content()->saveAsFilters(), this, i18n("Save to File")); + // TODO: Ask to overwrite ! + if (fileName.isEmpty()) + return; + + // TODO: Convert format, etc. (use NoteContent::saveAs(fileName)) + KIO::copy(url, KURL(fileName)); +} + +Note* Basket::selectedGroup() +{ + FOR_EACH_NOTE (note) { + Note *selectedGroup = note->selectedGroup(); + if (selectedGroup) { + // If the selected group is one group in a column, then return that group, and not the column, + // because the column is not ungrouppage, and the Ungroup action would be disabled. + if (selectedGroup->isColumn() && selectedGroup->firstChild() && !selectedGroup->firstChild()->next()) { + return selectedGroup->firstChild(); + } + return selectedGroup; + } + } + return 0; +} + +bool Basket::selectionIsOneGroup() +{ + return (selectedGroup() != 0); +} + +Note* Basket::firstSelected() +{ + Note *first = 0; + FOR_EACH_NOTE (note) { + first = note->firstSelected(); + if (first) + return first; + } + return 0; +} + +Note* Basket::lastSelected() +{ + Note *last = 0, *tmp = 0; + FOR_EACH_NOTE (note) { + tmp = note->lastSelected(); + if (tmp) + last = tmp; + } + return last; +} + +bool Basket::convertTexts() +{ + m_watcher->stopScan(); + bool convertedNotes = false; + + if (!isLoaded()) + load(); + + FOR_EACH_NOTE (note) + if (note->convertTexts()) + convertedNotes = true; + + if (convertedNotes) + save(); + m_watcher->startScan(); + return convertedNotes; +} + +void Basket::noteGroup() +{ +/* // Nothing to do? + if (isLocked() || countSelecteds() <= 1) + return; + + // If every selected notes are ALREADY in one group, then don't touch anything: + Note *selectedGroup = this->selectedGroup(); + if (selectedGroup && !selectedGroup->isColumn()) + return; +*/ + + // Copied from BNPView::updateNotesActions() + bool severalSelected = countSelecteds() >= 2; + Note *selectedGroup = (severalSelected ? this->selectedGroup() : 0); + bool enabled = !isLocked() && severalSelected && (!selectedGroup || selectedGroup->isColumn()); + if (!enabled) + return; + + // Get the first selected note: we will group selected items just before: + Note *first = firstSelected(); +// if (selectedGroup != 0 || first == 0) +// return; + + m_loaded = false; // Hack to avoid notes to be unselected and new notes to be selected: + + // Create and insert the receiving group: + Note *group = new Note(this); + if (first->isFree()) { + insertNote(group, 0L, Note::BottomColumn, QPoint(first->finalX(), first->finalY()), /*animateNewPosition=*/false); + } else { + insertNote(group, first, Note::TopInsert, QPoint(), /*animateNewPosition=*/false); + } + + // Put a FAKE UNSELECTED note in the new group, so if the new group is inside an allSelected() group, the parent group is not moved inside the new group! + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + insertNote(fakeNote, group, Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + + // Group the notes: + Note *nextNote; + Note *note = firstNote(); + while (note) { + nextNote = note->next(); + note->groupIn(group); + note = nextNote; + } + + m_loaded = true; // Part 2 / 2 of the workarround! + + // Do cleanup: + unplugNote(fakeNote); + unselectAll(); + group->setSelectedRecursivly(true); // Notes were unselected by unplugging + + relayoutNotes(true); + save(); +} + +void Basket::noteUngroup() +{ + Note *group = selectedGroup(); + if (group && !group->isColumn()) + ungroupNote(group); + save(); +} + +void Basket::unplugSelection(NoteSelection *selection) +{ + for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) + unplugNote(toUnplug->note); +} + +void Basket::insertSelection(NoteSelection *selection, Note *after) +{ + for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { + if (toUnplug->note->isGroup()) { + Note *group = new Note(this); + insertNote(group, after, Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + insertNote(fakeNote, group, Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + insertSelection(toUnplug->firstChild, fakeNote); + unplugNote(fakeNote); + after = group; + } else { + Note *note = toUnplug->note; + note->setPrev(0); + note->setNext(0); + insertNote(note, after, Note::BottomInsert, QPoint(), /*animateNewPosition=*/true); + after = note; + } + } +} + +void Basket::selectSelection(NoteSelection *selection) +{ + for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { + if (toUnplug->note->isGroup()) + selectSelection(toUnplug); + else + toUnplug->note->setSelected(true); + } +} + +void Basket::noteMoveOnTop() +{ + // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket + // TODO: Move on top/bottom... of the column or basjet + + NoteSelection *selection = selectedNotes(); + unplugSelection(selection); + // Replug the notes: + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + if (isColumnsLayout()) { + if (firstNote()->firstChild()) + insertNote(fakeNote, firstNote()->firstChild(), Note::TopInsert, QPoint(), /*animateNewPosition=*/false); + else + insertNote(fakeNote, firstNote(), Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + } else { + // TODO: Also allow to move notes on top of a group!!!!!!! + insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); + } + insertSelection(selection, fakeNote); + unplugNote(fakeNote); + selectSelection(selection); + relayoutNotes(true); + save(); +} + +void Basket::noteMoveOnBottom() +{ + + // TODO: Duplicate code: void noteMoveOn(); + + // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket + // TODO: Move on top/bottom... of the column or basjet + + NoteSelection *selection = selectedNotes(); + unplugSelection(selection); + // Replug the notes: + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + if (isColumnsLayout()) + insertNote(fakeNote, firstNote(), Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + else { + // TODO: Also allow to move notes on top of a group!!!!!!! + insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); + } + insertSelection(selection, fakeNote); + unplugNote(fakeNote); + selectSelection(selection); + relayoutNotes(true); + save(); +} + +void Basket::moveSelectionTo(Note *here, bool below/* = true*/) +{ + NoteSelection *selection = selectedNotes(); + unplugSelection(selection); + // Replug the notes: + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); +// if (isColumnsLayout()) + insertNote(fakeNote, here, (below ? Note::BottomInsert : Note::TopInsert), QPoint(), /*animateNewPosition=*/false); +// else { +// // TODO: Also allow to move notes on top of a group!!!!!!! +// insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); +// } + insertSelection(selection, fakeNote); + unplugNote(fakeNote); + selectSelection(selection); + relayoutNotes(true); + save(); +} + +void Basket::noteMoveNoteUp() +{ + + // TODO: Move between columns, even if they are empty !!!!!!! + + // TODO: if first note of a group, move just above the group! And let that even if there is no note before that group!!! + + Note *first = firstSelected(); + Note *previous = first->prevShownInStack(); + if (previous) + moveSelectionTo(previous, /*below=*/false); +} + +void Basket::noteMoveNoteDown() +{ + Note *first = lastSelected(); + Note *next = first->nextShownInStack(); + if (next) + moveSelectionTo(next, /*below=*/true); +} + +void Basket::wheelEvent(QWheelEvent *event) +{ + QScrollView::wheelEvent(event); +} + +void Basket::linkLookChanged() +{ + Note *note = m_firstNote; + while (note) { + note->linkLookChanged(); + note = note->next(); + } + relayoutNotes(true); +} + +void Basket::slotCopyingDone2(KIO::Job *job) +{ + if (job->error()) { + DEBUG_WIN << "Copy finished, ERROR"; + return; + } + KIO::FileCopyJob *fileCopyJob = (KIO::FileCopyJob*)job; + Note *note = noteForFullPath(fileCopyJob->destURL().path()); + DEBUG_WIN << "Copy finished, load note: " + fileCopyJob->destURL().path() + (note ? "" : " --- NO CORRESPONDING NOTE"); + if (note != 0L) { + note->content()->loadFromFile(/*lazyLoad=*/false); + if(isEncrypted()) + note->content()->saveToFile(); + if (m_focusedNote == note) // When inserting a new note we ensure it visble + ensureNoteVisible(note); // But after loading it has certainly grown and if it was + } // on bottom of the basket it's not visible entirly anymore +} + +Note* Basket::noteForFullPath(const QString &path) +{ + Note *note = firstNote(); + Note *found; + while (note) { + found = note->noteForFullPath(path); + if (found) + return found; + note = note->next(); + } + return 0; +} + +void Basket::deleteFiles() +{ + m_watcher->stopScan(); + Tools::deleteRecursively(fullPath()); +} + +QValueList<State*> Basket::usedStates() +{ + QValueList<State*> states; + FOR_EACH_NOTE (note) + note->usedStates(states); + return states; +} + +QString Basket::saveGradientBackground(const QColor &color, const QFont &font, const QString &folder) +{ + // Construct file name and return if the file already exists: + QString fileName = "note_background_" + color.name().lower().mid(1) + ".png"; + QString fullPath = folder + fileName; + if (QFile::exists(fullPath)) + return fileName; + + // Get the gradient top and bottom colors: + QColor topBgColor; + QColor bottomBgColor; + Note::getGradientColors(color, &topBgColor, &bottomBgColor); + + // Draw and save the gradient image: + int sampleTextHeight = QFontMetrics(font) + .boundingRect(0, 0, /*width=*/10000, /*height=*/0, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, "Test text") + .height(); + QPixmap noteGradient(100, sampleTextHeight + Note::NOTE_MARGIN); + QPainter painter(¬eGradient); + drawGradient(&painter, topBgColor, bottomBgColor, 0, 0, noteGradient.width(), noteGradient.height(), /*sunken=*/false, /*horz=*/true, /*flat=*/false); + painter.end(); + noteGradient.save(fullPath, "PNG"); + + // Return the name of the created file: + return fileName; +} + +void Basket::listUsedTags(QValueList<Tag*> &list) +{ + if (!isLoaded()) { + load(); + } + + FOR_EACH_NOTE (child) + child->listUsedTags(list); +} + + +/** Unfocus the previously focused note (unless it was null) + * and focus the new @param note (unless it is null) if hasFocus() + * Update m_focusedNote to the new one + */ +void Basket::setFocusedNote(Note *note) // void Basket::changeFocusTo(Note *note) +{ + // Don't focus an hidden note: + if (note != 0L && !note->isShown()) + return; + // When clicking a group, this group gets focused. But only content-based notes should be focused: + if (note && note->isGroup()) + note = note->firstRealChild(); + // The first time a note is focused, it becomes the start of the Shift selection: + if (m_startOfShiftSelectionNote == 0) + m_startOfShiftSelectionNote = note; + // Unfocus the old focused note: + if (m_focusedNote != 0L) + m_focusedNote->setFocused(false); + // Notify the new one to draw a focus rectangle... only if the basket is focused: + if (hasFocus() && note != 0L) + note->setFocused(true); + // Save the new focused note: + m_focusedNote = note; +} + +/** If no shown note is currently focused, try to find a shown note and focus it + * Also update m_focusedNote to the new one (or null if there isn't) + */ +void Basket::focusANote() +{ + if (countFounds() == 0) { // No note to focus + setFocusedNote(0L); +// m_startOfShiftSelectionNote = 0; + return; + } + + if (m_focusedNote == 0L) { // No focused note yet : focus the first shown + Note *toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); + setFocusedNote(toFocus); +// m_startOfShiftSelectionNote = m_focusedNote; + return; + } + + // Search a visible note to focus if the focused one isn't shown : + Note *toFocus = m_focusedNote; + if (toFocus && !toFocus->isShown()) + toFocus = toFocus->nextShownInStack(); + if (!toFocus && m_focusedNote) + toFocus = m_focusedNote->prevShownInStack(); + setFocusedNote(toFocus); +// m_startOfShiftSelectionNote = toFocus; +} + +Note* Basket::firstNoteInStack() +{ + if (!firstNote()) + return 0; + + if (firstNote()->content()) + return firstNote(); + else + return firstNote()->nextInStack(); +} + +Note* Basket::lastNoteInStack() +{ + Note *note = lastNote(); + while (note) { + if (note->content()) + return note; + Note *possibleNote = note->lastRealChild(); + if (possibleNote && possibleNote->content()) + return possibleNote; + note = note->prev(); + } + return 0; +} + +Note* Basket::firstNoteShownInStack() +{ + Note *first = firstNoteInStack(); + while (first && !first->isShown()) + first = first->nextInStack(); + return first; +} + +Note* Basket::lastNoteShownInStack() +{ + Note *last = lastNoteInStack(); + while (last && !last->isShown()) + last = last->prevInStack(); + return last; +} + +inline int abs(int n) +{ + return (n < 0 ? -n : n); +} + +Note* Basket::noteOn(NoteOn side) +{ + Note *bestNote = 0; + int distance = -1; + int bestDistance = contentsWidth() * contentsHeight() * 10; + + Note *note = firstNoteShownInStack(); + Note *primary = m_focusedNote->parentPrimaryNote(); + while (note) { + switch (side) { + case LEFT_SIDE: distance = m_focusedNote->distanceOnLeftRight(note, LEFT_SIDE); break; + case RIGHT_SIDE: distance = m_focusedNote->distanceOnLeftRight(note, RIGHT_SIDE); break; + case TOP_SIDE: distance = m_focusedNote->distanceOnTopBottom(note, TOP_SIDE); break; + case BOTTOM_SIDE: distance = m_focusedNote->distanceOnTopBottom(note, BOTTOM_SIDE); break; + } + if ((side == TOP_SIDE || side == BOTTOM_SIDE || primary != note->parentPrimaryNote()) && note != m_focusedNote && distance > 0 && distance < bestDistance) { + bestNote = note; + bestDistance = distance; + } + note = note ->nextShownInStack(); + } + + return bestNote; +} + +Note* Basket::firstNoteInGroup() +{ + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + while (parent) { + if (parent->firstChild() != child && !parent->isColumn()) + return parent->firstRealChild(); + child = parent; + parent = parent->parentNote(); + } + return 0; +} + +Note* Basket::noteOnHome() +{ + // First try to find the first note of the group containing the focused note: + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + while (parent) { + if (parent->nextShownInStack() != m_focusedNote) + return parent->nextShownInStack(); + child = parent; + parent = parent->parentNote(); + } + + // If it was not found, then focus the very first note in the basket: + if (isFreeLayout()) { + Note *first = firstNoteShownInStack(); // The effective first note found + Note *note = first; // The current note, to conpare with the previous first note, if this new note is more on top + if (note) + note = note->nextShownInStack(); + while (note) { + if (note->finalY() < first->finalY() || (note->finalY() == first->finalY() && note->finalX() < first->finalX())) + first = note; + note = note->nextShownInStack(); + } + return first; + } else + return firstNoteShownInStack(); +} + +Note* Basket::noteOnEnd() +{ + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + Note *lastChild; + while (parent) { + lastChild = parent->lastRealChild(); + if (lastChild && lastChild != m_focusedNote) { + if (lastChild->isShown()) + return lastChild; + lastChild = lastChild->prevShownInStack(); + if (lastChild && lastChild->isShown() && lastChild != m_focusedNote) + return lastChild; + } + child = parent; + parent = parent->parentNote(); + } + if (isFreeLayout()) { + Note *last; + Note *note; + last = note = firstNoteShownInStack(); + note = note->nextShownInStack(); + while (note) { + if (note->finalBottom() > last->finalBottom() || (note->finalBottom() == last->finalBottom() && note->finalX() > last->finalX())) + last = note; + note = note->nextShownInStack(); + } + return last; + } else + return lastNoteShownInStack(); +} + + +void Basket::keyPressEvent(QKeyEvent *event) +{ + if (isDuringEdit() && event->key() == Qt::Key_Return) { + //if (m_editor->lineEdit()) + // closeEditor(); + //else + m_editor->widget()->setFocus(); + } else if (event->key() == Qt::Key_Escape) { + if (isDuringEdit()) + closeEditor(); + else if (decoration()->filterData().isFiltering) + cancelFilter(); + else + unselectAll(); + } + + if (countFounds() == 0) + return; + + if (!m_focusedNote) + return; + + Note *toFocus = 0L; + + switch (event->key()) { + case Qt::Key_Down: + toFocus = (isFreeLayout() ? noteOn(BOTTOM_SIDE) : m_focusedNote->nextShownInStack()); + if (toFocus) + break; + scrollBy(0, 30); // This cases do not move focus to another note... + return; + case Qt::Key_Up: + toFocus = (isFreeLayout() ? noteOn(TOP_SIDE) : m_focusedNote->prevShownInStack()); + if (toFocus) + break; + scrollBy(0, -30); // This cases do not move focus to another note... + return; + case Qt::Key_PageDown: + if (isFreeLayout()) { + Note *lastFocused = m_focusedNote; + for (int i = 0; i < 10 && m_focusedNote; ++i) + m_focusedNote = noteOn(BOTTOM_SIDE); + toFocus = m_focusedNote; + m_focusedNote = lastFocused; + } else { + toFocus = m_focusedNote; + for (int i = 0; i < 10 && toFocus; ++i) + toFocus = toFocus->nextShownInStack(); + } + if (toFocus == 0L) + toFocus = (isFreeLayout() ? noteOnEnd() : lastNoteShownInStack()); + if (toFocus && toFocus != m_focusedNote) + break; + scrollBy(0, visibleHeight() / 2); // This cases do not move focus to another note... + return; + case Qt::Key_PageUp: + if (isFreeLayout()) { + Note *lastFocused = m_focusedNote; + for (int i = 0; i < 10 && m_focusedNote; ++i) + m_focusedNote = noteOn(TOP_SIDE); + toFocus = m_focusedNote; + m_focusedNote = lastFocused; + } else { + toFocus = m_focusedNote; + for (int i = 0; i < 10 && toFocus; ++i) + toFocus = toFocus->prevShownInStack(); + } + if (toFocus == 0L) + toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); + if (toFocus && toFocus != m_focusedNote) + break; + scrollBy(0, - visibleHeight() / 2); // This cases do not move focus to another note... + return; + case Qt::Key_Home: + toFocus = noteOnHome(); + break; + case Qt::Key_End: + toFocus = noteOnEnd(); + break; + case Qt::Key_Left: + if (m_focusedNote->tryFoldParent()) + return; + if ( (toFocus = noteOn(LEFT_SIDE)) ) + break; + if ( (toFocus = firstNoteInGroup()) ) + break; + scrollBy(-30, 0); // This cases do not move focus to another note... + return; + case Qt::Key_Right: + if (m_focusedNote->tryExpandParent()) + return; + if ( (toFocus = noteOn(RIGHT_SIDE)) ) + break; + scrollBy(30, 0); // This cases do not move focus to another note... + return; + case Qt::Key_Space: // This case do not move focus to another note... + if (m_focusedNote) { + m_focusedNote->setSelected( ! m_focusedNote->isSelected() ); + event->accept(); + } else + event->ignore(); + return; // ... so we return after the process + default: + return; + } + + if (toFocus == 0L) { // If no direction keys have been pressed OR reached out the begin or end + event->ignore(); // Important !! + return; + } + + if (event->state() & Qt::ShiftButton) { // Shift+arrowKeys selection + if (m_startOfShiftSelectionNote == 0L) + m_startOfShiftSelectionNote = toFocus; + ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! + selectRange(m_startOfShiftSelectionNote, toFocus); + setFocusedNote(toFocus); + event->accept(); + return; + } else /*if (toFocus != m_focusedNote)*/ { // Move focus to ANOTHER note... + ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! + setFocusedNote(toFocus); + m_startOfShiftSelectionNote = toFocus; + if ( ! (event->state() & Qt::ControlButton) ) // ... select only current note if Control + unselectAllBut(m_focusedNote); + event->accept(); + return; + } + + event->ignore(); // Important !! +} + +/** Select a range of notes and deselect the others. + * The order between start and end has no importance (end could be before start) + */ +void Basket::selectRange(Note *start, Note *end, bool unselectOthers /*= true*/) +{ + Note *cur; + Note *realEnd = 0L; + + // Avoid crash when start (or end) is null + if (start == 0L) + start = end; + else if (end == 0L) + end = start; + // And if *both* are null + if (start == 0L) { + if (unselectOthers) + unselectAll(); + return; + } + // In case there is only one note to select + if (start == end) { + if (unselectOthers) + unselectAllBut(start); + else + start->setSelected(true); + return; + } + + // Free layout baskets should select range as if we were drawing a rectangle between start and end: + if (isFreeLayout()) { + QRect startRect( start->finalX(), start->finalY(), start->width(), start->finalHeight() ); + QRect endRect( end->finalX(), end->finalY(), end->width(), end->finalHeight() ); + QRect toSelect = startRect.unite(endRect); + selectNotesIn(toSelect, /*invertSelection=*/false, unselectOthers); + return; + } + + // Search the REAL first (and deselect the others before it) : + for (cur = firstNoteInStack(); cur != 0L; cur = cur->nextInStack()) { + if (cur == start || cur == end) + break; + if (unselectOthers) + cur->setSelected(false); + } + + // Select the notes after REAL start, until REAL end : + if (cur == start) + realEnd = end; + else if (cur == end) + realEnd = start; + + for (/*cur = cur*/; cur != 0L; cur = cur->nextInStack()) { + cur->setSelected(cur->isShown()); // Select all notes in the range, but only if they are shown + if (cur == realEnd) + break; + } + + if (!unselectOthers) + return; + + // Deselect the remaining notes : + if (cur) + cur = cur->nextInStack(); + for (/*cur = cur*/; cur != 0L; cur = cur->nextInStack()) + cur->setSelected(false); +} + +void Basket::focusInEvent(QFocusEvent*) +{ + // Focus cannot be get with Tab when locked, but a click can focus the basket! + if (isLocked()) { + if (m_button) + QTimer::singleShot( 0, m_button, SLOT(setFocus()) ); + } else + focusANote(); // hasFocus() is true at this stage, note will be focused +} + +void Basket::focusOutEvent(QFocusEvent*) +{ + if (m_focusedNote != 0L) + m_focusedNote->setFocused(false); +} + +void Basket::ensureNoteVisible(Note *note) +{ + if (!note->isShown()) // Logical! + return; + + if (note == editedNote()) // HACK: When filtering while editing big notes, etc... cause unwanted scrolls + return; + + int finalBottom = note->finalY() + QMIN(note->finalHeight(), visibleHeight()); + int finalRight = note->finalX() + QMIN(note->width() + (note->hasResizer() ? Note::RESIZER_WIDTH : 0), visibleWidth()); + ensureVisible( finalRight, finalBottom, 0,0 ); + ensureVisible( note->finalX(), note->finalY(), 0,0 ); +} + +void Basket::addWatchedFile(const QString &fullPath) +{ +// DEBUG_WIN << "Watcher>Add Monitoring Of : <font color=blue>" + fullPath + "</font>"; + m_watcher->addFile(fullPath); +} + +void Basket::removeWatchedFile(const QString &fullPath) +{ +// DEBUG_WIN << "Watcher>Remove Monitoring Of : <font color=blue>" + fullPath + "</font>"; + m_watcher->removeFile(fullPath); +} + +void Basket::watchedFileModified(const QString &fullPath) +{ + if (!m_modifiedFiles.contains(fullPath)) + m_modifiedFiles.append(fullPath); + // If a big file is saved by an application, notifications are send several times. + // We wait they are not sent anymore to considere the file complete! + m_watcherTimer.start(200/*ms*/, true); + DEBUG_WIN << "Watcher>Modified : <font color=blue>" + fullPath + "</font>"; +} + +void Basket::watchedFileDeleted(const QString &fullPath) +{ + Note *note = noteForFullPath(fullPath); + removeWatchedFile(fullPath); + if (note) { + NoteSelection *selection = selectedNotes(); + unselectAllBut(note); + noteDeleteWithoutConfirmation(); + while (selection) { + selection->note->setSelected(true); + selection = selection->nextStacked(); + } + } + DEBUG_WIN << "Watcher>Removed : <font color=blue>" + fullPath + "</font>"; +} + +void Basket::updateModifiedNotes() +{ + for (QValueList<QString>::iterator it = m_modifiedFiles.begin(); it != m_modifiedFiles.end(); ++it) { + Note *note = noteForFullPath(*it); + if (note) + note->content()->loadFromFile(/*lazyLoad=*/false); + } + m_modifiedFiles.clear(); +} + +bool Basket::setProtection(int type, QString key) +{ +#ifdef HAVE_LIBGPGME + if(type == PasswordEncryption || // Ask a new password + m_encryptionType != type || m_encryptionKey != key) + { + int savedType = m_encryptionType; + QString savedKey = m_encryptionKey; + + m_encryptionType = type; + m_encryptionKey = key; + m_gpg->clearCache(); + + if(saveAgain()) + { + emit propertiesChanged(this); + } + else + { + m_encryptionType = savedType; + m_encryptionKey = savedKey; + m_gpg->clearCache(); + return false; + } + } + return true; +#else + m_encryptionType = type; + m_encryptionKey = key; + return false; +#endif +} + +bool Basket::saveAgain() +{ + bool result = false; + + m_watcher->stopScan(); + // Re-encrypt basket file: + result = save(); + // Re-encrypt every note files recursively: + if(result) + { + FOR_EACH_NOTE (note) + { + result = note->saveAgain(); + if(!result) + break; + } + } + m_watcher->startScan(); + return result; +} + +bool Basket::loadFromFile(const QString &fullPath, QString *string, bool isLocalEncoding) +{ + QByteArray array; + + if(loadFromFile(fullPath, &array)){ + if (isLocalEncoding) + *string = QString::fromLocal8Bit(array.data(), array.size()); + else + *string = QString::fromUtf8(array.data(), array.size()); + return true; + } + else + return false; +} + +bool Basket::isEncrypted() +{ + return (m_encryptionType != NoEncryption); +} + +bool Basket::isFileEncrypted() +{ + QFile file(fullPath() + ".basket"); + + if (file.open(IO_ReadOnly)){ + QString line; + + file.readLine(line, 32); + if(line.startsWith("-----BEGIN PGP MESSAGE-----")) + return true; + } + return false; +} + +bool Basket::loadFromFile(const QString &fullPath, QByteArray *array) +{ + QFile file(fullPath); + bool encrypted = false; + + if (file.open(IO_ReadOnly)){ + *array = file.readAll(); + const char* magic = "-----BEGIN PGP MESSAGE-----"; + uint i = 0; + + if(array->size() > strlen(magic)) + for (i = 0; array->at(i) == magic[i]; ++i) + ; + if (i == strlen(magic)) + { + encrypted = true; + } + file.close(); +#ifdef HAVE_LIBGPGME + if(encrypted) + { + QByteArray tmp(*array); + + tmp.detach(); + // Only use gpg-agent for private key encryption since it doesn't + // cache password used in symmetric encryption. + m_gpg->setUseGnuPGAgent(Settings::useGnuPGAgent() && m_encryptionType == PrivateKeyEncryption); + if(m_encryptionType == PrivateKeyEncryption) + m_gpg->setText(i18n("Please enter the password for the following private key:"), false); + else + m_gpg->setText(i18n("Please enter the password for the basket <b>%1</b>:").arg(basketName()), false); // Used when decrypting + return m_gpg->decrypt(tmp, array); + } +#else + if(encrypted) + { + return false; + } +#endif + return true; + } else + return false; +} + +bool Basket::saveToFile(const QString& fullPath, const QString& string, bool isLocalEncoding) +{ + QCString bytes = (isLocalEncoding ? string.local8Bit() : string.utf8()); + return saveToFile(fullPath, bytes, bytes.length()); +} + +bool Basket::saveToFile(const QString& fullPath, const QByteArray& array) +{ + return saveToFile(fullPath, array, array.size()); +} + +bool Basket::saveToFile(const QString& fullPath, const QByteArray& array, Q_ULONG length) +{ + bool success = true; + QByteArray tmp; + +#ifdef HAVE_LIBGPGME + if(isEncrypted()) + { + QString key = QString::null; + + // We only use gpg-agent for private key encryption and saving without + // public key doesn't need one. + m_gpg->setUseGnuPGAgent(false); + if(m_encryptionType == PrivateKeyEncryption) + { + key = m_encryptionKey; + // public key doesn't need password + m_gpg->setText("", false); + } + else + m_gpg->setText(i18n("Please assign a password to the basket <b>%1</b>:").arg(basketName()), true); // Used when defining a new password + + success = m_gpg->encrypt(array, length, &tmp, key); + length = tmp.size(); + } + else + tmp = array; + +#else + success = !isEncrypted(); + if(success) + tmp = array; +#endif + /*if (success && (success = file.open(IO_WriteOnly))){ + success = (file.writeBlock(tmp) == (Q_LONG)tmp.size()); + file.close(); + }*/ + + if (success) + return safelySaveToFile(fullPath, tmp, length); + else + return false; +} + +/** Same as saveToFile(), but it is static, and does not crypt the data if needed. + * Basically, to save a file owned by a basket (a basket or a note file), use saveToFile(). + * But to save another file (eg. the basket hierarchy), use this safelySaveToFile() static method. + */ +/*static*/ bool Basket::safelySaveToFile(const QString& fullPath, const QByteArray& array, Q_ULONG length) +{ + // Here, we take a double protection: + // - We use KSaveFile to write atomically to the file (either it's a success or the file is untouched) + // - We show a modal dialog to the user when no disk space is left or access is denied and retry every couple of seconds + + // Static, because safelySaveToFile() can be called a second time while blocked. + // Example: + // User type something and press Enter: safelySaveToFile() is called and block. + // Three seconds later, a timer ask to save changes, and this second safelySaveToFile() block too. + // Do not show the dialog twice in this case! + static DiskErrorDialog *dialog = 0; + + //std::cout << "---------- Saving " << fullPath << ":" << std::endl; + bool openSuccess; + bool closeSuccess; + bool errorWhileWritting; + do { + KSaveFile saveFile(fullPath); + //std::cout << "==>>" << std::endl << "SAVE FILE CREATED: " << strerror(saveFile.status()) << std::endl; + openSuccess = (saveFile.status() == 0 && saveFile.file() != 0); + if (openSuccess) { + saveFile.file()->writeBlock(array, length); + //std::cout << "FILE WRITTEN: " << strerror(saveFile.status()) << std::endl; + closeSuccess = saveFile.close(); + //std::cout << "FILE CLOSED: " << (closeSuccess ? "well" : "erroneous") << std::endl; + } + errorWhileWritting = (!openSuccess || !closeSuccess || saveFile.status() != 0); + if (errorWhileWritting) { + //std::cout << "ERROR DETECTED" << std::endl; + if (dialog == 0) { + //std::cout << "Opening dialog for " << fullPath << std::endl; + dialog = new DiskErrorDialog( + (openSuccess + ? i18n("Insufficient Disk Space to Save Basket Data") + : i18n("Wrong Basket File Permissions") + ), + (openSuccess + ? i18n("Please remove files on the disk <b>%1</b> to let the application safely save your changes.") + .arg(KIO::findPathMountPoint(fullPath)) + : i18n("File permissions are bad for <b>%1</b>. Please check that you have write access to it and the parent folders.") + .arg(fullPath) + ), + kapp->activeWindow() + ); + } + if (!dialog->isShown()) + dialog->show(); + const int retryDelay = 1000/*ms*/; + const int sleepDelay = 50/*ms*/; + for (int i = 0; i < retryDelay / sleepDelay; ++i) { + kapp->processEvents(); + usleep(sleepDelay); + } + } + } while (errorWhileWritting); + if (dialog) { + delete dialog; + dialog = 0; + } + + return true; // Hum...?! +} + +/*static*/ bool Basket::safelySaveToFile(const QString& fullPath, const QString& string, bool isLocalEncoding) +{ + QCString bytes = (isLocalEncoding ? string.local8Bit() : string.utf8()); + return safelySaveToFile(fullPath, bytes, bytes.length() - 1); +} + +/*static*/ bool Basket::safelySaveToFile(const QString& fullPath, const QByteArray& array) +{ + return safelySaveToFile(fullPath, array, array.size()); +} + +DiskErrorDialog::DiskErrorDialog(const QString &titleMessage, const QString &message, QWidget *parent) + : KDialogBase(KDialogBase::Plain, i18n("Save Error"), + (KDialogBase::ButtonCode)0, (KDialogBase::ButtonCode)0, parent, /*name=*/"DiskError") +{ + //enableButtonCancel(false); + //enableButtonClose(false); + //enableButton(Close, false); + //enableButtonOK(false); + setModal(true); + QHBoxLayout *layout = new QHBoxLayout(plainPage(), /*margin=*/0, spacingHint()); + QPixmap icon = kapp->iconLoader()->loadIcon("hdd_unmount", KIcon::NoGroup, 64, KIcon::DefaultState, /*path_store=*/0L, /*canReturnNull=*/true); + QLabel *iconLabel = new QLabel(plainPage()); + iconLabel->setPixmap(icon); + iconLabel->setFixedSize(iconLabel->sizeHint()); + QLabel *label = new QLabel("<p><nobr><b><font size='+1'>" + titleMessage + "</font></b></nobr></p><p>" + message + "</p>", plainPage()); + if (!icon.isNull()) + layout->addWidget(iconLabel); + layout->addWidget(label); +} + +DiskErrorDialog::~DiskErrorDialog() +{ +} + +void DiskErrorDialog::closeEvent(QCloseEvent *event) +{ + event->ignore(); +} + +void DiskErrorDialog::keyPressEvent(QKeyEvent*) +{ + // Escape should not close the window... +} + +void Basket::lock() +{ +#ifdef HAVE_LIBGPGME + closeEditor(); + m_gpg->clearCache(); + m_locked = true; + enableActions(); + deleteNotes(); + m_loaded = false; + m_loadingLaunched = false; + updateContents(); +#endif +} + +#if 0 + +#include <qlayout.h> +#include <qvbox.h> +#include <qstring.h> +#include <qpixmap.h> +#include <qcolor.h> +#include <kpopupmenu.h> +#include <kurllabel.h> +#include <qcheckbox.h> +#include <qpalette.h> +#include <qcursor.h> +#include <qaction.h> +#include <kstdaccel.h> +#include <kglobalsettings.h> +#include <qevent.h> + +#include <kapplication.h> +#include <kaboutdata.h> +#include <qinputdialog.h> +#include <qdragobject.h> +#include <kurldrag.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kmimetype.h> +#include <kfiledialog.h> +#include <qdir.h> +#include <kiconloader.h> +#include <qregexp.h> +#include <qfileinfo.h> + +#include <qstringlist.h> +#include <qdir.h> +#include <kurl.h> +#include <krun.h> +#include <kmessagebox.h> +#include <kdeversion.h> + +#include "kdirwatch.h" +#include <qstringlist.h> +#include <klineedit.h> + +#include <config.h> +#include <qtextcodec.h> + +#include "basket.h" +#include "note.h" +#include "notefactory.h" +#include "variouswidgets.h" +#include "linklabel.h" +#include "global.h" +#include "container.h" +#include "xmlwork.h" +#include "settings.h" +#include "popupmenu.h" +#include "debugwindow.h" +#include "exporterdialog.h" + + +/** Basket */ + +const int Basket::c_updateTime = 200; + + + +// Remove the note from the basket and delete the associated file +// If the note mirror a file, it will ask before deleting or not the file +// But if askForMirroredFile is false, it willn't ask NOR delete the MIRRORED file +// (it will anyway delete the file if it is not a mirror) +void Basket::delNote(Note *note, bool askForMirroredFile) +{ +//... + if (hasFocus()) + focusANote(); // We need note->next() and note->previous() here [BUT deleted note should be hidden] + if (note->isSelected()) + note->setSelected(false); //removeSelectedNote(); + + relayoutNotes(); + recolorizeNotes(); + resetInsertTo(); // If we delete the first or the last, pointer to it is invalid + save(); + + if (note == m_startOfShiftSelectionNote) + m_startOfShiftSelectionNote = 0L; + + if (isDuringEdit() && m_editor->editedNote() == note) + closeEditor(false); +//... +} + + +// Calculate where to paste or drop +void Basket::computeInsertPlace(const QPoint &cursorPosition) +{ + int y = cursorPosition.y(); + + if (countShown() == 0) + return; + + // TODO: Memorize the last hovered note to avoid a new computation on dragMoveEvent !! + // If the mouse is not over the last note, compute which new is : + // TODO: Optimization : start from m_insertAtNote and compare y position to search before or after (or the same) + for (Note *it = firstNote(); it != 0L; it = it->next()) + if ( (it->isShown()) && (it->y() + it->height() >= y) && (it->y() < y) ) { + int center = it->y() + (it->height() / 2); + m_insertAtNote = it; + m_insertAfter = y > center; + return; + } + // Else, there is at least one shown note but cursor hover NO note, so we are after the last shown note + m_insertAtNote = lastShownNote(); + m_insertAfter = true; + + // Code for rectangular notes : + /*QRect globalRect = it->rect(); + globalRect.moveTopLeft(it->pos() + contentsY()); + if ( globalRect.contains(curPos) ) { + it->doInterestingThing(); + }*/ +} + +void Basket::dragMoveEvent(QDragMoveEvent* event) +{ +// m_isDuringDrag = true; + + if (isLocked()) + return; + +// FIXME: viewportToContents does NOT work !!! +// QPoint pos = viewportToContents(event->pos()); + QPoint pos( event->pos().x() + contentsX(), event->pos().y() + contentsY() ); + +// if (insertAtCursorPos()) + computeInsertPlace(pos); + + showFrameInsertTo(); + acceptDropEvent(event); + + // A workarround since QScrollView::dragAutoScroll seem to have no effect : + ensureVisible(event->pos().x() + contentsX(), event->pos().y() + contentsY(), 30, 30); +// QScrollView::dragMoveEvent(event); +} + +void Basket::dropEvent(QDropEvent *event) +{ + QPoint pos = event->pos(); + std::cout << "Drop Event at position " << pos.x() << ":" << pos.y() << std::endl; + m_isDuringDrag = false; + emit resetStatusBarText(); + + if (isLocked()) + return; + + NoteFactory::dropNote( event, this, true, event->action(), dynamic_cast<Note*>(event->source()) ); + // TODO: need to know if we really inserted an (or several!!!!) note !!! + ensureNoteVisible(lastInsertedNote()); + unselectAllBut(lastInsertedNote()); + setFocusedNote(lastInsertedNote()); + + resetInsertTo(); +} + +void Basket::moveOnTop() +{ + if (m_countSelecteds == 0) + return; + + Note *endOfBrowse = firstShownNote(); + Note *topNote = firstNote(); + Note *prev; + for (Note *it = lastShownNote(); it != 0L; ) { + prev = it->previous(); + if (it->isSelected()) { + m_insertAtNote = topNote; + m_insertAfter = false; + changeNotePlace(it); + topNote = it; + } + if (it == endOfBrowse) + break; + it = prev; + } + ensureNoteVisible(firstShownNote()); + ensureNoteVisible(m_focusedNote); +} + +void Basket::moveOnBottom() +{ + if (m_countSelecteds == 0) + return; + + Note *endOfBrowse = lastShownNote(); + Note *bottomNote = lastNote(); + Note *next; + for (Note *it = firstShownNote(); it != 0L; ) { + next = it->next(); + if (it->isSelected()) { + m_insertAtNote = bottomNote; + m_insertAfter = true; + changeNotePlace(it); + bottomNote = it; + } + if (it == endOfBrowse) + break; + it = next; + } + ensureNoteVisible(lastShownNote()); + ensureNoteVisible(m_focusedNote); +} + +void Basket::moveNoteUp() +{ + if (m_countSelecteds == 0) + return; + + // Begin from the top (important move all selected notes one note up + // AND to quit early if a selected note is the first shown one + for (Note *it = firstShownNote(); it != 0L; it = it->next()) { + if (it->isSelected() && it->isShown()) { // it->isShown() not necessary, but in case... + if (it == firstShownNote()) + return; // No way... + m_insertAtNote = nextShownNoteFrom(it, -1); // Previous shown note + if (m_insertAtNote == 0L) { // Should not appends, since it's not the first shown note, + resetInsertTo(); // there SHOULD be one before + return; + } + m_insertAfter = false; + changeNotePlace(it); + } + if (it == lastShownNote()) + break; + } + ensureNoteVisible(m_focusedNote); +} + +void Basket::moveNoteDown() +{ + if (m_countSelecteds == 0) + return; + + // Begin from the bottom (important move all selected notes one note down + // AND to quit early if a selected note is the last shown one + for (Note *it = lastShownNote(); it != 0L; it = it->previous()) { + if (it->isSelected() && it->isShown()) { // it->isShown() not necessary, but in case... + if (it == lastShownNote()) + return; // No way... + m_insertAtNote = nextShownNoteFrom(it, 1); // Next shown note + if (m_insertAtNote == 0L) { // Should not appends, since it's not the last shown note, + resetInsertTo(); // there SHOULD be one before + return; + } + m_insertAfter = true; + changeNotePlace(it); + } + if (it == firstShownNote()) + break; + } + ensureNoteVisible(m_focusedNote); +} + +#endif // #if 0 + +#include "basket.moc" |