File indexing completed on 2024-04-14 15:50:52
0001 /** 0002 * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût <slaout@linux62.org> 0003 * SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "basketscene.h" 0007 0008 #include <QApplication> 0009 #include <QFileDialog> 0010 #include <QFrame> 0011 #include <QGraphicsProxyWidget> 0012 #include <QGridLayout> 0013 #include <QInputDialog> 0014 #include <QLabel> 0015 #include <QLineEdit> 0016 #include <QLocale> 0017 #include <QMenu> 0018 #include <QPushButton> 0019 #include <QSaveFile> 0020 #include <QScrollBar> 0021 #include <QToolTip> 0022 #include <QtCore/QDateTime> // seed for rand() 0023 #include <QtCore/QDir> 0024 #include <QtCore/QFile> 0025 #include <QtCore/QFileInfo> 0026 #include <QtCore/QList> 0027 #include <QtCore/QPoint> 0028 #include <QtCore/QStringList> 0029 #include <QtCore/QTimeLine> 0030 #include <QtGui/QAbstractTextDocumentLayout> 0031 #include <QtGui/QClipboard> 0032 #include <QtGui/QContextMenuEvent> 0033 #include <QtGui/QCursor> 0034 #include <QtGui/QDrag> 0035 #include <QtGui/QDragEnterEvent> 0036 #include <QtGui/QDragLeaveEvent> 0037 #include <QtGui/QDragMoveEvent> 0038 #include <QtGui/QDropEvent> 0039 #include <QtGui/QFocusEvent> 0040 #include <QtGui/QKeyEvent> 0041 #include <QtGui/QMouseEvent> 0042 #include <QtGui/QPainter> 0043 #include <QtGui/QResizeEvent> 0044 #include <QtGui/QTextDocument> 0045 #include <QtGui/QWheelEvent> 0046 #include <QtXml/QDomDocument> 0047 0048 #include <KTextEdit> 0049 #include <KAboutData> 0050 #include <KActionCollection> 0051 #include <KAuthorized> 0052 #include <KColorScheme> // for KStatefulBrush 0053 #include <KDirWatch> 0054 #include <KGlobalAccel> 0055 #include <KIconLoader> 0056 #include <KLocalizedString> 0057 #include <KMessageBox> 0058 #include <KOpenWithDialog> 0059 #include <KRun> 0060 #include <KService> 0061 #include <KDialogJobUiDelegate> 0062 0063 #include <KIO/ApplicationLauncherJob> 0064 #include <KIO/CopyJob> 0065 0066 #include <stdlib.h> // rand() function 0067 0068 #include "backgroundmanager.h" 0069 #include "basketview.h" 0070 #include "common.h" 0071 #include "debugwindow.h" 0072 #include "decoratedbasket.h" 0073 #include "focusedwidgets.h" 0074 #include "gitwrapper.h" 0075 #include "global.h" 0076 #include "note.h" 0077 #include "notedrag.h" 0078 #include "noteedit.h" 0079 #include "notefactory.h" 0080 #include "noteselection.h" 0081 #include "settings.h" 0082 #include "tagsedit.h" 0083 #include "tools.h" 0084 #include "xmlwork.h" 0085 0086 #include "config.h" 0087 0088 #ifdef HAVE_LIBGPGME 0089 #include "kgpgme.h" 0090 #endif 0091 0092 void debugZone(int zone) 0093 { 0094 QString s; 0095 switch (zone) { 0096 case Note::Handle: 0097 s = "Handle"; 0098 break; 0099 case Note::Group: 0100 s = "Group"; 0101 break; 0102 case Note::TagsArrow: 0103 s = "TagsArrow"; 0104 break; 0105 case Note::Custom0: 0106 s = "Custom0"; 0107 break; 0108 case Note::GroupExpander: 0109 s = "GroupExpander"; 0110 break; 0111 case Note::Content: 0112 s = "Content"; 0113 break; 0114 case Note::Link: 0115 s = "Link"; 0116 break; 0117 case Note::TopInsert: 0118 s = "TopInsert"; 0119 break; 0120 case Note::TopGroup: 0121 s = "TopGroup"; 0122 break; 0123 case Note::BottomInsert: 0124 s = "BottomInsert"; 0125 break; 0126 case Note::BottomGroup: 0127 s = "BottomGroup"; 0128 break; 0129 case Note::BottomColumn: 0130 s = "BottomColumn"; 0131 break; 0132 case Note::None: 0133 s = "None"; 0134 break; 0135 default: 0136 if (zone == Note::Emblem0) 0137 s = "Emblem0"; 0138 else 0139 s = "Emblem0+" + QString::number(zone - Note::Emblem0); 0140 break; 0141 } 0142 qDebug() << s; 0143 } 0144 0145 #define FOR_EACH_NOTE(noteVar) for (Note *noteVar = firstNote(); noteVar; noteVar = noteVar->next()) 0146 0147 0148 void BasketScene::appendNoteIn(Note *note, Note *in) 0149 { 0150 if (!note) 0151 // No note to append: 0152 return; 0153 0154 if (in) { 0155 // The normal case: 0156 preparePlug(note); 0157 0158 // Note *last = note->lastSibling(); 0159 Note *lastChild = in->lastChild(); 0160 0161 for (Note *n = note; n; n = n->next()) 0162 n->setParentNote(in); 0163 note->setPrev(lastChild); 0164 // last->setNext(0L); 0165 0166 if (!in->firstChild()) 0167 in->setFirstChild(note); 0168 0169 if (lastChild) 0170 lastChild->setNext(note); 0171 0172 if (m_loaded) 0173 signalCountsChanged(); 0174 } else 0175 // Prepend it directly in the basket: 0176 appendNoteAfter(note, lastNote()); 0177 } 0178 0179 void BasketScene::appendNoteAfter(Note *note, Note *after) 0180 { 0181 if (!note) 0182 // No note to append: 0183 return; 0184 0185 if (!after) 0186 // By default, insert after the last note: 0187 after = lastNote(); 0188 0189 if (m_loaded && after && !after->isFree() && !after->isColumn()) 0190 for (Note *n = note; n; n = n->next()) 0191 n->inheritTagsOf(after); 0192 0193 // if (!alreadyInBasket) 0194 preparePlug(note); 0195 0196 Note *last = note->lastSibling(); 0197 if (after) { 0198 // The normal case: 0199 for (Note *n = note; n; n = n->next()) 0200 n->setParentNote(after->parentNote()); 0201 note->setPrev(after); 0202 last->setNext(after->next()); 0203 after->setNext(note); 0204 if (last->next()) 0205 last->next()->setPrev(last); 0206 } else { 0207 // There is no note in the basket: 0208 for (Note *n = note; n; n = n->next()) 0209 n->setParentNote(nullptr); 0210 m_firstNote = note; 0211 // note->setPrev(0); 0212 // last->setNext(0); 0213 } 0214 0215 // if (!alreadyInBasket) 0216 if (m_loaded) 0217 signalCountsChanged(); 0218 } 0219 0220 void BasketScene::appendNoteBefore(Note *note, Note *before) 0221 { 0222 if (!note) 0223 // No note to append: 0224 return; 0225 0226 if (!before) 0227 // By default, insert before the first note: 0228 before = firstNote(); 0229 0230 if (m_loaded && before && !before->isFree() && !before->isColumn()) 0231 for (Note *n = note; n; n = n->next()) 0232 n->inheritTagsOf(before); 0233 0234 preparePlug(note); 0235 0236 Note *last = note->lastSibling(); 0237 if (before) { 0238 // The normal case: 0239 for (Note *n = note; n; n = n->next()) 0240 n->setParentNote(before->parentNote()); 0241 note->setPrev(before->prev()); 0242 last->setNext(before); 0243 before->setPrev(last); 0244 if (note->prev()) 0245 note->prev()->setNext(note); 0246 else { 0247 if (note->parentNote()) 0248 note->parentNote()->setFirstChild(note); 0249 else 0250 m_firstNote = note; 0251 } 0252 } else { 0253 // There is no note in the basket: 0254 for (Note *n = note; n; n = n->next()) 0255 n->setParentNote(nullptr); 0256 m_firstNote = note; 0257 // note->setPrev(0); 0258 // last->setNext(0); 0259 } 0260 0261 if (m_loaded) 0262 signalCountsChanged(); 0263 } 0264 0265 DecoratedBasket *BasketScene::decoration() 0266 { 0267 return (DecoratedBasket *)parent(); 0268 } 0269 0270 void BasketScene::preparePlug(Note *note) 0271 { 0272 // Select only the new notes, compute the new notes count and the new number of found notes: 0273 if (m_loaded) 0274 unselectAll(); 0275 int count = 0; 0276 int founds = 0; 0277 Note *last = nullptr; 0278 for (Note *n = note; n; n = n->next()) { 0279 if (m_loaded) 0280 n->setSelectedRecursively(true); // Notes should have a parent basket (and they have, so that's OK). 0281 count += n->count(); 0282 founds += n->newFilter(decoration()->filterData()); 0283 last = n; 0284 } 0285 m_count += count; 0286 m_countFounds += founds; 0287 0288 // Focus the last inserted note: 0289 if (m_loaded && last) { 0290 setFocusedNote(last); 0291 m_startOfShiftSelectionNote = (last->isGroup() ? last->lastRealChild() : last); 0292 } 0293 0294 // If some notes don't match (are hidden), tell it to the user: 0295 if (m_loaded && founds < count) { 0296 if (count == 1) { 0297 Q_EMIT postMessage(i18n("The new note does not match the filter and is hidden.")); 0298 } else if (founds == count - 1) { 0299 Q_EMIT postMessage(i18n("A new note does not match the filter and is hidden.")); 0300 } else if (founds > 0) { 0301 Q_EMIT postMessage(i18n("Some new notes do not match the filter and are hidden.")); 0302 } else { 0303 Q_EMIT postMessage(i18n("The new notes do not match the filter and are hidden.")); 0304 } 0305 } 0306 } 0307 0308 void BasketScene::unplugNote(Note *note) 0309 { 0310 // If there is nothing to do... 0311 if (!note) 0312 return; 0313 0314 // if (!willBeReplugged) { 0315 note->setSelectedRecursively(false); // To removeSelectedNote() and decrease the selectedsCount. 0316 m_count -= note->count(); 0317 m_countFounds -= note->newFilter(decoration()->filterData()); 0318 signalCountsChanged(); 0319 // } 0320 0321 // If it was the first note, change the first note: 0322 if (m_firstNote == note) 0323 m_firstNote = note->next(); 0324 0325 // Change previous and next notes: 0326 if (note->prev()) 0327 note->prev()->setNext(note->next()); 0328 if (note->next()) 0329 note->next()->setPrev(note->prev()); 0330 0331 if (note->parentNote()) { 0332 // If it was the first note of a group, change the first note of the group: 0333 if (note->parentNote()->firstChild() == note) 0334 note->parentNote()->setFirstChild(note->next()); 0335 0336 if (!note->parentNote()->isColumn()) { 0337 // Delete parent if now 0 notes inside parent group: 0338 if (!note->parentNote()->firstChild()) { 0339 unplugNote(note->parentNote()); 0340 // a group could call this method for one or more of its children, 0341 // each children could call this method for its parent's group... 0342 // we have to do the deletion later otherwise we may corrupt the current process 0343 m_notesToBeDeleted << note; 0344 if (m_notesToBeDeleted.count() == 1) { 0345 QTimer::singleShot(0, this, SLOT(doCleanUp())); 0346 } 0347 } 0348 // Ungroup if still 1 note inside parent group: 0349 else if (!note->parentNote()->firstChild()->next()) { 0350 ungroupNote(note->parentNote()); 0351 } 0352 } 0353 } 0354 0355 note->setParentNote(nullptr); 0356 note->setPrev(nullptr); 0357 note->setNext(nullptr); 0358 0359 // Reste focus and hover note if necessary 0360 if (m_focusedNote == note) 0361 m_focusedNote = nullptr; 0362 if (m_hoveredNote == note) 0363 m_hoveredNote = nullptr; 0364 0365 // 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. 0366 } 0367 0368 void BasketScene::ungroupNote(Note *group) 0369 { 0370 Note *note = group->firstChild(); 0371 Note *lastGroupedNote = group; 0372 Note *nextNote; 0373 0374 // Move all notes after the group (not before, to avoid to change m_firstNote or group->m_firstChild): 0375 while (note) { 0376 nextNote = note->next(); 0377 0378 if (lastGroupedNote->next()) 0379 lastGroupedNote->next()->setPrev(note); 0380 note->setNext(lastGroupedNote->next()); 0381 lastGroupedNote->setNext(note); 0382 note->setParentNote(group->parentNote()); 0383 note->setPrev(lastGroupedNote); 0384 0385 note->setGroupWidth(group->groupWidth() - Note::GROUP_WIDTH); 0386 lastGroupedNote = note; 0387 note = nextNote; 0388 } 0389 0390 // Unplug the group: 0391 group->setFirstChild(nullptr); 0392 unplugNote(group); 0393 // a group could call this method for one or more of its children, 0394 // each children could call this method for its parent's group... 0395 // we have to do the deletion later otherwise we may corrupt the current process 0396 m_notesToBeDeleted << group; 0397 if (m_notesToBeDeleted.count() == 1) { 0398 QTimer::singleShot(0, this, SLOT(doCleanUp())); 0399 } 0400 } 0401 0402 void BasketScene::groupNoteBefore(Note *note, Note *with) 0403 { 0404 if (!note || !with) 0405 // No note to group or nowhere to group it: 0406 return; 0407 0408 // if (m_loaded && before && !with->isFree() && !with->isColumn()) 0409 for (Note *n = note; n; n = n->next()) 0410 n->inheritTagsOf(with); 0411 0412 preparePlug(note); 0413 0414 Note *last = note->lastSibling(); 0415 0416 Note *group = new Note(this); 0417 group->setPrev(with->prev()); 0418 group->setNext(with->next()); 0419 group->setX(with->x()); 0420 group->setY(with->y()); 0421 if (with->parentNote() && with->parentNote()->firstChild() == with) 0422 with->parentNote()->setFirstChild(group); 0423 else if (m_firstNote == with) 0424 m_firstNote = group; 0425 group->setParentNote(with->parentNote()); 0426 group->setFirstChild(note); 0427 group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); 0428 0429 if (with->prev()) 0430 with->prev()->setNext(group); 0431 if (with->next()) 0432 with->next()->setPrev(group); 0433 with->setParentNote(group); 0434 with->setPrev(last); 0435 with->setNext(nullptr); 0436 0437 for (Note *n = note; n; n = n->next()) 0438 n->setParentNote(group); 0439 // note->setPrev(0L); 0440 last->setNext(with); 0441 0442 if (m_loaded) 0443 signalCountsChanged(); 0444 } 0445 0446 void BasketScene::groupNoteAfter(Note *note, Note *with) 0447 { 0448 if (!note || !with) 0449 // No note to group or nowhere to group it: 0450 return; 0451 0452 // if (m_loaded && before && !with->isFree() && !with->isColumn()) 0453 for (Note *n = note; n; n = n->next()) 0454 n->inheritTagsOf(with); 0455 0456 preparePlug(note); 0457 0458 // Note *last = note->lastSibling(); 0459 0460 Note *group = new Note(this); 0461 group->setPrev(with->prev()); 0462 group->setNext(with->next()); 0463 group->setX(with->x()); 0464 group->setY(with->y()); 0465 if (with->parentNote() && with->parentNote()->firstChild() == with) 0466 with->parentNote()->setFirstChild(group); 0467 else if (m_firstNote == with) 0468 m_firstNote = group; 0469 group->setParentNote(with->parentNote()); 0470 group->setFirstChild(with); 0471 group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); 0472 0473 if (with->prev()) 0474 with->prev()->setNext(group); 0475 if (with->next()) 0476 with->next()->setPrev(group); 0477 with->setParentNote(group); 0478 with->setPrev(nullptr); 0479 with->setNext(note); 0480 0481 for (Note *n = note; n; n = n->next()) 0482 n->setParentNote(group); 0483 note->setPrev(with); 0484 // last->setNext(0L); 0485 0486 if (m_loaded) 0487 signalCountsChanged(); 0488 } 0489 0490 void BasketScene::doCleanUp() 0491 { 0492 QSet<Note *>::iterator it = m_notesToBeDeleted.begin(); 0493 while (it != m_notesToBeDeleted.end()) { 0494 delete *it; 0495 it = m_notesToBeDeleted.erase(it); 0496 } 0497 } 0498 0499 void BasketScene::loadNotes(const QDomElement ¬es, Note *parent) 0500 { 0501 Note *note; 0502 for (QDomNode n = notes.firstChild(); !n.isNull(); n = n.nextSibling()) { 0503 QDomElement e = n.toElement(); 0504 if (e.isNull()) // Cannot handle that! 0505 continue; 0506 note = nullptr; 0507 // Load a Group: 0508 if (e.tagName() == "group") { 0509 note = new Note(this); // 1. Create the group... 0510 loadNotes(e, note); // 3. ... And populate it with child notes. 0511 int noteCount = note->count(); 0512 if (noteCount > 0 || (parent == nullptr && !isFreeLayout())) { // But don't remove columns! 0513 appendNoteIn(note, parent); // 2. ... Insert it... FIXME: Initially, the if() the insertion was the step 2. Was it on purpose? 0514 // The notes in the group are counted two times (it's why appendNoteIn() was called before loadNotes): 0515 m_count -= noteCount; // TODO: Recompute note count every time noteCount() is emitted! 0516 m_countFounds -= noteCount; 0517 } 0518 } 0519 // Load a Content-Based Note: 0520 if (e.tagName() == "note" || e.tagName() == "item") { // Keep compatible with 0.6.0 Alpha 1 0521 note = new Note(this); // Create the note... 0522 NoteFactory::loadNode(XMLWork::getElement(e, "content"), e.attribute("type"), note, /*lazyLoad=*/m_finishLoadOnFirstShow); // ... Populate it with content... 0523 if (e.attribute("type") == "text") 0524 m_shouldConvertPlainTextNotes = true; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! 0525 appendNoteIn(note, parent); // ... And insert it. 0526 // Load dates: 0527 if (e.hasAttribute("added")) 0528 note->setAddedDate(QDateTime::fromString(e.attribute("added"), Qt::ISODate)); 0529 if (e.hasAttribute("lastModification")) 0530 note->setLastModificationDate(QDateTime::fromString(e.attribute("lastModification"), Qt::ISODate)); 0531 } 0532 // If we successfully loaded a note: 0533 if (note) { 0534 // Free Note Properties: 0535 if (note->isFree()) { 0536 int x = e.attribute("x").toInt(); 0537 int y = e.attribute("y").toInt(); 0538 note->setX(x < 0 ? 0 : x); 0539 note->setY(y < 0 ? 0 : y); 0540 } 0541 // Resizeable Note Properties: 0542 if (note->hasResizer() || note->isColumn()) 0543 note->setGroupWidth(e.attribute("width", "200").toInt()); 0544 // Group Properties: 0545 if (note->isGroup() && !note->isColumn() && XMLWork::trueOrFalse(e.attribute("folded", "false"))) 0546 note->toggleFolded(); 0547 // Tags: 0548 if (note->content()) { 0549 QString tagsString = XMLWork::getElementText(e, QStringLiteral("tags"), QString()); 0550 QStringList tagsId = tagsString.split(';'); 0551 for (QStringList::iterator it = tagsId.begin(); it != tagsId.end(); ++it) { 0552 State *state = Tag::stateById(*it); 0553 if (state) 0554 note->addState(state, /*orReplace=*/true); 0555 } 0556 } 0557 } 0558 qApp->processEvents(); 0559 } 0560 } 0561 0562 void BasketScene::saveNotes(QXmlStreamWriter &stream, Note *parent) 0563 { 0564 Note *note = (parent ? parent->firstChild() : firstNote()); 0565 while (note) { 0566 // Create Element: 0567 stream.writeStartElement(note->isGroup() ? "group" : "note"); 0568 // Free Note Properties: 0569 if (note->isFree()) { 0570 stream.writeAttribute("x", QString::number(note->x())); 0571 stream.writeAttribute("y", QString::number(note->y())); 0572 } 0573 // Resizeable Note Properties: 0574 if (note->hasResizer()) 0575 stream.writeAttribute("width", QString::number(note->groupWidth())); 0576 // Group Properties: 0577 if (note->isGroup() && !note->isColumn()) 0578 stream.writeAttribute("folded", XMLWork::trueOrFalse(note->isFolded())); 0579 // Save Content: 0580 if (note->content()) { 0581 // Save Dates: 0582 stream.writeAttribute("added", note->addedDate().toString(Qt::ISODate)); 0583 stream.writeAttribute("lastModification", note->lastModificationDate().toString(Qt::ISODate)); 0584 // Save Content: 0585 stream.writeAttribute("type", note->content()->lowerTypeName()); 0586 note->content()->saveToNode(stream); 0587 // Save Tags: 0588 if (note->states().count() > 0) { 0589 QString tags; 0590 for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) { 0591 tags += (tags.isEmpty() ? QString() : QStringLiteral(";")) + (*it)->id(); 0592 } 0593 stream.writeTextElement("tags", tags); 0594 } 0595 } else { 0596 // Save Child Notes: 0597 saveNotes(stream, note); 0598 } 0599 stream.writeEndElement(); 0600 // Go to the Next One: 0601 note = note->next(); 0602 } 0603 } 0604 0605 void BasketScene::loadProperties(const QDomElement &properties) 0606 { 0607 // Compute Default Values for When Loading the Properties: 0608 QString defaultBackgroundColor = (backgroundColorSetting().isValid() ? backgroundColorSetting().name() : QString()); 0609 QString defaultTextColor = (textColorSetting().isValid() ? textColorSetting().name() : QString()); 0610 0611 // Load the Properties: 0612 QString icon = XMLWork::getElementText(properties, "icon", this->icon()); 0613 QString name = XMLWork::getElementText(properties, "name", basketName()); 0614 0615 QDomElement appearance = XMLWork::getElement(properties, "appearance"); 0616 // In 0.6.0-Alpha versions, there was a typo error: "backround" instead of "background" 0617 QString backgroundImage = appearance.attribute("backgroundImage", appearance.attribute("backroundImage", backgroundImageName())); 0618 QString backgroundColorString = appearance.attribute("backgroundColor", appearance.attribute("backroundColor", defaultBackgroundColor)); 0619 QString textColorString = appearance.attribute("textColor", defaultTextColor); 0620 QColor backgroundColor = (backgroundColorString.isEmpty() ? QColor() : QColor(backgroundColorString)); 0621 QColor textColor = (textColorString.isEmpty() ? QColor() : QColor(textColorString)); 0622 0623 QDomElement disposition = XMLWork::getElement(properties, "disposition"); 0624 bool free = XMLWork::trueOrFalse(disposition.attribute("free", XMLWork::trueOrFalse(isFreeLayout()))); 0625 int columnCount = disposition.attribute("columnCount", QString::number(this->columnsCount())).toInt(); 0626 bool mindMap = XMLWork::trueOrFalse(disposition.attribute("mindMap", XMLWork::trueOrFalse(isMindMap()))); 0627 0628 QDomElement shortcut = XMLWork::getElement(properties, "shortcut"); 0629 QString actionStrings[] = {"show", "globalShow", "globalSwitch"}; 0630 QKeySequence combination = QKeySequence(shortcut.attribute("combination", m_action->shortcut().toString())); 0631 QString actionString = shortcut.attribute("action"); 0632 int action = shortcutAction(); 0633 if (actionString == actionStrings[0]) 0634 action = 0; 0635 if (actionString == actionStrings[1]) 0636 action = 1; 0637 if (actionString == actionStrings[2]) 0638 action = 2; 0639 0640 QDomElement protection = XMLWork::getElement(properties, "protection"); 0641 m_encryptionType = protection.attribute("type").toInt(); 0642 m_encryptionKey = protection.attribute("key"); 0643 0644 // Apply the Properties: 0645 setDisposition((free ? (mindMap ? 2 : 1) : 0), columnCount); 0646 setShortcut(combination, action); 0647 setAppearance(icon, name, backgroundImage, backgroundColor, textColor); // Will emit propertiesChanged(this) 0648 } 0649 0650 void BasketScene::saveProperties(QXmlStreamWriter &stream) 0651 { 0652 stream.writeStartElement("properties"); 0653 0654 stream.writeTextElement("name", basketName()); 0655 stream.writeTextElement("icon", icon()); 0656 0657 stream.writeStartElement("appearance"); 0658 stream.writeAttribute("backgroundColor", backgroundColorSetting().isValid() ? backgroundColorSetting().name() : QString()); 0659 stream.writeAttribute("backgroundImage", backgroundImageName()); 0660 stream.writeAttribute("textColor", textColorSetting().isValid() ? textColorSetting().name() : QString()); 0661 stream.writeEndElement(); 0662 0663 stream.writeStartElement("disposition"); 0664 stream.writeAttribute("columnCount", QString::number(columnsCount())); 0665 stream.writeAttribute("free", XMLWork::trueOrFalse(isFreeLayout())); 0666 stream.writeAttribute("mindMap", XMLWork::trueOrFalse(isMindMap())); 0667 stream.writeEndElement(); 0668 0669 stream.writeStartElement("shortcut"); 0670 QString actionStrings[] = {"show", "globalShow", "globalSwitch"}; 0671 stream.writeAttribute("action", actionStrings[shortcutAction()]); 0672 stream.writeAttribute("combination", m_action->shortcut().toString()); 0673 stream.writeEndElement(); 0674 0675 stream.writeStartElement("protection"); 0676 stream.writeAttribute("key", m_encryptionKey); 0677 stream.writeAttribute("type", QString::number(m_encryptionType)); 0678 stream.writeEndElement(); 0679 0680 stream.writeEndElement(); 0681 } 0682 0683 void BasketScene::subscribeBackgroundImages() 0684 { 0685 if (!m_backgroundImageName.isEmpty()) { 0686 Global::backgroundManager->subscribe(m_backgroundImageName); 0687 Global::backgroundManager->subscribe(m_backgroundImageName, this->backgroundColor()); 0688 Global::backgroundManager->subscribe(m_backgroundImageName, selectionRectInsideColor()); 0689 m_backgroundPixmap = Global::backgroundManager->pixmap(m_backgroundImageName); 0690 m_opaqueBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, this->backgroundColor()); 0691 m_selectedBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, selectionRectInsideColor()); 0692 m_backgroundTiled = Global::backgroundManager->tiled(m_backgroundImageName); 0693 } 0694 } 0695 0696 void BasketScene::unsubscribeBackgroundImages() 0697 { 0698 if (hasBackgroundImage()) { 0699 Global::backgroundManager->unsubscribe(m_backgroundImageName); 0700 Global::backgroundManager->unsubscribe(m_backgroundImageName, this->backgroundColor()); 0701 Global::backgroundManager->unsubscribe(m_backgroundImageName, selectionRectInsideColor()); 0702 m_backgroundPixmap = nullptr; 0703 m_opaqueBackgroundPixmap = nullptr; 0704 m_selectedBackgroundPixmap = nullptr; 0705 } 0706 } 0707 0708 void BasketScene::setAppearance(const QString &icon, const QString &name, const QString &backgroundImage, const QColor &backgroundColor, const QColor &textColor) 0709 { 0710 unsubscribeBackgroundImages(); 0711 0712 m_basketName = name; 0713 m_backgroundImageName = backgroundImage; 0714 m_backgroundColorSetting = backgroundColor; 0715 m_textColorSetting = textColor; 0716 0717 // Where is this shown? 0718 m_action->setText("BASKET SHORTCUT: " + name); 0719 0720 // Basket should ALWAYS have an icon (the "basket" icon by default): 0721 QPixmap iconTest = KIconLoader::global()->loadIcon(icon, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true); 0722 if (!iconTest.isNull()) 0723 m_icon = icon; 0724 0725 // We don't request the background images if it's not loaded yet (to make the application startup fast). 0726 // When the basket is loading (because requested by the user: he/she want to access it) 0727 // it load the properties, subscribe to (and then load) the images, update the "Loading..." message with the image, 0728 // load all the notes and it's done! 0729 if (m_loadingLaunched) 0730 subscribeBackgroundImages(); 0731 0732 recomputeAllStyles(); // If a note have a tag with the same background color as the basket one, then display a "..." 0733 recomputeBlankRects(); // See the drawing of blank areas in BasketScene::drawContents() 0734 unbufferizeAll(); 0735 0736 if (isDuringEdit() && m_editor->graphicsWidget()) { 0737 QPalette palette; 0738 palette.setColor(m_editor->graphicsWidget()->widget()->backgroundRole(), m_editor->note()->backgroundColor()); 0739 palette.setColor(m_editor->graphicsWidget()->widget()->foregroundRole(), m_editor->note()->textColor()); 0740 m_editor->graphicsWidget()->setPalette(palette); 0741 } 0742 0743 Q_EMIT propertiesChanged(this); 0744 } 0745 0746 void BasketScene::setDisposition(int disposition, int columnCount) 0747 { 0748 static const int COLUMNS_LAYOUT = 0; 0749 static const int FREE_LAYOUT = 1; 0750 static const int MINDMAPS_LAYOUT = 2; 0751 0752 int currentDisposition = (isFreeLayout() ? (isMindMap() ? MINDMAPS_LAYOUT : FREE_LAYOUT) : COLUMNS_LAYOUT); 0753 0754 if (currentDisposition == COLUMNS_LAYOUT && disposition == COLUMNS_LAYOUT) { 0755 if (firstNote() && columnCount > m_columnsCount) { 0756 // Insert each new columns: 0757 for (int i = m_columnsCount; i < columnCount; ++i) { 0758 Note *newColumn = new Note(this); 0759 insertNote(newColumn, /*clicked=*/lastNote(), /*zone=*/Note::BottomInsert); 0760 } 0761 } else if (firstNote() && columnCount < m_columnsCount) { 0762 Note *column = firstNote(); 0763 Note *cuttedNotes = nullptr; 0764 for (int i = 1; i <= m_columnsCount; ++i) { 0765 Note *columnToRemove = column; 0766 column = column->next(); 0767 if (i > columnCount) { 0768 // Remove the columns that are too much: 0769 unplugNote(columnToRemove); 0770 // "Cut" the content in the columns to be deleted: 0771 if (columnToRemove->firstChild()) { 0772 for (Note *it = columnToRemove->firstChild(); it; it = it->next()) 0773 it->setParentNote(nullptr); 0774 if (!cuttedNotes) 0775 cuttedNotes = columnToRemove->firstChild(); 0776 else { 0777 Note *lastCuttedNote = cuttedNotes; 0778 while (lastCuttedNote->next()) 0779 lastCuttedNote = lastCuttedNote->next(); 0780 lastCuttedNote->setNext(columnToRemove->firstChild()); 0781 columnToRemove->firstChild()->setPrev(lastCuttedNote); 0782 } 0783 columnToRemove->setFirstChild(nullptr); 0784 } 0785 delete columnToRemove; 0786 } 0787 } 0788 // Paste the content in the last column: 0789 if (cuttedNotes) 0790 insertNote(cuttedNotes, /*clicked=*/lastNote(), /*zone=*/Note::BottomColumn); 0791 unselectAll(); 0792 } 0793 if (columnCount != m_columnsCount) { 0794 m_columnsCount = (columnCount <= 0 ? 1 : columnCount); 0795 equalizeColumnSizes(); // Will relayoutNotes() 0796 } 0797 } else if (currentDisposition == COLUMNS_LAYOUT && (disposition == FREE_LAYOUT || disposition == MINDMAPS_LAYOUT)) { 0798 Note *column = firstNote(); 0799 m_columnsCount = 0; // Now, so relayoutNotes() will not relayout the free notes as if they were columns! 0800 while (column) { 0801 // Move all childs on the first level: 0802 Note *nextColumn = column->next(); 0803 ungroupNote(column); 0804 column = nextColumn; 0805 } 0806 unselectAll(); 0807 m_mindMap = (disposition == MINDMAPS_LAYOUT); 0808 relayoutNotes(); 0809 } else if ((currentDisposition == FREE_LAYOUT || currentDisposition == MINDMAPS_LAYOUT) && disposition == COLUMNS_LAYOUT) { 0810 if (firstNote()) { 0811 // TODO: Reorder notes! 0812 // Remove all notes (but keep a reference to them, we're not crazy ;-) ): 0813 Note *notes = m_firstNote; 0814 m_firstNote = nullptr; 0815 m_count = 0; 0816 m_countFounds = 0; 0817 // Insert the number of columns that is needed: 0818 Note *lastInsertedColumn = nullptr; 0819 for (int i = 0; i < columnCount; ++i) { 0820 Note *column = new Note(this); 0821 if (lastInsertedColumn) 0822 insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert); 0823 else 0824 m_firstNote = column; 0825 lastInsertedColumn = column; 0826 } 0827 // Reinsert the old notes in the first column: 0828 insertNote(notes, /*clicked=*/firstNote(), /*zone=*/Note::BottomColumn); 0829 unselectAll(); 0830 } else { 0831 // Insert the number of columns that is needed: 0832 Note *lastInsertedColumn = nullptr; 0833 for (int i = 0; i < columnCount; ++i) { 0834 Note *column = new Note(this); 0835 if (lastInsertedColumn) 0836 insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert); 0837 else 0838 m_firstNote = column; 0839 lastInsertedColumn = column; 0840 } 0841 } 0842 m_columnsCount = (columnCount <= 0 ? 1 : columnCount); 0843 equalizeColumnSizes(); // Will relayoutNotes() 0844 } 0845 } 0846 0847 void BasketScene::equalizeColumnSizes() 0848 { 0849 if (!firstNote()) 0850 return; 0851 0852 // Necessary to know the available space; 0853 relayoutNotes(); 0854 0855 int availableSpace = m_view->viewport()->width(); 0856 int columnWidth = (availableSpace - (columnsCount() - 1) * Note::GROUP_WIDTH) / columnsCount(); 0857 int columnCount = columnsCount(); 0858 Note *column = firstNote(); 0859 while (column) { 0860 int minGroupWidth = column->minRight() - column->x(); 0861 if (minGroupWidth > columnWidth) { 0862 availableSpace -= minGroupWidth; 0863 --columnCount; 0864 } 0865 column = column->next(); 0866 } 0867 columnWidth = (availableSpace - (columnsCount() - 1) * Note::GROUP_WIDTH) / columnCount; 0868 0869 column = firstNote(); 0870 while (column) { 0871 int minGroupWidth = column->minRight() - column->x(); 0872 if (minGroupWidth > columnWidth) 0873 column->setGroupWidth(minGroupWidth); 0874 else 0875 column->setGroupWidth(columnWidth); 0876 column = column->next(); 0877 } 0878 0879 relayoutNotes(); 0880 } 0881 0882 void BasketScene::enableActions() 0883 { 0884 Global::bnpView->enableActions(); 0885 m_view->setFocusPolicy(isLocked() ? Qt::NoFocus : Qt::StrongFocus); 0886 if (isLocked()) 0887 m_view->viewport()->setCursor(Qt::ArrowCursor); // When locking, the cursor stays the last form it was 0888 } 0889 0890 bool BasketScene::save() 0891 { 0892 if (!m_loaded) 0893 return false; 0894 0895 DEBUG_WIN << "Basket[" + folderName() + "]: Saving..."; 0896 0897 QString data; 0898 QXmlStreamWriter stream(&data); 0899 XMLWork::setupXmlStream(stream, "basket"); 0900 0901 // Create Properties Element and Populate It: 0902 saveProperties(stream); 0903 0904 // Create Notes Element and Populate It: 0905 stream.writeStartElement("notes"); 0906 saveNotes(stream, nullptr); 0907 stream.writeEndElement(); 0908 0909 stream.writeEndElement(); 0910 stream.writeEndDocument(); 0911 0912 // Write to Disk: 0913 if (!FileStorage::saveToFile(fullPath() + ".basket", data, isEncrypted())) { 0914 DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to save</font>!"; 0915 return false; 0916 } 0917 0918 Global::bnpView->setUnsavedStatus(false); 0919 0920 m_commitdelay.start(10000); // delay is 10 seconds 0921 0922 return true; 0923 } 0924 0925 void BasketScene::commitEdit() 0926 { 0927 GitWrapper::commitBasket(this); 0928 } 0929 0930 void BasketScene::aboutToBeActivated() 0931 { 0932 if (m_finishLoadOnFirstShow) { 0933 FOR_EACH_NOTE(note) 0934 note->finishLazyLoad(); 0935 0936 // relayoutNotes(/*animate=*/false); 0937 setFocusedNote(nullptr); // So that during the focusInEvent that will come shortly, the FIRST note is focused. 0938 0939 m_finishLoadOnFirstShow = false; 0940 m_loaded = true; 0941 } 0942 } 0943 0944 void BasketScene::reload() 0945 { 0946 closeEditor(); 0947 unbufferizeAll(); // Keep the memory footprint low 0948 0949 m_firstNote = nullptr; 0950 0951 m_loaded = false; 0952 m_loadingLaunched = false; 0953 0954 invalidate(); 0955 } 0956 0957 void BasketScene::load() 0958 { 0959 // Load only once: 0960 if (m_loadingLaunched) 0961 return; 0962 m_loadingLaunched = true; 0963 0964 DEBUG_WIN << "Basket[" + folderName() + "]: Loading..."; 0965 QDomDocument *doc = nullptr; 0966 QString content; 0967 0968 // Load properties 0969 if (FileStorage::loadFromFile(fullPath() + ".basket", &content)) { 0970 doc = new QDomDocument("basket"); 0971 if (!doc->setContent(content)) { 0972 DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to parse XML</font>!"; 0973 delete doc; 0974 doc = nullptr; 0975 } 0976 } 0977 if (isEncrypted()) 0978 DEBUG_WIN << "Basket is encrypted."; 0979 if (!doc) { 0980 DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to load</font>!"; 0981 m_loadingLaunched = false; 0982 if (isEncrypted()) 0983 m_locked = true; 0984 Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar 0985 return; 0986 } 0987 m_locked = false; 0988 0989 QDomElement docElem = doc->documentElement(); 0990 QDomElement properties = XMLWork::getElement(docElem, "properties"); 0991 0992 loadProperties(properties); // Since we are loading, this time the background image will also be loaded! 0993 // Now that the background image is loaded and subscribed, we display it during the load process: 0994 delete doc; 0995 0996 // BEGIN Compatibility with 0.6.0 Pre-Alpha versions: 0997 QDomElement notes = XMLWork::getElement(docElem, "notes"); 0998 if (notes.isNull()) 0999 notes = XMLWork::getElement(docElem, "items"); 1000 m_watcher->stopScan(); 1001 m_shouldConvertPlainTextNotes = false; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! 1002 1003 // Load notes 1004 m_finishLoadOnFirstShow = (Global::bnpView->currentBasket() != this); 1005 loadNotes(notes, nullptr); 1006 if (m_shouldConvertPlainTextNotes) 1007 convertTexts(); 1008 m_watcher->startScan(); 1009 1010 signalCountsChanged(); 1011 if (isColumnsLayout()) { 1012 // Count the number of columns: 1013 int columnsCount = 0; 1014 Note *column = firstNote(); 1015 while (column) { 1016 ++columnsCount; 1017 column = column->next(); 1018 } 1019 m_columnsCount = columnsCount; 1020 } 1021 1022 relayoutNotes(); 1023 1024 // On application start, the current basket is not focused yet, so the focus rectangle is not shown when calling focusANote(): 1025 if (Global::bnpView->currentBasket() == this) 1026 setFocus(); 1027 focusANote(); 1028 1029 m_loaded = true; 1030 enableActions(); 1031 } 1032 1033 void BasketScene::filterAgain(bool andEnsureVisible /* = true*/) 1034 { 1035 newFilter(decoration()->filterData(), andEnsureVisible); 1036 } 1037 1038 void BasketScene::filterAgainDelayed() 1039 { 1040 QTimer::singleShot(0, this, SLOT(filterAgain())); 1041 } 1042 1043 void BasketScene::newFilter(const FilterData &data, bool andEnsureVisible /* = true*/) 1044 { 1045 if (!isLoaded()) 1046 return; 1047 1048 // StopWatch::start(20); 1049 1050 m_countFounds = 0; 1051 //Search within basket titles as well 1052 if (data.tagFilterType == FilterData::DontCareTagsFilter) 1053 if (!data.string.isEmpty()) 1054 if (basketName().contains(data.string, Qt::CaseInsensitive)){ 1055 ++m_countFounds; 1056 } 1057 1058 for (Note *note = firstNote(); note; note = note->next()) 1059 m_countFounds += note->newFilter(data); 1060 1061 relayoutNotes(); 1062 signalCountsChanged(); 1063 1064 if (hasFocus()) // if (!hasFocus()), focusANote() will be called at focusInEvent() 1065 focusANote(); // so, we avoid de-focus a note if it will be re-shown soon 1066 if (andEnsureVisible && m_focusedNote != nullptr) 1067 ensureNoteVisible(m_focusedNote); 1068 1069 Global::bnpView->setFiltering(data.isFiltering); 1070 1071 // StopWatch::check(20); 1072 } 1073 1074 bool BasketScene::isFiltering() 1075 { 1076 return decoration()->filterBar()->filterData().isFiltering; 1077 } 1078 1079 QString BasketScene::fullPath() 1080 { 1081 return Global::basketsFolder() + folderName(); 1082 } 1083 1084 QString BasketScene::fullPathForFileName(const QString &fileName) 1085 { 1086 return fullPath() + fileName; 1087 } 1088 1089 /*static*/ QString BasketScene::fullPathForFolderName(const QString &folderName) 1090 { 1091 return Global::basketsFolder() + folderName; 1092 } 1093 1094 void BasketScene::setShortcut(QKeySequence shortcut, int action) 1095 { 1096 QList<QKeySequence> shortcuts {shortcut}; 1097 m_action->setShortcuts(shortcuts); 1098 m_shortcutAction = action; 1099 1100 if (!shortcut.isEmpty()) { 1101 if (action > 0) { 1102 KGlobalAccel::self()->setShortcut(m_action, shortcuts, KGlobalAccel::Autoloading); 1103 KGlobalAccel::self()->setDefaultShortcut(m_action, shortcuts, KGlobalAccel::Autoloading); 1104 } 1105 } 1106 } 1107 1108 void BasketScene::activatedShortcut() 1109 { 1110 Global::bnpView->setCurrentBasket(this); 1111 1112 if (m_shortcutAction == 1) 1113 Global::bnpView->setActive(true); 1114 } 1115 1116 void BasketScene::signalCountsChanged() 1117 { 1118 if (!m_timerCountsChanged.isActive()) { 1119 m_timerCountsChanged.setSingleShot(true); 1120 m_timerCountsChanged.start(0); 1121 } 1122 } 1123 1124 void BasketScene::countsChangedTimeOut() 1125 { 1126 Q_EMIT countsChanged(this); 1127 } 1128 1129 BasketScene::BasketScene(QWidget *parent, const QString &folderName) 1130 //: Q3ScrollView(parent) 1131 : QGraphicsScene(parent) 1132 , m_noActionOnMouseRelease(false) 1133 , m_ignoreCloseEditorOnNextMouseRelease(false) 1134 , m_pressPos(-100, -100) 1135 , m_canDrag(false) 1136 , m_firstNote(nullptr) 1137 , m_columnsCount(1) 1138 , m_mindMap(false) 1139 , m_resizingNote(nullptr) 1140 , m_pickedResizer(0) 1141 , m_movingNote(nullptr) 1142 , m_pickedHandle(0, 0) 1143 , m_notesToBeDeleted() 1144 , m_clickedToInsert(nullptr) 1145 , m_zoneToInsert(0) 1146 , m_posToInsert(-1, -1) 1147 , m_isInsertPopupMenu(false) 1148 , m_insertMenuTitle(nullptr) 1149 , m_loaded(false) 1150 , m_loadingLaunched(false) 1151 , m_locked(false) 1152 , m_decryptBox(nullptr) 1153 , m_button(nullptr) 1154 , m_encryptionType(NoEncryption) 1155 #ifdef HAVE_LIBGPGME 1156 , m_gpg(0) 1157 #endif 1158 , m_backgroundPixmap(nullptr) 1159 , m_opaqueBackgroundPixmap(nullptr) 1160 , m_selectedBackgroundPixmap(nullptr) 1161 , m_action(nullptr) 1162 , m_shortcutAction(0) 1163 , m_hoveredNote(nullptr) 1164 , m_hoveredZone(Note::None) 1165 , m_lockedHovering(false) 1166 , m_underMouse(false) 1167 , m_inserterRect() 1168 , m_inserterShown(false) 1169 , m_inserterSplit(true) 1170 , m_inserterTop(false) 1171 , m_inserterGroup(false) 1172 , m_lastDisableClick(QTime::currentTime()) 1173 , m_isSelecting(false) 1174 , m_selectionStarted(false) 1175 , m_count(0) 1176 , m_countFounds(0) 1177 , m_countSelecteds(0) 1178 , m_icon("org.kde.basket") 1179 , m_folderName(folderName) 1180 , m_editor(nullptr) 1181 , m_redirectEditActions(false) 1182 , m_editorTrackMouseEvent(false) 1183 , m_editorWidth(-1) 1184 , m_editorHeight(-1) 1185 , m_doNotCloseEditor(false) 1186 , m_isDuringDrag(false) 1187 , m_draggedNotes() 1188 , m_focusedNote(nullptr) 1189 , m_startOfShiftSelectionNote(nullptr) 1190 , m_finishLoadOnFirstShow(false) 1191 , m_relayoutOnNextShow(false) 1192 { 1193 m_view = new BasketView(this); 1194 m_view->setFocusPolicy(Qt::StrongFocus); 1195 m_view->setAlignment(Qt::AlignLeft | Qt::AlignTop); 1196 1197 // We do this in the basket properties dialog (and keep it in sync with the 1198 // global one) 1199 if (!m_folderName.endsWith('/')) 1200 m_folderName += '/'; 1201 1202 KActionCollection *ac = Global::bnpView->actionCollection(); 1203 m_action = ac->addAction(m_folderName, this, SLOT(activatedShortcut())); 1204 ac->setShortcutsConfigurable(m_action, false); 1205 KGlobalAccel::setGlobalShortcut(m_action, (QKeySequence())); 1206 1207 // setDragAutoScroll(true); 1208 1209 // By default, there is no corner widget: we set one for the corner area to be painted! 1210 // If we don't set one and there are two scrollbars present, slowly resizing up the window show graphical glitches in that area! 1211 m_cornerWidget = new QWidget(m_view); 1212 m_view->setCornerWidget(m_cornerWidget); 1213 1214 m_view->viewport()->setAcceptDrops(true); 1215 m_view->viewport()->setMouseTracking(true); 1216 m_view->viewport()->setAutoFillBackground(false); // Do not clear the widget before paintEvent() because we always draw every pixels (faster and flicker-free) 1217 1218 // File Watcher: 1219 m_watcher = new KDirWatch(this); 1220 1221 connect(m_watcher, &KDirWatch::dirty, this, &BasketScene::watchedFileModified); 1222 //connect(m_watcher, &KDirWatch::deleted, this, &BasketScene::watchedFileDeleted); 1223 connect(&m_watcherTimer, &QTimer::timeout, this, &BasketScene::updateModifiedNotes); 1224 1225 // Various Connections: 1226 connect(&m_autoScrollSelectionTimer, &QTimer::timeout, this, &BasketScene::doAutoScrollSelection); 1227 connect(&m_timerCountsChanged, &QTimer::timeout, this, &BasketScene::countsChangedTimeOut); 1228 connect(&m_inactivityAutoSaveTimer, &QTimer::timeout, this, &BasketScene::inactivityAutoSaveTimeout); 1229 connect(&m_inactivityAutoLockTimer, &QTimer::timeout, this, &BasketScene::inactivityAutoLockTimeout); 1230 1231 #ifdef HAVE_LIBGPGME 1232 m_gpg = new KGpgMe(); 1233 #endif 1234 m_locked = isFileEncrypted(); 1235 1236 // setup the delayed commit timer 1237 m_commitdelay.setSingleShot(true); 1238 connect(&m_commitdelay, &QTimer::timeout, this, &BasketScene::commitEdit); 1239 } 1240 1241 void BasketScene::enterEvent(QEvent *) 1242 { 1243 m_underMouse = true; 1244 doHoverEffects(); 1245 } 1246 1247 void BasketScene::leaveEvent(QEvent *) 1248 { 1249 m_underMouse = false; 1250 doHoverEffects(); 1251 1252 if (m_lockedHovering) 1253 return; 1254 1255 removeInserter(); 1256 if (m_hoveredNote) { 1257 m_hoveredNote->setHovered(false); 1258 m_hoveredNote->setHoveredZone(Note::None); 1259 m_hoveredNote->update(); 1260 } 1261 m_hoveredNote = nullptr; 1262 } 1263 1264 void BasketScene::setFocusIfNotInPopupMenu() 1265 { 1266 if (!qApp->activePopupWidget()) { 1267 if (isDuringEdit()) 1268 m_editor->graphicsWidget()->setFocus(); 1269 else 1270 setFocus(); 1271 } 1272 } 1273 1274 void BasketScene::mousePressEvent(QGraphicsSceneMouseEvent *event) 1275 { 1276 // If user click the basket, focus it! 1277 // The focus is delayed because if the click results in showing a popup menu, 1278 // the interface flicker by showing the focused rectangle (as the basket gets focus) 1279 // and immediately removing it (because the popup menu now have focus). 1280 if (!isDuringEdit()) 1281 QTimer::singleShot(0, this, SLOT(setFocusIfNotInPopupMenu())); 1282 1283 // Convenient variables: 1284 bool controlPressed = event->modifiers() & Qt::ControlModifier; 1285 bool shiftPressed = event->modifiers() & Qt::ShiftModifier; 1286 1287 // Do nothing if we disabled the click some milliseconds sooner. 1288 // For instance when a popup menu has been closed with click, we should not do action: 1289 if (event->button() == Qt::LeftButton && (qApp->activePopupWidget() || m_lastDisableClick.msecsTo(QTime::currentTime()) <= 80)) { 1290 doHoverEffects(); 1291 m_noActionOnMouseRelease = true; 1292 // But we allow to select: 1293 // The code is the same as at the bottom of this method: 1294 if (event->button() == Qt::LeftButton) { 1295 m_selectionStarted = true; 1296 m_selectionBeginPoint = event->scenePos(); 1297 m_selectionInvert = controlPressed || shiftPressed; 1298 } 1299 1300 return; 1301 } 1302 1303 // if we are editing and no control key are pressed 1304 if (m_editor && !shiftPressed && !controlPressed) { 1305 // if the mouse is over the editor 1306 QPoint view_shift(m_view->horizontalScrollBar()->value(), m_view->verticalScrollBar()->value()); 1307 QGraphicsWidget *widget = dynamic_cast<QGraphicsWidget *>(m_view->itemAt((event->scenePos() - view_shift).toPoint())); 1308 if (widget && m_editor->graphicsWidget() == widget) { 1309 if (event->button() == Qt::LeftButton) { 1310 m_editorTrackMouseEvent = true; 1311 m_editor->startSelection(event->scenePos()); 1312 return; 1313 } else if (event->button() == Qt::MiddleButton) { 1314 m_editor->paste(event->scenePos(), QClipboard::Selection); 1315 return; 1316 } 1317 } 1318 } 1319 1320 // Figure out what is the clicked note and zone: 1321 Note *clicked = noteAt(event->scenePos()); 1322 if (m_editor && (!clicked || (clicked && !(editedNote() == clicked)))) { 1323 closeEditor(); 1324 } 1325 Note::Zone zone = (clicked ? clicked->zoneAt(event->scenePos() - QPointF(clicked->x(), clicked->y())) : Note::None); 1326 1327 // Popup Tags menu: 1328 if (zone == Note::TagsArrow && !controlPressed && !shiftPressed && event->button() != Qt::MidButton) { 1329 if (!clicked->allSelected()) 1330 unselectAllBut(clicked); 1331 setFocusedNote(clicked); /// /// /// 1332 m_startOfShiftSelectionNote = clicked; 1333 m_noActionOnMouseRelease = true; 1334 popupTagsMenu(clicked); 1335 1336 return; 1337 } 1338 1339 if (event->button() == Qt::LeftButton) { 1340 // Prepare to allow drag and drop when moving mouse further: 1341 if ((zone == Note::Handle || zone == Note::Group) || (clicked && clicked->allSelected() && (zone == Note::TagsArrow || zone == Note::Custom0 || zone == Note::Content || zone == Note::Link /**/ || zone >= Note::Emblem0 /**/))) { 1342 if (!shiftPressed && !controlPressed) { 1343 m_pressPos = event->scenePos(); // TODO: Allow to drag emblems to assign them to other notes. Then don't allow drag at Emblem0!! 1344 1345 m_canDrag = true; 1346 1347 // Saving where we were editing, because during a drag, the mouse can fly over the text edit and move the cursor position: 1348 if (m_editor && m_editor->textEdit()) { 1349 KTextEdit *editor = m_editor->textEdit(); 1350 m_textCursor = editor->textCursor(); 1351 } 1352 } 1353 } 1354 1355 // Initializing Resizer move: 1356 if (zone == Note::Resizer) { 1357 m_resizingNote = clicked; 1358 m_pickedResizer = event->scenePos().x() - clicked->rightLimit(); 1359 m_noActionOnMouseRelease = true; 1360 m_lockedHovering = true; 1361 1362 return; 1363 } 1364 1365 // Select note(s): 1366 if (zone == Note::Handle || zone == Note::Group || (zone == Note::GroupExpander && (controlPressed || shiftPressed))) { 1367 // closeEditor(); 1368 Note *end = clicked; 1369 if (clicked->isGroup() && shiftPressed) { 1370 if (clicked->containsNote(m_startOfShiftSelectionNote)) { 1371 m_startOfShiftSelectionNote = clicked->firstRealChild(); 1372 end = clicked->lastRealChild(); 1373 } else if (clicked->firstRealChild()->isAfter(m_startOfShiftSelectionNote)) { 1374 end = clicked->lastRealChild(); 1375 } else { 1376 end = clicked->firstRealChild(); 1377 } 1378 } 1379 if (controlPressed && shiftPressed) 1380 selectRange(m_startOfShiftSelectionNote, end, /*unselectOthers=*/false); 1381 else if (shiftPressed) 1382 selectRange(m_startOfShiftSelectionNote, end); 1383 else if (controlPressed) 1384 clicked->setSelectedRecursively(!clicked->allSelected()); 1385 else if (!clicked->allSelected()) 1386 unselectAllBut(clicked); 1387 setFocusedNote(end); /// /// /// 1388 m_startOfShiftSelectionNote = (end->isGroup() ? end->firstRealChild() : end); 1389 // m_noActionOnMouseRelease = false; 1390 m_noActionOnMouseRelease = true; 1391 1392 return; 1393 } 1394 1395 // Folding/Unfolding group: 1396 if (zone == Note::GroupExpander) { 1397 clicked->toggleFolded(); 1398 1399 relayoutNotes(); 1400 m_noActionOnMouseRelease = true; 1401 1402 return; 1403 } 1404 } 1405 1406 // Popup menu for tag emblems: 1407 if (event->button() == Qt::RightButton && zone >= Note::Emblem0) { 1408 if (!clicked->allSelected()) 1409 unselectAllBut(clicked); 1410 setFocusedNote(clicked); /// /// /// 1411 m_startOfShiftSelectionNote = clicked; 1412 popupEmblemMenu(clicked, zone - Note::Emblem0); 1413 m_noActionOnMouseRelease = true; 1414 1415 return; 1416 } 1417 1418 // Insertion Popup Menu: 1419 if ((event->button() == Qt::RightButton) && ((!clicked && isFreeLayout()) || (clicked && (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn)))) { 1420 unselectAll(); 1421 m_clickedToInsert = clicked; 1422 m_zoneToInsert = zone; 1423 m_posToInsert = event->scenePos(); 1424 1425 QMenu menu(m_view); 1426 menu.addActions(Global::bnpView->popupMenu("insert_popup")->actions()); 1427 1428 // If we already added a title, remove it because it would be kept and 1429 // then added several times. 1430 if (m_insertMenuTitle && menu.actions().contains(m_insertMenuTitle)) 1431 menu.removeAction(m_insertMenuTitle); 1432 1433 QAction *first = menu.actions().value(0); 1434 1435 // i18n: Verbs (for the "insert" menu) 1436 if (zone == Note::TopGroup || zone == Note::BottomGroup) 1437 m_insertMenuTitle = menu.insertSection(first, i18n("Group")); 1438 else 1439 m_insertMenuTitle = menu.insertSection(first, i18n("Insert")); 1440 1441 setInsertPopupMenu(); 1442 connect(&menu, &QMenu::aboutToHide, this, &BasketScene::delayedCancelInsertPopupMenu); 1443 connect(&menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); 1444 connect(&menu, &QMenu::aboutToHide, this, &BasketScene::disableNextClick); 1445 connect(&menu, &QMenu::aboutToHide, this, &BasketScene::hideInsertPopupMenu); 1446 doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! 1447 m_lockedHovering = true; 1448 menu.exec(QCursor::pos()); 1449 m_noActionOnMouseRelease = true; 1450 return; 1451 } 1452 1453 // Note Context Menu: 1454 if (event->button() == Qt::RightButton && clicked && !clicked->isColumn() && zone != Note::Resizer) { 1455 if (!clicked->allSelected()) 1456 unselectAllBut(clicked); 1457 setFocusedNote(clicked); /// /// /// 1458 if (editedNote() == clicked) { 1459 closeEditor(false); 1460 clicked->setSelected(true); 1461 } 1462 m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); 1463 QMenu *menu = Global::bnpView->popupMenu("note_popup"); 1464 connect(menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); 1465 connect(menu, &QMenu::aboutToHide, this, &BasketScene::disableNextClick); 1466 doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! 1467 m_lockedHovering = true; 1468 menu->exec(QCursor::pos()); 1469 m_noActionOnMouseRelease = true; 1470 return; 1471 } 1472 1473 // Paste selection under cursor (but not "create new primary note under cursor" because this is on moveRelease): 1474 if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { 1475 if ((Settings::middleAction() != 0) && (event->modifiers() == Qt::ShiftModifier)) { 1476 m_clickedToInsert = clicked; 1477 m_zoneToInsert = zone; 1478 m_posToInsert = event->scenePos(); 1479 // closeEditor(); 1480 removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, 1481 NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed 1482 switch (Settings::middleAction()) { 1483 case 1: 1484 m_isInsertPopupMenu = true; 1485 pasteNote(); 1486 break; 1487 case 2: 1488 type = NoteType::Image; 1489 break; 1490 case 3: 1491 type = NoteType::Link; 1492 break; 1493 case 4: 1494 type = NoteType::Launcher; 1495 break; 1496 default: 1497 m_noActionOnMouseRelease = false; 1498 return; 1499 } 1500 if (type != 0) { 1501 m_ignoreCloseEditorOnNextMouseRelease = true; 1502 Global::bnpView->insertEmpty(type); 1503 } 1504 } else { 1505 if (clicked) 1506 zone = clicked->zoneAt(event->scenePos() - QPoint(clicked->x(), clicked->y()), true); 1507 // closeEditor(); 1508 clickedToInsert(event, clicked, zone); 1509 save(); 1510 } 1511 m_noActionOnMouseRelease = true; 1512 return; 1513 } 1514 1515 // Finally, no action has been done during pressEvent, so an action can be done on releaseEvent: 1516 m_noActionOnMouseRelease = false; 1517 1518 /* Selection scenario: 1519 * On contentsMousePressEvent, put m_selectionStarted to true and init Begin and End selection point. 1520 * On contentsMouseMoveEvent, if m_selectionStarted, update End selection point, update selection rect, 1521 * and if it's larger, switching to m_isSelecting mode: we can draw the selection rectangle. 1522 */ 1523 // Prepare selection: 1524 if (event->button() == Qt::LeftButton) { 1525 m_selectionStarted = true; 1526 m_selectionBeginPoint = event->scenePos(); 1527 // We usually invert the selection with the Ctrl key, but some environments (like GNOME or The Gimp) do it with the Shift key. 1528 // Since the Shift key has no specific usage, we allow to invert selection ALSO with Shift for Gimp people 1529 m_selectionInvert = controlPressed || shiftPressed; 1530 } 1531 } 1532 1533 void BasketScene::delayedCancelInsertPopupMenu() 1534 { 1535 QTimer::singleShot(0, this, SLOT(cancelInsertPopupMenu())); 1536 } 1537 1538 void BasketScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) 1539 { 1540 if (event->reason() == QGraphicsSceneContextMenuEvent::Keyboard) { 1541 if (countFounds /*countShown*/ () == 0) { // TODO: Count shown!! 1542 QMenu *menu = Global::bnpView->popupMenu("insert_popup"); 1543 setInsertPopupMenu(); 1544 connect(menu, &QMenu::aboutToHide, this, &BasketScene::delayedCancelInsertPopupMenu); 1545 connect(menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); 1546 connect(menu, &QMenu::aboutToHide, this, &BasketScene::disableNextClick); 1547 removeInserter(); 1548 m_lockedHovering = true; 1549 menu->exec(m_view->mapToGlobal(QPoint(0, 0))); 1550 } else { 1551 if (!m_focusedNote->isSelected()) 1552 unselectAllBut(m_focusedNote); 1553 setFocusedNote(m_focusedNote); /// /// /// 1554 m_startOfShiftSelectionNote = (m_focusedNote->isGroup() ? m_focusedNote->firstRealChild() : m_focusedNote); 1555 // Popup at bottom (or top) of the focused note, if visible : 1556 QMenu *menu = Global::bnpView->popupMenu("note_popup"); 1557 connect(menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); 1558 connect(menu, &QMenu::aboutToHide, this, &BasketScene::disableNextClick); 1559 doHoverEffects(m_focusedNote, Note::Content); // In the case where another popup menu was open, we should do that manually! 1560 m_lockedHovering = true; 1561 menu->exec(noteVisibleRect(m_focusedNote).bottomLeft().toPoint()); 1562 } 1563 } 1564 } 1565 1566 QRectF BasketScene::noteVisibleRect(Note *note) 1567 { 1568 QRectF rect(QPointF(note->x(), note->y()), QSizeF(note->width(), note->height())); 1569 QPoint basketPoint = m_view->mapToGlobal(QPoint(0, 0)); 1570 rect.moveTopLeft(rect.topLeft() + basketPoint + QPoint(m_view->frameWidth(), m_view->frameWidth())); 1571 1572 // Now, rect contain the global note rectangle on the screen. 1573 // We have to clip it by the basket widget : 1574 // if (rect.bottom() > basketPoint.y() + visibleHeight() + 1) { // Bottom too... bottom 1575 // rect.setBottom(basketPoint.y() + visibleHeight() + 1); 1576 if (rect.bottom() > basketPoint.y() + m_view->viewport()->height() + 1) { // Bottom too... bottom 1577 rect.setBottom(basketPoint.y() + m_view->viewport()->height() + 1); 1578 if (rect.height() <= 0) // Have at least one visible pixel of height 1579 rect.setTop(rect.bottom()); 1580 } 1581 if (rect.top() < basketPoint.y() + m_view->frameWidth()) { // Top too... top 1582 rect.setTop(basketPoint.y() + m_view->frameWidth()); 1583 if (rect.height() <= 0) 1584 rect.setBottom(rect.top()); 1585 } 1586 // if (rect.right() > basketPoint.x() + visibleWidth() + 1) { // Right too... right 1587 // rect.setRight(basketPoint.x() + visibleWidth() + 1); 1588 if (rect.right() > basketPoint.x() + m_view->viewport()->width() + 1) { // Right too... right 1589 rect.setRight(basketPoint.x() + m_view->viewport()->width() + 1); 1590 if (rect.width() <= 0) // Have at least one visible pixel of width 1591 rect.setLeft(rect.right()); 1592 } 1593 if (rect.left() < basketPoint.x() + m_view->frameWidth()) { // Left too... left 1594 rect.setLeft(basketPoint.x() + m_view->frameWidth()); 1595 if (rect.width() <= 0) 1596 rect.setRight(rect.left()); 1597 } 1598 return rect; 1599 } 1600 1601 void BasketScene::disableNextClick() 1602 { 1603 m_lastDisableClick = QTime::currentTime(); 1604 } 1605 1606 void BasketScene::recomputeAllStyles() 1607 { 1608 FOR_EACH_NOTE(note) 1609 note->recomputeAllStyles(); 1610 } 1611 1612 void BasketScene::removedStates(const QList<State *> &deletedStates) 1613 { 1614 bool modifiedBasket = false; 1615 1616 FOR_EACH_NOTE(note) 1617 if (note->removedStates(deletedStates)) 1618 modifiedBasket = true; 1619 1620 if (modifiedBasket) 1621 save(); 1622 } 1623 1624 void BasketScene::insertNote(Note *note, Note *clicked, int zone, const QPointF &pos) 1625 { 1626 if (!note) { 1627 qDebug() << "Wanted to insert NO note"; 1628 return; 1629 } 1630 1631 if (clicked && zone == Note::BottomColumn) { 1632 // When inserting at the bottom of a column, it's obvious the new note SHOULD inherit tags. 1633 // We ensure that by changing the insertion point after the last note of the column: 1634 Note *last = clicked->lastChild(); 1635 if (last) { 1636 clicked = last; 1637 zone = Note::BottomInsert; 1638 } 1639 } 1640 1641 /// Insertion at the bottom of a column: 1642 if (clicked && zone == Note::BottomColumn) { 1643 note->setWidth(clicked->rightLimit() - clicked->x()); 1644 Note *lastChild = clicked->lastChild(); 1645 for (Note *n = note; n; n = n->next()) { 1646 n->setXRecursively(clicked->x()); 1647 n->setYRecursively((lastChild ? lastChild : clicked)->bottom() + 1); 1648 } 1649 appendNoteIn(note, clicked); 1650 1651 /// Insertion relative to a note (top/bottom, insert/group): 1652 } else if (clicked) { 1653 note->setWidth(clicked->width()); 1654 for (Note *n = note; n; n = n->next()) { 1655 if (zone == Note::TopGroup || zone == Note::BottomGroup) { 1656 n->setXRecursively(clicked->x() + Note::GROUP_WIDTH); 1657 } else { 1658 n->setXRecursively(clicked->x()); 1659 } 1660 if (zone == Note::TopInsert || zone == Note::TopGroup) { 1661 n->setYRecursively(clicked->y()); 1662 } else { 1663 n->setYRecursively(clicked->bottom() + 1); 1664 } 1665 } 1666 1667 if (zone == Note::TopInsert) { 1668 appendNoteBefore(note, clicked); 1669 } else if (zone == Note::BottomInsert) { 1670 appendNoteAfter(note, clicked); 1671 } else if (zone == Note::TopGroup) { 1672 groupNoteBefore(note, clicked); 1673 } else if (zone == Note::BottomGroup) { 1674 groupNoteAfter(note, clicked); 1675 } 1676 1677 /// Free insertion: 1678 } else if (isFreeLayout()) { 1679 // Group if note have siblings: 1680 if (note->next()) { 1681 Note *group = new Note(this); 1682 for (Note *n = note; n; n = n->next()) 1683 n->setParentNote(group); 1684 group->setFirstChild(note); 1685 note = group; 1686 } 1687 // Insert at cursor position: 1688 const int initialWidth = 250; 1689 note->setWidth(note->isGroup() ? Note::GROUP_WIDTH : initialWidth); 1690 if (note->isGroup() && note->firstChild()) 1691 note->setInitialHeight(note->firstChild()->height()); 1692 // note->setGroupWidth(initialWidth); 1693 note->setXRecursively(pos.x()); 1694 note->setYRecursively(pos.y()); 1695 appendNoteAfter(note, lastNote()); 1696 } 1697 1698 relayoutNotes(); 1699 } 1700 1701 void BasketScene::clickedToInsert(QGraphicsSceneMouseEvent *event, Note *clicked, /*Note::Zone*/ int zone) 1702 { 1703 Note *note; 1704 if (event->button() == Qt::MidButton) 1705 note = NoteFactory::dropNote(QApplication::clipboard()->mimeData(QClipboard::Selection), this); 1706 else 1707 note = NoteFactory::createNoteText(QString(), this); 1708 1709 if (!note) 1710 return; 1711 1712 insertNote(note, clicked, zone, QPointF(event->scenePos())); 1713 1714 // ensureNoteVisible(lastInsertedNote()); // TODO: in insertNote() 1715 1716 if (event->button() != Qt::MidButton) { 1717 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. 1718 closeEditor(); 1719 noteEdit(note, /*justAdded=*/true); 1720 } 1721 } 1722 1723 void BasketScene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) 1724 { 1725 m_isDuringDrag = true; 1726 Global::bnpView->updateStatusBarHint(); 1727 if (NoteDrag::basketOf(event->mimeData()) == this) { 1728 m_draggedNotes = NoteDrag::notesOf(event); 1729 NoteDrag::saveNoteSelectionToList(selectedNotes()); 1730 } 1731 event->accept(); 1732 } 1733 1734 void BasketScene::dragMoveEvent(QGraphicsSceneDragDropEvent *event) 1735 { 1736 // m_isDuringDrag = true; 1737 1738 // if (isLocked()) 1739 // return; 1740 1741 // FIXME: viewportToContents does NOT work !!! 1742 // QPoint pos = viewportToContents(event->pos()); 1743 // QPoint pos( event->pos().x() + contentsX(), event->pos().y() + contentsY() ); 1744 1745 // if (insertAtCursorPos()) 1746 // computeInsertPlace(pos); 1747 doHoverEffects(event->scenePos()); 1748 1749 // showFrameInsertTo(); 1750 if (isFreeLayout() || noteAt(event->scenePos())) // Cursor before rightLimit() or hovering the dragged source notes 1751 acceptDropEvent(event); 1752 else { 1753 event->setAccepted(false); 1754 } 1755 1756 /* Note *hoveredNote = noteAt(event->pos().x(), event->pos().y()); 1757 if ( (isColumnsLayout() && !hoveredNote) || (draggedNotes().contains(hoveredNote)) ) { 1758 event->acceptAction(false); 1759 event->accept(false); 1760 } else 1761 acceptDropEvent(event);*/ 1762 1763 // A workaround since QScrollView::dragAutoScroll seem to have no effect : 1764 // ensureVisible(event->pos().x() + contentsX(), event->pos().y() + contentsY(), 30, 30); 1765 // QScrollView::dragMoveEvent(event); 1766 } 1767 1768 void BasketScene::dragLeaveEvent(QGraphicsSceneDragDropEvent *) 1769 { 1770 // resetInsertTo(); 1771 m_isDuringDrag = false; 1772 m_draggedNotes.clear(); 1773 NoteDrag::selectedNotes.clear(); 1774 m_noActionOnMouseRelease = true; 1775 Q_EMIT resetStatusBarText(); 1776 doHoverEffects(); 1777 } 1778 1779 void BasketScene::dropEvent(QGraphicsSceneDragDropEvent *event) 1780 { 1781 QPointF pos = event->scenePos(); 1782 qDebug() << "Drop Event at position " << pos.x() << ":" << pos.y(); 1783 1784 m_isDuringDrag = false; 1785 Q_EMIT resetStatusBarText(); 1786 1787 // if (isLocked()) 1788 // return; 1789 1790 // Do NOT check the bottom&right borders. 1791 // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), 1792 // the note is first removed, and relayoutNotes() compute the new height that is smaller 1793 // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! 1794 // Should, of course, not return 0: 1795 Note *clicked = noteAt(pos); 1796 1797 if (NoteFactory::movingNotesInTheSameBasket(event->mimeData(), this, event->dropAction()) && event->dropAction() == Qt::MoveAction) { 1798 m_doNotCloseEditor = true; 1799 } 1800 1801 Note *note = NoteFactory::dropNote(event->mimeData(), this, true, event->dropAction(), dynamic_cast<Note *>(event->source())); 1802 1803 if (note) { 1804 Note::Zone zone = (clicked ? clicked->zoneAt(pos - QPointF(clicked->x(), clicked->y()), /*toAdd=*/true) : Note::None); 1805 bool animateNewPosition = NoteFactory::movingNotesInTheSameBasket(event->mimeData(), this, event->dropAction()); 1806 if (animateNewPosition) { 1807 FOR_EACH_NOTE(n) 1808 n->setOnTop(false); 1809 // FOR_EACH_NOTE_IN_CHUNK(note) 1810 for (Note *n = note; n; n = n->next()) 1811 n->setOnTop(true); 1812 } 1813 1814 insertNote(note, clicked, zone, pos); 1815 1816 // If moved a note on bottom, contentsHeight has been diminished, then view scrolled up, and we should re-scroll the view down: 1817 ensureNoteVisible(note); 1818 1819 // if (event->button() != Qt::MidButton) { 1820 // 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. 1821 // } 1822 1823 // resetInsertTo(); 1824 // doHoverEffects(); called by insertNote() 1825 save(); 1826 } 1827 1828 m_draggedNotes.clear(); 1829 NoteDrag::selectedNotes.clear(); 1830 1831 m_doNotCloseEditor = false; 1832 // When starting the drag, we saved where we were editing. 1833 // This is because during a drag, the mouse can fly over the text edit and move the cursor position, and even HIDE the cursor. 1834 // So we re-show the cursor, and re-position it at the right place: 1835 if (m_editor && m_editor->textEdit()) { 1836 KTextEdit *editor = m_editor->textEdit(); 1837 editor->setTextCursor(m_textCursor); 1838 } 1839 } 1840 1841 // handles dropping of a note to basket that is not shown 1842 // (usually through its entry in the basket list) 1843 void BasketScene::blindDrop(QGraphicsSceneDragDropEvent *event) 1844 { 1845 if (!m_isInsertPopupMenu && redirectEditActions()) { 1846 if (m_editor->textEdit()) 1847 m_editor->textEdit()->paste(); 1848 else if (m_editor->lineEdit()) 1849 m_editor->lineEdit()->paste(); 1850 } else { 1851 if (!isLoaded()) { 1852 load(); 1853 } 1854 closeEditor(); 1855 unselectAll(); 1856 Note *note = NoteFactory::dropNote(event->mimeData(), this, true, event->dropAction(), dynamic_cast<Note *>(event->source())); 1857 if (note) { 1858 insertCreatedNote(note); 1859 // unselectAllBut(note); 1860 } 1861 } 1862 save(); 1863 } 1864 1865 void BasketScene::blindDrop(const QMimeData *mimeData, Qt::DropAction dropAction, QObject *source) 1866 { 1867 if (!m_isInsertPopupMenu && redirectEditActions()) { 1868 if (m_editor->textEdit()) 1869 m_editor->textEdit()->paste(); 1870 else if (m_editor->lineEdit()) 1871 m_editor->lineEdit()->paste(); 1872 } else { 1873 if (!isLoaded()) { 1874 load(); 1875 } 1876 closeEditor(); 1877 unselectAll(); 1878 Note *note = NoteFactory::dropNote(mimeData, this, true, dropAction, dynamic_cast<Note *>(source)); 1879 if (note) { 1880 insertCreatedNote(note); 1881 // unselectAllBut(note); 1882 } 1883 } 1884 save(); 1885 } 1886 1887 void BasketScene::insertEmptyNote(int type) 1888 { 1889 if (!isLoaded()) 1890 load(); 1891 if (isDuringEdit()) 1892 closeEditor(); 1893 Note *note = NoteFactory::createEmptyNote((NoteType::Id)type, this); 1894 insertCreatedNote(note /*, / *edit=* /true*/); 1895 noteEdit(note, /*justAdded=*/true); 1896 } 1897 1898 void BasketScene::insertWizard(int type) 1899 { 1900 saveInsertionData(); 1901 Note *note = nullptr; 1902 switch (type) { 1903 default: 1904 case 1: 1905 note = NoteFactory::importKMenuLauncher(this); 1906 break; 1907 case 2: 1908 note = NoteFactory::importIcon(this); 1909 break; 1910 case 3: 1911 note = NoteFactory::importFileContent(this); 1912 break; 1913 } 1914 if (!note) 1915 return; 1916 restoreInsertionData(); 1917 insertCreatedNote(note); 1918 unselectAllBut(note); 1919 resetInsertionData(); 1920 } 1921 1922 void BasketScene::insertColor(const QColor &color) 1923 { 1924 Note *note = NoteFactory::createNoteColor(color, this); 1925 restoreInsertionData(); 1926 insertCreatedNote(note); 1927 unselectAllBut(note); 1928 resetInsertionData(); 1929 } 1930 1931 void BasketScene::insertImage(const QPixmap &image) 1932 { 1933 Note *note = NoteFactory::createNoteImage(image, this); 1934 restoreInsertionData(); 1935 insertCreatedNote(note); 1936 unselectAllBut(note); 1937 resetInsertionData(); 1938 } 1939 1940 void BasketScene::pasteNote(QClipboard::Mode mode) 1941 { 1942 if (!m_isInsertPopupMenu && redirectEditActions()) { 1943 if (m_editor->textEdit()) 1944 m_editor->textEdit()->paste(); 1945 else if (m_editor->lineEdit()) 1946 m_editor->lineEdit()->paste(); 1947 } else { 1948 if (!isLoaded()) { 1949 load(); 1950 } 1951 closeEditor(); 1952 unselectAll(); 1953 Note *note = NoteFactory::dropNote(QApplication::clipboard()->mimeData(mode), this); 1954 if (note) { 1955 insertCreatedNote(note); 1956 // unselectAllBut(note); 1957 } 1958 } 1959 } 1960 1961 void BasketScene::insertCreatedNote(Note *note) 1962 { 1963 // Get the insertion data if the user clicked inside the basket: 1964 Note *clicked = m_clickedToInsert; 1965 int zone = m_zoneToInsert; 1966 QPointF pos = m_posToInsert; 1967 1968 // If it isn't the case, use the default position: 1969 if (!clicked && (pos.x() < 0 || pos.y() < 0)) { 1970 // Insert right after the focused note: 1971 focusANote(); 1972 if (m_focusedNote) { 1973 clicked = m_focusedNote; 1974 zone = (m_focusedNote->isFree() ? Note::BottomGroup : Note::BottomInsert); 1975 pos = QPointF(m_focusedNote->x(), m_focusedNote->bottom()); 1976 // Insert at the end of the last column: 1977 } else if (isColumnsLayout()) { 1978 Note *column = /*(Settings::newNotesPlace == 0 ?*/ firstNote() /*: lastNote())*/; 1979 /*if (Settings::newNotesPlace == 0 && column->firstChild()) { // On Top, if at least one child in the column 1980 clicked = column->firstChild(); 1981 zone = Note::TopInsert; 1982 } else { // On Bottom*/ 1983 clicked = column; 1984 zone = Note::BottomColumn; 1985 /*}*/ 1986 // Insert at free position: 1987 } else { 1988 pos = QPointF(0, 0); 1989 } 1990 } 1991 1992 insertNote(note, clicked, zone, pos); 1993 // ensureNoteVisible(lastInsertedNote()); 1994 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. 1995 // resetInsertTo(); 1996 save(); 1997 } 1998 1999 void BasketScene::saveInsertionData() 2000 { 2001 m_savedClickedToInsert = m_clickedToInsert; 2002 m_savedZoneToInsert = m_zoneToInsert; 2003 m_savedPosToInsert = m_posToInsert; 2004 } 2005 2006 void BasketScene::restoreInsertionData() 2007 { 2008 m_clickedToInsert = m_savedClickedToInsert; 2009 m_zoneToInsert = m_savedZoneToInsert; 2010 m_posToInsert = m_savedPosToInsert; 2011 } 2012 2013 void BasketScene::resetInsertionData() 2014 { 2015 m_clickedToInsert = nullptr; 2016 m_zoneToInsert = 0; 2017 m_posToInsert = QPoint(-1, -1); 2018 } 2019 2020 void BasketScene::hideInsertPopupMenu() 2021 { 2022 QTimer::singleShot(50 /*ms*/, this, SLOT(timeoutHideInsertPopupMenu())); 2023 } 2024 2025 void BasketScene::timeoutHideInsertPopupMenu() 2026 { 2027 resetInsertionData(); 2028 } 2029 2030 void BasketScene::acceptDropEvent(QGraphicsSceneDragDropEvent *event, bool preCond) 2031 { 2032 // FIXME: Should not accept all actions! Or not all actions (link not supported?!) 2033 // event->acceptAction(preCond && 1); 2034 // event->accept(preCond); 2035 event->setAccepted(preCond); 2036 } 2037 2038 void BasketScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) 2039 { 2040 // Now disallow drag and mouse redirection 2041 m_canDrag = false; 2042 2043 if (m_editorTrackMouseEvent) { 2044 m_editorTrackMouseEvent = false; 2045 m_editor->endSelection(m_pressPos); 2046 return; 2047 } 2048 2049 // Cancel Resizer move: 2050 if (m_resizingNote) { 2051 m_resizingNote = nullptr; 2052 m_pickedResizer = 0; 2053 m_lockedHovering = false; 2054 doHoverEffects(); 2055 save(); 2056 } 2057 2058 // Cancel Note move: 2059 /* if (m_movingNote) { 2060 m_movingNote = 0; 2061 m_pickedHandle = QPoint(0, 0); 2062 m_lockedHovering = false; 2063 //doHoverEffects(); 2064 save(); 2065 }*/ 2066 2067 // Cancel Selection rectangle: 2068 if (m_isSelecting) { 2069 m_isSelecting = false; 2070 stopAutoScrollSelection(); 2071 resetWasInLastSelectionRect(); 2072 doHoverEffects(); 2073 invalidate(m_selectionRect); 2074 } 2075 m_selectionStarted = false; 2076 2077 Note *clicked = noteAt(event->scenePos()); 2078 Note::Zone zone = (clicked ? clicked->zoneAt(event->scenePos() - QPointF(clicked->x(), clicked->y())) : Note::None); 2079 if ((zone == Note::Handle || zone == Note::Group) && editedNote() && editedNote() == clicked) { 2080 if (m_ignoreCloseEditorOnNextMouseRelease) 2081 m_ignoreCloseEditorOnNextMouseRelease = false; 2082 else { 2083 bool editedNoteStillThere = closeEditor(); 2084 if (editedNoteStillThere) 2085 // clicked->setSelected(true); 2086 unselectAllBut(clicked); 2087 } 2088 } 2089 2090 /* 2091 if (event->buttons() == 0 && (zone == Note::Group || zone == Note::Handle)) { 2092 closeEditor(); 2093 unselectAllBut(clicked); 2094 } 2095 */ 2096 2097 // Do nothing if an action has already been made during mousePressEvent, 2098 // or if user made a selection and canceled it by regressing to a very small rectangle. 2099 if (m_noActionOnMouseRelease) 2100 return; 2101 2102 // We immediately set it to true, to avoid actions set on mouseRelease if NO mousePress event has been triggered. 2103 // This is the case when a popup menu is shown, and user click to the basket area to close it: 2104 // the menu then receive the mousePress event and the basket area ONLY receive the mouseRelease event. 2105 // Obviously, nothing should be done in this case: 2106 m_noActionOnMouseRelease = true; 2107 2108 if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { 2109 if ((Settings::middleAction() != 0) && (event->modifiers() == Qt::ShiftModifier)) { 2110 m_clickedToInsert = clicked; 2111 m_zoneToInsert = zone; 2112 m_posToInsert = event->scenePos(); 2113 closeEditor(); 2114 removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, 2115 NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed 2116 switch (Settings::middleAction()) { 2117 case 5: 2118 type = NoteType::Color; 2119 break; 2120 case 6: 2121 Global::bnpView->grabScreenshot(); 2122 return; 2123 case 7: 2124 Global::bnpView->slotColorFromScreen(); 2125 return; 2126 case 8: 2127 Global::bnpView->insertWizard(3); // loadFromFile 2128 return; 2129 case 9: 2130 Global::bnpView->insertWizard(1); // importKMenuLauncher 2131 return; 2132 case 10: 2133 Global::bnpView->insertWizard(2); // importIcon 2134 return; 2135 } 2136 if (type != 0) { 2137 m_ignoreCloseEditorOnNextMouseRelease = true; 2138 Global::bnpView->insertEmpty(type); 2139 return; 2140 } 2141 } 2142 } 2143 2144 // Note *clicked = noteAt(event->pos().x(), event->pos().y()); 2145 if (!clicked) { 2146 if (isFreeLayout() && event->button() == Qt::LeftButton) { 2147 clickedToInsert(event); 2148 save(); 2149 } 2150 2151 return; 2152 } 2153 // Note::Zone zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ); 2154 2155 // Convenient variables: 2156 bool controlPressed = event->modifiers() & Qt::ControlModifier; 2157 bool shiftPressed = event->modifiers() & Qt::ShiftModifier; 2158 2159 if (clicked && zone != Note::None && zone != Note::BottomColumn && zone != Note::Resizer && (controlPressed || shiftPressed)) { 2160 if (controlPressed && shiftPressed) 2161 selectRange(m_startOfShiftSelectionNote, clicked, /*unselectOthers=*/false); 2162 else if (shiftPressed) 2163 selectRange(m_startOfShiftSelectionNote, clicked); 2164 else if (controlPressed) 2165 clicked->setSelectedRecursively(!clicked->allSelected()); 2166 setFocusedNote(clicked); /// /// /// 2167 m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); 2168 m_noActionOnMouseRelease = true; 2169 2170 return; 2171 } 2172 2173 // Switch tag states: 2174 if (zone >= Note::Emblem0) { 2175 if (event->button() == Qt::LeftButton) { 2176 int icons = -1; 2177 for (State::List::iterator it = clicked->states().begin(); it != clicked->states().end(); ++it) { 2178 if (!(*it)->emblem().isEmpty()) 2179 icons++; 2180 if (icons == zone - Note::Emblem0) { 2181 State *state = (*it)->nextState(); 2182 if (!state) 2183 return; 2184 it = clicked->states().insert(it, state); 2185 ++it; 2186 clicked->states().erase(it); 2187 clicked->recomputeStyle(); 2188 clicked->unbufferize(); 2189 clicked->update(); 2190 updateEditorAppearance(); 2191 filterAgain(); 2192 save(); 2193 break; 2194 } 2195 } 2196 2197 return; 2198 } /* else if (event->button() == Qt::RightButton) { 2199 popupEmblemMenu(clicked, zone - Note::Emblem0); 2200 return; 2201 }*/ 2202 } 2203 2204 // Insert note or past clipboard: 2205 QString link; 2206 if (event->button() == Qt::MidButton && zone == Note::Resizer) { 2207 return; // zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), true ); 2208 } 2209 if (event->button() == Qt::RightButton && (clicked->isColumn() || zone == Note::Resizer)) { 2210 return; 2211 } 2212 if (clicked->isGroup() && zone == Note::None) { 2213 return; 2214 } 2215 switch (zone) { 2216 case Note::Handle: 2217 case Note::Group: 2218 // We select note on mousePress if it was unselected or Ctrl is pressed. 2219 // But the user can want to drag select_s_ notes, so it the note is selected, we only select it alone on mouseRelease: 2220 if (event->buttons() == 0) { 2221 qDebug() << "EXEC"; 2222 if (!(event->modifiers() & Qt::ControlModifier) && clicked->allSelected()) 2223 unselectAllBut(clicked); 2224 if (zone == Note::Handle && isDuringEdit() && editedNote() == clicked) { 2225 closeEditor(); 2226 clicked->setSelected(true); 2227 } 2228 } 2229 break; 2230 2231 case Note::Custom0: 2232 // unselectAllBut(clicked); 2233 setFocusedNote(clicked); 2234 noteOpen(clicked); 2235 break; 2236 2237 case Note::GroupExpander: 2238 case Note::TagsArrow: 2239 break; 2240 2241 case Note::Link: 2242 link = clicked->linkAt(event->scenePos() - QPoint(clicked->x(), clicked->y())); 2243 if (!link.isEmpty()) { 2244 if (link == "basket-internal-remove-basket") { 2245 // 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." 2246 Global::bnpView->doBasketDeletion(this); 2247 } else if (link == "basket-internal-import") { 2248 QMenu *menu = Global::bnpView->popupMenu("fileimport"); 2249 menu->exec(event->screenPos()); 2250 } else if (link.startsWith("basket://")) { 2251 Q_EMIT crossReference(link); 2252 } else { 2253 KRun *run = new KRun(QUrl::fromUserInput(link), m_view->window()); // open the URL. 2254 run->setAutoDelete(true); 2255 } 2256 break; 2257 } // If there is no link, edit note content 2258 Q_FALLTHROUGH(); 2259 case Note::Content: { 2260 if (m_editor && m_editor->note() == clicked && m_editor->graphicsWidget()) { 2261 m_editor->setCursorTo(event->scenePos()); 2262 } else { 2263 closeEditor(); 2264 unselectAllBut(clicked); 2265 noteEdit(clicked, /*justAdded=*/false, event->scenePos()); 2266 QGraphicsScene::mouseReleaseEvent(event); 2267 } 2268 break; 2269 } 2270 case Note::TopInsert: 2271 case Note::TopGroup: 2272 case Note::BottomInsert: 2273 case Note::BottomGroup: 2274 case Note::BottomColumn: 2275 clickedToInsert(event, clicked, zone); 2276 save(); 2277 break; 2278 2279 case Note::None: 2280 default: 2281 KMessageBox::information(m_view->viewport(), 2282 i18n("This message should never appear. If it does, this program is buggy! " 2283 "Please report the bug to the developer.")); 2284 break; 2285 } 2286 } 2287 2288 void BasketScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) 2289 { 2290 Note *clicked = noteAt(event->scenePos()); 2291 Note::Zone zone = (clicked ? clicked->zoneAt(event->scenePos() - QPointF(clicked->x(), clicked->y())) : Note::None); 2292 2293 if (event->button() == Qt::LeftButton && (zone == Note::Group || zone == Note::Handle)) { 2294 doCopy(CopyToSelection); 2295 m_noActionOnMouseRelease = true; 2296 } else 2297 mousePressEvent(event); 2298 } 2299 2300 void BasketScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) 2301 { 2302 // redirect this event to the editor if track mouse event is active 2303 if (m_editorTrackMouseEvent && (m_pressPos - event->scenePos()).manhattanLength() > QApplication::startDragDistance()) { 2304 m_editor->updateSelection(event->scenePos()); 2305 return; 2306 } 2307 2308 // Drag the notes: 2309 if (m_canDrag && (m_pressPos - event->scenePos()).manhattanLength() > QApplication::startDragDistance()) { 2310 m_canDrag = false; 2311 m_isSelecting = false; // Don't draw selection rectangle after drag! 2312 m_selectionStarted = false; 2313 2314 NoteSelection *selection = selectedNotes(); 2315 if (selection->firstStacked()) { 2316 QDrag *d = NoteDrag::dragObject(selection, /*cutting=*/false, /*source=*/m_view); // d will be deleted by QT 2317 /*bool shouldRemove = */ d->exec(); 2318 // delete selection; 2319 2320 // Never delete because URL is dragged and the file must be available for the extern application 2321 // if (shouldRemove && d->target() == 0) // If target is another application that request to remove the note 2322 // Q_EMIT wantDelete(this); 2323 } 2324 return; 2325 } 2326 2327 // Moving a Resizer: 2328 if (m_resizingNote) { 2329 qreal groupWidth = event->scenePos().x() - m_resizingNote->x() - m_pickedResizer; 2330 qreal minRight = m_resizingNote->minRight(); 2331 // int maxRight = 100 * contentsWidth(); // A big enough value (+infinity) for free layouts. 2332 qreal maxRight = 100 * sceneRect().width(); // A big enough value (+infinity) for free layouts. 2333 Note *nextColumn = m_resizingNote->next(); 2334 if (m_resizingNote->isColumn()) { 2335 if (nextColumn) 2336 maxRight = nextColumn->x() + nextColumn->rightLimit() - nextColumn->minRight() - Note::RESIZER_WIDTH; 2337 else 2338 // maxRight = contentsWidth(); 2339 maxRight = sceneRect().width(); 2340 } 2341 if (groupWidth > maxRight - m_resizingNote->x()) 2342 groupWidth = maxRight - m_resizingNote->x(); 2343 if (groupWidth < minRight - m_resizingNote->x()) 2344 groupWidth = minRight - m_resizingNote->x(); 2345 qreal delta = groupWidth - m_resizingNote->groupWidth(); 2346 m_resizingNote->setGroupWidth(groupWidth); 2347 // If resizing columns: 2348 if (m_resizingNote->isColumn()) { 2349 Note *column = m_resizingNote; 2350 if ((column = column->next())) { 2351 // Next columns should not have them X coordinate animated, because it would flicker: 2352 column->setXRecursively(column->x() + delta); 2353 // And the resizer should resize the TWO sibling columns, and not push the other columns on th right: 2354 column->setGroupWidth(column->groupWidth() - delta); 2355 } 2356 } 2357 relayoutNotes(); 2358 } 2359 2360 // Moving a Note: 2361 /* if (m_movingNote) { 2362 int x = event->pos().x() - m_pickedHandle.x(); 2363 int y = event->pos().y() - m_pickedHandle.y(); 2364 if (x < 0) x = 0; 2365 if (y < 0) y = 0; 2366 m_movingNote->setX(x); 2367 m_movingNote->setY(y); 2368 m_movingNote->relayoutAt(x, y, / *animate=* /false); 2369 relayoutNotes(true); 2370 } 2371 */ 2372 2373 // Dragging the selection rectangle: 2374 if (m_selectionStarted) 2375 doAutoScrollSelection(); 2376 2377 doHoverEffects(event->scenePos()); 2378 } 2379 2380 void BasketScene::doAutoScrollSelection() 2381 { 2382 static const int AUTO_SCROLL_MARGIN = 50; // pixels 2383 static const int AUTO_SCROLL_DELAY = 100; // milliseconds 2384 2385 QPoint pos = m_view->mapFromGlobal(QCursor::pos()); 2386 // Do the selection: 2387 if (m_isSelecting) 2388 invalidate(m_selectionRect); 2389 2390 m_selectionEndPoint = m_view->mapToScene(pos); 2391 m_selectionRect = QRectF(m_selectionBeginPoint, m_selectionEndPoint).normalized(); 2392 if (m_selectionRect.left() < 0) 2393 m_selectionRect.setLeft(0); 2394 if (m_selectionRect.top() < 0) 2395 m_selectionRect.setTop(0); 2396 // if (m_selectionRect.right() >= contentsWidth()) m_selectionRect.setRight(contentsWidth() - 1); 2397 // if (m_selectionRect.bottom() >= contentsHeight()) m_selectionRect.setBottom(contentsHeight() - 1); 2398 if (m_selectionRect.right() >= sceneRect().width()) 2399 m_selectionRect.setRight(sceneRect().width() - 1); 2400 if (m_selectionRect.bottom() >= sceneRect().height()) 2401 m_selectionRect.setBottom(sceneRect().height() - 1); 2402 2403 if ((m_selectionBeginPoint - m_selectionEndPoint).manhattanLength() > QApplication::startDragDistance()) { 2404 m_isSelecting = true; 2405 selectNotesIn(m_selectionRect, m_selectionInvert); 2406 invalidate(m_selectionRect); 2407 m_noActionOnMouseRelease = true; 2408 } else { 2409 // If the user was selecting but cancel by making the rectangle too small, cancel it really!!! 2410 if (m_isSelecting) { 2411 if (m_selectionInvert) 2412 selectNotesIn(QRectF(), m_selectionInvert); 2413 else 2414 unselectAllBut(nullptr); // TODO: unselectAll(); 2415 } 2416 if (m_isSelecting) 2417 resetWasInLastSelectionRect(); 2418 m_isSelecting = false; 2419 stopAutoScrollSelection(); 2420 2421 return; 2422 } 2423 2424 // Do the auto-scrolling: 2425 // FIXME: It's still flickering 2426 2427 // QRectF insideRect(AUTO_SCROLL_MARGIN, AUTO_SCROLL_MARGIN, visibleWidth() - 2*AUTO_SCROLL_MARGIN, visibleHeight() - 2*AUTO_SCROLL_MARGIN); 2428 // QRectF insideRect(AUTO_SCROLL_MARGIN, AUTO_SCROLL_MARGIN, m_view->viewport()->width() - 2 * AUTO_SCROLL_MARGIN, m_view->viewport()->height() - 2 * AUTO_SCROLL_MARGIN); 2429 2430 int dx = 0; 2431 int dy = 0; 2432 2433 if (pos.y() < AUTO_SCROLL_MARGIN) 2434 dy = pos.y() - AUTO_SCROLL_MARGIN; 2435 else if (pos.y() > m_view->viewport()->height() - AUTO_SCROLL_MARGIN) 2436 dy = pos.y() - m_view->viewport()->height() + AUTO_SCROLL_MARGIN; 2437 // else if (pos.y() > visibleHeight() - AUTO_SCROLL_MARGIN) 2438 // dy = pos.y() - visibleHeight() + AUTO_SCROLL_MARGIN; 2439 2440 if (pos.x() < AUTO_SCROLL_MARGIN) 2441 dx = pos.x() - AUTO_SCROLL_MARGIN; 2442 else if (pos.x() > m_view->viewport()->width() - AUTO_SCROLL_MARGIN) 2443 dx = pos.x() - m_view->viewport()->width() + AUTO_SCROLL_MARGIN; 2444 // else if (pos.x() > visibleWidth() - AUTO_SCROLL_MARGIN) 2445 // dx = pos.x() - visibleWidth() + AUTO_SCROLL_MARGIN; 2446 2447 if (dx || dy) { 2448 qApp->sendPostedEvents(); // Do the repaints, because the scrolling will make the area to repaint to be wrong 2449 // scrollBy(dx, dy); 2450 if (!m_autoScrollSelectionTimer.isActive()) 2451 m_autoScrollSelectionTimer.start(AUTO_SCROLL_DELAY); 2452 } else 2453 stopAutoScrollSelection(); 2454 } 2455 2456 void BasketScene::stopAutoScrollSelection() 2457 { 2458 m_autoScrollSelectionTimer.stop(); 2459 } 2460 2461 void BasketScene::resetWasInLastSelectionRect() 2462 { 2463 Note *note = m_firstNote; 2464 while (note) { 2465 note->resetWasInLastSelectionRect(); 2466 note = note->next(); 2467 } 2468 } 2469 2470 void BasketScene::selectAll() 2471 { 2472 if (redirectEditActions()) { 2473 if (m_editor->textEdit()) 2474 m_editor->textEdit()->selectAll(); 2475 else if (m_editor->lineEdit()) 2476 m_editor->lineEdit()->selectAll(); 2477 } else { 2478 // First select all in the group, then in the parent group... 2479 Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); 2480 while (parent) { 2481 if (!parent->allSelected()) { 2482 parent->setSelectedRecursively(true); 2483 return; 2484 } 2485 parent = parent->parentNote(); 2486 } 2487 // Then, select all: 2488 FOR_EACH_NOTE(note) 2489 note->setSelectedRecursively(true); 2490 } 2491 } 2492 2493 void BasketScene::unselectAll() 2494 { 2495 if (redirectEditActions()) { 2496 if (m_editor->textEdit()) { 2497 QTextCursor cursor = m_editor->textEdit()->textCursor(); 2498 cursor.clearSelection(); 2499 m_editor->textEdit()->setTextCursor(cursor); 2500 selectionChangedInEditor(); // THIS IS NOT EMITTED BY Qt!!! 2501 } else if (m_editor->lineEdit()) 2502 m_editor->lineEdit()->deselect(); 2503 } else { 2504 if (countSelecteds() > 0) // Optimization 2505 FOR_EACH_NOTE(note) 2506 note->setSelectedRecursively(false); 2507 } 2508 } 2509 2510 void BasketScene::invertSelection() 2511 { 2512 FOR_EACH_NOTE(note) 2513 note->invertSelectionRecursively(); 2514 } 2515 2516 void BasketScene::unselectAllBut(Note *toSelect) 2517 { 2518 FOR_EACH_NOTE(note) 2519 note->unselectAllBut(toSelect); 2520 } 2521 2522 void BasketScene::invertSelectionOf(Note *toSelect) 2523 { 2524 FOR_EACH_NOTE(note) 2525 note->invertSelectionOf(toSelect); 2526 } 2527 2528 void BasketScene::selectNotesIn(const QRectF &rect, bool invertSelection, bool unselectOthers /*= true*/) 2529 { 2530 FOR_EACH_NOTE(note) 2531 note->selectIn(rect, invertSelection, unselectOthers); 2532 } 2533 2534 void BasketScene::doHoverEffects() 2535 { 2536 doHoverEffects(m_view->mapToScene(m_view->viewport()->mapFromGlobal(QCursor::pos()))); 2537 } 2538 2539 void BasketScene::doHoverEffects(Note *note, Note::Zone zone, const QPointF &pos) 2540 { 2541 // Inform the old and new hovered note (if any): 2542 Note *oldHoveredNote = m_hoveredNote; 2543 if (note != m_hoveredNote) { 2544 if (m_hoveredNote) { 2545 m_hoveredNote->setHovered(false); 2546 m_hoveredNote->setHoveredZone(Note::None); 2547 m_hoveredNote->update(); 2548 } 2549 m_hoveredNote = note; 2550 if (m_hoveredNote) { 2551 m_hoveredNote->setHovered(true); 2552 } 2553 } 2554 2555 // If we are hovering a note, compute which zone is hovered and inform the note: 2556 if (m_hoveredNote) { 2557 if (zone != m_hoveredZone || oldHoveredNote != m_hoveredNote) { 2558 m_hoveredZone = zone; 2559 m_hoveredNote->setHoveredZone(zone); 2560 m_view->viewport()->setCursor(m_hoveredNote->cursorFromZone(zone)); 2561 2562 m_hoveredNote->update(); 2563 } 2564 2565 // If we are hovering an insert line zone, update this thing: 2566 if (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn) { 2567 placeInserter(m_hoveredNote, zone); 2568 } else { 2569 removeInserter(); 2570 } 2571 // If we are hovering an embedded link in a rich text element, show the destination in the statusbar: 2572 if (zone == Note::Link) 2573 Q_EMIT setStatusBarText(m_hoveredNote->linkAt(pos - QPoint(m_hoveredNote->x(), m_hoveredNote->y()))); 2574 else if (m_hoveredNote->content()) 2575 Q_EMIT setStatusBarText(m_hoveredNote->content()->statusBarMessage(m_hoveredZone)); // resetStatusBarText(); 2576 // If we aren't hovering a note, reset all: 2577 } else { 2578 if (isFreeLayout() && !isSelecting()) 2579 m_view->viewport()->setCursor(Qt::CrossCursor); 2580 else 2581 m_view->viewport()->unsetCursor(); 2582 m_hoveredZone = Note::None; 2583 removeInserter(); 2584 Q_EMIT resetStatusBarText(); 2585 } 2586 } 2587 2588 void BasketScene::doHoverEffects(const QPointF &pos) 2589 { 2590 // if (isDuringEdit()) 2591 // viewport()->unsetCursor(); 2592 2593 // Do we have the right to do hover effects? 2594 if (!m_loaded || m_lockedHovering) { 2595 return; 2596 } 2597 2598 // enterEvent() (mouse enter in the widget) set m_underMouse to true, and leaveEvent() make it false. 2599 // But some times the enterEvent() is not trigerred: eg. when dragging the scrollbar: 2600 // Ending the drag INSIDE the basket area will make NO hoverEffects() because m_underMouse is false. 2601 // User need to leave the area and re-enter it to get effects. 2602 // This hack solve that by dismissing the m_underMouse variable: 2603 2604 // Don't do hover effects when a popup menu is opened. 2605 // Primarily because the basket area will only receive mouseEnterEvent and mouveLeaveEvent. 2606 // It willn't be noticed of mouseMoveEvent, which would result in a apparently broken application state: 2607 bool underMouse = !qApp->activePopupWidget(); 2608 // if (qApp->activePopupWidget()) 2609 // underMouse = false; 2610 2611 // Compute which note is hovered: 2612 Note *note = (m_isSelecting || !underMouse ? nullptr : noteAt(pos)); 2613 Note::Zone zone = (note ? note->zoneAt(pos - QPointF(note->x(), note->y()), isDuringDrag()) : Note::None); 2614 2615 // Inform the old and new hovered note (if any) and update the areas: 2616 doHoverEffects(note, zone, pos); 2617 } 2618 2619 void BasketScene::mouseEnteredEditorWidget() 2620 { 2621 if (!m_lockedHovering && !qApp->activePopupWidget()) 2622 doHoverEffects(editedNote(), Note::Content, QPoint()); 2623 } 2624 2625 void BasketScene::removeInserter() 2626 { 2627 if (m_inserterShown) { // Do not hide (and then update/repaint the view) if it is already hidden! 2628 m_inserterShown = false; 2629 invalidate(m_inserterRect); 2630 } 2631 } 2632 2633 void BasketScene::placeInserter(Note *note, int zone) 2634 { 2635 // Remove the inserter: 2636 if (!note) { 2637 removeInserter(); 2638 return; 2639 } 2640 2641 // Update the old position: 2642 if (inserterShown()) { 2643 invalidate(m_inserterRect); 2644 } 2645 // Some commodities: 2646 m_inserterShown = true; 2647 m_inserterTop = (zone == Note::TopGroup || zone == Note::TopInsert); 2648 m_inserterGroup = (zone == Note::TopGroup || zone == Note::BottomGroup); 2649 // X and width: 2650 qreal groupIndent = (note->isGroup() ? note->width() : Note::HANDLE_WIDTH); 2651 qreal x = note->x(); 2652 qreal width = (note->isGroup() ? note->rightLimit() - note->x() : note->width()); 2653 if (m_inserterGroup) { 2654 x += groupIndent; 2655 width -= groupIndent; 2656 } 2657 m_inserterSplit = (Settings::groupOnInsertionLine() && note && !note->isGroup() && !note->isFree() && !note->isColumn()); 2658 // if (note->isGroup()) 2659 // width = note->rightLimit() - note->x() - (m_inserterGroup ? groupIndent : 0); 2660 // Y: 2661 qreal y = note->y() - (m_inserterGroup && m_inserterTop ? 1 : 3); 2662 if (!m_inserterTop) 2663 y += (note->isColumn() ? note->height() : note->height()); 2664 // Assigning result: 2665 m_inserterRect = QRectF(x, y, width, 6 - (m_inserterGroup ? 2 : 0)); 2666 // Update the new position: 2667 invalidate(m_inserterRect); 2668 } 2669 2670 inline void drawLineByRect(QPainter &painter, qreal x, qreal y, qreal width, qreal height) 2671 { 2672 painter.drawLine(x, y, x + width - 1, y + height - 1); 2673 } 2674 2675 void BasketScene::drawInserter(QPainter &painter, qreal xPainter, qreal yPainter) 2676 { 2677 if (!m_inserterShown) 2678 return; 2679 2680 QRectF rect = m_inserterRect; // For shorter code-lines when drawing! 2681 rect.translate(-xPainter, -yPainter); 2682 int lineY = (m_inserterGroup && m_inserterTop ? 0 : 2); 2683 int roundY = (m_inserterGroup && m_inserterTop ? 0 : 1); 2684 2685 KStatefulBrush statefulBrush(KColorScheme::View, KColorScheme::HoverColor); 2686 const QColor highlightColor = palette().color(QPalette::Highlight).lighter(); 2687 painter.setPen(highlightColor); 2688 // The horizontal line: 2689 // painter.drawRect( rect.x(), rect.y() + lineY, rect.width(), 2); 2690 int width = rect.width() - 4; 2691 painter.fillRect(rect.x() + 2, rect.y() + lineY, width, 2, highlightColor); 2692 // The left-most and right-most edges (biggest vertical lines): 2693 drawLineByRect(painter, rect.x(), rect.y(), 1, (m_inserterGroup ? 4 : 6)); 2694 drawLineByRect(painter, rect.x() + rect.width() - 1, rect.y(), 1, (m_inserterGroup ? 4 : 6)); 2695 // The left and right mid vertical lines: 2696 drawLineByRect(painter, rect.x() + 1, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); 2697 drawLineByRect(painter, rect.x() + rect.width() - 2, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); 2698 // Draw the split as a feedback to know where is the limit between insert and group: 2699 if (m_inserterSplit) { 2700 int noteWidth = rect.width() + (m_inserterGroup ? Note::HANDLE_WIDTH : 0); 2701 int xSplit = rect.x() - (m_inserterGroup ? Note::HANDLE_WIDTH : 0) + noteWidth / 2; 2702 painter.drawRect(xSplit - 2, rect.y() + lineY, 4, 2); 2703 painter.drawRect(xSplit - 1, rect.y() + lineY, 2, 2); 2704 } 2705 } 2706 2707 void BasketScene::helpEvent(QGraphicsSceneHelpEvent *event) 2708 { 2709 if (!m_loaded || !Settings::showNotesToolTip()) 2710 return; 2711 2712 QString message; 2713 QRectF rect; 2714 2715 QPointF contentPos = event->scenePos(); 2716 Note *note = noteAt(contentPos); 2717 2718 if (!note && isFreeLayout()) { 2719 message = i18n("Insert note here\nRight click for more options"); 2720 QRectF itRect; 2721 for (QList<QRectF>::iterator it = m_blankAreas.begin(); it != m_blankAreas.end(); ++it) { 2722 itRect = QRectF(0, 0, m_view->viewport()->width(), m_view->viewport()->height()).intersected(*it); 2723 if (itRect.contains(contentPos)) { 2724 rect = itRect; 2725 rect.moveLeft(rect.left() - sceneRect().x()); 2726 rect.moveTop(rect.top() - sceneRect().y()); 2727 break; 2728 } 2729 } 2730 } else { 2731 if (!note) 2732 return; 2733 2734 Note::Zone zone = note->zoneAt(contentPos - QPointF(note->x(), note->y())); 2735 2736 switch (zone) { 2737 case Note::Resizer: 2738 message = (note->isColumn() ? i18n("Resize those columns") : (note->isGroup() ? i18n("Resize this group") : i18n("Resize this note"))); 2739 break; 2740 case Note::Handle: 2741 message = i18n("Select or move this note"); 2742 break; 2743 case Note::Group: 2744 message = i18n("Select or move this group"); 2745 break; 2746 case Note::TagsArrow: 2747 message = i18n("Assign or remove tags from this note"); 2748 if (note->states().count() > 0) { 2749 QString tagsString; 2750 for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) { 2751 QString tagName = "<nobr>" + Tools::textToHTMLWithoutP((*it)->fullName()) + "</nobr>"; 2752 if (tagsString.isEmpty()) 2753 tagsString = tagName; 2754 else 2755 tagsString = i18n("%1, %2", tagsString, tagName); 2756 } 2757 message = "<qt><nobr>" + message + "</nobr><br>" + i18n("<b>Assigned Tags</b>: %1", tagsString); 2758 } 2759 break; 2760 case Note::Custom0: 2761 message = note->content()->zoneTip(zone); 2762 break; //"Open this link/Open this file/Open this sound file/Launch this application" 2763 case Note::GroupExpander: 2764 message = (note->isFolded() ? i18n("Expand this group") : i18n("Collapse this group")); 2765 break; 2766 case Note::Link: 2767 case Note::Content: 2768 message = note->content()->editToolTipText(); 2769 break; 2770 case Note::TopInsert: 2771 case Note::BottomInsert: 2772 message = i18n("Insert note here\nRight click for more options"); 2773 break; 2774 case Note::TopGroup: 2775 message = i18n("Group note with the one below\nRight click for more options"); 2776 break; 2777 case Note::BottomGroup: 2778 message = i18n("Group note with the one above\nRight click for more options"); 2779 break; 2780 case Note::BottomColumn: 2781 message = i18n("Insert note here\nRight click for more options"); 2782 break; 2783 case Note::None: 2784 message = "** Zone NONE: internal error **"; 2785 break; 2786 default: 2787 if (zone >= Note::Emblem0) 2788 message = note->stateForEmblemNumber(zone - Note::Emblem0)->fullName(); 2789 else 2790 message = QString(); 2791 break; 2792 } 2793 2794 if (zone == Note::Content || zone == Note::Link || zone == Note::Custom0) { 2795 const auto toolTip = QMultiMap<QString, QString> { 2796 { i18n("Added"), note->addedStringDate() }, 2797 { i18n("Last Modification"), note->lastModificationStringDate() } 2798 }.unite(note->content()->toolTipInfos()); 2799 2800 message = "<qt><nobr>" + message; 2801 for (const QString &key : toolTip.keys()) { 2802 message += "<br>" + i18nc("of the form 'key: value'", "<b>%1</b>: %2", key, toolTip.value(key)); 2803 } 2804 message += "</nobr></qt>"; 2805 } else if (m_inserterSplit && (zone == Note::TopInsert || zone == Note::BottomInsert)) 2806 message += '\n' + i18n("Click on the right to group instead of insert"); 2807 else if (m_inserterSplit && (zone == Note::TopGroup || zone == Note::BottomGroup)) 2808 message += '\n' + i18n("Click on the left to insert instead of group"); 2809 2810 rect = note->zoneRect(zone, contentPos - QPoint(note->x(), note->y())); 2811 2812 rect.moveLeft(rect.left() - sceneRect().x()); 2813 rect.moveTop(rect.top() - sceneRect().y()); 2814 2815 rect.moveLeft(rect.left() + note->x()); 2816 rect.moveTop(rect.top() + note->y()); 2817 } 2818 2819 QToolTip::showText(event->screenPos(), message, m_view, rect.toRect()); 2820 } 2821 2822 Note *BasketScene::lastNote() 2823 { 2824 Note *note = firstNote(); 2825 while (note && note->next()) 2826 note = note->next(); 2827 return note; 2828 } 2829 2830 void BasketScene::deleteNotes() 2831 { 2832 Note *note = m_firstNote; 2833 2834 while (note) { 2835 Note *tmp = note->next(); 2836 delete note; 2837 note = tmp; 2838 } 2839 m_firstNote = nullptr; 2840 m_resizingNote = nullptr; 2841 m_movingNote = nullptr; 2842 m_focusedNote = nullptr; 2843 m_startOfShiftSelectionNote = nullptr; 2844 m_tagPopupNote = nullptr; 2845 m_clickedToInsert = nullptr; 2846 m_savedClickedToInsert = nullptr; 2847 m_hoveredNote = nullptr; 2848 m_count = 0; 2849 m_countFounds = 0; 2850 m_countSelecteds = 0; 2851 2852 Q_EMIT resetStatusBarText(); 2853 Q_EMIT countsChanged(this); 2854 } 2855 2856 Note *BasketScene::noteAt(QPointF pos) 2857 { 2858 qreal x = pos.x(); 2859 qreal y = pos.y(); 2860 // NO: 2861 // // Do NOT check the bottom&right borders. 2862 // // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), 2863 // // the note is first removed, and relayoutNotes() compute the new height that is smaller 2864 // // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! 2865 // // Should, of course, not return 0: 2866 if (x < 0 || x > sceneRect().width() || y < 0 || y > sceneRect().height()) 2867 return nullptr; 2868 2869 // When resizing a note/group, keep it highlighted: 2870 if (m_resizingNote) 2871 return m_resizingNote; 2872 2873 // Search and return the hovered note: 2874 Note *note = m_firstNote; 2875 Note *possibleNote; 2876 while (note) { 2877 possibleNote = note->noteAt(pos); 2878 if (possibleNote) { 2879 if (NoteDrag::selectedNotes.contains(possibleNote) || draggedNotes().contains(possibleNote)) 2880 return nullptr; 2881 else 2882 return possibleNote; 2883 } 2884 note = note->next(); 2885 } 2886 2887 // If the basket is layouted in columns, return one of the columns to be able to add notes in them: 2888 if (isColumnsLayout()) { 2889 Note *column = m_firstNote; 2890 while (column) { 2891 if (x >= column->x() && x < column->rightLimit()) 2892 return column; 2893 column = column->next(); 2894 } 2895 } 2896 2897 // Nothing found, no note is hovered: 2898 return nullptr; 2899 } 2900 2901 BasketScene::~BasketScene() 2902 { 2903 m_commitdelay.stop(); // we don't know how long deleteNotes() last so we want to make extra sure that nobody will commit in between 2904 if (m_decryptBox) 2905 delete m_decryptBox; 2906 #ifdef HAVE_LIBGPGME 2907 delete m_gpg; 2908 #endif 2909 blockSignals(true); 2910 deleteNotes(); 2911 2912 if (m_view) 2913 delete m_view; 2914 } 2915 2916 QColor BasketScene::selectionRectInsideColor() 2917 { 2918 return Tools::mixColor(Tools::mixColor(backgroundColor(), palette().color(QPalette::Highlight)), backgroundColor()); 2919 } 2920 2921 QColor alphaBlendColors(const QColor &bgColor, const QColor &fgColor, const int a) 2922 { 2923 // normal button... 2924 QRgb rgb = bgColor.rgb(); 2925 QRgb rgb_b = fgColor.rgb(); 2926 int alpha = a; 2927 if (alpha > 255) 2928 alpha = 255; 2929 if (alpha < 0) 2930 alpha = 0; 2931 int inv_alpha = 255 - alpha; 2932 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)); 2933 2934 return result; 2935 } 2936 2937 void BasketScene::unlock() 2938 { 2939 QTimer::singleShot(0, this, SLOT(load())); 2940 } 2941 2942 void BasketScene::inactivityAutoLockTimeout() 2943 { 2944 lock(); 2945 } 2946 2947 void BasketScene::drawBackground(QPainter *painter, const QRectF &rect) 2948 { 2949 if (!m_loadingLaunched) { 2950 if (!m_locked) { 2951 QTimer::singleShot(0, this, SLOT(load())); 2952 return; 2953 } else { 2954 Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar 2955 } 2956 } 2957 2958 if (!hasBackgroundImage()) { 2959 painter->fillRect(rect, backgroundColor()); 2960 // It's either a background pixmap to draw or a background color to fill: 2961 } else if (isTiledBackground() || (rect.x() < backgroundPixmap()->width() && rect.y() < backgroundPixmap()->height())) { 2962 painter->fillRect(rect, backgroundColor()); 2963 blendBackground(*painter, rect, 0, 0, /*opaque=*/true); 2964 } else { 2965 painter->fillRect(rect, backgroundColor()); 2966 } 2967 } 2968 2969 void BasketScene::drawForeground(QPainter *painter, const QRectF &rect) 2970 { 2971 if (m_locked) { 2972 if (!m_decryptBox) { 2973 m_decryptBox = new QFrame(m_view); 2974 m_decryptBox->setFrameShape(QFrame::StyledPanel); 2975 m_decryptBox->setFrameShadow(QFrame::Plain); 2976 m_decryptBox->setLineWidth(1); 2977 2978 QGridLayout *layout = new QGridLayout(m_decryptBox); 2979 layout->setContentsMargins(11, 11, 11, 11); 2980 layout->setSpacing(6); 2981 2982 #ifdef HAVE_LIBGPGME 2983 m_button = new QPushButton(m_decryptBox); 2984 m_button->setText(i18n("&Unlock")); 2985 layout->addWidget(m_button, 1, 2); 2986 connect(m_button, &QButton::clicked, this, &BasketScene::unlock); 2987 #endif 2988 QLabel *label = new QLabel(m_decryptBox); 2989 QString text = "<b>" + i18n("Password protected basket.") + "</b><br/>"; 2990 #ifdef HAVE_LIBGPGME 2991 label->setText(text + i18n("Press Unlock to access it.")); 2992 #else 2993 label->setText(text + i18n("Encryption is not supported by<br/>this version of %1.", QGuiApplication::applicationDisplayName())); 2994 #endif 2995 label->setAlignment(Qt::AlignTop); 2996 layout->addWidget(label, 0, 1, 1, 2); 2997 QLabel *pixmap = new QLabel(m_decryptBox); 2998 pixmap->setPixmap(KIconLoader::global()->loadIcon("encrypted", KIconLoader::NoGroup, KIconLoader::SizeHuge)); 2999 layout->addWidget(pixmap, 0, 0, 2, 1); 3000 3001 QSpacerItem *spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); 3002 layout->addItem(spacer, 1, 1); 3003 3004 label = new QLabel("<small>" + 3005 i18n("To make baskets stay unlocked, change the automatic<br>" 3006 "locking duration in the application settings.") + 3007 "</small>", 3008 m_decryptBox); 3009 label->setAlignment(Qt::AlignTop); 3010 layout->addWidget(label, 2, 0, 1, 3); 3011 3012 m_decryptBox->resize(layout->sizeHint()); 3013 } 3014 if (m_decryptBox->isHidden()) { 3015 m_decryptBox->show(); 3016 } 3017 #ifdef HAVE_LIBGPGME 3018 m_button->setFocus(); 3019 #endif 3020 m_decryptBox->move((m_view->viewport()->width() - m_decryptBox->width()) / 2, (m_view->viewport()->height() - m_decryptBox->height()) / 2); 3021 } else { 3022 if (m_decryptBox && !m_decryptBox->isHidden()) 3023 m_decryptBox->hide(); 3024 } 3025 3026 if (!m_loaded) { 3027 setSceneRect(0, 0, m_view->viewport()->width(), m_view->viewport()->height()); 3028 QBrush brush(backgroundColor()); 3029 QPixmap pixmap(m_view->viewport()->width(), m_view->viewport()->height()); // TODO: Clip it to asked size only! 3030 QPainter painter2(&pixmap); 3031 QTextDocument rt; 3032 rt.setHtml(QString("<center>%1</center>").arg(i18n("Loading..."))); 3033 rt.setTextWidth(m_view->viewport()->width()); 3034 int hrt = rt.size().height(); 3035 painter2.fillRect(0, 0, m_view->viewport()->width(), m_view->viewport()->height(), brush); 3036 blendBackground(painter2, QRectF(0, 0, m_view->viewport()->width(), m_view->viewport()->height()), -1, -1, /*opaque=*/true); 3037 QPalette pal = palette(); 3038 pal.setColor(QPalette::WindowText, textColor()); 3039 painter2.translate(0, (m_view->viewport()->height() - hrt) / 2); 3040 QAbstractTextDocumentLayout::PaintContext context; 3041 context.palette = pal; 3042 rt.documentLayout()->draw(&painter2, context); 3043 painter2.end(); 3044 painter->drawPixmap(0, 0, pixmap); 3045 return; // TODO: Clip to the wanted rectangle 3046 } 3047 3048 enableActions(); 3049 3050 if ((inserterShown() && rect.intersects(inserterRect())) || (m_isSelecting && rect.intersects(m_selectionRect))) { 3051 // Draw inserter: 3052 if (inserterShown() && rect.intersects(inserterRect())) { 3053 drawInserter(*painter, 0, 0); 3054 } 3055 // Draw selection rect: 3056 if (m_isSelecting && rect.intersects(m_selectionRect)) { 3057 QRectF selectionRect = m_selectionRect; 3058 QRectF selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); 3059 if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { 3060 QColor insideColor = selectionRectInsideColor(); 3061 painter->fillRect(selectionRectInside, QBrush(insideColor, Qt::Dense4Pattern)); 3062 } 3063 painter->setPen(palette().color(QPalette::Highlight).darker()); 3064 painter->drawRect(selectionRect); 3065 painter->setPen(Tools::mixColor(palette().color(QPalette::Highlight).darker(), backgroundColor())); 3066 painter->drawPoint(selectionRect.topLeft()); 3067 painter->drawPoint(selectionRect.topRight()); 3068 painter->drawPoint(selectionRect.bottomLeft()); 3069 painter->drawPoint(selectionRect.bottomRight()); 3070 } 3071 } 3072 } 3073 3074 /* rect(x,y,width,height)==(xBackgroundToDraw,yBackgroundToDraw,widthToDraw,heightToDraw) 3075 */ 3076 void BasketScene::blendBackground(QPainter &painter, const QRectF &rect, qreal xPainter, qreal yPainter, bool opaque, QPixmap *bg) 3077 { 3078 painter.save(); 3079 if (xPainter == -1 && yPainter == -1) { 3080 xPainter = rect.x(); 3081 yPainter = rect.y(); 3082 } 3083 3084 if (hasBackgroundImage()) { 3085 const QPixmap *bgPixmap = (bg ? /* * */ bg : (opaque ? m_opaqueBackgroundPixmap : m_backgroundPixmap)); 3086 if (isTiledBackground()) { 3087 painter.drawTiledPixmap(rect.x() - xPainter, rect.y() - yPainter, rect.width(), rect.height(), *bgPixmap, rect.x(), rect.y()); 3088 } else { 3089 painter.drawPixmap(QPointF(rect.x() - xPainter, rect.y() - yPainter), *bgPixmap, rect); 3090 } 3091 } 3092 painter.restore(); 3093 } 3094 3095 void BasketScene::recomputeBlankRects() 3096 { 3097 m_blankAreas.clear(); 3098 return; 3099 3100 m_blankAreas.append(QRectF(0, 0, sceneRect().width(), sceneRect().height())); 3101 3102 FOR_EACH_NOTE(note) 3103 note->recomputeBlankRects(m_blankAreas); 3104 3105 // See the drawing of blank areas in BasketScene::drawContents() 3106 if (hasBackgroundImage() && !isTiledBackground()) 3107 substractRectOnAreas(QRectF(0, 0, backgroundPixmap()->width(), backgroundPixmap()->height()), m_blankAreas, false); 3108 } 3109 3110 void BasketScene::unsetNotesWidth() 3111 { 3112 Note *note = m_firstNote; 3113 while (note) { 3114 note->unsetWidth(); 3115 note = note->next(); 3116 } 3117 } 3118 3119 void BasketScene::relayoutNotes() 3120 { 3121 if (Global::bnpView->currentBasket() != this) 3122 return; // Optimize load time, and basket will be relaid out when activated, anyway 3123 3124 int h = 0; 3125 tmpWidth = 0; 3126 tmpHeight = 0; 3127 Note *note = m_firstNote; 3128 while (note) { 3129 if (note->matching()) { 3130 note->relayoutAt(0, h); 3131 if (note->hasResizer()) { 3132 int minGroupWidth = note->minRight() - note->x(); 3133 if (note->groupWidth() < minGroupWidth) { 3134 note->setGroupWidth(minGroupWidth); 3135 relayoutNotes(); // Redo the thing, but this time it should not recurse 3136 return; 3137 } 3138 } 3139 h += note->height(); 3140 } 3141 note = note->next(); 3142 } 3143 3144 if (isFreeLayout()) 3145 tmpHeight += 100; 3146 else 3147 tmpHeight += 15; 3148 3149 setSceneRect(0, 0, qMax((qreal)m_view->viewport()->width(), tmpWidth), qMax((qreal)m_view->viewport()->height(), tmpHeight)); 3150 3151 recomputeBlankRects(); 3152 placeEditor(); 3153 doHoverEffects(); 3154 invalidate(); 3155 } 3156 3157 void BasketScene::popupEmblemMenu(Note *note, int emblemNumber) 3158 { 3159 m_tagPopupNote = note; 3160 State *state = note->stateForEmblemNumber(emblemNumber); 3161 State *nextState = state->nextState(/*cycle=*/false); 3162 Tag *tag = state->parentTag(); 3163 m_tagPopup = tag; 3164 3165 QKeySequence sequence = tag->shortcut(); 3166 bool sequenceOnDelete = (nextState == nullptr && !tag->shortcut().isEmpty()); 3167 3168 QMenu menu(m_view); 3169 if (tag->countStates() == 1) { 3170 menu.addSection(/*SmallIcon(state->icon()), */ tag->name()); 3171 QAction *act; 3172 act = new QAction(QIcon::fromTheme("edit-delete"), i18n("&Remove"), &menu); 3173 act->setData(1); 3174 menu.addAction(act); 3175 act = new QAction(QIcon::fromTheme("configure"), i18n("&Customize..."), &menu); 3176 act->setData(2); 3177 menu.addAction(act); 3178 3179 menu.addSeparator(); 3180 3181 act = new QAction(QIcon::fromTheme("search-filter"), i18n("&Filter by this Tag"), &menu); 3182 act->setData(3); 3183 menu.addAction(act); 3184 } else { 3185 menu.addSection(tag->name()); 3186 QList<State *>::iterator it; 3187 State *currentState; 3188 3189 int i = 10; 3190 // QActionGroup makes the actions exclusive; turns checkboxes into radio 3191 // buttons on some styles. 3192 QActionGroup *emblemGroup = new QActionGroup(&menu); 3193 for (it = tag->states().begin(); it != tag->states().end(); ++it) { 3194 currentState = *it; 3195 QKeySequence sequence; 3196 if (currentState == nextState && !tag->shortcut().isEmpty()) 3197 sequence = tag->shortcut(); 3198 3199 StateAction *sa = new StateAction(currentState, QKeySequence(sequence), nullptr, false); 3200 sa->setChecked(state == currentState); 3201 sa->setActionGroup(emblemGroup); 3202 sa->setData(i); 3203 3204 menu.addAction(sa); 3205 if (currentState == nextState && !tag->shortcut().isEmpty()) 3206 sa->setShortcut(sequence); 3207 ++i; 3208 } 3209 3210 menu.addSeparator(); 3211 3212 QAction *act = new QAction(&menu); 3213 act->setIcon(QIcon::fromTheme("edit-delete")); 3214 act->setText(i18n("&Remove")); 3215 act->setShortcut(sequenceOnDelete ? sequence : QKeySequence()); 3216 act->setData(1); 3217 menu.addAction(act); 3218 act = new QAction(QIcon::fromTheme("configure"), i18n("&Customize..."), &menu); 3219 act->setData(2); 3220 menu.addAction(act); 3221 3222 menu.addSeparator(); 3223 3224 act = new QAction(QIcon::fromTheme("search-filter"), i18n("&Filter by this Tag"), &menu); 3225 act->setData(3); 3226 menu.addAction(act); 3227 act = new QAction(QIcon::fromTheme("search-filter"), i18n("Filter by this &State"), &menu); 3228 act->setData(4); 3229 menu.addAction(act); 3230 } 3231 3232 connect(&menu, &QMenu::triggered, this, &BasketScene::toggledStateInMenu); 3233 connect(&menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); 3234 connect(&menu, &QMenu::aboutToHide, this, &BasketScene::disableNextClick); 3235 3236 m_lockedHovering = true; 3237 menu.exec(QCursor::pos()); 3238 } 3239 3240 void BasketScene::toggledStateInMenu(QAction *action) 3241 { 3242 int id = action->data().toInt(); 3243 if (id == 1) { 3244 removeTagFromSelectedNotes(m_tagPopup); 3245 // m_tagPopupNote->removeTag(m_tagPopup); 3246 // m_tagPopupNote->setWidth(0); // To force a new layout computation 3247 updateEditorAppearance(); 3248 filterAgain(); 3249 save(); 3250 return; 3251 } 3252 if (id == 2) { // Customize this State: 3253 TagsEditDialog dialog(m_view, m_tagPopupNote->stateOfTag(m_tagPopup)); 3254 dialog.exec(); 3255 return; 3256 } 3257 if (id == 3) { // Filter by this Tag 3258 decoration()->filterBar()->filterTag(m_tagPopup); 3259 return; 3260 } 3261 if (id == 4) { // Filter by this State 3262 decoration()->filterBar()->filterState(m_tagPopupNote->stateOfTag(m_tagPopup)); 3263 return; 3264 } 3265 3266 /*addStateToSelectedNotes*/ changeStateOfSelectedNotes(m_tagPopup->states()[id - 10] /*, orReplace=true*/); 3267 // m_tagPopupNote->addState(m_tagPopup->states()[id - 10], true); 3268 filterAgain(); 3269 save(); 3270 } 3271 3272 State *BasketScene::stateForTagFromSelectedNotes(Tag *tag) 3273 { 3274 State *state = nullptr; 3275 3276 FOR_EACH_NOTE(note) 3277 if (note->stateForTagFromSelectedNotes(tag, &state) && state == nullptr) 3278 return nullptr; 3279 return state; 3280 } 3281 3282 void BasketScene::activatedTagShortcut(Tag *tag) 3283 { 3284 // Compute the next state to set: 3285 State *state = stateForTagFromSelectedNotes(tag); 3286 if (state) 3287 state = state->nextState(/*cycle=*/false); 3288 else 3289 state = tag->states().first(); 3290 3291 // Set or unset it: 3292 if (state) { 3293 FOR_EACH_NOTE(note) 3294 note->addStateToSelectedNotes(state, /*orReplace=*/true); 3295 updateEditorAppearance(); 3296 } else 3297 removeTagFromSelectedNotes(tag); 3298 3299 filterAgain(); 3300 save(); 3301 } 3302 3303 void BasketScene::popupTagsMenu(Note *note) 3304 { 3305 m_tagPopupNote = note; 3306 3307 QMenu menu(m_view); 3308 menu.addSection(i18n("Tags")); 3309 3310 Global::bnpView->populateTagsMenu(menu, note); 3311 3312 m_lockedHovering = true; 3313 menu.exec(QCursor::pos()); 3314 } 3315 3316 void BasketScene::unlockHovering() 3317 { 3318 m_lockedHovering = false; 3319 doHoverEffects(); 3320 } 3321 3322 void BasketScene::toggledTagInMenu(QAction *act) 3323 { 3324 int id = act->data().toInt(); 3325 if (id == 1) { // Assign new Tag... 3326 TagsEditDialog dialog(m_view, /*stateToEdit=*/nullptr, /*addNewTag=*/true); 3327 dialog.exec(); 3328 if (!dialog.addedStates().isEmpty()) { 3329 State::List states = dialog.addedStates(); 3330 for (State::List::iterator itState = states.begin(); itState != states.end(); ++itState) 3331 FOR_EACH_NOTE(note) 3332 note->addStateToSelectedNotes(*itState); 3333 updateEditorAppearance(); 3334 filterAgain(); 3335 save(); 3336 } 3337 return; 3338 } 3339 if (id == 2) { // Remove All 3340 removeAllTagsFromSelectedNotes(); 3341 filterAgain(); 3342 save(); 3343 return; 3344 } 3345 if (id == 3) { // Customize... 3346 TagsEditDialog dialog(m_view); 3347 dialog.exec(); 3348 return; 3349 } 3350 3351 Tag *tag = Tag::all[id - 10]; 3352 if (!tag) 3353 return; 3354 3355 if (m_tagPopupNote->hasTag(tag)) 3356 removeTagFromSelectedNotes(tag); 3357 else 3358 addTagToSelectedNotes(tag); 3359 m_tagPopupNote->setWidth(0); // To force a new layout computation 3360 filterAgain(); 3361 save(); 3362 } 3363 3364 void BasketScene::addTagToSelectedNotes(Tag *tag) 3365 { 3366 FOR_EACH_NOTE(note) 3367 note->addTagToSelectedNotes(tag); 3368 updateEditorAppearance(); 3369 } 3370 3371 void BasketScene::removeTagFromSelectedNotes(Tag *tag) 3372 { 3373 FOR_EACH_NOTE(note) 3374 note->removeTagFromSelectedNotes(tag); 3375 updateEditorAppearance(); 3376 } 3377 3378 void BasketScene::addStateToSelectedNotes(State *state) 3379 { 3380 FOR_EACH_NOTE(note) 3381 note->addStateToSelectedNotes(state); 3382 updateEditorAppearance(); 3383 } 3384 3385 void BasketScene::updateEditorAppearance() 3386 { 3387 if (isDuringEdit() && m_editor->graphicsWidget()) { 3388 m_editor->graphicsWidget()->setFont(m_editor->note()->font()); 3389 3390 if (m_editor->graphicsWidget()->widget()) { 3391 QPalette palette; 3392 palette.setColor(m_editor->graphicsWidget()->widget()->backgroundRole(), m_editor->note()->backgroundColor()); 3393 palette.setColor(m_editor->graphicsWidget()->widget()->foregroundRole(), m_editor->note()->textColor()); 3394 m_editor->graphicsWidget()->setPalette(palette); 3395 } 3396 3397 // Ugly Hack around Qt bugs: placeCursor() don't call any signal: 3398 HtmlEditor *htmlEditor = dynamic_cast<HtmlEditor *>(m_editor); 3399 if (htmlEditor) { 3400 if (m_editor->textEdit()->textCursor().atStart()) { 3401 m_editor->textEdit()->moveCursor(QTextCursor::Right); 3402 m_editor->textEdit()->moveCursor(QTextCursor::Left); 3403 } else { 3404 m_editor->textEdit()->moveCursor(QTextCursor::Left); 3405 m_editor->textEdit()->moveCursor(QTextCursor::Right); 3406 } 3407 htmlEditor->cursorPositionChanged(); // Does not work anyway :-( (when clicking on a red bold text, the toolbar still show black normal text) 3408 } 3409 } 3410 } 3411 3412 void BasketScene::editorPropertiesChanged() 3413 { 3414 if (isDuringEdit() && m_editor->note()->content()->type() == NoteType::Html) { 3415 m_editor->textEdit()->setAutoFormatting(Settings::autoBullet() ? QTextEdit::AutoAll : QTextEdit::AutoNone); 3416 } 3417 } 3418 3419 void BasketScene::changeStateOfSelectedNotes(State *state) 3420 { 3421 FOR_EACH_NOTE(note) 3422 note->changeStateOfSelectedNotes(state); 3423 updateEditorAppearance(); 3424 } 3425 3426 void BasketScene::removeAllTagsFromSelectedNotes() 3427 { 3428 FOR_EACH_NOTE(note) 3429 note->removeAllTagsFromSelectedNotes(); 3430 updateEditorAppearance(); 3431 } 3432 3433 bool BasketScene::selectedNotesHaveTags() 3434 { 3435 FOR_EACH_NOTE(note) 3436 if (note->selectedNotesHaveTags()) 3437 return true; 3438 return false; 3439 } 3440 3441 QColor BasketScene::backgroundColor() const 3442 { 3443 if (m_backgroundColorSetting.isValid()) 3444 return m_backgroundColorSetting; 3445 else 3446 return palette().color(QPalette::Base); 3447 } 3448 3449 QColor BasketScene::textColor() const 3450 { 3451 if (m_textColorSetting.isValid()) 3452 return m_textColorSetting; 3453 else 3454 return palette().color(QPalette::Text); 3455 } 3456 3457 void BasketScene::unbufferizeAll() 3458 { 3459 FOR_EACH_NOTE(note) 3460 note->unbufferizeAll(); 3461 } 3462 3463 Note *BasketScene::editedNote() 3464 { 3465 if (m_editor) 3466 return m_editor->note(); 3467 else 3468 return nullptr; 3469 } 3470 3471 bool BasketScene::hasTextInEditor() 3472 { 3473 if (!isDuringEdit() || !redirectEditActions()) 3474 return false; 3475 3476 if (m_editor->textEdit()) 3477 return !m_editor->textEdit()->document()->isEmpty(); 3478 else if (m_editor->lineEdit()) 3479 return !m_editor->lineEdit()->displayText().isEmpty(); 3480 else 3481 return false; 3482 } 3483 3484 bool BasketScene::hasSelectedTextInEditor() 3485 { 3486 if (!isDuringEdit() || !redirectEditActions()) 3487 return false; 3488 3489 if (m_editor->textEdit()) { 3490 // The following line does NOT work if one letter is selected and the user press Shift+Left or Shift+Right to unselect than letter: 3491 // Qt mysteriously tell us there is an invisible selection!! 3492 // return m_editor->textEdit()->hasSelectedText(); 3493 return !m_editor->textEdit()->textCursor().selectedText().isEmpty(); 3494 } else if (m_editor->lineEdit()) 3495 return m_editor->lineEdit()->hasSelectedText(); 3496 else 3497 return false; 3498 } 3499 3500 bool BasketScene::selectedAllTextInEditor() 3501 { 3502 if (!isDuringEdit() || !redirectEditActions()) 3503 return false; 3504 3505 if (m_editor->textEdit()) { 3506 return m_editor->textEdit()->document()->isEmpty() || m_editor->textEdit()->toPlainText() == m_editor->textEdit()->textCursor().selectedText(); 3507 } else if (m_editor->lineEdit()) 3508 return m_editor->lineEdit()->displayText().isEmpty() || m_editor->lineEdit()->displayText() == m_editor->lineEdit()->selectedText(); 3509 else 3510 return false; 3511 } 3512 3513 void BasketScene::selectionChangedInEditor() 3514 { 3515 Global::bnpView->notesStateChanged(); 3516 } 3517 3518 void BasketScene::contentChangedInEditor() 3519 { 3520 // 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): 3521 if (m_editor->textEdit()) 3522 m_editor->autoSave(/*toFileToo=*/false); 3523 // else { 3524 if (m_inactivityAutoSaveTimer.isActive()) 3525 m_inactivityAutoSaveTimer.stop(); 3526 m_inactivityAutoSaveTimer.setSingleShot(true); 3527 m_inactivityAutoSaveTimer.start(3 * 1000); 3528 Global::bnpView->setUnsavedStatus(true); 3529 // } 3530 } 3531 3532 void BasketScene::inactivityAutoSaveTimeout() 3533 { 3534 if (m_editor) 3535 m_editor->autoSave(/*toFileToo=*/true); 3536 } 3537 3538 void BasketScene::placeEditorAndEnsureVisible() 3539 { 3540 placeEditor(/*andEnsureVisible=*/true); 3541 } 3542 3543 // TODO: [kw] Oh boy, this will probably require some tweaking. 3544 void BasketScene::placeEditor(bool /*andEnsureVisible*/ /*= false*/) 3545 { 3546 if (!isDuringEdit()) 3547 return; 3548 3549 QFrame *editorQFrame = dynamic_cast<QFrame *>(m_editor->graphicsWidget()->widget()); 3550 KTextEdit *textEdit = m_editor->textEdit(); 3551 Note *note = m_editor->note(); 3552 3553 qreal frameWidth = (editorQFrame ? editorQFrame->frameWidth() : 0); 3554 qreal x = note->x() + note->contentX() + note->content()->xEditorIndent() - frameWidth; 3555 qreal y; 3556 qreal maxHeight = qMax((qreal)m_view->viewport()->height(), sceneRect().height()); 3557 qreal height, width; 3558 3559 if (textEdit) { 3560 // Need to do it 2 times, because it's wrong otherwise 3561 // (sometimes, width depends on height, and sometimes, height depends on with): 3562 for (int i = 0; i < 2; i++) { 3563 // FIXME: CRASH: Select all text, press Del or [<--] and editor->removeSelectedText() is called: 3564 // editor->sync() CRASH!! 3565 // editor->sync(); 3566 y = note->y() + Note::NOTE_MARGIN - frameWidth; 3567 height = note->height() - 2 * frameWidth - 2 * Note::NOTE_MARGIN; 3568 width = note->x() + note->width() - x + 1; 3569 if (y + height > maxHeight) 3570 y = maxHeight - height; 3571 3572 m_editor->graphicsWidget()->setMaximumSize(width, height); 3573 textEdit->setFixedSize(width, height); 3574 textEdit->viewport()->setFixedSize(width, height); 3575 } 3576 } else { 3577 height = note->height() - 2 * Note::NOTE_MARGIN + 2 * frameWidth; 3578 width = note->x() + note->width() - x; // note->rightLimit() - x + 2*m_view->frameWidth; 3579 if (m_editor->graphicsWidget()) 3580 m_editor->graphicsWidget()->widget()->setFixedSize(width, height); 3581 x -= 1; 3582 y = note->y() + Note::NOTE_MARGIN - frameWidth; 3583 } 3584 if ((m_editorWidth > 0 && m_editorWidth != width) || (m_editorHeight > 0 && m_editorHeight != height)) { 3585 m_editorWidth = width; // Avoid infinite recursion!!! 3586 m_editorHeight = height; 3587 m_editor->autoSave(/*toFileToo=*/true); 3588 } 3589 m_editorWidth = width; 3590 m_editorHeight = height; 3591 m_editor->graphicsWidget()->setPos(x, y); 3592 m_editorX = x; 3593 m_editorY = y; 3594 3595 // if (andEnsureVisible) 3596 // ensureNoteVisible(note); 3597 } 3598 3599 void BasketScene::editorCursorPositionChanged() 3600 { 3601 if (!isDuringEdit()) 3602 return; 3603 3604 FocusedTextEdit *textEdit = dynamic_cast<FocusedTextEdit *>(m_editor->textEdit()); 3605 3606 if (textEdit) { 3607 QPoint cursorPoint = textEdit->viewport()->mapToGlobal(textEdit->cursorRect().center()); 3608 3609 // QPointF contentsCursor = m_view->mapToScene( m_view->viewport()->mapFromGlobal(cursorPoint) ); 3610 // m_view->ensureVisible(contentsCursor.x(), contentsCursor.y(),1,1); 3611 } 3612 } 3613 3614 void BasketScene::closeEditorDelayed() 3615 { 3616 setFocus(); 3617 QTimer::singleShot(0, this, SLOT(closeEditor())); 3618 } 3619 3620 bool BasketScene::closeEditor(bool deleteEmptyNote /* =true*/) 3621 { 3622 if (!isDuringEdit()) 3623 return true; 3624 3625 if (m_doNotCloseEditor) 3626 return true; 3627 3628 if (m_redirectEditActions) { 3629 if (m_editor->textEdit()) { 3630 disconnect(m_editor->textEdit(), &KTextEdit::selectionChanged, this, &BasketScene::selectionChangedInEditor); 3631 disconnect(m_editor->textEdit(), &KTextEdit::textChanged, this, &BasketScene::selectionChangedInEditor); 3632 disconnect(m_editor->textEdit(), &KTextEdit::textChanged, this, &BasketScene::contentChangedInEditor); 3633 } else if (m_editor->lineEdit()) { 3634 disconnect(m_editor->lineEdit(), &QLineEdit::selectionChanged, this, &BasketScene::selectionChangedInEditor); 3635 disconnect(m_editor->lineEdit(), &QLineEdit::textChanged, this, &BasketScene::selectionChangedInEditor); 3636 disconnect(m_editor->lineEdit(), &QLineEdit::textChanged, this, &BasketScene::contentChangedInEditor); 3637 } 3638 } 3639 m_editorTrackMouseEvent = false; 3640 m_editor->graphicsWidget()->widget()->disconnect(); 3641 removeItem(m_editor->graphicsWidget()); 3642 m_editor->validate(); 3643 3644 Note *note = m_editor->note(); 3645 3646 // Delete the editor BEFORE unselecting the note because unselecting the note would trigger closeEditor() recursivly: 3647 bool isEmpty = m_editor->isEmpty(); 3648 delete m_editor; 3649 3650 m_editor = nullptr; 3651 m_redirectEditActions = false; 3652 m_editorWidth = -1; 3653 m_editorHeight = -1; 3654 m_inactivityAutoSaveTimer.stop(); 3655 3656 // Delete the note if it is now empty: 3657 if (isEmpty && deleteEmptyNote) { 3658 focusANonSelectedNoteAboveOrThenBelow(); 3659 note->setSelected(true); 3660 note->deleteSelectedNotes(); 3661 if (m_hoveredNote == note) 3662 m_hoveredNote = nullptr; 3663 if (m_focusedNote == note) 3664 m_focusedNote = nullptr; 3665 delete note; 3666 save(); 3667 note = nullptr; 3668 } 3669 3670 unlockHovering(); 3671 filterAgain(/*andEnsureVisible=*/false); 3672 3673 // Does not work: 3674 // if (Settings::playAnimations()) 3675 // note->setOnTop(true); // So if it grew, do not obscure it temporarily while the notes below it are moving 3676 3677 if (note) 3678 note->setSelected(false); // unselectAll(); 3679 doHoverEffects(); 3680 // save(); 3681 3682 Global::bnpView->m_actEditNote->setEnabled(!isLocked() && countSelecteds() == 1 /*&& !isDuringEdit()*/); 3683 3684 Q_EMIT resetStatusBarText(); // Remove the "Editing. ... to validate." text. 3685 3686 // if (qApp->activeWindow() == Global::mainContainer) 3687 3688 // 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: 3689 if (!decoration()->filterBar()->lineEdit()->hasFocus()) 3690 setFocus(); 3691 3692 // Return true if the note is still there: 3693 return (note != nullptr); 3694 } 3695 3696 void BasketScene::closeBasket() 3697 { 3698 closeEditor(); 3699 unbufferizeAll(); // Keep the memory footprint low 3700 if (isEncrypted()) { 3701 if (Settings::enableReLockTimeout()) { 3702 int seconds = Settings::reLockTimeoutMinutes() * 60; 3703 m_inactivityAutoLockTimer.setSingleShot(true); 3704 m_inactivityAutoLockTimer.start(seconds * 1000); 3705 } 3706 } 3707 } 3708 3709 void BasketScene::openBasket() 3710 { 3711 if (m_inactivityAutoLockTimer.isActive()) 3712 m_inactivityAutoLockTimer.stop(); 3713 } 3714 3715 Note *BasketScene::theSelectedNote() 3716 { 3717 if (countSelecteds() != 1) { 3718 qDebug() << "NO SELECTED NOTE !!!!"; 3719 return nullptr; 3720 } 3721 3722 Note *selectedOne; 3723 FOR_EACH_NOTE(note) 3724 { 3725 selectedOne = note->theSelectedNote(); 3726 if (selectedOne) 3727 return selectedOne; 3728 } 3729 3730 qDebug() << "One selected note, BUT NOT FOUND !!!!"; 3731 3732 return nullptr; 3733 } 3734 3735 NoteSelection *BasketScene::selectedNotes() 3736 { 3737 NoteSelection selection; 3738 3739 FOR_EACH_NOTE(note) 3740 selection.append(note->selectedNotes()); 3741 3742 if (!selection.firstChild) 3743 return nullptr; 3744 3745 for (NoteSelection *node = selection.firstChild; node; node = node->next) 3746 node->parent = nullptr; 3747 3748 // If the top-most groups are columns, export only children of those groups 3749 // (because user is not aware that columns are groups, and don't care: it's not what she want): 3750 if (selection.firstChild->note->isColumn()) { 3751 NoteSelection tmpSelection; 3752 NoteSelection *nextNode; 3753 NoteSelection *nextSubNode; 3754 for (NoteSelection *node = selection.firstChild; node; node = nextNode) { 3755 nextNode = node->next; 3756 if (node->note->isColumn()) { 3757 for (NoteSelection *subNode = node->firstChild; subNode; subNode = nextSubNode) { 3758 nextSubNode = subNode->next; 3759 tmpSelection.append(subNode); 3760 subNode->parent = nullptr; 3761 subNode->next = nullptr; 3762 } 3763 } else { 3764 tmpSelection.append(node); 3765 node->parent = nullptr; 3766 node->next = nullptr; 3767 } 3768 } 3769 // debugSel(tmpSelection.firstChild); 3770 return tmpSelection.firstChild; 3771 } else { 3772 // debugSel(selection.firstChild); 3773 return selection.firstChild; 3774 } 3775 } 3776 3777 void BasketScene::showEditedNoteWhileFiltering() 3778 { 3779 if (m_editor) { 3780 Note *note = m_editor->note(); 3781 filterAgain(); 3782 note->setSelected(true); 3783 relayoutNotes(); 3784 note->setX(note->x()); 3785 note->setY(note->y()); 3786 filterAgainDelayed(); 3787 } 3788 } 3789 3790 void BasketScene::noteEdit(Note *note, bool justAdded, const QPointF &clickedPoint) // TODO: Remove the first parameter!!! 3791 { 3792 if (!note) 3793 note = theSelectedNote(); // TODO: Or pick the focused note! 3794 if (!note) 3795 return; 3796 3797 if (isDuringEdit()) { 3798 closeEditor(); // Validate the noteeditors in QLineEdit that does not intercept Enter key press (and edit is triggered with Enter too... Can conflict) 3799 return; 3800 } 3801 3802 if (note != m_focusedNote) { 3803 setFocusedNote(note); 3804 m_startOfShiftSelectionNote = note; 3805 } 3806 3807 if (justAdded && isFiltering()) { 3808 QTimer::singleShot(0, this, SLOT(showEditedNoteWhileFiltering())); 3809 } 3810 3811 doHoverEffects(note, Note::Content); // Be sure (in the case Edit was triggered by menu or Enter key...): better feedback! 3812 3813 NoteEditor *editor = NoteEditor::editNoteContent(note->content(), nullptr); 3814 if (editor->graphicsWidget()) { 3815 m_editor = editor; 3816 3817 addItem(m_editor->graphicsWidget()); 3818 3819 placeEditorAndEnsureVisible(); // placeEditor(); // FIXME: After? 3820 m_redirectEditActions = m_editor->lineEdit() || m_editor->textEdit(); 3821 if (m_redirectEditActions) { 3822 // In case there is NO text, "Select All" is disabled. But if the user press a key the there is now a text: 3823 // selection has not changed but "Select All" should be re-enabled: 3824 m_editor->connectActions(this); 3825 } 3826 3827 m_editor->graphicsWidget()->setFocus(); 3828 connect(m_editor, &NoteEditor::askValidation, this, &BasketScene::closeEditorDelayed); 3829 connect(m_editor, &NoteEditor::mouseEnteredEditorWidget, this, &BasketScene::mouseEnteredEditorWidget); 3830 3831 if (clickedPoint != QPoint()) { 3832 m_editor->setCursorTo(clickedPoint); 3833 updateEditorAppearance(); 3834 } 3835 3836 // qApp->processEvents(); // Show the editor toolbar before ensuring the note is visible 3837 ensureNoteVisible(note); // because toolbar can create a new line and then partially hide the note 3838 m_editor->graphicsWidget()->setFocus(); // When clicking in the basket, a QTimer::singleShot(0, ...) focus the basket! So we focus the widget after qApp->processEvents() 3839 Q_EMIT resetStatusBarText(); // Display "Editing. ... to validate." 3840 } else { 3841 // Delete the note user have canceled the addition: 3842 if ((justAdded && editor->canceled()) || editor->isEmpty() /*) && editor->note()->states().count() <= 0*/) { 3843 focusANonSelectedNoteAboveOrThenBelow(); 3844 editor->note()->setSelected(true); 3845 editor->note()->deleteSelectedNotes(); 3846 if (m_hoveredNote == editor->note()) 3847 m_hoveredNote = nullptr; 3848 if (m_focusedNote == editor->note()) 3849 m_focusedNote = nullptr; 3850 delete editor->note(); 3851 save(); 3852 } 3853 editor->deleteLater(); 3854 unlockHovering(); 3855 filterAgain(); 3856 unselectAll(); 3857 } 3858 // Must set focus to the editor, otherwise edit cursor is not seen and precomposed characters cannot be entered 3859 if (m_editor != nullptr && m_editor->textEdit() != nullptr) 3860 m_editor->textEdit()->setFocus(); 3861 3862 Global::bnpView->m_actEditNote->setEnabled(false); 3863 } 3864 3865 void BasketScene::noteDelete() 3866 { 3867 if (redirectEditActions()) { 3868 if (m_editor->textEdit()) 3869 m_editor->textEdit()->textCursor().deleteChar(); 3870 else if (m_editor->lineEdit()) 3871 m_editor->lineEdit()->del(); 3872 return; 3873 } 3874 3875 if (countSelecteds() <= 0) 3876 return; 3877 int really = KMessageBox::Yes; 3878 if (Settings::confirmNoteDeletion()) 3879 really = KMessageBox::questionYesNo(m_view, 3880 i18np("<qt>Do you really want to delete this note?</qt>", "<qt>Do you really want to delete these <b>%1</b> notes?</qt>", countSelecteds()), 3881 i18np("Delete Note", "Delete Notes", countSelecteds()), 3882 KStandardGuiItem::del(), 3883 KStandardGuiItem::cancel()); 3884 if (really == KMessageBox::No) 3885 return; 3886 3887 noteDeleteWithoutConfirmation(); 3888 } 3889 3890 void BasketScene::focusANonSelectedNoteBelow(bool inSameColumn) 3891 { 3892 // First focus another unselected one below it...: 3893 if (m_focusedNote && m_focusedNote->isSelected()) { 3894 Note *next = m_focusedNote->nextShownInStack(); 3895 while (next && next->isSelected()) 3896 next = next->nextShownInStack(); 3897 if (next) { 3898 if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == next->parentPrimaryNote()) { 3899 setFocusedNote(next); 3900 m_startOfShiftSelectionNote = next; 3901 } 3902 } 3903 } 3904 } 3905 3906 void BasketScene::focusANonSelectedNoteAbove(bool inSameColumn) 3907 { 3908 // ... Or above it: 3909 if (m_focusedNote && m_focusedNote->isSelected()) { 3910 Note *prev = m_focusedNote->prevShownInStack(); 3911 while (prev && prev->isSelected()) 3912 prev = prev->prevShownInStack(); 3913 if (prev) { 3914 if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == prev->parentPrimaryNote()) { 3915 setFocusedNote(prev); 3916 m_startOfShiftSelectionNote = prev; 3917 } 3918 } 3919 } 3920 } 3921 3922 void BasketScene::focusANonSelectedNoteBelowOrThenAbove() 3923 { 3924 focusANonSelectedNoteBelow(/*inSameColumn=*/true); 3925 focusANonSelectedNoteAbove(/*inSameColumn=*/true); 3926 focusANonSelectedNoteBelow(/*inSameColumn=*/false); 3927 focusANonSelectedNoteAbove(/*inSameColumn=*/false); 3928 } 3929 3930 void BasketScene::focusANonSelectedNoteAboveOrThenBelow() 3931 { 3932 focusANonSelectedNoteAbove(/*inSameColumn=*/true); 3933 focusANonSelectedNoteBelow(/*inSameColumn=*/true); 3934 focusANonSelectedNoteAbove(/*inSameColumn=*/false); 3935 focusANonSelectedNoteBelow(/*inSameColumn=*/false); 3936 } 3937 3938 void BasketScene::noteDeleteWithoutConfirmation(bool deleteFilesToo) 3939 { 3940 // If the currently focused note is selected, it will be deleted. 3941 focusANonSelectedNoteBelowOrThenAbove(); 3942 3943 // Do the deletion: 3944 Note *note = firstNote(); 3945 Note *next; 3946 while (note) { 3947 next = note->next(); // If we delete 'note' on the next line, note->next() will be 0! 3948 note->deleteSelectedNotes(deleteFilesToo, &m_notesToBeDeleted); 3949 note = next; 3950 } 3951 3952 if (!m_notesToBeDeleted.isEmpty()) { 3953 doCleanUp(); 3954 } 3955 3956 relayoutNotes(); // FIXME: filterAgain()? 3957 save(); 3958 } 3959 3960 void BasketScene::doCopy(CopyMode copyMode) 3961 { 3962 QClipboard *cb = QApplication::clipboard(); 3963 QClipboard::Mode mode = ((copyMode == CopyToSelection) ? QClipboard::Selection : QClipboard::Clipboard); 3964 3965 NoteSelection *selection = selectedNotes(); 3966 int countCopied = countSelecteds(); 3967 if (selection->firstStacked()) { 3968 QDrag *d = NoteDrag::dragObject(selection, copyMode == CutToClipboard, /*source=*/nullptr); // d will be deleted by QT 3969 3970 cb->setMimeData(d->mimeData(), mode); 3971 3972 if (copyMode == CutToClipboard) { 3973 noteDeleteWithoutConfirmation(/*deleteFilesToo=*/false); 3974 focusANote(); 3975 } 3976 3977 switch (copyMode) { 3978 default: 3979 case CopyToClipboard: 3980 Q_EMIT postMessage(i18np("Copied note to clipboard.", "Copied notes to clipboard.", countCopied)); 3981 break; 3982 case CutToClipboard: 3983 Q_EMIT postMessage(i18np("Cut note to clipboard.", "Cut notes to clipboard.", countCopied)); 3984 break; 3985 case CopyToSelection: 3986 Q_EMIT postMessage(i18np("Copied note to selection.", "Copied notes to selection.", countCopied)); 3987 break; 3988 } 3989 } 3990 } 3991 3992 void BasketScene::noteCopy() 3993 { 3994 if (redirectEditActions()) { 3995 if (m_editor->textEdit()) 3996 m_editor->textEdit()->copy(); 3997 else if (m_editor->lineEdit()) 3998 m_editor->lineEdit()->copy(); 3999 } else 4000 doCopy(CopyToClipboard); 4001 } 4002 4003 void BasketScene::noteCut() 4004 { 4005 if (redirectEditActions()) { 4006 if (m_editor->textEdit()) 4007 m_editor->textEdit()->cut(); 4008 else if (m_editor->lineEdit()) 4009 m_editor->lineEdit()->cut(); 4010 } else 4011 doCopy(CutToClipboard); 4012 } 4013 4014 void BasketScene::clearFormattingNote(Note *note) 4015 { 4016 if (!note) 4017 note = theSelectedNote(); 4018 if (!note) 4019 return; 4020 4021 HtmlContent* contentHtml = dynamic_cast<HtmlContent*>(note->content()); 4022 if (contentHtml){ 4023 contentHtml->setHtml(Tools::textToHTML(note->content()->toText(""))); 4024 } 4025 4026 noteEdit(note); 4027 } 4028 4029 void BasketScene::noteOpen(Note *note) 4030 { 4031 /* 4032 GetSelectedNotes 4033 NoSelectedNote || Count == 0 ? return 4034 AllTheSameType ? 4035 Get { url, message(count) } 4036 */ 4037 4038 // TODO: Open ALL selected notes! 4039 if (note == nullptr) { 4040 note = theSelectedNote(); 4041 } 4042 if (note == nullptr) { 4043 return; 4044 } 4045 4046 QUrl url = note->content()->urlToOpen(/*with=*/false); 4047 QString message = note->content()->messageWhenOpening(NoteContent::OpenOne /*NoteContent::OpenSeveral*/); 4048 if (url.isEmpty()) { 4049 if (message.isEmpty()) { 4050 Q_EMIT postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); 4051 } else { 4052 int result = KMessageBox::warningContinueCancel(m_view, message, /*caption=*/QString(), KGuiItem(i18n("&Edit"), "edit")); 4053 if (result == KMessageBox::Continue) { 4054 noteEdit(note); 4055 } 4056 } 4057 } else { 4058 Q_EMIT postMessage(message); // "Opening link target..." / "Launching application..." / "Opening note file..." 4059 // Finally do the opening job: 4060 QString customCommand = note->content()->customOpenCommand(); 4061 4062 if (url.url().startsWith(QStringLiteral("basket://"))) { 4063 Q_EMIT crossReference(url.url()); 4064 } else if (customCommand.isEmpty()) { 4065 KRun *run = new KRun(url, m_view->window()); 4066 run->setAutoDelete(true); 4067 } else { 4068 QList<QUrl> urls {url}; 4069 KService::Ptr service(new KService(QStringLiteral("noteOpen"), customCommand, QString())); 4070 auto *job = new KIO::ApplicationLauncherJob(service); 4071 job->setUrls(urls); 4072 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_view->window())); 4073 job->start(); 4074 } 4075 } 4076 } 4077 4078 /** Code from bool KRun::displayOpenWithDialog(const KUrl::List& lst, bool tempFiles) 4079 * It does not allow to set a text, so I ripped it to do that: 4080 */ 4081 bool KRun__displayOpenWithDialog(const QList<QUrl> &lst, QWidget *window, bool tempFiles, const QString &text) 4082 { 4083 if (qApp && !KAuthorized::authorizeAction(QStringLiteral("openwith"))) { 4084 KMessageBox::error(window, i18n("You are not authorized to open this file.")); // TODO: Better message, i18n freeze :-( 4085 return false; 4086 } 4087 KOpenWithDialog l(lst, text, QString(), nullptr); 4088 if (l.exec() > 0) { 4089 KService::Ptr service = l.service(); 4090 if (!service) { 4091 service = new KService(QStringLiteral("noteOpenWith"), l.text(), QString()); 4092 } 4093 4094 auto *job = new KIO::ApplicationLauncherJob(service); 4095 job->setUrls(lst); 4096 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); 4097 if (tempFiles) { 4098 job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); 4099 } 4100 4101 /// \todo job runs synchronously... API suggests asynchronous start() instead. 4102 /// This requires a result handler. noteOpenWith also just emits a message to the status bar. 4103 return job->exec(); 4104 4105 } 4106 return false; 4107 } 4108 4109 void BasketScene::noteOpenWith(Note *note) 4110 { 4111 if (note == nullptr) { 4112 note = theSelectedNote(); 4113 } 4114 if (note == nullptr) { 4115 return; 4116 } 4117 4118 QUrl url = note->content()->urlToOpen(/*with=*/true); 4119 QString message = note->content()->messageWhenOpening(NoteContent::OpenOneWith); 4120 QString text = note->content()->messageWhenOpening(NoteContent::OpenOneWithDialog); 4121 if (url.isEmpty()) { 4122 Q_EMIT postMessage(i18n("Unable to open this note.")); 4123 } else { 4124 QList<QUrl> urls{url}; 4125 if (KRun__displayOpenWithDialog(urls, m_view->window(), false, text)) { 4126 Q_EMIT postMessage(message); // "Opening link target with..." / "Opening note file with..." 4127 } 4128 } 4129 } 4130 4131 void BasketScene::noteSaveAs() 4132 { 4133 // if (!note) 4134 // note = theSelectedNote(); 4135 Note *note = theSelectedNote(); 4136 if (!note) 4137 return; 4138 4139 QUrl url = note->content()->urlToOpen(/*with=*/false); 4140 if (url.isEmpty()) 4141 return; 4142 4143 QString fileName = QFileDialog::getSaveFileName(m_view, i18n("Save to File"), url.fileName(), note->content()->saveAsFilters()); 4144 // TODO: Ask to overwrite ! 4145 if (fileName.isEmpty()) 4146 return; 4147 4148 // TODO: Convert format, etc. (use NoteContent::saveAs(fileName)) 4149 KIO::copy(url, QUrl::fromLocalFile(fileName)); 4150 } 4151 4152 Note *BasketScene::selectedGroup() 4153 { 4154 FOR_EACH_NOTE(note) 4155 { 4156 Note *selectedGroup = note->selectedGroup(); 4157 if (selectedGroup) { 4158 // If the selected group is one group in a column, then return that group, and not the column, 4159 // because the column is not ungrouppage, and the Ungroup action would be disabled. 4160 if (selectedGroup->isColumn() && selectedGroup->firstChild() && !selectedGroup->firstChild()->next()) { 4161 return selectedGroup->firstChild(); 4162 } 4163 return selectedGroup; 4164 } 4165 } 4166 return nullptr; 4167 } 4168 4169 bool BasketScene::selectionIsOneGroup() 4170 { 4171 return (selectedGroup() != nullptr); 4172 } 4173 4174 Note *BasketScene::firstSelected() 4175 { 4176 Note *first = nullptr; 4177 FOR_EACH_NOTE(note) 4178 { 4179 first = note->firstSelected(); 4180 if (first) 4181 return first; 4182 } 4183 return nullptr; 4184 } 4185 4186 Note *BasketScene::lastSelected() 4187 { 4188 Note *last = nullptr, *tmp = nullptr; 4189 FOR_EACH_NOTE(note) 4190 { 4191 tmp = note->lastSelected(); 4192 if (tmp) 4193 last = tmp; 4194 } 4195 return last; 4196 } 4197 4198 bool BasketScene::convertTexts() 4199 { 4200 m_watcher->stopScan(); 4201 bool convertedNotes = false; 4202 4203 if (!isLoaded()) 4204 load(); 4205 4206 FOR_EACH_NOTE(note) 4207 if (note->convertTexts()) 4208 convertedNotes = true; 4209 4210 if (convertedNotes) 4211 save(); 4212 m_watcher->startScan(); 4213 return convertedNotes; 4214 } 4215 4216 void BasketScene::noteGroup() 4217 { 4218 /* // Nothing to do? 4219 if (isLocked() || countSelecteds() <= 1) 4220 return; 4221 4222 // If every selected notes are ALREADY in one group, then don't touch anything: 4223 Note *selectedGroup = this->selectedGroup(); 4224 if (selectedGroup && !selectedGroup->isColumn()) 4225 return; 4226 */ 4227 4228 // Copied from BNPView::updateNotesActions() 4229 bool severalSelected = countSelecteds() >= 2; 4230 Note *selectedGroup = (severalSelected ? this->selectedGroup() : nullptr); 4231 bool enabled = !isLocked() && severalSelected && (!selectedGroup || selectedGroup->isColumn()); 4232 if (!enabled) 4233 return; 4234 4235 // Get the first selected note: we will group selected items just before: 4236 Note *first = firstSelected(); 4237 // if (selectedGroup != 0 || first == 0) 4238 // return; 4239 4240 m_loaded = false; // Hack to avoid notes to be unselected and new notes to be selected: 4241 4242 // Create and insert the receiving group: 4243 Note *group = new Note(this); 4244 if (first->isFree()) { 4245 insertNote(group, nullptr, Note::BottomColumn, QPointF(first->x(), first->y())); 4246 } else { 4247 insertNote(group, first, Note::TopInsert); 4248 } 4249 4250 // 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! 4251 Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); 4252 insertNote(fakeNote, group, Note::BottomColumn); 4253 4254 // Group the notes: 4255 Note *nextNote; 4256 Note *note = firstNote(); 4257 while (note) { 4258 nextNote = note->next(); 4259 note->groupIn(group); 4260 note = nextNote; 4261 } 4262 4263 m_loaded = true; // Part 2 / 2 of the workaround! 4264 4265 // Do cleanup: 4266 unplugNote(fakeNote); 4267 delete fakeNote; 4268 unselectAll(); 4269 group->setSelectedRecursively(true); // Notes were unselected by unplugging 4270 4271 relayoutNotes(); 4272 save(); 4273 } 4274 4275 void BasketScene::noteUngroup() 4276 { 4277 Note *group = selectedGroup(); 4278 if (group && !group->isColumn()) { 4279 ungroupNote(group); 4280 relayoutNotes(); 4281 } 4282 save(); 4283 } 4284 4285 void BasketScene::unplugSelection(NoteSelection *selection) 4286 { 4287 for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { 4288 unplugNote(toUnplug->note); 4289 } 4290 } 4291 4292 void BasketScene::insertSelection(NoteSelection *selection, Note *after) 4293 { 4294 for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { 4295 if (toUnplug->note->isGroup()) { 4296 Note *group = new Note(this); 4297 insertNote(group, after, Note::BottomInsert); 4298 Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); 4299 insertNote(fakeNote, group, Note::BottomColumn); 4300 insertSelection(toUnplug->firstChild, fakeNote); 4301 unplugNote(fakeNote); 4302 delete fakeNote; 4303 after = group; 4304 } else { 4305 Note *note = toUnplug->note; 4306 note->setPrev(nullptr); 4307 note->setNext(nullptr); 4308 insertNote(note, after, Note::BottomInsert); 4309 after = note; 4310 } 4311 } 4312 } 4313 4314 void BasketScene::selectSelection(NoteSelection *selection) 4315 { 4316 for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { 4317 if (toUnplug->note->isGroup()) 4318 selectSelection(toUnplug); 4319 else 4320 toUnplug->note->setSelected(true); 4321 } 4322 } 4323 4324 void BasketScene::noteMoveOnTop() 4325 { 4326 // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket 4327 // TODO: Move on top/bottom... of the column or basjet 4328 4329 NoteSelection *selection = selectedNotes(); 4330 unplugSelection(selection); 4331 // Replug the notes: 4332 Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); 4333 if (isColumnsLayout()) { 4334 if (firstNote()->firstChild()) 4335 insertNote(fakeNote, firstNote()->firstChild(), Note::TopInsert); 4336 else 4337 insertNote(fakeNote, firstNote(), Note::BottomColumn); 4338 } else { 4339 // TODO: Also allow to move notes on top of a group!!!!!!! 4340 insertNote(fakeNote, nullptr, Note::BottomInsert); 4341 } 4342 insertSelection(selection, fakeNote); 4343 unplugNote(fakeNote); 4344 delete fakeNote; 4345 selectSelection(selection); 4346 relayoutNotes(); 4347 save(); 4348 } 4349 4350 void BasketScene::noteMoveOnBottom() 4351 { 4352 // TODO: Duplicate code: void noteMoveOn(); 4353 4354 // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket 4355 // TODO: Move on top/bottom... of the column or basjet 4356 4357 NoteSelection *selection = selectedNotes(); 4358 unplugSelection(selection); 4359 // Replug the notes: 4360 Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); 4361 if (isColumnsLayout()) 4362 insertNote(fakeNote, firstNote(), Note::BottomColumn); 4363 else { 4364 // TODO: Also allow to move notes on top of a group!!!!!!! 4365 insertNote(fakeNote, nullptr, Note::BottomInsert); 4366 } 4367 insertSelection(selection, fakeNote); 4368 unplugNote(fakeNote); 4369 delete fakeNote; 4370 selectSelection(selection); 4371 relayoutNotes(); 4372 save(); 4373 } 4374 4375 void BasketScene::moveSelectionTo(Note *here, bool below /* = true*/) 4376 { 4377 NoteSelection *selection = selectedNotes(); 4378 unplugSelection(selection); 4379 // Replug the notes: 4380 Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); 4381 // if (isColumnsLayout()) 4382 insertNote(fakeNote, here, (below ? Note::BottomInsert : Note::TopInsert)); 4383 // else { 4384 // // TODO: Also allow to move notes on top of a group!!!!!!! 4385 // insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); 4386 // } 4387 insertSelection(selection, fakeNote); 4388 unplugNote(fakeNote); 4389 delete fakeNote; 4390 selectSelection(selection); 4391 relayoutNotes(); 4392 save(); 4393 } 4394 4395 void BasketScene::noteMoveNoteUp() 4396 { 4397 // TODO: Move between columns, even if they are empty !!!!!!! 4398 4399 // TODO: if first note of a group, move just above the group! And let that even if there is no note before that group!!! 4400 4401 Note *first = firstSelected(); 4402 Note *previous = first->prevShownInStack(); 4403 if (previous) 4404 moveSelectionTo(previous, /*below=*/false); 4405 } 4406 4407 void BasketScene::noteMoveNoteDown() 4408 { 4409 Note *first = lastSelected(); 4410 Note *next = first->nextShownInStack(); 4411 if (next) 4412 moveSelectionTo(next, /*below=*/true); 4413 } 4414 4415 void BasketScene::wheelEvent(QGraphicsSceneWheelEvent *event) 4416 { 4417 // Q3ScrollView::wheelEvent(event); 4418 QGraphicsScene::wheelEvent(event); 4419 } 4420 4421 void BasketScene::linkLookChanged() 4422 { 4423 Note *note = m_firstNote; 4424 while (note) { 4425 note->linkLookChanged(); 4426 note = note->next(); 4427 } 4428 relayoutNotes(); 4429 } 4430 4431 void BasketScene::slotCopyingDone2(KIO::Job *job, const QUrl & /*from*/, const QUrl &to) 4432 { 4433 if (job->error()) { 4434 DEBUG_WIN << "Copy finished, ERROR"; 4435 return; 4436 } 4437 Note *note = noteForFullPath(to.path()); 4438 DEBUG_WIN << "Copy finished, load note: " + to.path() + (note ? QString() : " --- NO CORRESPONDING NOTE"); 4439 if (note != nullptr) { 4440 note->content()->loadFromFile(/*lazyLoad=*/false); 4441 if (isEncrypted()) 4442 note->content()->saveToFile(); 4443 if (m_focusedNote == note) // When inserting a new note we ensure it visible 4444 ensureNoteVisible(note); // But after loading it has certainly grown and if it was 4445 } // on bottom of the basket it's not visible entirely anymore 4446 } 4447 4448 Note *BasketScene::noteForFullPath(const QString &path) 4449 { 4450 Note *note = firstNote(); 4451 Note *found; 4452 while (note) { 4453 found = note->noteForFullPath(path); 4454 if (found) 4455 return found; 4456 note = note->next(); 4457 } 4458 return nullptr; 4459 } 4460 4461 void BasketScene::deleteFiles() 4462 { 4463 m_watcher->stopScan(); 4464 Tools::deleteRecursively(fullPath()); 4465 } 4466 4467 QList<State *> BasketScene::usedStates() 4468 { 4469 QList<State *> states; 4470 FOR_EACH_NOTE(note) 4471 note->usedStates(states); 4472 return states; 4473 } 4474 4475 void BasketScene::listUsedTags(QList<Tag *> &list) 4476 { 4477 if (!isLoaded()) { 4478 load(); 4479 } 4480 4481 FOR_EACH_NOTE(child) 4482 child->listUsedTags(list); 4483 } 4484 4485 /** Unfocus the previously focused note (unless it was null) 4486 * and focus the new @param note (unless it is null) if hasFocus() 4487 * Update m_focusedNote to the new one 4488 */ 4489 void BasketScene::setFocusedNote(Note *note) // void BasketScene::changeFocusTo(Note *note) 4490 { 4491 // Don't focus an hidden note: 4492 if (note != nullptr && !note->isShown()) 4493 return; 4494 // When clicking a group, this group gets focused. But only content-based notes should be focused: 4495 if (note && note->isGroup()) 4496 note = note->firstRealChild(); 4497 // The first time a note is focused, it becomes the start of the Shift selection: 4498 if (m_startOfShiftSelectionNote == nullptr) 4499 m_startOfShiftSelectionNote = note; 4500 // Unfocus the old focused note: 4501 if (m_focusedNote != nullptr) 4502 m_focusedNote->setFocused(false); 4503 // Notify the new one to draw a focus rectangle... only if the basket is focused: 4504 if (hasFocus() && note != nullptr) 4505 note->setFocused(true); 4506 // Save the new focused note: 4507 m_focusedNote = note; 4508 } 4509 4510 /** If no shown note is currently focused, try to find a shown note and focus it 4511 * Also update m_focusedNote to the new one (or null if there isn't) 4512 */ 4513 void BasketScene::focusANote() 4514 { 4515 if (countFounds() == 0) { // No note to focus 4516 setFocusedNote(nullptr); 4517 // m_startOfShiftSelectionNote = 0; 4518 return; 4519 } 4520 4521 if (m_focusedNote == nullptr) { // No focused note yet : focus the first shown 4522 Note *toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); 4523 setFocusedNote(toFocus); 4524 // m_startOfShiftSelectionNote = m_focusedNote; 4525 return; 4526 } 4527 4528 // Search a visible note to focus if the focused one isn't shown : 4529 Note *toFocus = m_focusedNote; 4530 if (toFocus && !toFocus->isShown()) 4531 toFocus = toFocus->nextShownInStack(); 4532 if (!toFocus && m_focusedNote) 4533 toFocus = m_focusedNote->prevShownInStack(); 4534 setFocusedNote(toFocus); 4535 // m_startOfShiftSelectionNote = toFocus; 4536 } 4537 4538 Note *BasketScene::firstNoteInStack() 4539 { 4540 if (!firstNote()) 4541 return nullptr; 4542 4543 if (firstNote()->content()) 4544 return firstNote(); 4545 else 4546 return firstNote()->nextInStack(); 4547 } 4548 4549 Note *BasketScene::lastNoteInStack() 4550 { 4551 Note *note = lastNote(); 4552 while (note) { 4553 if (note->content()) 4554 return note; 4555 Note *possibleNote = note->lastRealChild(); 4556 if (possibleNote && possibleNote->content()) 4557 return possibleNote; 4558 note = note->prev(); 4559 } 4560 return nullptr; 4561 } 4562 4563 Note *BasketScene::firstNoteShownInStack() 4564 { 4565 Note *first = firstNoteInStack(); 4566 while (first && !first->isShown()) 4567 first = first->nextInStack(); 4568 return first; 4569 } 4570 4571 Note *BasketScene::lastNoteShownInStack() 4572 { 4573 Note *last = lastNoteInStack(); 4574 while (last && !last->isShown()) 4575 last = last->prevInStack(); 4576 return last; 4577 } 4578 4579 Note *BasketScene::noteOn(NoteOn side) 4580 { 4581 Note *bestNote = nullptr; 4582 int distance = -1; 4583 // int bestDistance = contentsWidth() * contentsHeight() * 10; 4584 int bestDistance = sceneRect().width() * sceneRect().height() * 10; 4585 4586 Note *note = firstNoteShownInStack(); 4587 Note *primary = m_focusedNote->parentPrimaryNote(); 4588 while (note) { 4589 switch (side) { 4590 case LEFT_SIDE: 4591 distance = m_focusedNote->distanceOnLeftRight(note, LEFT_SIDE); 4592 break; 4593 case RIGHT_SIDE: 4594 distance = m_focusedNote->distanceOnLeftRight(note, RIGHT_SIDE); 4595 break; 4596 case TOP_SIDE: 4597 distance = m_focusedNote->distanceOnTopBottom(note, TOP_SIDE); 4598 break; 4599 case BOTTOM_SIDE: 4600 distance = m_focusedNote->distanceOnTopBottom(note, BOTTOM_SIDE); 4601 break; 4602 } 4603 if ((side == TOP_SIDE || side == BOTTOM_SIDE || primary != note->parentPrimaryNote()) && note != m_focusedNote && distance > 0 && distance < bestDistance) { 4604 bestNote = note; 4605 bestDistance = distance; 4606 } 4607 note = note->nextShownInStack(); 4608 } 4609 4610 return bestNote; 4611 } 4612 4613 Note *BasketScene::firstNoteInGroup() 4614 { 4615 Note *child = m_focusedNote; 4616 Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); 4617 while (parent) { 4618 if (parent->firstChild() != child && !parent->isColumn()) 4619 return parent->firstRealChild(); 4620 child = parent; 4621 parent = parent->parentNote(); 4622 } 4623 return nullptr; 4624 } 4625 4626 Note *BasketScene::noteOnHome() 4627 { 4628 // First try to find the first note of the group containing the focused note: 4629 Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); 4630 while (parent) { 4631 if (parent->nextShownInStack() != m_focusedNote) 4632 return parent->nextShownInStack(); 4633 parent = parent->parentNote(); 4634 } 4635 4636 // If it was not found, then focus the very first note in the basket: 4637 if (isFreeLayout()) { 4638 Note *first = firstNoteShownInStack(); // The effective first note found 4639 Note *note = first; // The current note, to compare with the previous first note, if this new note is more on top 4640 if (note) 4641 note = note->nextShownInStack(); 4642 while (note) { 4643 if (note->y() < first->y() || (note->y() == first->y() && note->x() < first->x())) 4644 first = note; 4645 note = note->nextShownInStack(); 4646 } 4647 return first; 4648 } else 4649 return firstNoteShownInStack(); 4650 } 4651 4652 Note *BasketScene::noteOnEnd() 4653 { 4654 Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); 4655 Note *lastChild; 4656 while (parent) { 4657 lastChild = parent->lastRealChild(); 4658 if (lastChild && lastChild != m_focusedNote) { 4659 if (lastChild->isShown()) 4660 return lastChild; 4661 lastChild = lastChild->prevShownInStack(); 4662 if (lastChild && lastChild->isShown() && lastChild != m_focusedNote) 4663 return lastChild; 4664 } 4665 parent = parent->parentNote(); 4666 } 4667 if (isFreeLayout()) { 4668 Note *last; 4669 Note *note; 4670 last = note = firstNoteShownInStack(); 4671 note = note->nextShownInStack(); 4672 while (note) { 4673 if (note->bottom() > last->bottom() || (note->bottom() == last->bottom() && note->x() > last->x())) 4674 last = note; 4675 note = note->nextShownInStack(); 4676 } 4677 return last; 4678 } else 4679 return lastNoteShownInStack(); 4680 } 4681 4682 void BasketScene::keyPressEvent(QKeyEvent *event) 4683 { 4684 if (isDuringEdit()) { 4685 QGraphicsScene::keyPressEvent(event); 4686 /*if( event->key() == Qt::Key_Return ) 4687 { 4688 m_editor->graphicsWidget()->setFocus(); 4689 } 4690 else if( event->key() == Qt::Key_Escape) 4691 { 4692 closeEditor(); 4693 }*/ 4694 event->accept(); 4695 return; 4696 } 4697 4698 if (event->key() == Qt::Key_Escape) { 4699 if (decoration()->filterData().isFiltering) 4700 decoration()->filterBar()->reset(); 4701 else 4702 unselectAll(); 4703 } 4704 4705 if (countFounds() == 0) 4706 return; 4707 4708 if (!m_focusedNote) 4709 return; 4710 4711 Note *toFocus = nullptr; 4712 4713 switch (event->key()) { 4714 case Qt::Key_Down: 4715 toFocus = (isFreeLayout() ? noteOn(BOTTOM_SIDE) : m_focusedNote->nextShownInStack()); 4716 if (toFocus) 4717 break; 4718 // scrollBy(0, 30); // This cases do not move focus to another note... 4719 return; 4720 case Qt::Key_Up: 4721 toFocus = (isFreeLayout() ? noteOn(TOP_SIDE) : m_focusedNote->prevShownInStack()); 4722 if (toFocus) 4723 break; 4724 // scrollBy(0, -30); // This cases do not move focus to another note... 4725 return; 4726 case Qt::Key_PageDown: 4727 if (isFreeLayout()) { 4728 Note *lastFocused = m_focusedNote; 4729 for (int i = 0; i < 10 && m_focusedNote; ++i) 4730 m_focusedNote = noteOn(BOTTOM_SIDE); 4731 toFocus = m_focusedNote; 4732 m_focusedNote = lastFocused; 4733 } else { 4734 toFocus = m_focusedNote; 4735 for (int i = 0; i < 10 && toFocus; ++i) 4736 toFocus = toFocus->nextShownInStack(); 4737 } 4738 if (toFocus == nullptr) 4739 toFocus = (isFreeLayout() ? noteOnEnd() : lastNoteShownInStack()); 4740 if (toFocus && toFocus != m_focusedNote) 4741 break; 4742 // scrollBy(0, visibleHeight() / 2); // This cases do not move focus to another note... 4743 // scrollBy(0, viewport()->height() / 2); // This cases do not move focus to another note... 4744 return; 4745 case Qt::Key_PageUp: 4746 if (isFreeLayout()) { 4747 Note *lastFocused = m_focusedNote; 4748 for (int i = 0; i < 10 && m_focusedNote; ++i) 4749 m_focusedNote = noteOn(TOP_SIDE); 4750 toFocus = m_focusedNote; 4751 m_focusedNote = lastFocused; 4752 } else { 4753 toFocus = m_focusedNote; 4754 for (int i = 0; i < 10 && toFocus; ++i) 4755 toFocus = toFocus->prevShownInStack(); 4756 } 4757 if (toFocus == nullptr) 4758 toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); 4759 if (toFocus && toFocus != m_focusedNote) 4760 break; 4761 // scrollBy(0, - visibleHeight() / 2); // This cases do not move focus to another note... 4762 // scrollBy(0, - viewport()->height() / 2); // This cases do not move focus to another note... 4763 return; 4764 case Qt::Key_Home: 4765 toFocus = noteOnHome(); 4766 break; 4767 case Qt::Key_End: 4768 toFocus = noteOnEnd(); 4769 break; 4770 case Qt::Key_Left: 4771 if (m_focusedNote->tryFoldParent()) 4772 return; 4773 if ((toFocus = noteOn(LEFT_SIDE))) 4774 break; 4775 if ((toFocus = firstNoteInGroup())) 4776 break; 4777 // scrollBy(-30, 0); // This cases do not move focus to another note... 4778 return; 4779 case Qt::Key_Right: 4780 if (m_focusedNote->tryExpandParent()) 4781 return; 4782 if ((toFocus = noteOn(RIGHT_SIDE))) 4783 break; 4784 // scrollBy(30, 0); // This cases do not move focus to another note... 4785 return; 4786 case Qt::Key_Space: // This case do not move focus to another note... 4787 if (m_focusedNote) { 4788 m_focusedNote->setSelected(!m_focusedNote->isSelected()); 4789 event->accept(); 4790 } else 4791 event->ignore(); 4792 return; // ... so we return after the process 4793 default: 4794 return; 4795 } 4796 4797 if (toFocus == nullptr) { // If no direction keys have been pressed OR reached out the begin or end 4798 event->ignore(); // Important !! 4799 return; 4800 } 4801 4802 if (event->modifiers() & Qt::ShiftModifier) { // Shift+arrowKeys selection 4803 if (m_startOfShiftSelectionNote == nullptr) 4804 m_startOfShiftSelectionNote = toFocus; 4805 ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! 4806 selectRange(m_startOfShiftSelectionNote, toFocus); 4807 setFocusedNote(toFocus); 4808 event->accept(); 4809 return; 4810 } else /*if (toFocus != m_focusedNote)*/ { // Move focus to ANOTHER note... 4811 ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! 4812 setFocusedNote(toFocus); 4813 m_startOfShiftSelectionNote = toFocus; 4814 if (!(event->modifiers() & Qt::ControlModifier)) // ... select only current note if Control 4815 unselectAllBut(m_focusedNote); 4816 event->accept(); 4817 return; 4818 } 4819 4820 event->ignore(); // Important !! 4821 } 4822 4823 /** Select a range of notes and deselect the others. 4824 * The order between start and end has no importance (end could be before start) 4825 */ 4826 void BasketScene::selectRange(Note *start, Note *end, bool unselectOthers /*= true*/) 4827 { 4828 Note *cur; 4829 Note *realEnd = nullptr; 4830 4831 // Avoid crash when start (or end) is null 4832 if (start == nullptr) 4833 start = end; 4834 else if (end == nullptr) 4835 end = start; 4836 // And if *both* are null 4837 if (start == nullptr) { 4838 if (unselectOthers) 4839 unselectAll(); 4840 return; 4841 } 4842 // In case there is only one note to select 4843 if (start == end) { 4844 if (unselectOthers) 4845 unselectAllBut(start); 4846 else 4847 start->setSelected(true); 4848 return; 4849 } 4850 4851 // Free layout baskets should select range as if we were drawing a rectangle between start and end: 4852 if (isFreeLayout()) { 4853 QRectF startRect(start->x(), start->y(), start->width(), start->height()); 4854 QRectF endRect(end->x(), end->y(), end->width(), end->height()); 4855 QRectF toSelect = startRect.united(endRect); 4856 selectNotesIn(toSelect, /*invertSelection=*/false, unselectOthers); 4857 return; 4858 } 4859 4860 // Search the REAL first (and deselect the others before it) : 4861 for (cur = firstNoteInStack(); cur != nullptr; cur = cur->nextInStack()) { 4862 if (cur == start || cur == end) 4863 break; 4864 if (unselectOthers) 4865 cur->setSelected(false); 4866 } 4867 4868 // Select the notes after REAL start, until REAL end : 4869 if (cur == start) 4870 realEnd = end; 4871 else if (cur == end) 4872 realEnd = start; 4873 4874 for (/*cur = cur*/; cur != nullptr; cur = cur->nextInStack()) { 4875 cur->setSelected(cur->isShown()); // Select all notes in the range, but only if they are shown 4876 if (cur == realEnd) 4877 break; 4878 } 4879 4880 if (!unselectOthers) 4881 return; 4882 4883 // Deselect the remaining notes : 4884 if (cur) 4885 cur = cur->nextInStack(); 4886 for (/*cur = cur*/; cur != nullptr; cur = cur->nextInStack()) 4887 cur->setSelected(false); 4888 } 4889 4890 void BasketScene::focusInEvent(QFocusEvent *event) 4891 { 4892 // Focus cannot be get with Tab when locked, but a click can focus the basket! 4893 if (isLocked()) { 4894 if (m_button) { 4895 QGraphicsScene::focusInEvent(event); 4896 QTimer::singleShot(0, m_button, SLOT(setFocus())); 4897 } 4898 } else { 4899 QGraphicsScene::focusInEvent(event); 4900 focusANote(); // hasFocus() is true at this stage, note will be focused 4901 } 4902 } 4903 4904 void BasketScene::focusOutEvent(QFocusEvent *) 4905 { 4906 if (m_focusedNote != nullptr) 4907 m_focusedNote->setFocused(false); 4908 } 4909 4910 void BasketScene::ensureNoteVisible(Note *note) 4911 { 4912 if (!note->isShown()) // Logical! 4913 return; 4914 4915 if (note == editedNote()) // HACK: When filtering while editing big notes, etc... cause unwanted scrolls 4916 return; 4917 4918 m_view->ensureVisible(note); 4919 /*// int bottom = note->y() + qMin(note->height(), visibleHeight()); 4920 // int finalRight = note->x() + qMin(note->width() + (note->hasResizer() ? Note::RESIZER_WIDTH : 0), visibleWidth()); 4921 qreal bottom = note->y() + qMin(note->height(), (qreal)m_view->viewport()->height()); 4922 qreal finalRight = note->x() + qMin(note->width() + (note->hasResizer() ? Note::RESIZER_WIDTH : 0), (qreal)m_view->viewport()->width()); 4923 m_view->ensureVisible(finalRight, bottom, 0, 0); 4924 m_view->ensureVisible(note->x(), note->y(), 0, 0);*/ 4925 } 4926 4927 void BasketScene::addWatchedFile(const QString &fullPath) 4928 { 4929 // DEBUG_WIN << "Watcher>Add Monitoring Of : <font color=blue>" + fullPath + "</font>"; 4930 m_watcher->addFile(fullPath); 4931 } 4932 4933 void BasketScene::removeWatchedFile(const QString &fullPath) 4934 { 4935 // DEBUG_WIN << "Watcher>Remove Monitoring Of : <font color=blue>" + fullPath + "</font>"; 4936 m_watcher->removeFile(fullPath); 4937 } 4938 4939 void BasketScene::watchedFileModified(const QString &fullPath) 4940 { 4941 if (!m_modifiedFiles.contains(fullPath)) 4942 m_modifiedFiles.append(fullPath); 4943 // If a big file is saved by an application, notifications are send several times. 4944 // We wait they are not sent anymore to consider the file complete! 4945 m_watcherTimer.setSingleShot(true); 4946 m_watcherTimer.start(200); 4947 DEBUG_WIN << "Watcher>Modified : <font color=blue>" + fullPath + "</font>"; 4948 } 4949 4950 void BasketScene::watchedFileDeleted(const QString &fullPath) 4951 { 4952 Note *note = noteForFullPath(fullPath); 4953 removeWatchedFile(fullPath); 4954 if (note) { 4955 NoteSelection *selection = selectedNotes(); 4956 unselectAllBut(note); 4957 noteDeleteWithoutConfirmation(); 4958 while (selection) { 4959 selection->note->setSelected(true); 4960 selection = selection->nextStacked(); 4961 } 4962 } 4963 DEBUG_WIN << "Watcher>Removed : <font color=blue>" + fullPath + "</font>"; 4964 } 4965 4966 void BasketScene::updateModifiedNotes() 4967 { 4968 for (QList<QString>::iterator it = m_modifiedFiles.begin(); it != m_modifiedFiles.end(); ++it) { 4969 Note *note = noteForFullPath(*it); 4970 if (note) 4971 note->content()->loadFromFile(/*lazyLoad=*/false); 4972 } 4973 m_modifiedFiles.clear(); 4974 } 4975 4976 bool BasketScene::setProtection(int type, QString key) 4977 { 4978 #ifdef HAVE_LIBGPGME 4979 if (type == PasswordEncryption || // Ask a new password 4980 m_encryptionType != type || m_encryptionKey != key) { 4981 int savedType = m_encryptionType; 4982 QString savedKey = m_encryptionKey; 4983 4984 m_encryptionType = type; 4985 m_encryptionKey = key; 4986 m_gpg->clearCache(); 4987 4988 if (saveAgain()) { 4989 Q_EMIT propertiesChanged(this); 4990 } else { 4991 m_encryptionType = savedType; 4992 m_encryptionKey = savedKey; 4993 m_gpg->clearCache(); 4994 return false; 4995 } 4996 } 4997 return true; 4998 #else 4999 m_encryptionType = type; 5000 m_encryptionKey = key; 5001 5002 return false; 5003 #endif 5004 } 5005 5006 bool BasketScene::saveAgain() 5007 { 5008 bool result = false; 5009 5010 m_watcher->stopScan(); 5011 // Re-encrypt basket file: 5012 result = save(); 5013 // Re-encrypt every note files recursively: 5014 if (result) { 5015 FOR_EACH_NOTE(note) 5016 { 5017 result = note->saveAgain(); 5018 if (!result) 5019 break; 5020 } 5021 } 5022 m_watcher->startScan(); 5023 return result; 5024 } 5025 5026 bool BasketScene::isEncrypted() 5027 { 5028 return (m_encryptionType != NoEncryption); 5029 } 5030 5031 bool BasketScene::isFileEncrypted() 5032 { 5033 QFile file(fullPath() + ".basket"); 5034 5035 if (file.open(QIODevice::ReadOnly)) { 5036 // Should be ASCII anyways 5037 QString line = file.readLine(32); 5038 if (line.startsWith("-----BEGIN PGP MESSAGE-----")) 5039 return true; 5040 } 5041 return false; 5042 } 5043 5044 void BasketScene::lock() 5045 { 5046 #ifdef HAVE_LIBGPGME 5047 closeEditor(); 5048 m_gpg->clearCache(); 5049 m_locked = true; 5050 enableActions(); 5051 deleteNotes(); 5052 m_loaded = false; 5053 m_loadingLaunched = false; 5054 #endif 5055 } 5056 5057 #include "moc_basketscene.cpp"