diff --git a/src/basketscene.cpp b/src/basketscene.cpp index 4426e2a..adfc6e6 100644 --- a/src/basketscene.cpp +++ b/src/basketscene.cpp @@ -1,5219 +1,5219 @@ /** * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #include "basketscene.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // seed for rand() #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for KStatefulBrush #include #include #include #include #include #include #include #include #include #include // rand() function #include "backgroundmanager.h" #include "basketview.h" #include "debugwindow.h" #include "decoratedbasket.h" #include "diskerrordialog.h" #include "focusedwidgets.h" #include "gitwrapper.h" #include "global.h" #include "note.h" #include "notedrag.h" #include "noteedit.h" #include "notefactory.h" #include "noteselection.h" #include "settings.h" #include "tagsedit.h" #include "tools.h" #include "transparentwidget.h" #include "xmlwork.h" #include "config.h" #ifdef HAVE_LIBGPGME #include "kgpgme.h" #endif void debugZone(int zone) { QString s; switch (zone) { case Note::Handle: s = "Handle"; break; case Note::Group: s = "Group"; break; case Note::TagsArrow: s = "TagsArrow"; break; case Note::Custom0: s = "Custom0"; break; case Note::GroupExpander: s = "GroupExpander"; break; case Note::Content: s = "Content"; break; case Note::Link: s = "Link"; break; case Note::TopInsert: s = "TopInsert"; break; case Note::TopGroup: s = "TopGroup"; break; case Note::BottomInsert: s = "BottomInsert"; break; case Note::BottomGroup: s = "BottomGroup"; break; case Note::BottomColumn: s = "BottomColumn"; break; case Note::None: s = "None"; break; default: if (zone == Note::Emblem0) s = "Emblem0"; else s = "Emblem0+" + QString::number(zone - Note::Emblem0); break; } qDebug() << s; } #define FOR_EACH_NOTE(noteVar) for (Note *noteVar = firstNote(); noteVar; noteVar = noteVar->next()) void BasketScene::prependNoteIn(Note *note, Note *in) { if (!note) // No note to prepend: return; if (in) { // The normal case: preparePlug(note); Note *last = note->lastSibling(); for (Note *n = note; n; n = n->next()) n->setParentNote(in); // note->setPrev(0L); last->setNext(in->firstChild()); if (in->firstChild()) in->firstChild()->setPrev(last); in->setFirstChild(note); if (m_loaded) signalCountsChanged(); } else // Prepend it directly in the basket: appendNoteBefore(note, firstNote()); } void BasketScene::appendNoteIn(Note *note, Note *in) { if (!note) // No note to append: return; if (in) { // The normal case: preparePlug(note); // Note *last = note->lastSibling(); Note *lastChild = in->lastChild(); for (Note *n = note; n; n = n->next()) n->setParentNote(in); note->setPrev(lastChild); // last->setNext(0L); if (!in->firstChild()) in->setFirstChild(note); if (lastChild) lastChild->setNext(note); if (m_loaded) signalCountsChanged(); } else // Prepend it directly in the basket: appendNoteAfter(note, lastNote()); } void BasketScene::appendNoteAfter(Note *note, Note *after) { if (!note) // No note to append: return; if (!after) // By default, insert after the last note: after = lastNote(); if (m_loaded && after && !after->isFree() && !after->isColumn()) for (Note *n = note; n; n = n->next()) n->inheritTagsOf(after); // if (!alreadyInBasket) preparePlug(note); Note *last = note->lastSibling(); if (after) { // The normal case: for (Note *n = note; n; n = n->next()) n->setParentNote(after->parentNote()); note->setPrev(after); last->setNext(after->next()); after->setNext(note); if (last->next()) last->next()->setPrev(last); } else { // There is no note in the basket: for (Note *n = note; n; n = n->next()) n->setParentNote(nullptr); m_firstNote = note; // note->setPrev(0); // last->setNext(0); } // if (!alreadyInBasket) if (m_loaded) signalCountsChanged(); } void BasketScene::appendNoteBefore(Note *note, Note *before) { if (!note) // No note to append: return; if (!before) // By default, insert before the first note: before = firstNote(); if (m_loaded && before && !before->isFree() && !before->isColumn()) for (Note *n = note; n; n = n->next()) n->inheritTagsOf(before); preparePlug(note); Note *last = note->lastSibling(); if (before) { // The normal case: for (Note *n = note; n; n = n->next()) n->setParentNote(before->parentNote()); note->setPrev(before->prev()); last->setNext(before); before->setPrev(last); if (note->prev()) note->prev()->setNext(note); else { if (note->parentNote()) note->parentNote()->setFirstChild(note); else m_firstNote = note; } } else { // There is no note in the basket: for (Note *n = note; n; n = n->next()) n->setParentNote(nullptr); m_firstNote = note; // note->setPrev(0); // last->setNext(0); } if (m_loaded) signalCountsChanged(); } DecoratedBasket *BasketScene::decoration() { return (DecoratedBasket *)parent(); } void BasketScene::preparePlug(Note *note) { // Select only the new notes, compute the new notes count and the new number of found notes: if (m_loaded) unselectAll(); int count = 0; int founds = 0; Note *last = nullptr; for (Note *n = note; n; n = n->next()) { if (m_loaded) n->setSelectedRecursively(true); // Notes should have a parent basket (and they have, so that's OK). count += n->count(); founds += n->newFilter(decoration()->filterData()); last = n; } m_count += count; m_countFounds += founds; // Focus the last inserted note: if (m_loaded && last) { setFocusedNote(last); m_startOfShiftSelectionNote = (last->isGroup() ? last->lastRealChild() : last); } // If some notes don't match (are hidden), tell it to the user: if (m_loaded && founds < count) { if (count == 1) postMessage(i18n("The new note does not match the filter and is hidden.")); else if (founds == count - 1) postMessage(i18n("A new note does not match the filter and is hidden.")); else if (founds > 0) postMessage(i18n("Some new notes do not match the filter and are hidden.")); else postMessage(i18n("The new notes do not match the filter and are hidden.")); } } void BasketScene::unplugNote(Note *note) { // If there is nothing to do... if (!note) return; // if (!willBeReplugged) { note->setSelectedRecursively(false); // To removeSelectedNote() and decrease the selectedsCount. m_count -= note->count(); m_countFounds -= note->newFilter(decoration()->filterData()); signalCountsChanged(); // } // If it was the first note, change the first note: if (m_firstNote == note) m_firstNote = note->next(); // Change previous and next notes: if (note->prev()) note->prev()->setNext(note->next()); if (note->next()) note->next()->setPrev(note->prev()); if (note->parentNote()) { // If it was the first note of a group, change the first note of the group: if (note->parentNote()->firstChild() == note) note->parentNote()->setFirstChild(note->next()); if (!note->parentNote()->isColumn()) { // Delete parent if now 0 notes inside parent group: if (!note->parentNote()->firstChild()) { unplugNote(note->parentNote()); // a group could call this method for one or more of its children, // each children could call this method for its parent's group... // we have to do the deletion later otherwise we may corrupt the current process m_notesToBeDeleted << note; if (m_notesToBeDeleted.count() == 1) { QTimer::singleShot(0, this, SLOT(doCleanUp())); } } // Ungroup if still 1 note inside parent group: else if (!note->parentNote()->firstChild()->next()) { ungroupNote(note->parentNote()); } } } note->setParentNote(nullptr); note->setPrev(nullptr); note->setNext(nullptr); // Reste focus and hover note if necessary if (m_focusedNote == note) m_focusedNote = nullptr; if (m_hoveredNote == note) m_hoveredNote = nullptr; // recomputeBlankRects(); // FIXME: called too much time. It's here because when dragging and moving a note to another basket and then go back to the original basket, the note is deleted but the note rect is not painter anymore. } void BasketScene::ungroupNote(Note *group) { Note *note = group->firstChild(); Note *lastGroupedNote = group; Note *nextNote; // Move all notes after the group (not before, to avoid to change m_firstNote or group->m_firstChild): while (note) { nextNote = note->next(); if (lastGroupedNote->next()) lastGroupedNote->next()->setPrev(note); note->setNext(lastGroupedNote->next()); lastGroupedNote->setNext(note); note->setParentNote(group->parentNote()); note->setPrev(lastGroupedNote); note->setGroupWidth(group->groupWidth() - Note::GROUP_WIDTH); lastGroupedNote = note; note = nextNote; } // Unplug the group: group->setFirstChild(nullptr); unplugNote(group); // a group could call this method for one or more of its children, // each children could call this method for its parent's group... // we have to do the deletion later otherwise we may corrupt the current process m_notesToBeDeleted << group; if (m_notesToBeDeleted.count() == 1) { QTimer::singleShot(0, this, SLOT(doCleanUp())); } } void BasketScene::groupNoteBefore(Note *note, Note *with) { if (!note || !with) // No note to group or nowhere to group it: return; // if (m_loaded && before && !with->isFree() && !with->isColumn()) for (Note *n = note; n; n = n->next()) n->inheritTagsOf(with); preparePlug(note); Note *last = note->lastSibling(); Note *group = new Note(this); group->setPrev(with->prev()); group->setNext(with->next()); group->setX(with->x()); group->setY(with->y()); if (with->parentNote() && with->parentNote()->firstChild() == with) with->parentNote()->setFirstChild(group); else if (m_firstNote == with) m_firstNote = group; group->setParentNote(with->parentNote()); group->setFirstChild(note); group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); if (with->prev()) with->prev()->setNext(group); if (with->next()) with->next()->setPrev(group); with->setParentNote(group); with->setPrev(last); with->setNext(nullptr); for (Note *n = note; n; n = n->next()) n->setParentNote(group); // note->setPrev(0L); last->setNext(with); if (m_loaded) signalCountsChanged(); } void BasketScene::groupNoteAfter(Note *note, Note *with) { if (!note || !with) // No note to group or nowhere to group it: return; // if (m_loaded && before && !with->isFree() && !with->isColumn()) for (Note *n = note; n; n = n->next()) n->inheritTagsOf(with); preparePlug(note); // Note *last = note->lastSibling(); Note *group = new Note(this); group->setPrev(with->prev()); group->setNext(with->next()); group->setX(with->x()); group->setY(with->y()); if (with->parentNote() && with->parentNote()->firstChild() == with) with->parentNote()->setFirstChild(group); else if (m_firstNote == with) m_firstNote = group; group->setParentNote(with->parentNote()); group->setFirstChild(with); group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); if (with->prev()) with->prev()->setNext(group); if (with->next()) with->next()->setPrev(group); with->setParentNote(group); with->setPrev(nullptr); with->setNext(note); for (Note *n = note; n; n = n->next()) n->setParentNote(group); note->setPrev(with); // last->setNext(0L); if (m_loaded) signalCountsChanged(); } void BasketScene::doCleanUp() { QSet::iterator it = m_notesToBeDeleted.begin(); while (it != m_notesToBeDeleted.end()) { delete *it; it = m_notesToBeDeleted.erase(it); } } void BasketScene::loadNotes(const QDomElement ¬es, Note *parent) { Note *note; for (QDomNode n = notes.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if (e.isNull()) // Cannot handle that! continue; note = nullptr; // Load a Group: if (e.tagName() == "group") { note = new Note(this); // 1. Create the group... loadNotes(e, note); // 3. ... And populate it with child notes. int noteCount = note->count(); if (noteCount > 0 || (parent == nullptr && !isFreeLayout())) { // But don't remove columns! appendNoteIn(note, parent); // 2. ... Insert it... FIXME: Initially, the if() the insertion was the step 2. Was it on purpose? // The notes in the group are counted two times (it's why appendNoteIn() was called before loadNotes): m_count -= noteCount; // TODO: Recompute note count every time noteCount() is emitted! m_countFounds -= noteCount; } } // Load a Content-Based Note: if (e.tagName() == "note" || e.tagName() == "item") { // Keep compatible with 0.6.0 Alpha 1 note = new Note(this); // Create the note... - NoteFactory__loadNode(XMLWork::getElement(e, "content"), e.attribute("type"), note, /*lazyLoad=*/m_finishLoadOnFirstShow); // ... Populate it with content... + NoteFactory::loadNode(XMLWork::getElement(e, "content"), e.attribute("type"), note, /*lazyLoad=*/m_finishLoadOnFirstShow); // ... Populate it with content... if (e.attribute("type") == "text") m_shouldConvertPlainTextNotes = true; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! appendNoteIn(note, parent); // ... And insert it. // Load dates: if (e.hasAttribute("added")) note->setAddedDate(QDateTime::fromString(e.attribute("added"), Qt::ISODate)); if (e.hasAttribute("lastModification")) note->setLastModificationDate(QDateTime::fromString(e.attribute("lastModification"), Qt::ISODate)); } // If we successfully loaded a note: if (note) { // Free Note Properties: if (note->isFree()) { int x = e.attribute("x").toInt(); int y = e.attribute("y").toInt(); note->setX(x < 0 ? 0 : x); note->setY(y < 0 ? 0 : y); } // Resizeable Note Properties: if (note->hasResizer() || note->isColumn()) note->setGroupWidth(e.attribute("width", "200").toInt()); // Group Properties: if (note->isGroup() && !note->isColumn() && XMLWork::trueOrFalse(e.attribute("folded", "false"))) note->toggleFolded(); // Tags: if (note->content()) { QString tagsString = XMLWork::getElementText(e, QStringLiteral("tags"), QString()); QStringList tagsId = tagsString.split(';'); for (QStringList::iterator it = tagsId.begin(); it != tagsId.end(); ++it) { State *state = Tag::stateForId(*it); if (state) note->addState(state, /*orReplace=*/true); } } } qApp->processEvents(); } } void BasketScene::saveNotes(QXmlStreamWriter &stream, Note *parent) { Note *note = (parent ? parent->firstChild() : firstNote()); while (note) { // Create Element: stream.writeStartElement(note->isGroup() ? "group" : "note"); // Free Note Properties: if (note->isFree()) { stream.writeAttribute("x", QString::number(note->x())); stream.writeAttribute("y", QString::number(note->y())); } // Resizeable Note Properties: if (note->hasResizer()) stream.writeAttribute("width", QString::number(note->groupWidth())); // Group Properties: if (note->isGroup() && !note->isColumn()) stream.writeAttribute("folded", XMLWork::trueOrFalse(note->isFolded())); // Save Content: if (note->content()) { // Save Dates: stream.writeAttribute("added", note->addedDate().toString(Qt::ISODate)); stream.writeAttribute("lastModification", note->lastModificationDate().toString(Qt::ISODate)); // Save Content: stream.writeAttribute("type", note->content()->lowerTypeName()); note->content()->saveToNode(stream); // Save Tags: if (note->states().count() > 0) { QString tags; for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) { tags += (tags.isEmpty() ? QString() : QStringLiteral(";")) + (*it)->id(); } stream.writeTextElement("tags", tags); } } else { // Save Child Notes: saveNotes(stream, note); } stream.writeEndElement(); // Go to the Next One: note = note->next(); } } void BasketScene::loadProperties(const QDomElement &properties) { // Compute Default Values for When Loading the Properties: QString defaultBackgroundColor = (backgroundColorSetting().isValid() ? backgroundColorSetting().name() : QString()); QString defaultTextColor = (textColorSetting().isValid() ? textColorSetting().name() : QString()); // Load the Properties: QString icon = XMLWork::getElementText(properties, "icon", this->icon()); QString name = XMLWork::getElementText(properties, "name", basketName()); QDomElement appearance = XMLWork::getElement(properties, "appearance"); // In 0.6.0-Alpha versions, there was a typo error: "backround" instead of "background" QString backgroundImage = appearance.attribute("backgroundImage", appearance.attribute("backroundImage", backgroundImageName())); QString backgroundColorString = appearance.attribute("backgroundColor", appearance.attribute("backroundColor", defaultBackgroundColor)); QString textColorString = appearance.attribute("textColor", defaultTextColor); QColor backgroundColor = (backgroundColorString.isEmpty() ? QColor() : QColor(backgroundColorString)); QColor textColor = (textColorString.isEmpty() ? QColor() : QColor(textColorString)); QDomElement disposition = XMLWork::getElement(properties, "disposition"); bool free = XMLWork::trueOrFalse(disposition.attribute("free", XMLWork::trueOrFalse(isFreeLayout()))); int columnCount = disposition.attribute("columnCount", QString::number(this->columnsCount())).toInt(); bool mindMap = XMLWork::trueOrFalse(disposition.attribute("mindMap", XMLWork::trueOrFalse(isMindMap()))); QDomElement shortcut = XMLWork::getElement(properties, "shortcut"); QString actionStrings[] = {"show", "globalShow", "globalSwitch"}; QKeySequence combination = QKeySequence(shortcut.attribute("combination", m_action->shortcut().toString())); QString actionString = shortcut.attribute("action"); int action = shortcutAction(); if (actionString == actionStrings[0]) action = 0; if (actionString == actionStrings[1]) action = 1; if (actionString == actionStrings[2]) action = 2; QDomElement protection = XMLWork::getElement(properties, "protection"); m_encryptionType = protection.attribute("type").toInt(); m_encryptionKey = protection.attribute("key"); // Apply the Properties: setDisposition((free ? (mindMap ? 2 : 1) : 0), columnCount); setShortcut(combination, action); setAppearance(icon, name, backgroundImage, backgroundColor, textColor); // Will emit propertiesChanged(this) } void BasketScene::saveProperties(QXmlStreamWriter &stream) { stream.writeStartElement("properties"); stream.writeTextElement("name", basketName()); stream.writeTextElement("icon", icon()); stream.writeStartElement("appearance"); stream.writeAttribute("backgroundColor", backgroundColorSetting().isValid() ? backgroundColorSetting().name() : QString()); stream.writeAttribute("backgroundImage", backgroundImageName()); stream.writeAttribute("textColor", textColorSetting().isValid() ? textColorSetting().name() : QString()); stream.writeEndElement(); stream.writeStartElement("disposition"); stream.writeAttribute("columnCount", QString::number(columnsCount())); stream.writeAttribute("free", XMLWork::trueOrFalse(isFreeLayout())); stream.writeAttribute("mindMap", XMLWork::trueOrFalse(isMindMap())); stream.writeEndElement(); stream.writeStartElement("shortcut"); QString actionStrings[] = {"show", "globalShow", "globalSwitch"}; stream.writeAttribute("action", actionStrings[shortcutAction()]); stream.writeAttribute("combination", m_action->shortcut().toString()); stream.writeEndElement(); stream.writeStartElement("protection"); stream.writeAttribute("key", m_encryptionKey); stream.writeAttribute("type", QString::number(m_encryptionType)); stream.writeEndElement(); stream.writeEndElement(); } void BasketScene::subscribeBackgroundImages() { if (!m_backgroundImageName.isEmpty()) { Global::backgroundManager->subscribe(m_backgroundImageName); Global::backgroundManager->subscribe(m_backgroundImageName, this->backgroundColor()); Global::backgroundManager->subscribe(m_backgroundImageName, selectionRectInsideColor()); m_backgroundPixmap = Global::backgroundManager->pixmap(m_backgroundImageName); m_opaqueBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, this->backgroundColor()); m_selectedBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, selectionRectInsideColor()); m_backgroundTiled = Global::backgroundManager->tiled(m_backgroundImageName); } } void BasketScene::unsubscribeBackgroundImages() { if (hasBackgroundImage()) { Global::backgroundManager->unsubscribe(m_backgroundImageName); Global::backgroundManager->unsubscribe(m_backgroundImageName, this->backgroundColor()); Global::backgroundManager->unsubscribe(m_backgroundImageName, selectionRectInsideColor()); m_backgroundPixmap = nullptr; m_opaqueBackgroundPixmap = nullptr; m_selectedBackgroundPixmap = nullptr; } } void BasketScene::setAppearance(const QString &icon, const QString &name, const QString &backgroundImage, const QColor &backgroundColor, const QColor &textColor) { unsubscribeBackgroundImages(); m_icon = icon; m_basketName = name; m_backgroundImageName = backgroundImage; m_backgroundColorSetting = backgroundColor; m_textColorSetting = textColor; // Where is this shown? m_action->setText("BASKET SHORTCUT: " + name); // Basket should ALWAYS have an icon (the "basket" icon by default): QPixmap iconTest = KIconLoader::global()->loadIcon(m_icon, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true); if (iconTest.isNull()) m_icon = "basket"; // We don't request the background images if it's not loaded yet (to make the application startup fast). // When the basket is loading (because requested by the user: he/she want to access it) // it load the properties, subscribe to (and then load) the images, update the "Loading..." message with the image, // load all the notes and it's done! if (m_loadingLaunched) subscribeBackgroundImages(); recomputeAllStyles(); // If a note have a tag with the same background color as the basket one, then display a "..." recomputeBlankRects(); // See the drawing of blank areas in BasketScene::drawContents() unbufferizeAll(); if (isDuringEdit() && m_editor->graphicsWidget()) { QPalette palette; palette.setColor(m_editor->graphicsWidget()->widget()->backgroundRole(), m_editor->note()->backgroundColor()); palette.setColor(m_editor->graphicsWidget()->widget()->foregroundRole(), m_editor->note()->textColor()); m_editor->graphicsWidget()->setPalette(palette); } emit propertiesChanged(this); } void BasketScene::setDisposition(int disposition, int columnCount) { static const int COLUMNS_LAYOUT = 0; static const int FREE_LAYOUT = 1; static const int MINDMAPS_LAYOUT = 2; int currentDisposition = (isFreeLayout() ? (isMindMap() ? MINDMAPS_LAYOUT : FREE_LAYOUT) : COLUMNS_LAYOUT); if (currentDisposition == COLUMNS_LAYOUT && disposition == COLUMNS_LAYOUT) { if (firstNote() && columnCount > m_columnsCount) { // Insert each new columns: for (int i = m_columnsCount; i < columnCount; ++i) { Note *newColumn = new Note(this); insertNote(newColumn, /*clicked=*/lastNote(), /*zone=*/Note::BottomInsert, QPointF(), /*animateNewPosition=*/false); } } else if (firstNote() && columnCount < m_columnsCount) { Note *column = firstNote(); Note *cuttedNotes = nullptr; for (int i = 1; i <= m_columnsCount; ++i) { Note *columnToRemove = column; column = column->next(); if (i > columnCount) { // Remove the columns that are too much: unplugNote(columnToRemove); // "Cut" the content in the columns to be deleted: if (columnToRemove->firstChild()) { for (Note *it = columnToRemove->firstChild(); it; it = it->next()) it->setParentNote(nullptr); if (!cuttedNotes) cuttedNotes = columnToRemove->firstChild(); else { Note *lastCuttedNote = cuttedNotes; while (lastCuttedNote->next()) lastCuttedNote = lastCuttedNote->next(); lastCuttedNote->setNext(columnToRemove->firstChild()); columnToRemove->firstChild()->setPrev(lastCuttedNote); } columnToRemove->setFirstChild(nullptr); } delete columnToRemove; } } // Paste the content in the last column: if (cuttedNotes) insertNote(cuttedNotes, /*clicked=*/lastNote(), /*zone=*/Note::BottomColumn, QPointF(), /*animateNewPosition=*/true); unselectAll(); } if (columnCount != m_columnsCount) { m_columnsCount = (columnCount <= 0 ? 1 : columnCount); equalizeColumnSizes(); // Will relayoutNotes() } } else if (currentDisposition == COLUMNS_LAYOUT && (disposition == FREE_LAYOUT || disposition == MINDMAPS_LAYOUT)) { Note *column = firstNote(); m_columnsCount = 0; // Now, so relayoutNotes() will not relayout the free notes as if they were columns! while (column) { // Move all childs on the first level: Note *nextColumn = column->next(); ungroupNote(column); column = nextColumn; } unselectAll(); m_mindMap = (disposition == MINDMAPS_LAYOUT); relayoutNotes(); } else if ((currentDisposition == FREE_LAYOUT || currentDisposition == MINDMAPS_LAYOUT) && disposition == COLUMNS_LAYOUT) { if (firstNote()) { // TODO: Reorder notes! // Remove all notes (but keep a reference to them, we're not crazy ;-) ): Note *notes = m_firstNote; m_firstNote = nullptr; m_count = 0; m_countFounds = 0; // Insert the number of columns that is needed: Note *lastInsertedColumn = nullptr; for (int i = 0; i < columnCount; ++i) { Note *column = new Note(this); if (lastInsertedColumn) insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert, QPointF(), /*animateNewPosition=*/false); else m_firstNote = column; lastInsertedColumn = column; } // Reinsert the old notes in the first column: insertNote(notes, /*clicked=*/firstNote(), /*zone=*/Note::BottomColumn, QPointF(), /*animateNewPosition=*/true); unselectAll(); } else { // Insert the number of columns that is needed: Note *lastInsertedColumn = nullptr; for (int i = 0; i < columnCount; ++i) { Note *column = new Note(this); if (lastInsertedColumn) insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert, QPointF(), /*animateNewPosition=*/false); else m_firstNote = column; lastInsertedColumn = column; } } m_columnsCount = (columnCount <= 0 ? 1 : columnCount); equalizeColumnSizes(); // Will relayoutNotes() } } void BasketScene::equalizeColumnSizes() { if (!firstNote()) return; // Necessary to know the available space; relayoutNotes(); int availableSpace = m_view->viewport()->width(); int columnWidth = (availableSpace - (columnsCount() - 1) * Note::GROUP_WIDTH) / columnsCount(); int columnCount = columnsCount(); Note *column = firstNote(); while (column) { int minGroupWidth = column->minRight() - column->x(); if (minGroupWidth > columnWidth) { availableSpace -= minGroupWidth; --columnCount; } column = column->next(); } columnWidth = (availableSpace - (columnsCount() - 1) * Note::GROUP_WIDTH) / columnCount; column = firstNote(); while (column) { int minGroupWidth = column->minRight() - column->x(); if (minGroupWidth > columnWidth) column->setGroupWidth(minGroupWidth); else column->setGroupWidth(columnWidth); column = column->next(); } relayoutNotes(); } void BasketScene::enableActions() { Global::bnpView->enableActions(); m_view->setFocusPolicy(isLocked() ? Qt::NoFocus : Qt::StrongFocus); if (isLocked()) m_view->viewport()->setCursor(Qt::ArrowCursor); // When locking, the cursor stays the last form it was } bool BasketScene::save() { if (!m_loaded) return false; DEBUG_WIN << "Basket[" + folderName() + "]: Saving..."; QString data; QXmlStreamWriter stream(&data); XMLWork::setupXmlStream(stream, "basket"); // Create Properties Element and Populate It: saveProperties(stream); // Create Notes Element and Populate It: stream.writeStartElement("notes"); saveNotes(stream, nullptr); stream.writeEndElement(); stream.writeEndElement(); stream.writeEndDocument(); // Write to Disk: if (!saveToFile(fullPath() + ".basket", data)) { DEBUG_WIN << "Basket[" + folderName() + "]: FAILED to save!"; return false; } Global::bnpView->setUnsavedStatus(false); m_commitdelay.start(10000); // delay is 10 seconds return true; } void BasketScene::commitEdit() { GitWrapper::commitBasket(this); } void BasketScene::aboutToBeActivated() { if (m_finishLoadOnFirstShow) { FOR_EACH_NOTE(note) note->finishLazyLoad(); // relayoutNotes(/*animate=*/false); setFocusedNote(nullptr); // So that during the focusInEvent that will come shortly, the FIRST note is focused. m_finishLoadOnFirstShow = false; m_loaded = true; } } void BasketScene::reload() { closeEditor(); unbufferizeAll(); // Keep the memory footprint low m_firstNote = nullptr; m_loaded = false; m_loadingLaunched = false; invalidate(); } void BasketScene::load() { // Load only once: if (m_loadingLaunched) return; m_loadingLaunched = true; DEBUG_WIN << "Basket[" + folderName() + "]: Loading..."; QDomDocument *doc = nullptr; QString content; // Load properties if (loadFromFile(fullPath() + ".basket", &content)) { doc = new QDomDocument("basket"); if (!doc->setContent(content)) { DEBUG_WIN << "Basket[" + folderName() + "]: FAILED to parse XML!"; delete doc; doc = nullptr; } } if (isEncrypted()) DEBUG_WIN << "Basket is encrypted."; if (!doc) { DEBUG_WIN << "Basket[" + folderName() + "]: FAILED to load!"; m_loadingLaunched = false; if (isEncrypted()) m_locked = true; Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar return; } m_locked = false; QDomElement docElem = doc->documentElement(); QDomElement properties = XMLWork::getElement(docElem, "properties"); loadProperties(properties); // Since we are loading, this time the background image will also be loaded! // Now that the background image is loaded and subscribed, we display it during the load process: delete doc; // BEGIN Compatibility with 0.6.0 Pre-Alpha versions: QDomElement notes = XMLWork::getElement(docElem, "notes"); if (notes.isNull()) notes = XMLWork::getElement(docElem, "items"); m_watcher->stopScan(); m_shouldConvertPlainTextNotes = false; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! // Load notes m_finishLoadOnFirstShow = (Global::bnpView->currentBasket() != this); loadNotes(notes, nullptr); if (m_shouldConvertPlainTextNotes) convertTexts(); m_watcher->startScan(); signalCountsChanged(); if (isColumnsLayout()) { // Count the number of columns: int columnsCount = 0; Note *column = firstNote(); while (column) { ++columnsCount; column = column->next(); } m_columnsCount = columnsCount; } relayoutNotes(); // On application start, the current basket is not focused yet, so the focus rectangle is not shown when calling focusANote(): if (Global::bnpView->currentBasket() == this) setFocus(); focusANote(); m_loaded = true; enableActions(); } void BasketScene::filterAgain(bool andEnsureVisible /* = true*/) { newFilter(decoration()->filterData(), andEnsureVisible); } void BasketScene::filterAgainDelayed() { QTimer::singleShot(0, this, SLOT(filterAgain())); } void BasketScene::newFilter(const FilterData &data, bool andEnsureVisible /* = true*/) { if (!isLoaded()) return; // StopWatch::start(20); m_countFounds = 0; for (Note *note = firstNote(); note; note = note->next()) m_countFounds += note->newFilter(data); relayoutNotes(); signalCountsChanged(); if (hasFocus()) // if (!hasFocus()), focusANote() will be called at focusInEvent() focusANote(); // so, we avoid de-focus a note if it will be re-shown soon if (andEnsureVisible && m_focusedNote != nullptr) ensureNoteVisible(m_focusedNote); Global::bnpView->setFiltering(data.isFiltering); // StopWatch::check(20); } bool BasketScene::isFiltering() { return decoration()->filterBar()->filterData().isFiltering; } QString BasketScene::fullPath() { return Global::basketsFolder() + folderName(); } QString BasketScene::fullPathForFileName(const QString &fileName) { return fullPath() + fileName; } /*static*/ QString BasketScene::fullPathForFolderName(const QString &folderName) { return Global::basketsFolder() + folderName; } void BasketScene::setShortcut(QKeySequence shortcut, int action) { QList shortcuts {shortcut}; if (action > 0) { KGlobalAccel::self()->setShortcut(m_action, shortcuts, KGlobalAccel::Autoloading); KGlobalAccel::self()->setDefaultShortcut(m_action, shortcuts, KGlobalAccel::Autoloading); } m_shortcutAction = action; } void BasketScene::activatedShortcut() { Global::bnpView->setCurrentBasket(this); if (m_shortcutAction == 1) Global::bnpView->setActive(true); } void BasketScene::signalCountsChanged() { if (!m_timerCountsChanged.isActive()) { m_timerCountsChanged.setSingleShot(true); m_timerCountsChanged.start(0); } } void BasketScene::countsChangedTimeOut() { emit countsChanged(this); } BasketScene::BasketScene(QWidget *parent, const QString &folderName) //: Q3ScrollView(parent) : QGraphicsScene(parent) , m_noActionOnMouseRelease(false) , m_ignoreCloseEditorOnNextMouseRelease(false) , m_pressPos(-100, -100) , m_canDrag(false) , m_firstNote(nullptr) , m_columnsCount(1) , m_mindMap(false) , m_resizingNote(nullptr) , m_pickedResizer(0) , m_movingNote(nullptr) , m_pickedHandle(0, 0) , m_notesToBeDeleted() , m_clickedToInsert(nullptr) , m_zoneToInsert(0) , m_posToInsert(-1, -1) , m_isInsertPopupMenu(false) , m_insertMenuTitle(nullptr) , m_loaded(false) , m_loadingLaunched(false) , m_locked(false) , m_decryptBox(nullptr) , m_button(nullptr) , m_encryptionType(NoEncryption) #ifdef HAVE_LIBGPGME , m_gpg(0) #endif , m_backgroundPixmap(nullptr) , m_opaqueBackgroundPixmap(nullptr) , m_selectedBackgroundPixmap(nullptr) , m_action(nullptr) , m_shortcutAction(0) , m_hoveredNote(nullptr) , m_hoveredZone(Note::None) , m_lockedHovering(false) , m_underMouse(false) , m_inserterRect() , m_inserterShown(false) , m_inserterSplit(true) , m_inserterTop(false) , m_inserterGroup(false) , m_lastDisableClick(QTime::currentTime()) , m_isSelecting(false) , m_selectionStarted(false) , m_count(0) , m_countFounds(0) , m_countSelecteds(0) , m_folderName(folderName) , m_editor(nullptr) , m_leftEditorBorder(nullptr) , m_rightEditorBorder(nullptr) , m_redirectEditActions(false) , m_editorTrackMouseEvent(false) , m_editorWidth(-1) , m_editorHeight(-1) , m_doNotCloseEditor(false) , m_isDuringDrag(false) , m_draggedNotes() , m_focusedNote(nullptr) , m_startOfShiftSelectionNote(nullptr) , m_finishLoadOnFirstShow(false) , m_relayoutOnNextShow(false) { m_view = new BasketView(this); m_view->setFocusPolicy(Qt::StrongFocus); m_view->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_action = new QAction(this); connect(m_action, SIGNAL(triggered()), this, SLOT(activatedShortcut())); m_action->setObjectName(folderName); KGlobalAccel::self()->setGlobalShortcut(m_action, (QKeySequence())); // We do this in the basket properties dialog (and keep it in sync with the // global one) KActionCollection *ac = Global::bnpView->actionCollection(); ac->setShortcutsConfigurable(m_action, false); if (!m_folderName.endsWith('/')) m_folderName += '/'; // setDragAutoScroll(true); // By default, there is no corner widget: we set one for the corner area to be painted! // If we don't set one and there are two scrollbars present, slowly resizing up the window show graphical glitches in that area! m_cornerWidget = new QWidget(m_view); m_view->setCornerWidget(m_cornerWidget); m_view->viewport()->setAcceptDrops(true); m_view->viewport()->setMouseTracking(true); m_view->viewport()->setAutoFillBackground(false); // Do not clear the widget before paintEvent() because we always draw every pixels (faster and flicker-free) // File Watcher: m_watcher = new KDirWatch(this); connect(m_watcher, SIGNAL(dirty(const QString &)), this, SLOT(watchedFileModified(const QString &))); // connect(m_watcher, SIGNAL(deleted(const QString&)), this, SLOT(watchedFileDeleted(const QString&))); connect(&m_watcherTimer, SIGNAL(timeout()), this, SLOT(updateModifiedNotes())); // Various Connections: connect(&m_autoScrollSelectionTimer, SIGNAL(timeout()), this, SLOT(doAutoScrollSelection())); connect(&m_timerCountsChanged, SIGNAL(timeout()), this, SLOT(countsChangedTimeOut())); connect(&m_inactivityAutoSaveTimer, SIGNAL(timeout()), this, SLOT(inactivityAutoSaveTimeout())); connect(&m_inactivityAutoLockTimer, SIGNAL(timeout()), this, SLOT(inactivityAutoLockTimeout())); #ifdef HAVE_LIBGPGME m_gpg = new KGpgMe(); #endif m_locked = isFileEncrypted(); // setup the delayed commit timer m_commitdelay.setSingleShot(true); connect(&m_commitdelay, SIGNAL(timeout()), this, SLOT(commitEdit())); } void BasketScene::enterEvent(QEvent *) { m_underMouse = true; doHoverEffects(); } void BasketScene::leaveEvent(QEvent *) { m_underMouse = false; doHoverEffects(); if (m_lockedHovering) return; removeInserter(); if (m_hoveredNote) { m_hoveredNote->setHovered(false); m_hoveredNote->setHoveredZone(Note::None); m_hoveredNote->update(); } m_hoveredNote = nullptr; } void BasketScene::setFocusIfNotInPopupMenu() { if (!qApp->activePopupWidget()) { if (isDuringEdit()) m_editor->graphicsWidget()->setFocus(); else setFocus(); } } void BasketScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { // If user click the basket, focus it! // The focus is delayed because if the click results in showing a popup menu, // the interface flicker by showing the focused rectangle (as the basket gets focus) // and immediately removing it (because the popup menu now have focus). if (!isDuringEdit()) QTimer::singleShot(0, this, SLOT(setFocusIfNotInPopupMenu())); // Convenient variables: bool controlPressed = event->modifiers() & Qt::ControlModifier; bool shiftPressed = event->modifiers() & Qt::ShiftModifier; // Do nothing if we disabled the click some milliseconds sooner. // For instance when a popup menu has been closed with click, we should not do action: if (event->button() == Qt::LeftButton && (qApp->activePopupWidget() || m_lastDisableClick.msecsTo(QTime::currentTime()) <= 80)) { doHoverEffects(); m_noActionOnMouseRelease = true; // But we allow to select: // The code is the same as at the bottom of this method: if (event->button() == Qt::LeftButton) { m_selectionStarted = true; m_selectionBeginPoint = event->scenePos(); m_selectionInvert = controlPressed || shiftPressed; } return; } // if we are editing and no control key are pressed if (m_editor && !shiftPressed && !controlPressed) { // if the mouse is over the editor QPoint view_shift(m_view->horizontalScrollBar()->value(), m_view->verticalScrollBar()->value()); QGraphicsWidget *widget = dynamic_cast(m_view->itemAt((event->scenePos() - view_shift).toPoint())); if (widget && m_editor->graphicsWidget() == widget) { if (event->button() == Qt::LeftButton) { m_editorTrackMouseEvent = true; m_editor->startSelection(event->scenePos()); return; } else if (event->button() == Qt::MiddleButton) { m_editor->paste(event->scenePos(), QClipboard::Selection); return; } } } // Figure out what is the clicked note and zone: Note *clicked = noteAt(event->scenePos()); if (m_editor && (!clicked || (clicked && !(editedNote() == clicked)))) { closeEditor(); } Note::Zone zone = (clicked ? clicked->zoneAt(event->scenePos() - QPointF(clicked->x(), clicked->y())) : Note::None); // Popup Tags menu: if (zone == Note::TagsArrow && !controlPressed && !shiftPressed && event->button() != Qt::MidButton) { if (!clicked->allSelected()) unselectAllBut(clicked); setFocusedNote(clicked); /// /// /// m_startOfShiftSelectionNote = clicked; m_noActionOnMouseRelease = true; popupTagsMenu(clicked); return; } if (event->button() == Qt::LeftButton) { // Prepare to allow drag and drop when moving mouse further: if ((zone == Note::Handle || zone == Note::Group) || (clicked && clicked->allSelected() && (zone == Note::TagsArrow || zone == Note::Custom0 || zone == Note::Content || zone == Note::Link /**/ || zone >= Note::Emblem0 /**/))) { if (!shiftPressed && !controlPressed) { m_pressPos = event->scenePos(); // TODO: Allow to drag emblems to assign them to other notes. Then don't allow drag at Emblem0!! m_canDrag = true; // Saving where we were editing, because during a drag, the mouse can fly over the text edit and move the cursor position: if (m_editor && m_editor->textEdit()) { KTextEdit *editor = m_editor->textEdit(); m_textCursor = editor->textCursor(); } } } // Initializing Resizer move: if (zone == Note::Resizer) { m_resizingNote = clicked; m_pickedResizer = event->scenePos().x() - clicked->rightLimit(); m_noActionOnMouseRelease = true; m_lockedHovering = true; return; } // Select note(s): if (zone == Note::Handle || zone == Note::Group || (zone == Note::GroupExpander && (controlPressed || shiftPressed))) { // closeEditor(); Note *end = clicked; if (clicked->isGroup() && shiftPressed) { if (clicked->containsNote(m_startOfShiftSelectionNote)) { m_startOfShiftSelectionNote = clicked->firstRealChild(); end = clicked->lastRealChild(); } else if (clicked->firstRealChild()->isAfter(m_startOfShiftSelectionNote)) { end = clicked->lastRealChild(); } else { end = clicked->firstRealChild(); } } if (controlPressed && shiftPressed) selectRange(m_startOfShiftSelectionNote, end, /*unselectOthers=*/false); else if (shiftPressed) selectRange(m_startOfShiftSelectionNote, end); else if (controlPressed) clicked->setSelectedRecursively(!clicked->allSelected()); else if (!clicked->allSelected()) unselectAllBut(clicked); setFocusedNote(end); /// /// /// m_startOfShiftSelectionNote = (end->isGroup() ? end->firstRealChild() : end); // m_noActionOnMouseRelease = false; m_noActionOnMouseRelease = true; return; } // Folding/Unfolding group: if (zone == Note::GroupExpander) { clicked->toggleFolded(); if (/*m_animationTimeLine == 0 && */ Settings::playAnimations()) { qWarning() << "Folding animation to be done"; } relayoutNotes(); m_noActionOnMouseRelease = true; return; } } // Popup menu for tag emblems: if (event->button() == Qt::RightButton && zone >= Note::Emblem0) { if (!clicked->allSelected()) unselectAllBut(clicked); setFocusedNote(clicked); /// /// /// m_startOfShiftSelectionNote = clicked; popupEmblemMenu(clicked, zone - Note::Emblem0); m_noActionOnMouseRelease = true; return; } // Insertion Popup Menu: if ((event->button() == Qt::RightButton) && ((!clicked && isFreeLayout()) || (clicked && (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn)))) { unselectAll(); m_clickedToInsert = clicked; m_zoneToInsert = zone; m_posToInsert = event->scenePos(); QMenu menu(m_view); menu.addActions(Global::bnpView->popupMenu("insert_popup")->actions()); // If we already added a title, remove it because it would be kept and // then added several times. if (m_insertMenuTitle && menu.actions().contains(m_insertMenuTitle)) menu.removeAction(m_insertMenuTitle); QAction *first = menu.actions().value(0); // i18n: Verbs (for the "insert" menu) if (zone == Note::TopGroup || zone == Note::BottomGroup) m_insertMenuTitle = menu.insertSection(first, i18n("Group")); else m_insertMenuTitle = menu.insertSection(first, i18n("Insert")); setInsertPopupMenu(); connect(&menu, SIGNAL(aboutToHide()), this, SLOT(delayedCancelInsertPopupMenu())); connect(&menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering())); connect(&menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick())); connect(&menu, SIGNAL(aboutToHide()), this, SLOT(hideInsertPopupMenu())); doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! m_lockedHovering = true; menu.exec(QCursor::pos()); m_noActionOnMouseRelease = true; return; } // Note Context Menu: if (event->button() == Qt::RightButton && clicked && !clicked->isColumn() && zone != Note::Resizer) { if (!clicked->allSelected()) unselectAllBut(clicked); setFocusedNote(clicked); /// /// /// if (editedNote() == clicked) { closeEditor(false); clicked->setSelected(true); } m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); QMenu *menu = Global::bnpView->popupMenu("note_popup"); connect(menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering())); connect(menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick())); doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! m_lockedHovering = true; menu->exec(QCursor::pos()); m_noActionOnMouseRelease = true; return; } // Paste selection under cursor (but not "create new primary note under cursor" because this is on moveRelease): if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { if ((Settings::middleAction() != 0) && (event->modifiers() == Qt::ShiftModifier)) { m_clickedToInsert = clicked; m_zoneToInsert = zone; m_posToInsert = event->scenePos(); // closeEditor(); removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed switch (Settings::middleAction()) { case 1: m_isInsertPopupMenu = true; pasteNote(); break; case 2: type = NoteType::Image; break; case 3: type = NoteType::Link; break; case 4: type = NoteType::Launcher; break; default: m_noActionOnMouseRelease = false; return; } if (type != 0) { m_ignoreCloseEditorOnNextMouseRelease = true; Global::bnpView->insertEmpty(type); } } else { if (clicked) zone = clicked->zoneAt(event->scenePos() - QPoint(clicked->x(), clicked->y()), true); // closeEditor(); clickedToInsert(event, clicked, zone); save(); } m_noActionOnMouseRelease = true; return; } // Finally, no action has been done during pressEvent, so an action can be done on releaseEvent: m_noActionOnMouseRelease = false; /* Selection scenario: * On contentsMousePressEvent, put m_selectionStarted to true and init Begin and End selection point. * On contentsMouseMoveEvent, if m_selectionStarted, update End selection point, update selection rect, * and if it's larger, switching to m_isSelecting mode: we can draw the selection rectangle. */ // Prepare selection: if (event->button() == Qt::LeftButton) { m_selectionStarted = true; m_selectionBeginPoint = event->scenePos(); // We usually invert the selection with the Ctrl key, but some environments (like GNOME or The Gimp) do it with the Shift key. // Since the Shift key has no specific usage, we allow to invert selection ALSO with Shift for Gimp people m_selectionInvert = controlPressed || shiftPressed; } } void BasketScene::delayedCancelInsertPopupMenu() { QTimer::singleShot(0, this, SLOT(cancelInsertPopupMenu())); } void BasketScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { if (event->reason() == QGraphicsSceneContextMenuEvent::Keyboard) { if (countFounds /*countShown*/ () == 0) { // TODO: Count shown!! QMenu *menu = Global::bnpView->popupMenu("insert_popup"); setInsertPopupMenu(); connect(menu, SIGNAL(aboutToHide()), this, SLOT(delayedCancelInsertPopupMenu())); connect(menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering())); connect(menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick())); removeInserter(); m_lockedHovering = true; menu->exec(m_view->mapToGlobal(QPoint(0, 0))); } else { if (!m_focusedNote->isSelected()) unselectAllBut(m_focusedNote); setFocusedNote(m_focusedNote); /// /// /// m_startOfShiftSelectionNote = (m_focusedNote->isGroup() ? m_focusedNote->firstRealChild() : m_focusedNote); // Popup at bottom (or top) of the focused note, if visible : QMenu *menu = Global::bnpView->popupMenu("note_popup"); connect(menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering())); connect(menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick())); doHoverEffects(m_focusedNote, Note::Content); // In the case where another popup menu was open, we should do that manually! m_lockedHovering = true; menu->exec(noteVisibleRect(m_focusedNote).bottomLeft().toPoint()); } } } QRectF BasketScene::noteVisibleRect(Note *note) { QRectF rect(QPointF(note->x(), note->y()), QSizeF(note->width(), note->height())); QPoint basketPoint = m_view->mapToGlobal(QPoint(0, 0)); rect.moveTopLeft(rect.topLeft() + basketPoint + QPoint(m_view->frameWidth(), m_view->frameWidth())); // Now, rect contain the global note rectangle on the screen. // We have to clip it by the basket widget : // if (rect.bottom() > basketPoint.y() + visibleHeight() + 1) { // Bottom too... bottom // rect.setBottom(basketPoint.y() + visibleHeight() + 1); if (rect.bottom() > basketPoint.y() + m_view->viewport()->height() + 1) { // Bottom too... bottom rect.setBottom(basketPoint.y() + m_view->viewport()->height() + 1); if (rect.height() <= 0) // Have at least one visible pixel of height rect.setTop(rect.bottom()); } if (rect.top() < basketPoint.y() + m_view->frameWidth()) { // Top too... top rect.setTop(basketPoint.y() + m_view->frameWidth()); if (rect.height() <= 0) rect.setBottom(rect.top()); } // if (rect.right() > basketPoint.x() + visibleWidth() + 1) { // Right too... right // rect.setRight(basketPoint.x() + visibleWidth() + 1); if (rect.right() > basketPoint.x() + m_view->viewport()->width() + 1) { // Right too... right rect.setRight(basketPoint.x() + m_view->viewport()->width() + 1); if (rect.width() <= 0) // Have at least one visible pixel of width rect.setLeft(rect.right()); } if (rect.left() < basketPoint.x() + m_view->frameWidth()) { // Left too... left rect.setLeft(basketPoint.x() + m_view->frameWidth()); if (rect.width() <= 0) rect.setRight(rect.left()); } return rect; } void BasketScene::disableNextClick() { m_lastDisableClick = QTime::currentTime(); } void BasketScene::recomputeAllStyles() { FOR_EACH_NOTE(note) note->recomputeAllStyles(); } void BasketScene::removedStates(const QList &deletedStates) { bool modifiedBasket = false; FOR_EACH_NOTE(note) if (note->removedStates(deletedStates)) modifiedBasket = true; if (modifiedBasket) save(); } void BasketScene::insertNote(Note *note, Note *clicked, int zone, const QPointF &pos, bool animateNewPosition) { if (!note) { qDebug() << "Wanted to insert NO note"; return; } if (clicked && zone == Note::BottomColumn) { // When inserting at the bottom of a column, it's obvious the new note SHOULD inherit tags. // We ensure that by changing the insertion point after the last note of the column: Note *last = clicked->lastChild(); if (last) { clicked = last; zone = Note::BottomInsert; } } /// Insertion at the bottom of a column: if (clicked && zone == Note::BottomColumn) { note->setWidth(clicked->rightLimit() - clicked->x()); Note *lastChild = clicked->lastChild(); for (Note *n = note; n; n = n->next()) { n->setXRecursively(clicked->x()); n->setYRecursively((lastChild ? lastChild : clicked)->bottom() + 1); } appendNoteIn(note, clicked); /// Insertion relative to a note (top/bottom, insert/group): } else if (clicked) { note->setWidth(clicked->width()); for (Note *n = note; n; n = n->next()) { if (zone == Note::TopGroup || zone == Note::BottomGroup) { n->setXRecursively(clicked->x() + Note::GROUP_WIDTH); } else { n->setXRecursively(clicked->x()); } if (zone == Note::TopInsert || zone == Note::TopGroup) { n->setYRecursively(clicked->y()); } else { n->setYRecursively(clicked->bottom() + 1); } } if (zone == Note::TopInsert) { appendNoteBefore(note, clicked); } else if (zone == Note::BottomInsert) { appendNoteAfter(note, clicked); } else if (zone == Note::TopGroup) { groupNoteBefore(note, clicked); } else if (zone == Note::BottomGroup) { groupNoteAfter(note, clicked); } /// Free insertion: } else if (isFreeLayout()) { // Group if note have siblings: if (note->next()) { Note *group = new Note(this); for (Note *n = note; n; n = n->next()) n->setParentNote(group); group->setFirstChild(note); note = group; } // Insert at cursor position: const int initialWidth = 250; note->setWidth(note->isGroup() ? Note::GROUP_WIDTH : initialWidth); if (note->isGroup() && note->firstChild()) note->setInitialHeight(note->firstChild()->height()); // note->setGroupWidth(initialWidth); note->setXRecursively(pos.x()); note->setYRecursively(pos.y()); appendNoteAfter(note, lastNote()); } relayoutNotes(); } void BasketScene::clickedToInsert(QGraphicsSceneMouseEvent *event, Note *clicked, /*Note::Zone*/ int zone) { Note *note; if (event->button() == Qt::MidButton) note = NoteFactory::dropNote(QApplication::clipboard()->mimeData(QClipboard::Selection), this); else note = NoteFactory::createNoteText(QString(), this); if (!note) return; insertNote(note, clicked, zone, QPointF(event->scenePos()), /*animateNewPosition=*/false); // ensureNoteVisible(lastInsertedNote()); // TODO: in insertNote() if (event->button() != Qt::MidButton) { removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. closeEditor(); noteEdit(note, /*justAdded=*/true); } } void BasketScene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) { m_isDuringDrag = true; Global::bnpView->updateStatusBarHint(); if (NoteDrag::basketOf(event->mimeData()) == this) { m_draggedNotes = NoteDrag::notesOf(event); NoteDrag::saveNoteSelectionToList(selectedNotes()); } event->accept(); } void BasketScene::dragMoveEvent(QGraphicsSceneDragDropEvent *event) { // m_isDuringDrag = true; // if (isLocked()) // return; // FIXME: viewportToContents does NOT work !!! // QPoint pos = viewportToContents(event->pos()); // QPoint pos( event->pos().x() + contentsX(), event->pos().y() + contentsY() ); // if (insertAtCursorPos()) // computeInsertPlace(pos); doHoverEffects(event->scenePos()); // showFrameInsertTo(); if (isFreeLayout() || noteAt(event->scenePos())) // Cursor before rightLimit() or hovering the dragged source notes acceptDropEvent(event); else { event->setAccepted(false); } /* Note *hoveredNote = noteAt(event->pos().x(), event->pos().y()); if ( (isColumnsLayout() && !hoveredNote) || (draggedNotes().contains(hoveredNote)) ) { event->acceptAction(false); event->accept(false); } else acceptDropEvent(event);*/ // A workaround since QScrollView::dragAutoScroll seem to have no effect : // ensureVisible(event->pos().x() + contentsX(), event->pos().y() + contentsY(), 30, 30); // QScrollView::dragMoveEvent(event); } void BasketScene::dragLeaveEvent(QGraphicsSceneDragDropEvent *) { // resetInsertTo(); m_isDuringDrag = false; m_draggedNotes.clear(); NoteDrag::selectedNotes.clear(); m_noActionOnMouseRelease = true; emit resetStatusBarText(); doHoverEffects(); } void BasketScene::dropEvent(QGraphicsSceneDragDropEvent *event) { QPointF pos = event->scenePos(); qDebug() << "Drop Event at position " << pos.x() << ":" << pos.y(); m_isDuringDrag = false; emit resetStatusBarText(); // if (isLocked()) // return; // Do NOT check the bottom&right borders. // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), // the note is first removed, and relayoutNotes() compute the new height that is smaller // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! // Should, of course, not return 0: Note *clicked = noteAt(pos); if (NoteFactory::movingNotesInTheSameBasket(event->mimeData(), this, event->dropAction()) && event->dropAction() == Qt::MoveAction) { m_doNotCloseEditor = true; } Note *note = NoteFactory::dropNote(event->mimeData(), this, true, event->dropAction(), dynamic_cast(event->source())); if (note) { Note::Zone zone = (clicked ? clicked->zoneAt(pos - QPointF(clicked->x(), clicked->y()), /*toAdd=*/true) : Note::None); bool animateNewPosition = NoteFactory::movingNotesInTheSameBasket(event->mimeData(), this, event->dropAction()); if (animateNewPosition) { FOR_EACH_NOTE(n) n->setOnTop(false); // FOR_EACH_NOTE_IN_CHUNK(note) for (Note *n = note; n; n = n->next()) n->setOnTop(true); } insertNote(note, clicked, zone, pos, animateNewPosition); // If moved a note on bottom, contentsHeight has been diminished, then view scrolled up, and we should re-scroll the view down: ensureNoteVisible(note); // if (event->button() != Qt::MidButton) { // removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. // } // resetInsertTo(); // doHoverEffects(); called by insertNote() save(); } m_draggedNotes.clear(); NoteDrag::selectedNotes.clear(); m_doNotCloseEditor = false; // When starting the drag, we saved where we were editing. // This is because during a drag, the mouse can fly over the text edit and move the cursor position, and even HIDE the cursor. // So we re-show the cursor, and re-position it at the right place: if (m_editor && m_editor->textEdit()) { KTextEdit *editor = m_editor->textEdit(); editor->setTextCursor(m_textCursor); } } // handles dropping of a note to basket that is not shown // (usually through its entry in the basket list) void BasketScene::blindDrop(QGraphicsSceneDragDropEvent *event) { if (!m_isInsertPopupMenu && redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->paste(); else if (m_editor->lineEdit()) m_editor->lineEdit()->paste(); } else { if (!isLoaded()) { Global::bnpView->showPassiveLoading(this); load(); } closeEditor(); unselectAll(); Note *note = NoteFactory::dropNote(event->mimeData(), this, true, event->dropAction(), dynamic_cast(event->source())); if (note) { insertCreatedNote(note); // unselectAllBut(note); if (Settings::usePassivePopup()) Global::bnpView->showPassiveDropped(i18n("Dropped to basket %1", m_basketName)); } } save(); } void BasketScene::blindDrop(const QMimeData *mimeData, Qt::DropAction dropAction, QObject *source) { if (!m_isInsertPopupMenu && redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->paste(); else if (m_editor->lineEdit()) m_editor->lineEdit()->paste(); } else { if (!isLoaded()) { Global::bnpView->showPassiveLoading(this); load(); } closeEditor(); unselectAll(); Note *note = NoteFactory::dropNote(mimeData, this, true, dropAction, dynamic_cast(source)); if (note) { insertCreatedNote(note); // unselectAllBut(note); if (Settings::usePassivePopup()) Global::bnpView->showPassiveDropped(i18n("Dropped to basket %1", m_basketName)); } } save(); } void BasketScene::insertEmptyNote(int type) { if (!isLoaded()) load(); if (isDuringEdit()) closeEditor(); Note *note = NoteFactory::createEmptyNote((NoteType::Id)type, this); insertCreatedNote(note /*, / *edit=* /true*/); noteEdit(note, /*justAdded=*/true); } void BasketScene::insertWizard(int type) { saveInsertionData(); Note *note = nullptr; switch (type) { default: case 1: note = NoteFactory::importKMenuLauncher(this); break; case 2: note = NoteFactory::importIcon(this); break; case 3: note = NoteFactory::importFileContent(this); break; } if (!note) return; restoreInsertionData(); insertCreatedNote(note); unselectAllBut(note); resetInsertionData(); } void BasketScene::insertColor(const QColor &color) { Note *note = NoteFactory::createNoteColor(color, this); restoreInsertionData(); insertCreatedNote(note); unselectAllBut(note); resetInsertionData(); } void BasketScene::insertImage(const QPixmap &image) { Note *note = NoteFactory::createNoteImage(image, this); restoreInsertionData(); insertCreatedNote(note); unselectAllBut(note); resetInsertionData(); } void BasketScene::pasteNote(QClipboard::Mode mode) { if (!m_isInsertPopupMenu && redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->paste(); else if (m_editor->lineEdit()) m_editor->lineEdit()->paste(); } else { if (!isLoaded()) { Global::bnpView->showPassiveLoading(this); load(); } closeEditor(); unselectAll(); Note *note = NoteFactory::dropNote(QApplication::clipboard()->mimeData(mode), this); if (note) { insertCreatedNote(note); // unselectAllBut(note); } } } void BasketScene::insertCreatedNote(Note *note) { // Get the insertion data if the user clicked inside the basket: Note *clicked = m_clickedToInsert; int zone = m_zoneToInsert; QPointF pos = m_posToInsert; // If it isn't the case, use the default position: if (!clicked && (pos.x() < 0 || pos.y() < 0)) { // Insert right after the focused note: focusANote(); if (m_focusedNote) { clicked = m_focusedNote; zone = (m_focusedNote->isFree() ? Note::BottomGroup : Note::BottomInsert); pos = QPointF(m_focusedNote->x(), m_focusedNote->bottom()); // Insert at the end of the last column: } else if (isColumnsLayout()) { Note *column = /*(Settings::newNotesPlace == 0 ?*/ firstNote() /*: lastNote())*/; /*if (Settings::newNotesPlace == 0 && column->firstChild()) { // On Top, if at least one child in the column clicked = column->firstChild(); zone = Note::TopInsert; } else { // On Bottom*/ clicked = column; zone = Note::BottomColumn; /*}*/ // Insert at free position: } else { pos = QPointF(0, 0); } } insertNote(note, clicked, zone, pos); // ensureNoteVisible(lastInsertedNote()); removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. // resetInsertTo(); save(); } void BasketScene::saveInsertionData() { m_savedClickedToInsert = m_clickedToInsert; m_savedZoneToInsert = m_zoneToInsert; m_savedPosToInsert = m_posToInsert; } void BasketScene::restoreInsertionData() { m_clickedToInsert = m_savedClickedToInsert; m_zoneToInsert = m_savedZoneToInsert; m_posToInsert = m_savedPosToInsert; } void BasketScene::resetInsertionData() { m_clickedToInsert = nullptr; m_zoneToInsert = 0; m_posToInsert = QPoint(-1, -1); } void BasketScene::hideInsertPopupMenu() { QTimer::singleShot(50 /*ms*/, this, SLOT(timeoutHideInsertPopupMenu())); } void BasketScene::timeoutHideInsertPopupMenu() { resetInsertionData(); } void BasketScene::acceptDropEvent(QGraphicsSceneDragDropEvent *event, bool preCond) { // FIXME: Should not accept all actions! Or not all actions (link not supported?!) // event->acceptAction(preCond && 1); // event->accept(preCond); event->setAccepted(preCond); } void BasketScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { // Now disallow drag and mouse redirection m_canDrag = false; if (m_editorTrackMouseEvent) { m_editorTrackMouseEvent = false; m_editor->endSelection(m_pressPos); return; } // Cancel Resizer move: if (m_resizingNote) { m_resizingNote = nullptr; m_pickedResizer = 0; m_lockedHovering = false; doHoverEffects(); save(); } // Cancel Note move: /* if (m_movingNote) { m_movingNote = 0; m_pickedHandle = QPoint(0, 0); m_lockedHovering = false; //doHoverEffects(); save(); }*/ // Cancel Selection rectangle: if (m_isSelecting) { m_isSelecting = false; stopAutoScrollSelection(); resetWasInLastSelectionRect(); doHoverEffects(); invalidate(m_selectionRect); } m_selectionStarted = false; Note *clicked = noteAt(event->scenePos()); Note::Zone zone = (clicked ? clicked->zoneAt(event->scenePos() - QPointF(clicked->x(), clicked->y())) : Note::None); if ((zone == Note::Handle || zone == Note::Group) && editedNote() && editedNote() == clicked) { if (m_ignoreCloseEditorOnNextMouseRelease) m_ignoreCloseEditorOnNextMouseRelease = false; else { bool editedNoteStillThere = closeEditor(); if (editedNoteStillThere) // clicked->setSelected(true); unselectAllBut(clicked); } } /* if (event->buttons() == 0 && (zone == Note::Group || zone == Note::Handle)) { closeEditor(); unselectAllBut(clicked); } */ // Do nothing if an action has already been made during mousePressEvent, // or if user made a selection and canceled it by regressing to a very small rectangle. if (m_noActionOnMouseRelease) return; // We immediately set it to true, to avoid actions set on mouseRelease if NO mousePress event has been triggered. // This is the case when a popup menu is shown, and user click to the basket area to close it: // the menu then receive the mousePress event and the basket area ONLY receive the mouseRelease event. // Obviously, nothing should be done in this case: m_noActionOnMouseRelease = true; if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { if ((Settings::middleAction() != 0) && (event->modifiers() == Qt::ShiftModifier)) { m_clickedToInsert = clicked; m_zoneToInsert = zone; m_posToInsert = event->scenePos(); closeEditor(); removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed switch (Settings::middleAction()) { case 5: type = NoteType::Color; break; case 6: Global::bnpView->grabScreenshot(); return; case 7: Global::bnpView->slotColorFromScreen(); return; case 8: Global::bnpView->insertWizard(3); // loadFromFile return; case 9: Global::bnpView->insertWizard(1); // importKMenuLauncher return; case 10: Global::bnpView->insertWizard(2); // importIcon return; } if (type != 0) { m_ignoreCloseEditorOnNextMouseRelease = true; Global::bnpView->insertEmpty(type); return; } } } // Note *clicked = noteAt(event->pos().x(), event->pos().y()); if (!clicked) { if (isFreeLayout() && event->button() == Qt::LeftButton) { clickedToInsert(event); save(); } return; } // Note::Zone zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ); // Convenient variables: bool controlPressed = event->modifiers() & Qt::ControlModifier; bool shiftPressed = event->modifiers() & Qt::ShiftModifier; if (clicked && zone != Note::None && zone != Note::BottomColumn && zone != Note::Resizer && (controlPressed || shiftPressed)) { if (controlPressed && shiftPressed) selectRange(m_startOfShiftSelectionNote, clicked, /*unselectOthers=*/false); else if (shiftPressed) selectRange(m_startOfShiftSelectionNote, clicked); else if (controlPressed) clicked->setSelectedRecursively(!clicked->allSelected()); setFocusedNote(clicked); /// /// /// m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); m_noActionOnMouseRelease = true; return; } // Switch tag states: if (zone >= Note::Emblem0) { if (event->button() == Qt::LeftButton) { int icons = -1; for (State::List::iterator it = clicked->states().begin(); it != clicked->states().end(); ++it) { if (!(*it)->emblem().isEmpty()) icons++; if (icons == zone - Note::Emblem0) { State *state = (*it)->nextState(); if (!state) return; it = clicked->states().insert(it, state); ++it; clicked->states().erase(it); clicked->recomputeStyle(); clicked->unbufferize(); clicked->update(); updateEditorAppearance(); filterAgain(); save(); break; } } return; } /* else if (event->button() == Qt::RightButton) { popupEmblemMenu(clicked, zone - Note::Emblem0); return; }*/ } // Insert note or past clipboard: QString text; // Note *note; QString link; // int zone = zone; if (event->button() == Qt::MidButton && zone == Note::Resizer) return; // zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), true ); if (event->button() == Qt::RightButton && (clicked->isColumn() || zone == Note::Resizer)) return; if (clicked->isGroup() && zone == Note::None) return; switch (zone) { case Note::Handle: case Note::Group: // We select note on mousePress if it was unselected or Ctrl is pressed. // But the user can want to drag select_s_ notes, so it the note is selected, we only select it alone on mouseRelease: if (event->buttons() == 0) { qDebug() << "EXEC"; if (!(event->modifiers() & Qt::ControlModifier) && clicked->allSelected()) unselectAllBut(clicked); if (zone == Note::Handle && isDuringEdit() && editedNote() == clicked) { closeEditor(); clicked->setSelected(true); } } break; case Note::Custom0: // unselectAllBut(clicked); setFocusedNote(clicked); noteOpen(clicked); break; case Note::GroupExpander: case Note::TagsArrow: break; case Note::Link: link = clicked->linkAt(event->scenePos() - QPoint(clicked->x(), clicked->y())); if (!link.isEmpty()) { if (link == "basket-internal-remove-basket") { // TODO: ask confirmation: "Do you really want to delete the welcome baskets?\n You can re-add them at any time in the Help menu." Global::bnpView->doBasketDeletion(this); } else if (link == "basket-internal-import") { QMenu *menu = Global::bnpView->popupMenu("fileimport"); menu->exec(event->screenPos()); } else if (link.startsWith("basket://")) { emit crossReference(link); } else { KRun *run = new KRun(QUrl::fromUserInput(link), m_view->window()); // open the URL. run->setAutoDelete(true); } break; } // If there is no link, edit note content case Note::Content: { if (m_editor && m_editor->note() == clicked && m_editor->graphicsWidget()) { m_editor->setCursorTo(event->scenePos()); } else { closeEditor(); unselectAllBut(clicked); noteEdit(clicked, /*justAdded=*/false, event->scenePos()); QGraphicsScene::mouseReleaseEvent(event); } break; } case Note::TopInsert: case Note::TopGroup: case Note::BottomInsert: case Note::BottomGroup: case Note::BottomColumn: clickedToInsert(event, clicked, zone); save(); break; case Note::None: default: KMessageBox::information(m_view->viewport(), i18n("This message should never appear. If it does, this program is buggy! " "Please report the bug to the developer.")); break; } } void BasketScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { Note *clicked = noteAt(event->scenePos()); Note::Zone zone = (clicked ? clicked->zoneAt(event->scenePos() - QPointF(clicked->x(), clicked->y())) : Note::None); if (event->button() == Qt::LeftButton && (zone == Note::Group || zone == Note::Handle)) { doCopy(CopyToSelection); m_noActionOnMouseRelease = true; } else mousePressEvent(event); } void BasketScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { // redirect this event to the editor if track mouse event is active if (m_editorTrackMouseEvent && (m_pressPos - event->scenePos()).manhattanLength() > QApplication::startDragDistance()) { m_editor->updateSelection(event->scenePos()); return; } // Drag the notes: if (m_canDrag && (m_pressPos - event->scenePos()).manhattanLength() > QApplication::startDragDistance()) { m_canDrag = false; m_isSelecting = false; // Don't draw selection rectangle ater drag! m_selectionStarted = false; NoteSelection *selection = selectedNotes(); if (selection->firstStacked()) { QDrag *d = NoteDrag::dragObject(selection, /*cutting=*/false, /*source=*/m_view); // d will be deleted by QT /*bool shouldRemove = */ d->exec(); // delete selection; // Never delete because URL is dragged and the file must be available for the extern application // if (shouldRemove && d->target() == 0) // If target is another application that request to remove the note // emit wantDelete(this); } return; } // Moving a Resizer: if (m_resizingNote) { qreal groupWidth = event->scenePos().x() - m_resizingNote->x() - m_pickedResizer; qreal minRight = m_resizingNote->minRight(); // int maxRight = 100 * contentsWidth(); // A big enough value (+infinity) for free layouts. qreal maxRight = 100 * sceneRect().width(); // A big enough value (+infinity) for free layouts. Note *nextColumn = m_resizingNote->next(); if (m_resizingNote->isColumn()) { if (nextColumn) maxRight = nextColumn->x() + nextColumn->rightLimit() - nextColumn->minRight() - Note::RESIZER_WIDTH; else // maxRight = contentsWidth(); maxRight = sceneRect().width(); } if (groupWidth > maxRight - m_resizingNote->x()) groupWidth = maxRight - m_resizingNote->x(); if (groupWidth < minRight - m_resizingNote->x()) groupWidth = minRight - m_resizingNote->x(); qreal delta = groupWidth - m_resizingNote->groupWidth(); m_resizingNote->setGroupWidth(groupWidth); // If resizing columns: if (m_resizingNote->isColumn()) { Note *column = m_resizingNote; if ((column = column->next())) { // Next columns should not have them X coordinate animated, because it would flicker: column->setXRecursively(column->x() + delta); // And the resizer should resize the TWO sibling columns, and not push the other columns on th right: column->setGroupWidth(column->groupWidth() - delta); } } relayoutNotes(); } // Moving a Note: /* if (m_movingNote) { int x = event->pos().x() - m_pickedHandle.x(); int y = event->pos().y() - m_pickedHandle.y(); if (x < 0) x = 0; if (y < 0) y = 0; m_movingNote->setX(x); m_movingNote->setY(y); m_movingNote->relayoutAt(x, y, / *animate=* /false); relayoutNotes(true); } */ // Dragging the selection rectangle: if (m_selectionStarted) doAutoScrollSelection(); doHoverEffects(event->scenePos()); } void BasketScene::doAutoScrollSelection() { static const int AUTO_SCROLL_MARGIN = 50; // pixels static const int AUTO_SCROLL_DELAY = 100; // milliseconds QPoint pos = m_view->mapFromGlobal(QCursor::pos()); // Do the selection: if (m_isSelecting) invalidate(m_selectionRect); m_selectionEndPoint = m_view->mapToScene(pos); m_selectionRect = QRectF(m_selectionBeginPoint, m_selectionEndPoint).normalized(); if (m_selectionRect.left() < 0) m_selectionRect.setLeft(0); if (m_selectionRect.top() < 0) m_selectionRect.setTop(0); // if (m_selectionRect.right() >= contentsWidth()) m_selectionRect.setRight(contentsWidth() - 1); // if (m_selectionRect.bottom() >= contentsHeight()) m_selectionRect.setBottom(contentsHeight() - 1); if (m_selectionRect.right() >= sceneRect().width()) m_selectionRect.setRight(sceneRect().width() - 1); if (m_selectionRect.bottom() >= sceneRect().height()) m_selectionRect.setBottom(sceneRect().height() - 1); if ((m_selectionBeginPoint - m_selectionEndPoint).manhattanLength() > QApplication::startDragDistance()) { m_isSelecting = true; selectNotesIn(m_selectionRect, m_selectionInvert); invalidate(m_selectionRect); m_noActionOnMouseRelease = true; } else { // If the user was selecting but cancel by making the rectangle too small, cancel it really!!! if (m_isSelecting) { if (m_selectionInvert) selectNotesIn(QRectF(), m_selectionInvert); else unselectAllBut(nullptr); // TODO: unselectAll(); } if (m_isSelecting) resetWasInLastSelectionRect(); m_isSelecting = false; stopAutoScrollSelection(); return; } // Do the auto-scrolling: // FIXME: It's still flickering // QRectF insideRect(AUTO_SCROLL_MARGIN, AUTO_SCROLL_MARGIN, visibleWidth() - 2*AUTO_SCROLL_MARGIN, visibleHeight() - 2*AUTO_SCROLL_MARGIN); QRectF insideRect(AUTO_SCROLL_MARGIN, AUTO_SCROLL_MARGIN, m_view->viewport()->width() - 2 * AUTO_SCROLL_MARGIN, m_view->viewport()->height() - 2 * AUTO_SCROLL_MARGIN); int dx = 0; int dy = 0; if (pos.y() < AUTO_SCROLL_MARGIN) dy = pos.y() - AUTO_SCROLL_MARGIN; else if (pos.y() > m_view->viewport()->height() - AUTO_SCROLL_MARGIN) dy = pos.y() - m_view->viewport()->height() + AUTO_SCROLL_MARGIN; // else if (pos.y() > visibleHeight() - AUTO_SCROLL_MARGIN) // dy = pos.y() - visibleHeight() + AUTO_SCROLL_MARGIN; if (pos.x() < AUTO_SCROLL_MARGIN) dx = pos.x() - AUTO_SCROLL_MARGIN; else if (pos.x() > m_view->viewport()->width() - AUTO_SCROLL_MARGIN) dx = pos.x() - m_view->viewport()->width() + AUTO_SCROLL_MARGIN; // else if (pos.x() > visibleWidth() - AUTO_SCROLL_MARGIN) // dx = pos.x() - visibleWidth() + AUTO_SCROLL_MARGIN; if (dx || dy) { qApp->sendPostedEvents(); // Do the repaints, because the scrolling will make the area to repaint to be wrong // scrollBy(dx, dy); if (!m_autoScrollSelectionTimer.isActive()) m_autoScrollSelectionTimer.start(AUTO_SCROLL_DELAY); } else stopAutoScrollSelection(); } void BasketScene::stopAutoScrollSelection() { m_autoScrollSelectionTimer.stop(); } void BasketScene::resetWasInLastSelectionRect() { Note *note = m_firstNote; while (note) { note->resetWasInLastSelectionRect(); note = note->next(); } } void BasketScene::selectAll() { if (redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->selectAll(); else if (m_editor->lineEdit()) m_editor->lineEdit()->selectAll(); } else { // First select all in the group, then in the parent group... Note *child = m_focusedNote; Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); while (parent) { if (!parent->allSelected()) { parent->setSelectedRecursively(true); return; } child = parent; parent = parent->parentNote(); } // Then, select all: FOR_EACH_NOTE(note) note->setSelectedRecursively(true); } } void BasketScene::unselectAll() { if (redirectEditActions()) { if (m_editor->textEdit()) { QTextCursor cursor = m_editor->textEdit()->textCursor(); cursor.clearSelection(); m_editor->textEdit()->setTextCursor(cursor); selectionChangedInEditor(); // THIS IS NOT EMITTED BY Qt!!! } else if (m_editor->lineEdit()) m_editor->lineEdit()->deselect(); } else { if (countSelecteds() > 0) // Optimization FOR_EACH_NOTE(note) note->setSelectedRecursively(false); } } void BasketScene::invertSelection() { FOR_EACH_NOTE(note) note->invertSelectionRecursively(); } void BasketScene::unselectAllBut(Note *toSelect) { FOR_EACH_NOTE(note) note->unselectAllBut(toSelect); } void BasketScene::invertSelectionOf(Note *toSelect) { FOR_EACH_NOTE(note) note->invertSelectionOf(toSelect); } void BasketScene::selectNotesIn(const QRectF &rect, bool invertSelection, bool unselectOthers /*= true*/) { FOR_EACH_NOTE(note) note->selectIn(rect, invertSelection, unselectOthers); } void BasketScene::doHoverEffects() { doHoverEffects(m_view->mapToScene(m_view->viewport()->mapFromGlobal(QCursor::pos()))); } void BasketScene::doHoverEffects(Note *note, Note::Zone zone, const QPointF &pos) { // Inform the old and new hovered note (if any): Note *oldHoveredNote = m_hoveredNote; if (note != m_hoveredNote) { if (m_hoveredNote) { m_hoveredNote->setHovered(false); m_hoveredNote->setHoveredZone(Note::None); m_hoveredNote->update(); } m_hoveredNote = note; if (m_hoveredNote) { m_hoveredNote->setHovered(true); } } // If we are hovering a note, compute which zone is hovered and inform the note: if (m_hoveredNote) { if (zone != m_hoveredZone || oldHoveredNote != m_hoveredNote) { m_hoveredZone = zone; m_hoveredNote->setHoveredZone(zone); m_view->viewport()->setCursor(m_hoveredNote->cursorFromZone(zone)); m_hoveredNote->update(); } // If we are hovering an insert line zone, update this thing: if (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn) { placeInserter(m_hoveredNote, zone); } else { removeInserter(); } // If we are hovering an embedded link in a rich text element, show the destination in the statusbar: if (zone == Note::Link) emit setStatusBarText(m_hoveredNote->linkAt(pos - QPoint(m_hoveredNote->x(), m_hoveredNote->y()))); else if (m_hoveredNote->content()) emit setStatusBarText(m_hoveredNote->content()->statusBarMessage(m_hoveredZone)); // resetStatusBarText(); // If we aren't hovering a note, reset all: } else { if (isFreeLayout() && !isSelecting()) m_view->viewport()->setCursor(Qt::CrossCursor); else m_view->viewport()->unsetCursor(); m_hoveredZone = Note::None; removeInserter(); emit resetStatusBarText(); } } void BasketScene::doHoverEffects(const QPointF &pos) { // if (isDuringEdit()) // viewport()->unsetCursor(); // Do we have the right to do hover effects? if (!m_loaded || m_lockedHovering) { return; } // enterEvent() (mouse enter in the widget) set m_underMouse to true, and leaveEvent() make it false. // But some times the enterEvent() is not trigerred: eg. when dragging the scrollbar: // Ending the drag INSIDE the basket area will make NO hoverEffects() because m_underMouse is false. // User need to leave the area and re-enter it to get effects. // This hack solve that by dismissing the m_underMouse variable: // Don't do hover effects when a popup menu is opened. // Primarily because the basket area will only receive mouseEnterEvent and mouveLeaveEvent. // It willn't be noticed of mouseMoveEvent, which would result in a apparently broken application state: bool underMouse = !qApp->activePopupWidget(); // if (qApp->activePopupWidget()) // underMouse = false; // Compute which note is hovered: Note *note = (m_isSelecting || !underMouse ? nullptr : noteAt(pos)); Note::Zone zone = (note ? note->zoneAt(pos - QPointF(note->x(), note->y()), isDuringDrag()) : Note::None); // Inform the old and new hovered note (if any) and update the areas: doHoverEffects(note, zone, pos); } void BasketScene::mouseEnteredEditorWidget() { if (!m_lockedHovering && !qApp->activePopupWidget()) doHoverEffects(editedNote(), Note::Content, QPoint()); } void BasketScene::removeInserter() { if (m_inserterShown) { // Do not hide (and then update/repaint the view) if it is already hidden! m_inserterShown = false; invalidate(m_inserterRect); } } void BasketScene::placeInserter(Note *note, int zone) { // Remove the inserter: if (!note) { removeInserter(); return; } // Update the old position: if (inserterShown()) { invalidate(m_inserterRect); } // Some commodities: m_inserterShown = true; m_inserterTop = (zone == Note::TopGroup || zone == Note::TopInsert); m_inserterGroup = (zone == Note::TopGroup || zone == Note::BottomGroup); // X and width: qreal groupIndent = (note->isGroup() ? note->width() : Note::HANDLE_WIDTH); qreal x = note->x(); qreal width = (note->isGroup() ? note->rightLimit() - note->x() : note->width()); if (m_inserterGroup) { x += groupIndent; width -= groupIndent; } m_inserterSplit = (Settings::groupOnInsertionLine() && note && !note->isGroup() && !note->isFree() && !note->isColumn()); // if (note->isGroup()) // width = note->rightLimit() - note->x() - (m_inserterGroup ? groupIndent : 0); // Y: qreal y = note->y() - (m_inserterGroup && m_inserterTop ? 1 : 3); if (!m_inserterTop) y += (note->isColumn() ? note->height() : note->height()); // Assigning result: m_inserterRect = QRectF(x, y, width, 6 - (m_inserterGroup ? 2 : 0)); // Update the new position: invalidate(m_inserterRect); } inline void drawLineByRect(QPainter &painter, qreal x, qreal y, qreal width, qreal height) { painter.drawLine(x, y, x + width - 1, y + height - 1); } void BasketScene::drawInserter(QPainter &painter, qreal xPainter, qreal yPainter) { if (!m_inserterShown) return; QRectF rect = m_inserterRect; // For shorter code-lines when drawing! rect.translate(-xPainter, -yPainter); int lineY = (m_inserterGroup && m_inserterTop ? 0 : 2); int roundY = (m_inserterGroup && m_inserterTop ? 0 : 1); KStatefulBrush statefulBrush(KColorScheme::View, KColorScheme::HoverColor); const QColor highlightColor = palette().color(QPalette::Highlight).lighter(); painter.setPen(highlightColor); // The horizontal line: // painter.drawRect( rect.x(), rect.y() + lineY, rect.width(), 2); int width = rect.width() - 4; painter.fillRect(rect.x() + 2, rect.y() + lineY, width, 2, highlightColor); // The left-most and right-most edges (biggest vertical lines): drawLineByRect(painter, rect.x(), rect.y(), 1, (m_inserterGroup ? 4 : 6)); drawLineByRect(painter, rect.x() + rect.width() - 1, rect.y(), 1, (m_inserterGroup ? 4 : 6)); // The left and right mid vertical lines: drawLineByRect(painter, rect.x() + 1, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); drawLineByRect(painter, rect.x() + rect.width() - 2, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); // Draw the split as a feedback to know where is the limit between insert and group: if (m_inserterSplit) { int noteWidth = rect.width() + (m_inserterGroup ? Note::HANDLE_WIDTH : 0); int xSplit = rect.x() - (m_inserterGroup ? Note::HANDLE_WIDTH : 0) + noteWidth / 2; painter.drawRect(xSplit - 2, rect.y() + lineY, 4, 2); painter.drawRect(xSplit - 1, rect.y() + lineY, 2, 2); } } void BasketScene::helpEvent(QGraphicsSceneHelpEvent *event) { if (!m_loaded || !Settings::showNotesToolTip()) return; QString message; QRectF rect; QPointF contentPos = event->scenePos(); Note *note = noteAt(contentPos); if (!note && isFreeLayout()) { message = i18n("Insert note here\nRight click for more options"); QRectF itRect; for (QList::iterator it = m_blankAreas.begin(); it != m_blankAreas.end(); ++it) { itRect = QRectF(0, 0, m_view->viewport()->width(), m_view->viewport()->height()).intersected(*it); if (itRect.contains(contentPos)) { rect = itRect; rect.moveLeft(rect.left() - sceneRect().x()); rect.moveTop(rect.top() - sceneRect().y()); break; } } } else { if (!note) return; Note::Zone zone = note->zoneAt(contentPos - QPointF(note->x(), note->y())); switch (zone) { case Note::Resizer: message = (note->isColumn() ? i18n("Resize those columns") : (note->isGroup() ? i18n("Resize this group") : i18n("Resize this note"))); break; case Note::Handle: message = i18n("Select or move this note"); break; case Note::Group: message = i18n("Select or move this group"); break; case Note::TagsArrow: message = i18n("Assign or remove tags from this note"); if (note->states().count() > 0) { QString tagsString; for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) { QString tagName = "" + Tools::textToHTMLWithoutP((*it)->fullName()) + ""; if (tagsString.isEmpty()) tagsString = tagName; else tagsString = i18n("%1, %2", tagsString, tagName); } message = "" + message + "
" + i18n("Assigned Tags: %1", tagsString); } break; case Note::Custom0: message = note->content()->zoneTip(zone); break; //"Open this link/Open this file/Open this sound file/Launch this application" case Note::GroupExpander: message = (note->isFolded() ? i18n("Expand this group") : i18n("Collapse this group")); break; case Note::Link: case Note::Content: message = note->content()->editToolTipText(); break; case Note::TopInsert: case Note::BottomInsert: message = i18n("Insert note here\nRight click for more options"); break; case Note::TopGroup: message = i18n("Group note with the one below\nRight click for more options"); break; case Note::BottomGroup: message = i18n("Group note with the one above\nRight click for more options"); break; case Note::BottomColumn: message = i18n("Insert note here\nRight click for more options"); break; case Note::None: message = "** Zone NONE: internal error **"; break; default: if (zone >= Note::Emblem0) message = note->stateForEmblemNumber(zone - Note::Emblem0)->fullName(); else message = QString(); break; } if (zone == Note::Content || zone == Note::Link || zone == Note::Custom0) { QStringList keys; QStringList values; note->content()->toolTipInfos(&keys, &values); keys.append(i18n("Added")); keys.append(i18n("Last Modification")); values.append(note->addedStringDate()); values.append(note->lastModificationStringDate()); message = "" + message; QStringList::iterator key; QStringList::iterator value; for (key = keys.begin(), value = values.begin(); key != keys.end() && value != values.end(); ++key, ++value) message += "
" + i18nc("of the form 'key: value'", "%1: %2", *key, *value); message += "
"; } else if (m_inserterSplit && (zone == Note::TopInsert || zone == Note::BottomInsert)) message += '\n' + i18n("Click on the right to group instead of insert"); else if (m_inserterSplit && (zone == Note::TopGroup || zone == Note::BottomGroup)) message += '\n' + i18n("Click on the left to insert instead of group"); rect = note->zoneRect(zone, contentPos - QPoint(note->x(), note->y())); rect.moveLeft(rect.left() - sceneRect().x()); rect.moveTop(rect.top() - sceneRect().y()); rect.moveLeft(rect.left() + note->x()); rect.moveTop(rect.top() + note->y()); } QToolTip::showText(event->screenPos(), message, m_view, rect.toRect()); } Note *BasketScene::lastNote() { Note *note = firstNote(); while (note && note->next()) note = note->next(); return note; } void BasketScene::deleteNotes() { Note *note = m_firstNote; while (note) { Note *tmp = note->next(); delete note; note = tmp; } m_firstNote = nullptr; m_resizingNote = nullptr; m_movingNote = nullptr; m_focusedNote = nullptr; m_startOfShiftSelectionNote = nullptr; m_tagPopupNote = nullptr; m_clickedToInsert = nullptr; m_savedClickedToInsert = nullptr; m_hoveredNote = nullptr; m_count = 0; m_countFounds = 0; m_countSelecteds = 0; emit resetStatusBarText(); emit countsChanged(this); } Note *BasketScene::noteAt(QPointF pos) { qreal x = pos.x(); qreal y = pos.y(); // NO: // // Do NOT check the bottom&right borders. // // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), // // the note is first removed, and relayoutNotes() compute the new height that is smaller // // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! // // Should, of course, not return 0: if (x < 0 || x > sceneRect().width() || y < 0 || y > sceneRect().height()) return nullptr; // When resizing a note/group, keep it highlighted: if (m_resizingNote) return m_resizingNote; // Search and return the hovered note: Note *note = m_firstNote; Note *possibleNote; while (note) { possibleNote = note->noteAt(pos); if (possibleNote) { if (NoteDrag::selectedNotes.contains(possibleNote) || draggedNotes().contains(possibleNote)) return nullptr; else return possibleNote; } note = note->next(); } // If the basket is layouted in columns, return one of the columns to be able to add notes in them: if (isColumnsLayout()) { Note *column = m_firstNote; while (column) { if (x >= column->x() && x < column->rightLimit()) return column; column = column->next(); } } // Nothing found, no note is hovered: return nullptr; } BasketScene::~BasketScene() { m_commitdelay.stop(); // we don't know how long deleteNotes() last so we want to make extra sure that nobody will commit in between if (m_decryptBox) delete m_decryptBox; #ifdef HAVE_LIBGPGME delete m_gpg; #endif deleteNotes(); if (m_view) delete m_view; } QColor BasketScene::selectionRectInsideColor() { return Tools::mixColor(Tools::mixColor(backgroundColor(), palette().color(QPalette::Highlight)), backgroundColor()); } QColor alphaBlendColors(const QColor &bgColor, const QColor &fgColor, const int a) { // normal button... QRgb rgb = bgColor.rgb(); QRgb rgb_b = fgColor.rgb(); int alpha = a; if (alpha > 255) alpha = 255; if (alpha < 0) alpha = 0; int inv_alpha = 255 - alpha; QColor result = QColor(qRgb(qRed(rgb_b) * inv_alpha / 255 + qRed(rgb) * alpha / 255, qGreen(rgb_b) * inv_alpha / 255 + qGreen(rgb) * alpha / 255, qBlue(rgb_b) * inv_alpha / 255 + qBlue(rgb) * alpha / 255)); return result; } void BasketScene::unlock() { QTimer::singleShot(0, this, SLOT(load())); } void BasketScene::inactivityAutoLockTimeout() { lock(); } void BasketScene::drawBackground(QPainter *painter, const QRectF &rect) { if (!m_loadingLaunched) { if (!m_locked) { QTimer::singleShot(0, this, SLOT(load())); return; } else { Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar } } if (!hasBackgroundImage()) { painter->fillRect(rect, backgroundColor()); // It's either a background pixmap to draw or a background color to fill: } else if (isTiledBackground() || (rect.x() < backgroundPixmap()->width() && rect.y() < backgroundPixmap()->height())) { painter->fillRect(rect, backgroundColor()); blendBackground(*painter, rect, 0, 0, /*opaque=*/true); } else { painter->fillRect(rect, backgroundColor()); } } void BasketScene::drawForeground(QPainter *painter, const QRectF &rect) { if (m_locked) { if (!m_decryptBox) { m_decryptBox = new QFrame(m_view); m_decryptBox->setFrameShape(QFrame::StyledPanel); m_decryptBox->setFrameShadow(QFrame::Plain); m_decryptBox->setLineWidth(1); QGridLayout *layout = new QGridLayout(m_decryptBox); layout->setContentsMargins(11, 11, 11, 11); layout->setSpacing(6); #ifdef HAVE_LIBGPGME m_button = new QPushButton(m_decryptBox); m_button->setText(i18n("&Unlock")); layout->addWidget(m_button, 1, 2); connect(m_button, SIGNAL(clicked()), this, SLOT(unlock())); #endif QLabel *label = new QLabel(m_decryptBox); QString text = "" + i18n("Password protected basket.") + "
"; #ifdef HAVE_LIBGPGME label->setText(text + i18n("Press Unlock to access it.")); #else label->setText(text + i18n("Encryption is not supported by
this version of %1.", QGuiApplication::applicationDisplayName())); #endif label->setAlignment(Qt::AlignTop); layout->addWidget(label, 0, 1, 1, 2); QLabel *pixmap = new QLabel(m_decryptBox); pixmap->setPixmap(KIconLoader::global()->loadIcon("encrypted", KIconLoader::NoGroup, KIconLoader::SizeHuge)); layout->addWidget(pixmap, 0, 0, 2, 1); QSpacerItem *spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); layout->addItem(spacer, 1, 1); label = new QLabel("" + i18n("To make baskets stay unlocked, change the automatic
" "locking duration in the application settings.") + "
", m_decryptBox); label->setAlignment(Qt::AlignTop); layout->addWidget(label, 2, 0, 1, 3); m_decryptBox->resize(layout->sizeHint()); } if (m_decryptBox->isHidden()) { m_decryptBox->show(); } #ifdef HAVE_LIBGPGME m_button->setFocus(); #endif m_decryptBox->move((m_view->viewport()->width() - m_decryptBox->width()) / 2, (m_view->viewport()->height() - m_decryptBox->height()) / 2); } else { if (m_decryptBox && !m_decryptBox->isHidden()) m_decryptBox->hide(); } if (!m_loaded) { setSceneRect(0, 0, m_view->viewport()->width(), m_view->viewport()->height()); QBrush brush(backgroundColor()); QPixmap pixmap(m_view->viewport()->width(), m_view->viewport()->height()); // TODO: Clip it to asked size only! QPainter painter2(&pixmap); QTextDocument rt; rt.setHtml(QString("
%1
").arg(i18n("Loading..."))); rt.setTextWidth(m_view->viewport()->width()); int hrt = rt.size().height(); painter2.fillRect(0, 0, m_view->viewport()->width(), m_view->viewport()->height(), brush); blendBackground(painter2, QRectF(0, 0, m_view->viewport()->width(), m_view->viewport()->height()), -1, -1, /*opaque=*/true); QPalette pal = palette(); pal.setColor(QPalette::WindowText, textColor()); painter2.translate(0, (m_view->viewport()->height() - hrt) / 2); QAbstractTextDocumentLayout::PaintContext context; context.palette = pal; rt.documentLayout()->draw(&painter2, context); painter2.end(); painter->drawPixmap(0, 0, pixmap); return; // TODO: Clip to the wanted rectangle } enableActions(); if ((inserterShown() && rect.intersects(inserterRect())) || (m_isSelecting && rect.intersects(m_selectionRect))) { // Draw inserter: if (inserterShown() && rect.intersects(inserterRect())) { drawInserter(*painter, 0, 0); } // Draw selection rect: if (m_isSelecting && rect.intersects(m_selectionRect)) { QRectF selectionRect = m_selectionRect; QRectF selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { QColor insideColor = selectionRectInsideColor(); painter->fillRect(selectionRectInside, QBrush(insideColor, Qt::Dense4Pattern)); } painter->setPen(palette().color(QPalette::Highlight).darker()); painter->drawRect(selectionRect); painter->setPen(Tools::mixColor(palette().color(QPalette::Highlight).darker(), backgroundColor())); painter->drawPoint(selectionRect.topLeft()); painter->drawPoint(selectionRect.topRight()); painter->drawPoint(selectionRect.bottomLeft()); painter->drawPoint(selectionRect.bottomRight()); } } } /* rect(x,y,width,height)==(xBackgroundToDraw,yBackgroundToDraw,widthToDraw,heightToDraw) */ void BasketScene::blendBackground(QPainter &painter, const QRectF &rect, qreal xPainter, qreal yPainter, bool opaque, QPixmap *bg) { painter.save(); if (xPainter == -1 && yPainter == -1) { xPainter = rect.x(); yPainter = rect.y(); } if (hasBackgroundImage()) { const QPixmap *bgPixmap = (bg ? /* * */ bg : (opaque ? m_opaqueBackgroundPixmap : m_backgroundPixmap)); if (isTiledBackground()) { painter.drawTiledPixmap(rect.x() - xPainter, rect.y() - yPainter, rect.width(), rect.height(), *bgPixmap, rect.x(), rect.y()); } else { painter.drawPixmap(QPointF(rect.x() - xPainter, rect.y() - yPainter), *bgPixmap, rect); } } painter.restore(); } void BasketScene::recomputeBlankRects() { m_blankAreas.clear(); return; m_blankAreas.append(QRectF(0, 0, sceneRect().width(), sceneRect().height())); FOR_EACH_NOTE(note) note->recomputeBlankRects(m_blankAreas); // See the drawing of blank areas in BasketScene::drawContents() if (hasBackgroundImage() && !isTiledBackground()) substractRectOnAreas(QRectF(0, 0, backgroundPixmap()->width(), backgroundPixmap()->height()), m_blankAreas, false); } void BasketScene::unsetNotesWidth() { Note *note = m_firstNote; while (note) { note->unsetWidth(); note = note->next(); } } void BasketScene::relayoutNotes() { if (Global::bnpView->currentBasket() != this) return; // Optimize load time, and basket will be relaid out when activated, anyway int h = 0; tmpWidth = 0; tmpHeight = 0; Note *note = m_firstNote; while (note) { if (note->matching()) { note->relayoutAt(0, h); if (note->hasResizer()) { int minGroupWidth = note->minRight() - note->x(); if (note->groupWidth() < minGroupWidth) { note->setGroupWidth(minGroupWidth); relayoutNotes(); // Redo the thing, but this time it should not recurse return; } } h += note->height(); } note = note->next(); } if (isFreeLayout()) tmpHeight += 100; else tmpHeight += 15; setSceneRect(0, 0, qMax((qreal)m_view->viewport()->width(), tmpWidth), qMax((qreal)m_view->viewport()->height(), tmpHeight)); recomputeBlankRects(); placeEditor(); doHoverEffects(); invalidate(); } void BasketScene::popupEmblemMenu(Note *note, int emblemNumber) { m_tagPopupNote = note; State *state = note->stateForEmblemNumber(emblemNumber); State *nextState = state->nextState(/*cycle=*/false); Tag *tag = state->parentTag(); m_tagPopup = tag; QKeySequence sequence = tag->shortcut(); bool sequenceOnDelete = (nextState == nullptr && !tag->shortcut().isEmpty()); QMenu menu(m_view); if (tag->countStates() == 1) { menu.addSection(/*SmallIcon(state->icon()), */ tag->name()); QAction *act; act = new QAction(QIcon::fromTheme("edit-delete"), i18n("&Remove"), &menu); act->setData(1); menu.addAction(act); act = new QAction(QIcon::fromTheme("configure"), i18n("&Customize..."), &menu); act->setData(2); menu.addAction(act); menu.addSeparator(); act = new QAction(QIcon::fromTheme("search-filter"), i18n("&Filter by this Tag"), &menu); act->setData(3); menu.addAction(act); } else { menu.addSection(tag->name()); QList::iterator it; State *currentState; int i = 10; // QActionGroup makes the actions exclusive; turns checkboxes into radio // buttons on some styles. QActionGroup *emblemGroup = new QActionGroup(&menu); for (it = tag->states().begin(); it != tag->states().end(); ++it) { currentState = *it; QKeySequence sequence; if (currentState == nextState && !tag->shortcut().isEmpty()) sequence = tag->shortcut(); StateAction *sa = new StateAction(currentState, QKeySequence(sequence), nullptr, false); sa->setChecked(state == currentState); sa->setActionGroup(emblemGroup); sa->setData(i); menu.addAction(sa); if (currentState == nextState && !tag->shortcut().isEmpty()) sa->setShortcut(sequence); ++i; } menu.addSeparator(); QAction *act = new QAction(&menu); act->setIcon(QIcon::fromTheme("edit-delete")); act->setText(i18n("&Remove")); act->setShortcut(sequenceOnDelete ? sequence : QKeySequence()); act->setData(1); menu.addAction(act); act = new QAction(QIcon::fromTheme("configure"), i18n("&Customize..."), &menu); act->setData(2); menu.addAction(act); menu.addSeparator(); act = new QAction(QIcon::fromTheme("search-filter"), i18n("&Filter by this Tag"), &menu); act->setData(3); menu.addAction(act); act = new QAction(QIcon::fromTheme("search-filter"), i18n("Filter by this &State"), &menu); act->setData(4); menu.addAction(act); } connect(&menu, SIGNAL(triggered(QAction *)), this, SLOT(toggledStateInMenu(QAction *))); connect(&menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering())); connect(&menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick())); m_lockedHovering = true; menu.exec(QCursor::pos()); } void BasketScene::toggledStateInMenu(QAction *action) { int id = action->data().toInt(); if (id == 1) { removeTagFromSelectedNotes(m_tagPopup); // m_tagPopupNote->removeTag(m_tagPopup); // m_tagPopupNote->setWidth(0); // To force a new layout computation updateEditorAppearance(); filterAgain(); save(); return; } if (id == 2) { // Customize this State: TagsEditDialog dialog(m_view, m_tagPopupNote->stateOfTag(m_tagPopup)); dialog.exec(); return; } if (id == 3) { // Filter by this Tag decoration()->filterBar()->filterTag(m_tagPopup); return; } if (id == 4) { // Filter by this State decoration()->filterBar()->filterState(m_tagPopupNote->stateOfTag(m_tagPopup)); return; } /*addStateToSelectedNotes*/ changeStateOfSelectedNotes(m_tagPopup->states()[id - 10] /*, orReplace=true*/); // m_tagPopupNote->addState(m_tagPopup->states()[id - 10], true); filterAgain(); save(); } State *BasketScene::stateForTagFromSelectedNotes(Tag *tag) { State *state = nullptr; FOR_EACH_NOTE(note) if (note->stateForTagFromSelectedNotes(tag, &state) && state == nullptr) return nullptr; return state; } void BasketScene::activatedTagShortcut(Tag *tag) { // Compute the next state to set: State *state = stateForTagFromSelectedNotes(tag); if (state) state = state->nextState(/*cycle=*/false); else state = tag->states().first(); // Set or unset it: if (state) { FOR_EACH_NOTE(note) note->addStateToSelectedNotes(state, /*orReplace=*/true); updateEditorAppearance(); } else removeTagFromSelectedNotes(tag); filterAgain(); save(); } void BasketScene::popupTagsMenu(Note *note) { m_tagPopupNote = note; QMenu menu(m_view); menu.addSection(i18n("Tags")); Global::bnpView->populateTagsMenu(menu, note); m_lockedHovering = true; menu.exec(QCursor::pos()); } void BasketScene::unlockHovering() { m_lockedHovering = false; doHoverEffects(); } void BasketScene::toggledTagInMenu(QAction *act) { int id = act->data().toInt(); if (id == 1) { // Assign new Tag... TagsEditDialog dialog(m_view, /*stateToEdit=*/nullptr, /*addNewTag=*/true); dialog.exec(); if (!dialog.addedStates().isEmpty()) { State::List states = dialog.addedStates(); for (State::List::iterator itState = states.begin(); itState != states.end(); ++itState) FOR_EACH_NOTE(note) note->addStateToSelectedNotes(*itState); updateEditorAppearance(); filterAgain(); save(); } return; } if (id == 2) { // Remove All removeAllTagsFromSelectedNotes(); filterAgain(); save(); return; } if (id == 3) { // Customize... TagsEditDialog dialog(m_view); dialog.exec(); return; } Tag *tag = Tag::all[id - 10]; if (!tag) return; if (m_tagPopupNote->hasTag(tag)) removeTagFromSelectedNotes(tag); else addTagToSelectedNotes(tag); m_tagPopupNote->setWidth(0); // To force a new layout computation filterAgain(); save(); } void BasketScene::addTagToSelectedNotes(Tag *tag) { FOR_EACH_NOTE(note) note->addTagToSelectedNotes(tag); updateEditorAppearance(); } void BasketScene::removeTagFromSelectedNotes(Tag *tag) { FOR_EACH_NOTE(note) note->removeTagFromSelectedNotes(tag); updateEditorAppearance(); } void BasketScene::addStateToSelectedNotes(State *state) { FOR_EACH_NOTE(note) note->addStateToSelectedNotes(state); updateEditorAppearance(); } void BasketScene::updateEditorAppearance() { if (isDuringEdit() && m_editor->graphicsWidget()) { m_editor->graphicsWidget()->setFont(m_editor->note()->font()); if (m_editor->graphicsWidget()->widget()) { QPalette palette; palette.setColor(m_editor->graphicsWidget()->widget()->backgroundRole(), m_editor->note()->backgroundColor()); palette.setColor(m_editor->graphicsWidget()->widget()->foregroundRole(), m_editor->note()->textColor()); m_editor->graphicsWidget()->setPalette(palette); } // Ugly Hack around Qt bugs: placeCursor() don't call any signal: HtmlEditor *htmlEditor = dynamic_cast(m_editor); if (htmlEditor) { if (m_editor->textEdit()->textCursor().atStart()) { m_editor->textEdit()->moveCursor(QTextCursor::Right); m_editor->textEdit()->moveCursor(QTextCursor::Left); } else { m_editor->textEdit()->moveCursor(QTextCursor::Left); m_editor->textEdit()->moveCursor(QTextCursor::Right); } htmlEditor->cursorPositionChanged(); // Does not work anyway :-( (when clicking on a red bold text, the toolbar still show black normal text) } } } void BasketScene::editorPropertiesChanged() { if (isDuringEdit() && m_editor->note()->content()->type() == NoteType::Html) { m_editor->textEdit()->setAutoFormatting(Settings::autoBullet() ? QTextEdit::AutoAll : QTextEdit::AutoNone); } } void BasketScene::changeStateOfSelectedNotes(State *state) { FOR_EACH_NOTE(note) note->changeStateOfSelectedNotes(state); updateEditorAppearance(); } void BasketScene::removeAllTagsFromSelectedNotes() { FOR_EACH_NOTE(note) note->removeAllTagsFromSelectedNotes(); updateEditorAppearance(); } bool BasketScene::selectedNotesHaveTags() { FOR_EACH_NOTE(note) if (note->selectedNotesHaveTags()) return true; return false; } QColor BasketScene::backgroundColor() const { if (m_backgroundColorSetting.isValid()) return m_backgroundColorSetting; else return palette().color(QPalette::Base); } QColor BasketScene::textColor() const { if (m_textColorSetting.isValid()) return m_textColorSetting; else return palette().color(QPalette::Text); } void BasketScene::unbufferizeAll() { FOR_EACH_NOTE(note) note->unbufferizeAll(); } Note *BasketScene::editedNote() { if (m_editor) return m_editor->note(); else return nullptr; } bool BasketScene::hasTextInEditor() { if (!isDuringEdit() || !redirectEditActions()) return false; if (m_editor->textEdit()) return !m_editor->textEdit()->document()->isEmpty(); else if (m_editor->lineEdit()) return !m_editor->lineEdit()->displayText().isEmpty(); else return false; } bool BasketScene::hasSelectedTextInEditor() { if (!isDuringEdit() || !redirectEditActions()) return false; if (m_editor->textEdit()) { // The following line does NOT work if one letter is selected and the user press Shift+Left or Shift+Right to unselect than letter: // Qt mysteriously tell us there is an invisible selection!! // return m_editor->textEdit()->hasSelectedText(); return !m_editor->textEdit()->textCursor().selectedText().isEmpty(); } else if (m_editor->lineEdit()) return m_editor->lineEdit()->hasSelectedText(); else return false; } bool BasketScene::selectedAllTextInEditor() { if (!isDuringEdit() || !redirectEditActions()) return false; if (m_editor->textEdit()) { return m_editor->textEdit()->document()->isEmpty() || m_editor->textEdit()->toPlainText() == m_editor->textEdit()->textCursor().selectedText(); } else if (m_editor->lineEdit()) return m_editor->lineEdit()->displayText().isEmpty() || m_editor->lineEdit()->displayText() == m_editor->lineEdit()->selectedText(); else return false; } void BasketScene::selectionChangedInEditor() { Global::bnpView->notesStateChanged(); } void BasketScene::contentChangedInEditor() { // Do not wait 3 seconds, because we need the note to expand as needed (if a line is too wider... the note should grow wider): if (m_editor->textEdit()) m_editor->autoSave(/*toFileToo=*/false); // else { if (m_inactivityAutoSaveTimer.isActive()) m_inactivityAutoSaveTimer.stop(); m_inactivityAutoSaveTimer.setSingleShot(true); m_inactivityAutoSaveTimer.start(3 * 1000); Global::bnpView->setUnsavedStatus(true); // } } void BasketScene::inactivityAutoSaveTimeout() { if (m_editor) m_editor->autoSave(/*toFileToo=*/true); } void BasketScene::placeEditorAndEnsureVisible() { placeEditor(/*andEnsureVisible=*/true); } // TODO: [kw] Oh boy, this will probably require some tweaking. void BasketScene::placeEditor(bool /*andEnsureVisible*/ /*= false*/) { if (!isDuringEdit()) return; QFrame *editorQFrame = dynamic_cast(m_editor->graphicsWidget()->widget()); KTextEdit *textEdit = m_editor->textEdit(); Note *note = m_editor->note(); qreal frameWidth = (editorQFrame ? editorQFrame->frameWidth() : 0); qreal x = note->x() + note->contentX() + note->content()->xEditorIndent() - frameWidth; qreal y; qreal maxHeight = qMax((qreal)m_view->viewport()->height(), sceneRect().height()); qreal height, width; if (textEdit) { // Need to do it 2 times, because it's wrong overwise // (sometimes, width depends on height, and sometimes, height depends on with): for (int i = 0; i < 2; i++) { // FIXME: CRASH: Select all text, press Del or [<--] and editor->removeSelectedText() is called: // editor->sync() CRASH!! // editor->sync(); y = note->y() + Note::NOTE_MARGIN - frameWidth; height = note->height() - 2 * frameWidth - 2 * Note::NOTE_MARGIN; width = note->x() + note->width() - x + 1; if (y + height > maxHeight) y = maxHeight - height; m_editor->graphicsWidget()->setMaximumSize(width, height); textEdit->setFixedSize(width, height); textEdit->viewport()->setFixedSize(width, height); } } else { height = note->height() - 2 * Note::NOTE_MARGIN + 2 * frameWidth; width = note->x() + note->width() - x; // note->rightLimit() - x + 2*m_view->frameWidth; if (m_editor->graphicsWidget()) m_editor->graphicsWidget()->widget()->setFixedSize(width, height); x -= 1; y = note->y() + Note::NOTE_MARGIN - frameWidth; } if ((m_editorWidth > 0 && m_editorWidth != width) || (m_editorHeight > 0 && m_editorHeight != height)) { m_editorWidth = width; // Avoid infinite recursion!!! m_editorHeight = height; m_editor->autoSave(/*toFileToo=*/true); } m_editorWidth = width; m_editorHeight = height; m_editor->graphicsWidget()->setPos(x, y); m_editorX = x; m_editorY = y; // if (andEnsureVisible) // ensureNoteVisible(note); } void BasketScene::editorCursorPositionChanged() { if (!isDuringEdit()) return; FocusedTextEdit *textEdit = dynamic_cast(m_editor->textEdit()); if (textEdit) { QPoint cursorPoint = textEdit->viewport()->mapToGlobal(textEdit->cursorRect().center()); // QPointF contentsCursor = m_view->mapToScene( m_view->viewport()->mapFromGlobal(cursorPoint) ); // m_view->ensureVisible(contentsCursor.x(), contentsCursor.y(),1,1); } } void BasketScene::closeEditorDelayed() { setFocus(); QTimer::singleShot(0, this, SLOT(closeEditor())); } bool BasketScene::closeEditor(bool deleteEmptyNote /* =true*/) { if (!isDuringEdit()) return true; if (m_doNotCloseEditor) return true; if (m_redirectEditActions) { if (m_editor->textEdit()) { disconnect(m_editor->textEdit(), SIGNAL(selectionChanged()), this, SLOT(selectionChangedInEditor())); disconnect(m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(selectionChangedInEditor())); disconnect(m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(contentChangedInEditor())); } else if (m_editor->lineEdit()) { disconnect(m_editor->lineEdit(), SIGNAL(selectionChanged()), this, SLOT(selectionChangedInEditor())); disconnect(m_editor->lineEdit(), SIGNAL(textChanged(const QString &)), this, SLOT(selectionChangedInEditor())); disconnect(m_editor->lineEdit(), SIGNAL(textChanged(const QString &)), this, SLOT(contentChangedInEditor())); } } m_editorTrackMouseEvent = false; m_editor->graphicsWidget()->widget()->disconnect(); removeItem(m_editor->graphicsWidget()); m_editor->validate(); Note *note = m_editor->note(); // Delete the editor BEFORE unselecting the note because unselecting the note would trigger closeEditor() recursivly: bool isEmpty = m_editor->isEmpty(); delete m_editor; m_editor = nullptr; m_redirectEditActions = false; m_editorWidth = -1; m_editorHeight = -1; m_inactivityAutoSaveTimer.stop(); // Delete the note if it is now empty: if (isEmpty && deleteEmptyNote) { focusANonSelectedNoteAboveOrThenBelow(); note->setSelected(true); note->deleteSelectedNotes(); if (m_hoveredNote == note) m_hoveredNote = nullptr; if (m_focusedNote == note) m_focusedNote = nullptr; delete note; save(); note = nullptr; } unlockHovering(); filterAgain(/*andEnsureVisible=*/false); // Does not work: // if (Settings::playAnimations()) // note->setOnTop(true); // So if it grew, do not obscure it temporarily while the notes below it are moving if (note) note->setSelected(false); // unselectAll(); doHoverEffects(); // save(); Global::bnpView->m_actEditNote->setEnabled(!isLocked() && countSelecteds() == 1 /*&& !isDuringEdit()*/); emit resetStatusBarText(); // Remove the "Editing. ... to validate." text. // if (qApp->activeWindow() == Global::mainContainer) // Set focus to the basket, unless the user pressed a letter key in the filter bar and the currently edited note came hidden, then editing closed: if (!decoration()->filterBar()->lineEdit()->hasFocus()) setFocus(); // Return true if the note is still there: return (note != nullptr); } void BasketScene::closeBasket() { closeEditor(); unbufferizeAll(); // Keep the memory footprint low if (isEncrypted()) { if (Settings::enableReLockTimeout()) { int seconds = Settings::reLockTimeoutMinutes() * 60; m_inactivityAutoLockTimer.setSingleShot(true); m_inactivityAutoLockTimer.start(seconds * 1000); } } } void BasketScene::openBasket() { if (m_inactivityAutoLockTimer.isActive()) m_inactivityAutoLockTimer.stop(); } Note *BasketScene::theSelectedNote() { if (countSelecteds() != 1) { qDebug() << "NO SELECTED NOTE !!!!"; return nullptr; } Note *selectedOne; FOR_EACH_NOTE(note) { selectedOne = note->theSelectedNote(); if (selectedOne) return selectedOne; } qDebug() << "One selected note, BUT NOT FOUND !!!!"; return nullptr; } NoteSelection *BasketScene::selectedNotes() { NoteSelection selection; FOR_EACH_NOTE(note) selection.append(note->selectedNotes()); if (!selection.firstChild) return nullptr; for (NoteSelection *node = selection.firstChild; node; node = node->next) node->parent = nullptr; // If the top-most groups are columns, export only children of those groups // (because user is not aware that columns are groups, and don't care: it's not what she want): if (selection.firstChild->note->isColumn()) { NoteSelection tmpSelection; NoteSelection *nextNode; NoteSelection *nextSubNode; for (NoteSelection *node = selection.firstChild; node; node = nextNode) { nextNode = node->next; if (node->note->isColumn()) { for (NoteSelection *subNode = node->firstChild; subNode; subNode = nextSubNode) { nextSubNode = subNode->next; tmpSelection.append(subNode); subNode->parent = nullptr; subNode->next = nullptr; } } else { tmpSelection.append(node); node->parent = nullptr; node->next = nullptr; } } // debugSel(tmpSelection.firstChild); return tmpSelection.firstChild; } else { // debugSel(selection.firstChild); return selection.firstChild; } } void BasketScene::showEditedNoteWhileFiltering() { if (m_editor) { Note *note = m_editor->note(); filterAgain(); note->setSelected(true); relayoutNotes(); note->setX(note->x()); note->setY(note->y()); filterAgainDelayed(); } } void BasketScene::noteEdit(Note *note, bool justAdded, const QPointF &clickedPoint) // TODO: Remove the first parameter!!! { if (!note) note = theSelectedNote(); // TODO: Or pick the focused note! if (!note) return; if (isDuringEdit()) { closeEditor(); // Validate the noteeditors in QLineEdit that does not intercept Enter key press (and edit is triggered with Enter too... Can conflict) return; } if (note != m_focusedNote) { setFocusedNote(note); m_startOfShiftSelectionNote = note; } if (justAdded && isFiltering()) { QTimer::singleShot(0, this, SLOT(showEditedNoteWhileFiltering())); } doHoverEffects(note, Note::Content); // Be sure (in the case Edit was triggered by menu or Enter key...): better feedback! NoteEditor *editor = NoteEditor::editNoteContent(note->content(), nullptr); if (editor->graphicsWidget()) { m_editor = editor; addItem(m_editor->graphicsWidget()); placeEditorAndEnsureVisible(); // placeEditor(); // FIXME: After? m_redirectEditActions = m_editor->lineEdit() || m_editor->textEdit(); if (m_redirectEditActions) { // In case there is NO text, "Select All" is disabled. But if the user press a key the there is now a text: // selection has not changed but "Select All" should be re-enabled: m_editor->connectActions(this); } m_editor->graphicsWidget()->setFocus(); connect(m_editor, SIGNAL(askValidation()), this, SLOT(closeEditorDelayed())); connect(m_editor, SIGNAL(mouseEnteredEditorWidget()), this, SLOT(mouseEnteredEditorWidget())); if (clickedPoint != QPoint()) { m_editor->setCursorTo(clickedPoint); updateEditorAppearance(); } // qApp->processEvents(); // Show the editor toolbar before ensuring the note is visible ensureNoteVisible(note); // because toolbar can create a new line and then partially hide the note m_editor->graphicsWidget()->setFocus(); // When clicking in the basket, a QTimer::singleShot(0, ...) focus the basket! So we focus the widget after qApp->processEvents() emit resetStatusBarText(); // Display "Editing. ... to validate." } else { // Delete the note user have canceled the addition: if ((justAdded && editor->canceled()) || editor->isEmpty() /*) && editor->note()->states().count() <= 0*/) { focusANonSelectedNoteAboveOrThenBelow(); editor->note()->setSelected(true); editor->note()->deleteSelectedNotes(); if (m_hoveredNote == editor->note()) m_hoveredNote = nullptr; if (m_focusedNote == editor->note()) m_focusedNote = nullptr; delete editor->note(); save(); } editor->deleteLater(); unlockHovering(); filterAgain(); unselectAll(); } // Must set focus to the editor, otherwise edit cursor is not seen and precomposed characters cannot be entered if (m_editor != nullptr && m_editor->textEdit() != nullptr) m_editor->textEdit()->setFocus(); Global::bnpView->m_actEditNote->setEnabled(false); } void BasketScene::noteDelete() { if (redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->textCursor().deleteChar(); else if (m_editor->lineEdit()) m_editor->lineEdit()->del(); return; } if (countSelecteds() <= 0) return; int really = KMessageBox::Yes; if (Settings::confirmNoteDeletion()) really = KMessageBox::questionYesNo(m_view, i18np("Do you really want to delete this note?", "Do you really want to delete these %1 notes?", countSelecteds()), i18np("Delete Note", "Delete Notes", countSelecteds()), KStandardGuiItem::del(), KStandardGuiItem::cancel()); if (really == KMessageBox::No) return; noteDeleteWithoutConfirmation(); } void BasketScene::focusANonSelectedNoteBelow(bool inSameColumn) { // First focus another unselected one below it...: if (m_focusedNote && m_focusedNote->isSelected()) { Note *next = m_focusedNote->nextShownInStack(); while (next && next->isSelected()) next = next->nextShownInStack(); if (next) { if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == next->parentPrimaryNote()) { setFocusedNote(next); m_startOfShiftSelectionNote = next; } } } } void BasketScene::focusANonSelectedNoteAbove(bool inSameColumn) { // ... Or above it: if (m_focusedNote && m_focusedNote->isSelected()) { Note *prev = m_focusedNote->prevShownInStack(); while (prev && prev->isSelected()) prev = prev->prevShownInStack(); if (prev) { if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == prev->parentPrimaryNote()) { setFocusedNote(prev); m_startOfShiftSelectionNote = prev; } } } } void BasketScene::focusANonSelectedNoteBelowOrThenAbove() { focusANonSelectedNoteBelow(/*inSameColumn=*/true); focusANonSelectedNoteAbove(/*inSameColumn=*/true); focusANonSelectedNoteBelow(/*inSameColumn=*/false); focusANonSelectedNoteAbove(/*inSameColumn=*/false); } void BasketScene::focusANonSelectedNoteAboveOrThenBelow() { focusANonSelectedNoteAbove(/*inSameColumn=*/true); focusANonSelectedNoteBelow(/*inSameColumn=*/true); focusANonSelectedNoteAbove(/*inSameColumn=*/false); focusANonSelectedNoteBelow(/*inSameColumn=*/false); } void BasketScene::noteDeleteWithoutConfirmation(bool deleteFilesToo) { // If the currently focused note is selected, it will be deleted. focusANonSelectedNoteBelowOrThenAbove(); // Do the deletion: Note *note = firstNote(); Note *next; while (note) { next = note->next(); // If we delete 'note' on the next line, note->next() will be 0! note->deleteSelectedNotes(deleteFilesToo, &m_notesToBeDeleted); note = next; } if (!m_notesToBeDeleted.isEmpty()) { doCleanUp(); } relayoutNotes(); // FIXME: filterAgain()? save(); } void BasketScene::doCopy(CopyMode copyMode) { QClipboard *cb = QApplication::clipboard(); QClipboard::Mode mode = ((copyMode == CopyToSelection) ? QClipboard::Selection : QClipboard::Clipboard); NoteSelection *selection = selectedNotes(); int countCopied = countSelecteds(); if (selection->firstStacked()) { QDrag *d = NoteDrag::dragObject(selection, copyMode == CutToClipboard, /*source=*/nullptr); // d will be deleted by QT // /*bool shouldRemove = */d->drag(); // delete selection; cb->setMimeData(d->mimeData(), mode); // NoteMultipleDrag will be deleted by QT // if (copyMode == CutToClipboard && !note->useFile()) // If useFile(), NoteDrag::dragObject() will delete it TODO // note->slotDelete(); if (copyMode == CutToClipboard) { noteDeleteWithoutConfirmation(/*deleteFilesToo=*/false); focusANote(); } switch (copyMode) { default: case CopyToClipboard: emit postMessage(i18np("Copied note to clipboard.", "Copied notes to clipboard.", countCopied)); break; case CutToClipboard: emit postMessage(i18np("Cut note to clipboard.", "Cut notes to clipboard.", countCopied)); break; case CopyToSelection: emit postMessage(i18np("Copied note to selection.", "Copied notes to selection.", countCopied)); break; } } } void BasketScene::noteCopy() { if (redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->copy(); else if (m_editor->lineEdit()) m_editor->lineEdit()->copy(); } else doCopy(CopyToClipboard); } void BasketScene::noteCut() { if (redirectEditActions()) { if (m_editor->textEdit()) m_editor->textEdit()->cut(); else if (m_editor->lineEdit()) m_editor->lineEdit()->cut(); } else doCopy(CutToClipboard); } void BasketScene::noteOpen(Note *note) { /* GetSelectedNotes NoSelectedNote || Count == 0 ? return AllTheSameType ? Get { url, message(count) } */ // TODO: Open ALL selected notes! if (!note) note = theSelectedNote(); if (!note) return; QUrl url = note->content()->urlToOpen(/*with=*/false); QString message = note->content()->messageWhenOpening(NoteContent::OpenOne /*NoteContent::OpenSeveral*/); if (url.isEmpty()) { if (message.isEmpty()) emit postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); else { int result = KMessageBox::warningContinueCancel(m_view, message, /*caption=*/QString(), KGuiItem(i18n("&Edit"), "edit")); if (result == KMessageBox::Continue) noteEdit(note); } } else { emit postMessage(message); // "Opening link target..." / "Launching application..." / "Opening note file..." // Finally do the opening job: QString customCommand = note->content()->customOpenCommand(); if (url.url().startsWith("basket://")) { emit crossReference(url.url()); } else if (customCommand.isEmpty()) { KRun *run = new KRun(url, m_view->window()); run->setAutoDelete(true); } else { QList urls {url}; KRun::run(customCommand, urls, m_view->window()); } } } /** Code from bool KRun::displayOpenWithDialog(const KUrl::List& lst, bool tempFiles) * It does not allow to set a text, so I ripped it to do that: */ bool KRun__displayOpenWithDialog(const QList &lst, QWidget *window, bool tempFiles, const QString &text) { if (qApp && !KAuthorized::authorizeAction("openwith")) { KMessageBox::sorry(window, i18n("You are not authorized to open this file.")); // TODO: Better message, i18n freeze :-( return false; } KOpenWithDialog l(lst, text, QString(), nullptr); if (l.exec()) { KService::Ptr service = l.service(); if (!!service) return KRun::runApplication(*service, lst, window, tempFiles ? KRun::DeleteTemporaryFiles : KRun::RunFlags()); // qDebug(250) << "No service set, running " << l.text() << endl; return KRun::run(l.text(), lst, window); // TODO handle tempFiles } return false; } void BasketScene::noteOpenWith(Note *note) { if (!note) note = theSelectedNote(); if (!note) return; QUrl url = note->content()->urlToOpen(/*with=*/true); QString message = note->content()->messageWhenOpening(NoteContent::OpenOneWith /*NoteContent::OpenSeveralWith*/); QString text = note->content()->messageWhenOpening(NoteContent::OpenOneWithDialog /*NoteContent::OpenSeveralWithDialog*/); if (url.isEmpty()) { emit postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); } else { QList urls {url}; if (KRun__displayOpenWithDialog(urls, m_view->window(), false, text)) emit postMessage(message); // "Opening link target with..." / "Opening note file with..." } } void BasketScene::noteSaveAs() { // if (!note) // note = theSelectedNote(); Note *note = theSelectedNote(); if (!note) return; QUrl url = note->content()->urlToOpen(/*with=*/false); if (url.isEmpty()) return; QString fileName = QFileDialog::getSaveFileName(m_view, i18n("Save to File"), url.fileName(), note->content()->saveAsFilters()); // TODO: Ask to overwrite ! if (fileName.isEmpty()) return; // TODO: Convert format, etc. (use NoteContent::saveAs(fileName)) KIO::copy(url, QUrl::fromLocalFile(fileName)); } Note *BasketScene::selectedGroup() { FOR_EACH_NOTE(note) { Note *selectedGroup = note->selectedGroup(); if (selectedGroup) { // If the selected group is one group in a column, then return that group, and not the column, // because the column is not ungrouppage, and the Ungroup action would be disabled. if (selectedGroup->isColumn() && selectedGroup->firstChild() && !selectedGroup->firstChild()->next()) { return selectedGroup->firstChild(); } return selectedGroup; } } return nullptr; } bool BasketScene::selectionIsOneGroup() { return (selectedGroup() != nullptr); } Note *BasketScene::firstSelected() { Note *first = nullptr; FOR_EACH_NOTE(note) { first = note->firstSelected(); if (first) return first; } return nullptr; } Note *BasketScene::lastSelected() { Note *last = nullptr, *tmp = nullptr; FOR_EACH_NOTE(note) { tmp = note->lastSelected(); if (tmp) last = tmp; } return last; } bool BasketScene::convertTexts() { m_watcher->stopScan(); bool convertedNotes = false; if (!isLoaded()) load(); FOR_EACH_NOTE(note) if (note->convertTexts()) convertedNotes = true; if (convertedNotes) save(); m_watcher->startScan(); return convertedNotes; } void BasketScene::noteGroup() { /* // Nothing to do? if (isLocked() || countSelecteds() <= 1) return; // If every selected notes are ALREADY in one group, then don't touch anything: Note *selectedGroup = this->selectedGroup(); if (selectedGroup && !selectedGroup->isColumn()) return; */ // Copied from BNPView::updateNotesActions() bool severalSelected = countSelecteds() >= 2; Note *selectedGroup = (severalSelected ? this->selectedGroup() : nullptr); bool enabled = !isLocked() && severalSelected && (!selectedGroup || selectedGroup->isColumn()); if (!enabled) return; // Get the first selected note: we will group selected items just before: Note *first = firstSelected(); // if (selectedGroup != 0 || first == 0) // return; m_loaded = false; // Hack to avoid notes to be unselected and new notes to be selected: // Create and insert the receiving group: Note *group = new Note(this); if (first->isFree()) { insertNote(group, nullptr, Note::BottomColumn, QPointF(first->x(), first->y()), /*animateNewPosition=*/false); } else { insertNote(group, first, Note::TopInsert, QPointF(), /*animateNewPosition=*/false); } // Put a FAKE UNSELECTED note in the new group, so if the new group is inside an allSelected() group, the parent group is not moved inside the new group! Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); insertNote(fakeNote, group, Note::BottomColumn, QPointF(), /*animateNewPosition=*/false); // Group the notes: Note *nextNote; Note *note = firstNote(); while (note) { nextNote = note->next(); note->groupIn(group); note = nextNote; } m_loaded = true; // Part 2 / 2 of the workaround! // Do cleanup: unplugNote(fakeNote); delete fakeNote; unselectAll(); group->setSelectedRecursively(true); // Notes were unselected by unplugging relayoutNotes(); save(); } void BasketScene::noteUngroup() { Note *group = selectedGroup(); if (group && !group->isColumn()) { ungroupNote(group); relayoutNotes(); } save(); } void BasketScene::unplugSelection(NoteSelection *selection) { for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { unplugNote(toUnplug->note); } } void BasketScene::insertSelection(NoteSelection *selection, Note *after) { for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { if (toUnplug->note->isGroup()) { Note *group = new Note(this); insertNote(group, after, Note::BottomInsert, QPointF(), /*animateNewPosition=*/false); Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); insertNote(fakeNote, group, Note::BottomColumn, QPointF(), /*animateNewPosition=*/false); insertSelection(toUnplug->firstChild, fakeNote); unplugNote(fakeNote); delete fakeNote; after = group; } else { Note *note = toUnplug->note; note->setPrev(nullptr); note->setNext(nullptr); insertNote(note, after, Note::BottomInsert, QPointF(), /*animateNewPosition=*/true); after = note; } } } void BasketScene::selectSelection(NoteSelection *selection) { for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { if (toUnplug->note->isGroup()) selectSelection(toUnplug); else toUnplug->note->setSelected(true); } } void BasketScene::noteMoveOnTop() { // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket // TODO: Move on top/bottom... of the column or basjet NoteSelection *selection = selectedNotes(); unplugSelection(selection); // Replug the notes: Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); if (isColumnsLayout()) { if (firstNote()->firstChild()) insertNote(fakeNote, firstNote()->firstChild(), Note::TopInsert, QPointF(), /*animateNewPosition=*/false); else insertNote(fakeNote, firstNote(), Note::BottomColumn, QPointF(), /*animateNewPosition=*/false); } else { // TODO: Also allow to move notes on top of a group!!!!!!! insertNote(fakeNote, nullptr, Note::BottomInsert, QPointF(0, 0), /*animateNewPosition=*/false); } insertSelection(selection, fakeNote); unplugNote(fakeNote); delete fakeNote; selectSelection(selection); relayoutNotes(); save(); } void BasketScene::noteMoveOnBottom() { // TODO: Duplicate code: void noteMoveOn(); // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket // TODO: Move on top/bottom... of the column or basjet NoteSelection *selection = selectedNotes(); unplugSelection(selection); // Replug the notes: Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); if (isColumnsLayout()) insertNote(fakeNote, firstNote(), Note::BottomColumn, QPointF(), /*animateNewPosition=*/false); else { // TODO: Also allow to move notes on top of a group!!!!!!! insertNote(fakeNote, nullptr, Note::BottomInsert, QPointF(0, 0), /*animateNewPosition=*/false); } insertSelection(selection, fakeNote); unplugNote(fakeNote); delete fakeNote; selectSelection(selection); relayoutNotes(); save(); } void BasketScene::moveSelectionTo(Note *here, bool below /* = true*/) { NoteSelection *selection = selectedNotes(); unplugSelection(selection); // Replug the notes: Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); // if (isColumnsLayout()) insertNote(fakeNote, here, (below ? Note::BottomInsert : Note::TopInsert), QPointF(), /*animateNewPosition=*/false); // else { // // TODO: Also allow to move notes on top of a group!!!!!!! // insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); // } insertSelection(selection, fakeNote); unplugNote(fakeNote); delete fakeNote; selectSelection(selection); relayoutNotes(); save(); } void BasketScene::noteMoveNoteUp() { // TODO: Move between columns, even if they are empty !!!!!!! // TODO: if first note of a group, move just above the group! And let that even if there is no note before that group!!! Note *first = firstSelected(); Note *previous = first->prevShownInStack(); if (previous) moveSelectionTo(previous, /*below=*/false); } void BasketScene::noteMoveNoteDown() { Note *first = lastSelected(); Note *next = first->nextShownInStack(); if (next) moveSelectionTo(next, /*below=*/true); } void BasketScene::wheelEvent(QGraphicsSceneWheelEvent *event) { // Q3ScrollView::wheelEvent(event); QGraphicsScene::wheelEvent(event); } void BasketScene::linkLookChanged() { Note *note = m_firstNote; while (note) { note->linkLookChanged(); note = note->next(); } relayoutNotes(); } void BasketScene::slotCopyingDone2(KIO::Job *job, const QUrl & /*from*/, const QUrl &to) { if (job->error()) { DEBUG_WIN << "Copy finished, ERROR"; return; } Note *note = noteForFullPath(to.path()); DEBUG_WIN << "Copy finished, load note: " + to.path() + (note ? QString() : " --- NO CORRESPONDING NOTE"); if (note != nullptr) { note->content()->loadFromFile(/*lazyLoad=*/false); if (isEncrypted()) note->content()->saveToFile(); if (m_focusedNote == note) // When inserting a new note we ensure it visible ensureNoteVisible(note); // But after loading it has certainly grown and if it was } // on bottom of the basket it's not visible entirely anymore } Note *BasketScene::noteForFullPath(const QString &path) { Note *note = firstNote(); Note *found; while (note) { found = note->noteForFullPath(path); if (found) return found; note = note->next(); } return nullptr; } void BasketScene::deleteFiles() { m_watcher->stopScan(); Tools::deleteRecursively(fullPath()); } QList BasketScene::usedStates() { QList states; FOR_EACH_NOTE(note) note->usedStates(states); return states; } void BasketScene::listUsedTags(QList &list) { if (!isLoaded()) { load(); } FOR_EACH_NOTE(child) child->listUsedTags(list); } /** Unfocus the previously focused note (unless it was null) * and focus the new @param note (unless it is null) if hasFocus() * Update m_focusedNote to the new one */ void BasketScene::setFocusedNote(Note *note) // void BasketScene::changeFocusTo(Note *note) { // Don't focus an hidden note: if (note != nullptr && !note->isShown()) return; // When clicking a group, this group gets focused. But only content-based notes should be focused: if (note && note->isGroup()) note = note->firstRealChild(); // The first time a note is focused, it becomes the start of the Shift selection: if (m_startOfShiftSelectionNote == nullptr) m_startOfShiftSelectionNote = note; // Unfocus the old focused note: if (m_focusedNote != nullptr) m_focusedNote->setFocused(false); // Notify the new one to draw a focus rectangle... only if the basket is focused: if (hasFocus() && note != nullptr) note->setFocused(true); // Save the new focused note: m_focusedNote = note; } /** If no shown note is currently focused, try to find a shown note and focus it * Also update m_focusedNote to the new one (or null if there isn't) */ void BasketScene::focusANote() { if (countFounds() == 0) { // No note to focus setFocusedNote(nullptr); // m_startOfShiftSelectionNote = 0; return; } if (m_focusedNote == nullptr) { // No focused note yet : focus the first shown Note *toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); setFocusedNote(toFocus); // m_startOfShiftSelectionNote = m_focusedNote; return; } // Search a visible note to focus if the focused one isn't shown : Note *toFocus = m_focusedNote; if (toFocus && !toFocus->isShown()) toFocus = toFocus->nextShownInStack(); if (!toFocus && m_focusedNote) toFocus = m_focusedNote->prevShownInStack(); setFocusedNote(toFocus); // m_startOfShiftSelectionNote = toFocus; } Note *BasketScene::firstNoteInStack() { if (!firstNote()) return nullptr; if (firstNote()->content()) return firstNote(); else return firstNote()->nextInStack(); } Note *BasketScene::lastNoteInStack() { Note *note = lastNote(); while (note) { if (note->content()) return note; Note *possibleNote = note->lastRealChild(); if (possibleNote && possibleNote->content()) return possibleNote; note = note->prev(); } return nullptr; } Note *BasketScene::firstNoteShownInStack() { Note *first = firstNoteInStack(); while (first && !first->isShown()) first = first->nextInStack(); return first; } Note *BasketScene::lastNoteShownInStack() { Note *last = lastNoteInStack(); while (last && !last->isShown()) last = last->prevInStack(); return last; } Note *BasketScene::noteOn(NoteOn side) { Note *bestNote = nullptr; int distance = -1; // int bestDistance = contentsWidth() * contentsHeight() * 10; int bestDistance = sceneRect().width() * sceneRect().height() * 10; Note *note = firstNoteShownInStack(); Note *primary = m_focusedNote->parentPrimaryNote(); while (note) { switch (side) { case LEFT_SIDE: distance = m_focusedNote->distanceOnLeftRight(note, LEFT_SIDE); break; case RIGHT_SIDE: distance = m_focusedNote->distanceOnLeftRight(note, RIGHT_SIDE); break; case TOP_SIDE: distance = m_focusedNote->distanceOnTopBottom(note, TOP_SIDE); break; case BOTTOM_SIDE: distance = m_focusedNote->distanceOnTopBottom(note, BOTTOM_SIDE); break; } if ((side == TOP_SIDE || side == BOTTOM_SIDE || primary != note->parentPrimaryNote()) && note != m_focusedNote && distance > 0 && distance < bestDistance) { bestNote = note; bestDistance = distance; } note = note->nextShownInStack(); } return bestNote; } Note *BasketScene::firstNoteInGroup() { Note *child = m_focusedNote; Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); while (parent) { if (parent->firstChild() != child && !parent->isColumn()) return parent->firstRealChild(); child = parent; parent = parent->parentNote(); } return nullptr; } Note *BasketScene::noteOnHome() { // First try to find the first note of the group containing the focused note: Note *child = m_focusedNote; Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); while (parent) { if (parent->nextShownInStack() != m_focusedNote) return parent->nextShownInStack(); child = parent; parent = parent->parentNote(); } // If it was not found, then focus the very first note in the basket: if (isFreeLayout()) { Note *first = firstNoteShownInStack(); // The effective first note found Note *note = first; // The current note, to compare with the previous first note, if this new note is more on top if (note) note = note->nextShownInStack(); while (note) { if (note->y() < first->y() || (note->y() == first->y() && note->x() < first->x())) first = note; note = note->nextShownInStack(); } return first; } else return firstNoteShownInStack(); } Note *BasketScene::noteOnEnd() { Note *child = m_focusedNote; Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : nullptr); Note *lastChild; while (parent) { lastChild = parent->lastRealChild(); if (lastChild && lastChild != m_focusedNote) { if (lastChild->isShown()) return lastChild; lastChild = lastChild->prevShownInStack(); if (lastChild && lastChild->isShown() && lastChild != m_focusedNote) return lastChild; } child = parent; parent = parent->parentNote(); } if (isFreeLayout()) { Note *last; Note *note; last = note = firstNoteShownInStack(); note = note->nextShownInStack(); while (note) { if (note->bottom() > last->bottom() || (note->bottom() == last->bottom() && note->x() > last->x())) last = note; note = note->nextShownInStack(); } return last; } else return lastNoteShownInStack(); } void BasketScene::keyPressEvent(QKeyEvent *event) { if (isDuringEdit()) { QGraphicsScene::keyPressEvent(event); /*if( event->key() == Qt::Key_Return ) { m_editor->graphicsWidget()->setFocus(); } else if( event->key() == Qt::Key_Escape) { closeEditor(); }*/ event->accept(); return; } if (event->key() == Qt::Key_Escape) { if (decoration()->filterData().isFiltering) decoration()->filterBar()->reset(); else unselectAll(); } if (countFounds() == 0) return; if (!m_focusedNote) return; Note *toFocus = nullptr; switch (event->key()) { case Qt::Key_Down: toFocus = (isFreeLayout() ? noteOn(BOTTOM_SIDE) : m_focusedNote->nextShownInStack()); if (toFocus) break; // scrollBy(0, 30); // This cases do not move focus to another note... return; case Qt::Key_Up: toFocus = (isFreeLayout() ? noteOn(TOP_SIDE) : m_focusedNote->prevShownInStack()); if (toFocus) break; // scrollBy(0, -30); // This cases do not move focus to another note... return; case Qt::Key_PageDown: if (isFreeLayout()) { Note *lastFocused = m_focusedNote; for (int i = 0; i < 10 && m_focusedNote; ++i) m_focusedNote = noteOn(BOTTOM_SIDE); toFocus = m_focusedNote; m_focusedNote = lastFocused; } else { toFocus = m_focusedNote; for (int i = 0; i < 10 && toFocus; ++i) toFocus = toFocus->nextShownInStack(); } if (toFocus == nullptr) toFocus = (isFreeLayout() ? noteOnEnd() : lastNoteShownInStack()); if (toFocus && toFocus != m_focusedNote) break; // scrollBy(0, visibleHeight() / 2); // This cases do not move focus to another note... // scrollBy(0, viewport()->height() / 2); // This cases do not move focus to another note... return; case Qt::Key_PageUp: if (isFreeLayout()) { Note *lastFocused = m_focusedNote; for (int i = 0; i < 10 && m_focusedNote; ++i) m_focusedNote = noteOn(TOP_SIDE); toFocus = m_focusedNote; m_focusedNote = lastFocused; } else { toFocus = m_focusedNote; for (int i = 0; i < 10 && toFocus; ++i) toFocus = toFocus->prevShownInStack(); } if (toFocus == nullptr) toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); if (toFocus && toFocus != m_focusedNote) break; // scrollBy(0, - visibleHeight() / 2); // This cases do not move focus to another note... // scrollBy(0, - viewport()->height() / 2); // This cases do not move focus to another note... return; case Qt::Key_Home: toFocus = noteOnHome(); break; case Qt::Key_End: toFocus = noteOnEnd(); break; case Qt::Key_Left: if (m_focusedNote->tryFoldParent()) return; if ((toFocus = noteOn(LEFT_SIDE))) break; if ((toFocus = firstNoteInGroup())) break; // scrollBy(-30, 0); // This cases do not move focus to another note... return; case Qt::Key_Right: if (m_focusedNote->tryExpandParent()) return; if ((toFocus = noteOn(RIGHT_SIDE))) break; // scrollBy(30, 0); // This cases do not move focus to another note... return; case Qt::Key_Space: // This case do not move focus to another note... if (m_focusedNote) { m_focusedNote->setSelected(!m_focusedNote->isSelected()); event->accept(); } else event->ignore(); return; // ... so we return after the process default: return; } if (toFocus == nullptr) { // If no direction keys have been pressed OR reached out the begin or end event->ignore(); // Important !! return; } if (event->modifiers() & Qt::ShiftModifier) { // Shift+arrowKeys selection if (m_startOfShiftSelectionNote == nullptr) m_startOfShiftSelectionNote = toFocus; ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! selectRange(m_startOfShiftSelectionNote, toFocus); setFocusedNote(toFocus); event->accept(); return; } else /*if (toFocus != m_focusedNote)*/ { // Move focus to ANOTHER note... ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! setFocusedNote(toFocus); m_startOfShiftSelectionNote = toFocus; if (!(event->modifiers() & Qt::ControlModifier)) // ... select only current note if Control unselectAllBut(m_focusedNote); event->accept(); return; } event->ignore(); // Important !! } /** Select a range of notes and deselect the others. * The order between start and end has no importance (end could be before start) */ void BasketScene::selectRange(Note *start, Note *end, bool unselectOthers /*= true*/) { Note *cur; Note *realEnd = nullptr; // Avoid crash when start (or end) is null if (start == nullptr) start = end; else if (end == nullptr) end = start; // And if *both* are null if (start == nullptr) { if (unselectOthers) unselectAll(); return; } // In case there is only one note to select if (start == end) { if (unselectOthers) unselectAllBut(start); else start->setSelected(true); return; } // Free layout baskets should select range as if we were drawing a rectangle between start and end: if (isFreeLayout()) { QRectF startRect(start->x(), start->y(), start->width(), start->height()); QRectF endRect(end->x(), end->y(), end->width(), end->height()); QRectF toSelect = startRect.united(endRect); selectNotesIn(toSelect, /*invertSelection=*/false, unselectOthers); return; } // Search the REAL first (and deselect the others before it) : for (cur = firstNoteInStack(); cur != nullptr; cur = cur->nextInStack()) { if (cur == start || cur == end) break; if (unselectOthers) cur->setSelected(false); } // Select the notes after REAL start, until REAL end : if (cur == start) realEnd = end; else if (cur == end) realEnd = start; for (/*cur = cur*/; cur != nullptr; cur = cur->nextInStack()) { cur->setSelected(cur->isShown()); // Select all notes in the range, but only if they are shown if (cur == realEnd) break; } if (!unselectOthers) return; // Deselect the remaining notes : if (cur) cur = cur->nextInStack(); for (/*cur = cur*/; cur != nullptr; cur = cur->nextInStack()) cur->setSelected(false); } void BasketScene::focusInEvent(QFocusEvent *event) { // Focus cannot be get with Tab when locked, but a click can focus the basket! if (isLocked()) { if (m_button) { QGraphicsScene::focusInEvent(event); QTimer::singleShot(0, m_button, SLOT(setFocus())); } } else { QGraphicsScene::focusInEvent(event); focusANote(); // hasFocus() is true at this stage, note will be focused } } void BasketScene::focusOutEvent(QFocusEvent *) { if (m_focusedNote != nullptr) m_focusedNote->setFocused(false); } void BasketScene::ensureNoteVisible(Note *note) { if (!note->isShown()) // Logical! return; if (note == editedNote()) // HACK: When filtering while editing big notes, etc... cause unwanted scrolls return; m_view->ensureVisible(note); /*// int bottom = note->y() + qMin(note->height(), visibleHeight()); // int finalRight = note->x() + qMin(note->width() + (note->hasResizer() ? Note::RESIZER_WIDTH : 0), visibleWidth()); qreal bottom = note->y() + qMin(note->height(), (qreal)m_view->viewport()->height()); qreal finalRight = note->x() + qMin(note->width() + (note->hasResizer() ? Note::RESIZER_WIDTH : 0), (qreal)m_view->viewport()->width()); m_view->ensureVisible(finalRight, bottom, 0, 0); m_view->ensureVisible(note->x(), note->y(), 0, 0);*/ } void BasketScene::addWatchedFile(const QString &fullPath) { // DEBUG_WIN << "Watcher>Add Monitoring Of : " + fullPath + ""; m_watcher->addFile(fullPath); } void BasketScene::removeWatchedFile(const QString &fullPath) { // DEBUG_WIN << "Watcher>Remove Monitoring Of : " + fullPath + ""; m_watcher->removeFile(fullPath); } void BasketScene::watchedFileModified(const QString &fullPath) { if (!m_modifiedFiles.contains(fullPath)) m_modifiedFiles.append(fullPath); // If a big file is saved by an application, notifications are send several times. // We wait they are not sent anymore to consider the file complete! m_watcherTimer.setSingleShot(true); m_watcherTimer.start(200); DEBUG_WIN << "Watcher>Modified : " + fullPath + ""; } void BasketScene::watchedFileDeleted(const QString &fullPath) { Note *note = noteForFullPath(fullPath); removeWatchedFile(fullPath); if (note) { NoteSelection *selection = selectedNotes(); unselectAllBut(note); noteDeleteWithoutConfirmation(); while (selection) { selection->note->setSelected(true); selection = selection->nextStacked(); } } DEBUG_WIN << "Watcher>Removed : " + fullPath + ""; } void BasketScene::updateModifiedNotes() { for (QList::iterator it = m_modifiedFiles.begin(); it != m_modifiedFiles.end(); ++it) { Note *note = noteForFullPath(*it); if (note) note->content()->loadFromFile(/*lazyLoad=*/false); } m_modifiedFiles.clear(); } bool BasketScene::setProtection(int type, QString key) { #ifdef HAVE_LIBGPGME if (type == PasswordEncryption || // Ask a new password m_encryptionType != type || m_encryptionKey != key) { int savedType = m_encryptionType; QString savedKey = m_encryptionKey; m_encryptionType = type; m_encryptionKey = key; m_gpg->clearCache(); if (saveAgain()) { emit propertiesChanged(this); } else { m_encryptionType = savedType; m_encryptionKey = savedKey; m_gpg->clearCache(); return false; } } return true; #else m_encryptionType = type; m_encryptionKey = key; return false; #endif } bool BasketScene::saveAgain() { bool result = false; m_watcher->stopScan(); // Re-encrypt basket file: result = save(); // Re-encrypt every note files recursively: if (result) { FOR_EACH_NOTE(note) { result = note->saveAgain(); if (!result) break; } } m_watcher->startScan(); return result; } bool BasketScene::loadFromFile(const QString &fullPath, QString *string) { QByteArray array; if (loadFromFile(fullPath, &array)) { *string = QString::fromUtf8(array.data(), array.size()); return true; } else return false; } bool BasketScene::isEncrypted() { return (m_encryptionType != NoEncryption); } bool BasketScene::isFileEncrypted() { QFile file(fullPath() + ".basket"); if (file.open(QIODevice::ReadOnly)) { // Should be ASCII anyways QString line = file.readLine(32); if (line.startsWith("-----BEGIN PGP MESSAGE-----")) return true; } return false; } bool BasketScene::loadFromFile(const QString &fullPath, QByteArray *array) { QFile file(fullPath); bool encrypted = false; if (file.open(QIODevice::ReadOnly)) { *array = file.readAll(); QByteArray magic = "-----BEGIN PGP MESSAGE-----"; int i = 0; if (array->size() > magic.size()) for (i = 0; array->at(i) == magic[i]; ++i) ; if (i == magic.size()) { encrypted = true; } file.close(); #ifdef HAVE_LIBGPGME if (encrypted) { QByteArray tmp(*array); tmp.detach(); // Only use gpg-agent for private key encryption since it doesn't // cache password used in symmetric encryption. m_gpg->setUseGnuPGAgent(Settings::useGnuPGAgent() && m_encryptionType == PrivateKeyEncryption); if (m_encryptionType == PrivateKeyEncryption) m_gpg->setText(i18n("Please enter the password for the following private key:"), false); else m_gpg->setText(i18n("Please enter the password for the basket %1:", basketName()), false); // Used when decrypting return m_gpg->decrypt(tmp, array); } #else if (encrypted) { return false; } #endif return true; } else return false; } bool BasketScene::saveToFile(const QString &fullPath, const QString &string) { QByteArray array = string.toUtf8(); return saveToFile(fullPath, array); } bool BasketScene::saveToFile(const QString &fullPath, const QByteArray &array) { ulong length = array.size(); bool success = true; QByteArray tmp; #ifdef HAVE_LIBGPGME if (isEncrypted()) { QString key; // We only use gpg-agent for private key encryption and saving without // public key doesn't need one. m_gpg->setUseGnuPGAgent(false); if (m_encryptionType == PrivateKeyEncryption) { key = m_encryptionKey; // public key doesn't need password m_gpg->setText(QString(), false); } else m_gpg->setText(i18n("Please assign a password to the basket %1:", basketName()), true); // Used when defining a new password success = m_gpg->encrypt(array, length, &tmp, key); length = tmp.size(); } else tmp = array; #else success = !isEncrypted(); if (success) tmp = array; #endif /*if (success && (success = file.open(QIODevice::WriteOnly))){ success = (file.write(tmp) == (Q_LONG)tmp.size()); file.close(); }*/ if (success) return safelySaveToFile(fullPath, tmp, length); else return false; } /** * A safer version of saveToFile, that doesn't perform encryption. To save a * file owned by a basket (i.e. a basket or a note file), use saveToFile(), but * to save to another file, (e.g. the basket hierarchy), use this function * instead. */ /*static*/ bool BasketScene::safelySaveToFile(const QString &fullPath, const QByteArray &array, unsigned long length) { // Modulus operandi: // 1. Use QSaveFile to try and save the file // 2. Show a modal dialog (with the error) when bad things happen // 3. We keep trying (at increasing intervals, up until every minute) // until we finally save the file. // The error dialog is static to make sure we never show the dialog twice, static DiskErrorDialog *dialog = nullptr; static const uint maxDelay = 60 * 1000; // ms uint retryDelay = 1000; // ms bool success = false; do { QSaveFile saveFile(fullPath); if (saveFile.open(QIODevice::WriteOnly)) { saveFile.write(array, length); if (saveFile.commit()) success = true; } if (!success) { if (!dialog) { dialog = new DiskErrorDialog(i18n("Error while saving"), saveFile.errorString(), qApp->activeWindow()); } if (!dialog->isVisible()) dialog->show(); static const uint sleepDelay = 50; // ms for (uint i = 0; i < retryDelay / sleepDelay; ++i) { qApp->processEvents(); } // Double the retry delay, but don't go over the max. retryDelay = qMin(maxDelay, retryDelay * 2); // ms } } while (!success); if (dialog) dialog->deleteLater(); dialog = nullptr; return true; // Guess we can't really return a fail } /*static*/ bool BasketScene::safelySaveToFile(const QString &fullPath, const QString &string) { QByteArray bytes = string.toUtf8(); return safelySaveToFile(fullPath, bytes, bytes.length()); } void BasketScene::lock() { #ifdef HAVE_LIBGPGME closeEditor(); m_gpg->clearCache(); m_locked = true; enableActions(); deleteNotes(); m_loaded = false; m_loadingLaunched = false; #endif } diff --git a/src/notecontent.cpp b/src/notecontent.cpp index 8399e08..94f27da 100644 --- a/src/notecontent.cpp +++ b/src/notecontent.cpp @@ -1,2836 +1,2539 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "notecontent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //For m_simpleRichText->documentLayout() #include //For QPixmap::createHeuristicMask() #include #include #include #include #include #include #include #include #include #include #include //For KIO::file_preview(...) #include #include #include #include #include "basketscene.h" #include "config.h" #include "debugwindow.h" #include "file_metadata.h" #include "filter.h" #include "global.h" #include "htmlexporter.h" #include "note.h" #include "notefactory.h" #include "settings.h" #include "tools.h" #include "xmlwork.h" /** * LinkDisplayItem definition * */ QRectF LinkDisplayItem::boundingRect() const { if (m_note) { return QRect(0, 0, m_note->width() - m_note->contentX() - Note::NOTE_MARGIN, m_note->height() - 2 * Note::NOTE_MARGIN); } return QRectF(); } void LinkDisplayItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (!m_note) return; QRectF rect = boundingRect(); m_linkDisplay.paint(painter, 0, 0, rect.width(), rect.height(), m_note->palette(), true, m_note->isSelected(), m_note->hovered(), m_note->hovered() && m_note->hoveredZone() == Note::Custom0); } /** class NoteContent: */ const int NoteContent::FEEDBACK_DARKING = 105; NoteContent::NoteContent(Note *parent, const QString &fileName) : m_note(parent) { parent->setContent(this); setFileName(fileName); } void NoteContent::saveToNode(QXmlStreamWriter &stream) { if (useFile()) { stream.writeStartElement("content"); stream.writeCharacters(fileName()); stream.writeEndElement(); } } QRectF NoteContent::zoneRect(int zone, const QPointF & /*pos*/) { if (zone == Note::Content) return QRectF(0, 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else return QRectF(); } QUrl NoteContent::urlToOpen(bool /*with*/) { return (useFile() ? QUrl::fromLocalFile(fullPath()) : QUrl()); } void NoteContent::setFileName(const QString &fileName) { m_fileName = fileName; } bool NoteContent::trySetFileName(const QString &fileName) { if (useFile() && fileName != m_fileName) { QString newFileName = Tools::fileNameForNewFile(fileName, basket()->fullPath()); QDir dir; dir.rename(fullPath(), basket()->fullPathForFileName(newFileName)); return true; } return false; // !useFile() or unsuccessful rename } QString NoteContent::fullPath() { if (note() && useFile()) return note()->fullPath(); else return QString(); } void NoteContent::contentChanged(qreal newMinWidth) { m_minWidth = newMinWidth; if (note()) { // note()->unbufferize(); note()->requestRelayout(); // TODO: It should re-set the width! m_width = 0 ? contentChanged: setWidth, geteight, if size havent changed, only repaint and not relayout } } BasketScene *NoteContent::basket() { if (note()) return note()->basket(); else return nullptr; } void NoteContent::setEdited() { note()->setLastModificationDate(QDateTime::currentDateTime()); basket()->save(); } /** All the Content Classes: */ NoteType::Id TextContent::type() const { return NoteType::Text; } NoteType::Id HtmlContent::type() const { return NoteType::Html; } NoteType::Id ImageContent::type() const { return NoteType::Image; } NoteType::Id AnimationContent::type() const { return NoteType::Animation; } NoteType::Id SoundContent::type() const { return NoteType::Sound; } NoteType::Id FileContent::type() const { return NoteType::File; } NoteType::Id LinkContent::type() const { return NoteType::Link; } NoteType::Id CrossReferenceContent::type() const { return NoteType::CrossReference; } NoteType::Id LauncherContent::type() const { return NoteType::Launcher; } NoteType::Id ColorContent::type() const { return NoteType::Color; } NoteType::Id UnknownContent::type() const { return NoteType::Unknown; } QString TextContent::typeName() const { return i18n("Plain Text"); } QString HtmlContent::typeName() const { return i18n("Text"); } QString ImageContent::typeName() const { return i18n("Image"); } QString AnimationContent::typeName() const { return i18n("Animation"); } QString SoundContent::typeName() const { return i18n("Sound"); } QString FileContent::typeName() const { return i18n("File"); } QString LinkContent::typeName() const { return i18n("Link"); } QString CrossReferenceContent::typeName() const { return i18n("Cross Reference"); } QString LauncherContent::typeName() const { return i18n("Launcher"); } QString ColorContent::typeName() const { return i18n("Color"); } QString UnknownContent::typeName() const { return i18n("Unknown"); } QString TextContent::lowerTypeName() const { return "text"; } QString HtmlContent::lowerTypeName() const { return "html"; } QString ImageContent::lowerTypeName() const { return "image"; } QString AnimationContent::lowerTypeName() const { return "animation"; } QString SoundContent::lowerTypeName() const { return "sound"; } QString FileContent::lowerTypeName() const { return "file"; } QString LinkContent::lowerTypeName() const { return "link"; } QString CrossReferenceContent::lowerTypeName() const { return "cross_reference"; } QString LauncherContent::lowerTypeName() const { return "launcher"; } QString ColorContent::lowerTypeName() const { return "color"; } QString UnknownContent::lowerTypeName() const { return "unknown"; } QString NoteContent::toText(const QString &cuttedFullPath) { return (cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } QString TextContent::toText(const QString & /*cuttedFullPath*/) { return text(); } QString HtmlContent::toText(const QString & /*cuttedFullPath*/) { return Tools::htmlToText(html()); } QString LinkContent::toText(const QString & /*cuttedFullPath*/) { if (autoTitle()) return url().toDisplayString(); else if (title().isEmpty() && url().isEmpty()) return QString(); else if (url().isEmpty()) return title(); else if (title().isEmpty()) return url().toDisplayString(); else return QString("%1 <%2>").arg(title(), url().toDisplayString()); } QString CrossReferenceContent::toText(const QString & /*cuttedFullPath*/) { if (title().isEmpty() && url().isEmpty()) return QString(); else if (url().isEmpty()) return title(); else if (title().isEmpty()) return url().toDisplayString(); else return QString("%1 <%2>").arg(title(), url().toDisplayString()); } QString ColorContent::toText(const QString & /*cuttedFullPath*/) { return color().name(); } QString UnknownContent::toText(const QString & /*cuttedFullPath*/) { return QString(); } // TODO: If imageName.isEmpty() return fullPath() because it's for external use, else return fileName() because it's to display in a tooltip QString TextContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return Tools::textToHTMLWithoutP(text()); } QString HtmlContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { // return Tools::htmlToParagraph(html()); QTextDocument simpleRichText; simpleRichText.setHtml(html()); return Tools::textDocumentToMinimalHTML(&simpleRichText); } QString ImageContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("").arg(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } QString AnimationContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("").arg(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } QString SoundContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("%2").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), fileName()); } // With the icon? QString FileContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("%2").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), fileName()); } // With the icon? QString LinkContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString("%2").arg(url().toDisplayString(), title()); } // With the icon? QString CrossReferenceContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString("%2").arg(url().toDisplayString(), title()); } // With the icon? QString LauncherContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("%2").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), name()); } // With the icon? QString ColorContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString("%2").arg(color().name(), color().name()); } QString UnknownContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString(); } QPixmap ImageContent::toPixmap() { return pixmap(); } QPixmap AnimationContent::toPixmap() { return m_movie->currentPixmap(); } void NoteContent::toLink(QUrl *url, QString *title, const QString &cuttedFullPath) { if (useFile()) { *url = QUrl::fromUserInput(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); *title = (cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } else { *url = QUrl(); title->clear(); } } void LinkContent::toLink(QUrl *url, QString *title, const QString & /*cuttedFullPath*/) { *url = this->url(); *title = this->title(); } void CrossReferenceContent::toLink(QUrl *url, QString *title, const QString & /*cuttedFullPath*/) { *url = this->url(); *title = this->title(); } void LauncherContent::toLink(QUrl *url, QString *title, const QString &cuttedFullPath) { *url = QUrl::fromUserInput(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); *title = name(); } void UnknownContent::toLink(QUrl *url, QString *title, const QString & /*cuttedFullPath*/) { *url = QUrl(); *title = QString(); } bool TextContent::useFile() const { return true; } bool HtmlContent::useFile() const { return true; } bool ImageContent::useFile() const { return true; } bool AnimationContent::useFile() const { return true; } bool SoundContent::useFile() const { return true; } bool FileContent::useFile() const { return true; } bool LinkContent::useFile() const { return false; } bool CrossReferenceContent::useFile() const { return false; } bool LauncherContent::useFile() const { return true; } bool ColorContent::useFile() const { return false; } bool UnknownContent::useFile() const { return true; } bool TextContent::canBeSavedAs() const { return true; } bool HtmlContent::canBeSavedAs() const { return true; } bool ImageContent::canBeSavedAs() const { return true; } bool AnimationContent::canBeSavedAs() const { return true; } bool SoundContent::canBeSavedAs() const { return true; } bool FileContent::canBeSavedAs() const { return true; } bool LinkContent::canBeSavedAs() const { return true; } bool CrossReferenceContent::canBeSavedAs() const { return true; } bool LauncherContent::canBeSavedAs() const { return true; } bool ColorContent::canBeSavedAs() const { return false; } bool UnknownContent::canBeSavedAs() const { return false; } QString TextContent::saveAsFilters() const { return "text/plain"; } QString HtmlContent::saveAsFilters() const { return "text/html"; } QString ImageContent::saveAsFilters() const { return "image/png"; } // TODO: Offer more types QString AnimationContent::saveAsFilters() const { return "image/gif"; } // TODO: MNG... QString SoundContent::saveAsFilters() const { return "audio/mp3 audio/ogg"; } // TODO: OGG... QString FileContent::saveAsFilters() const { return "*"; } // TODO: Get MIME type of the url target QString LinkContent::saveAsFilters() const { return "*"; } // TODO: idem File + If isDir() const: return QString CrossReferenceContent::saveAsFilters() const { return "*"; } // TODO: idem File + If isDir() const: return QString LauncherContent::saveAsFilters() const { return "application/x-desktop"; } QString ColorContent::saveAsFilters() const { return QString(); } QString UnknownContent::saveAsFilters() const { return QString(); } bool TextContent::match(const FilterData &data) { return text().contains(data.string); } bool HtmlContent::match(const FilterData &data) { return m_textEquivalent /*toText(QString())*/.contains(data.string); } // OPTIM_FILTER bool ImageContent::match(const FilterData & /*data*/) { return false; } bool AnimationContent::match(const FilterData & /*data*/) { return false; } bool SoundContent::match(const FilterData &data) { return fileName().contains(data.string); } bool FileContent::match(const FilterData &data) { return fileName().contains(data.string); } bool LinkContent::match(const FilterData &data) { return title().contains(data.string) || url().toDisplayString().contains(data.string); } bool CrossReferenceContent::match(const FilterData &data) { return title().contains(data.string) || url().toDisplayString().contains(data.string); } bool LauncherContent::match(const FilterData &data) { return exec().contains(data.string) || name().contains(data.string); } bool ColorContent::match(const FilterData &data) { return color().name().contains(data.string); } bool UnknownContent::match(const FilterData &data) { return mimeTypes().contains(data.string); } QString TextContent::editToolTipText() const { return i18n("Edit this plain text"); } QString HtmlContent::editToolTipText() const { return i18n("Edit this text"); } QString ImageContent::editToolTipText() const { return i18n("Edit this image"); } QString AnimationContent::editToolTipText() const { return i18n("Edit this animation"); } QString SoundContent::editToolTipText() const { return i18n("Edit the file name of this sound"); } QString FileContent::editToolTipText() const { return i18n("Edit the name of this file"); } QString LinkContent::editToolTipText() const { return i18n("Edit this link"); } QString CrossReferenceContent::editToolTipText() const { return i18n("Edit this cross reference"); } QString LauncherContent::editToolTipText() const { return i18n("Edit this launcher"); } QString ColorContent::editToolTipText() const { return i18n("Edit this color"); } QString UnknownContent::editToolTipText() const { return i18n("Edit this unknown object"); } QString TextContent::cssClass() const { return QString(); } QString HtmlContent::cssClass() const { return QString(); } QString ImageContent::cssClass() const { return QString(); } QString AnimationContent::cssClass() const { return QString(); } QString SoundContent::cssClass() const { return "sound"; } QString FileContent::cssClass() const { return "file"; } QString LinkContent::cssClass() const { return (LinkLook::lookForURL(m_url) == LinkLook::localLinkLook ? "local" : "network"); } QString CrossReferenceContent::cssClass() const { return "cross_reference"; } QString LauncherContent::cssClass() const { return "launcher"; } QString ColorContent::cssClass() const { return QString(); } QString UnknownContent::cssClass() const { return QString(); } void TextContent::fontChanged() { setText(text()); } void HtmlContent::fontChanged() { QTextDocument *richDoc = m_graphicsTextItem.document(); // This check is important when applying style to a note which is not loaded yet. Example: // Filter all -> open some basket for the first time -> close filter: if a note was tagged as TODO, then it would display no text if (!richDoc->isEmpty()) setHtml(Tools::textDocumentToMinimalHTML(richDoc)); } void ImageContent::fontChanged() { setPixmap(pixmap()); } void AnimationContent::fontChanged() { /*startMovie();*/ } void FileContent::fontChanged() { setFileName(fileName()); } void LinkContent::fontChanged() { setLink(url(), title(), icon(), autoTitle(), autoIcon()); } void CrossReferenceContent::fontChanged() { setCrossReference(url(), title(), icon()); } void LauncherContent::fontChanged() { setLauncher(name(), icon(), exec()); } void ColorContent::fontChanged() { setColor(color()); } void UnknownContent::fontChanged() { loadFromFile(/*lazyLoad=*/false); } // TODO: Optimize: setMimeTypes() // QString TextContent::customOpenCommand() { return (Settings::isTextUseProg() && ! Settings::textProg().isEmpty() ? Settings::textProg() : QString()); } QString HtmlContent::customOpenCommand() { return (Settings::isHtmlUseProg() && !Settings::htmlProg().isEmpty() ? Settings::htmlProg() : QString()); } QString ImageContent::customOpenCommand() { return (Settings::isImageUseProg() && !Settings::imageProg().isEmpty() ? Settings::imageProg() : QString()); } QString AnimationContent::customOpenCommand() { return (Settings::isAnimationUseProg() && !Settings::animationProg().isEmpty() ? Settings::animationProg() : QString()); } QString SoundContent::customOpenCommand() { return (Settings::isSoundUseProg() && !Settings::soundProg().isEmpty() ? Settings::soundProg() : QString()); } void LinkContent::serialize(QDataStream &stream) { stream << url() << title() << icon() << (quint64)autoTitle() << (quint64)autoIcon(); } void CrossReferenceContent::serialize(QDataStream &stream) { stream << url() << title() << icon(); } void ColorContent::serialize(QDataStream &stream) { stream << color(); } QPixmap TextContent::feedbackPixmap(qreal width, qreal height) { QRectF textRect = QFontMetrics(note()->font()).boundingRect(0, 0, width, height, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, text()); QPixmap pixmap(qMin(width, textRect.width()), qMin(height, textRect.height())); pixmap.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&pixmap); painter.setPen(note()->textColor()); painter.setFont(note()->font()); painter.drawText(0, 0, pixmap.width(), pixmap.height(), Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, text()); painter.end(); return pixmap; } QPixmap HtmlContent::feedbackPixmap(qreal width, qreal height) { QTextDocument richText; richText.setHtml(html()); richText.setDefaultFont(note()->font()); richText.setTextWidth(width); QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::Text, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); QPixmap pixmap(qMin(width, richText.idealWidth()), qMin(height, richText.size().height())); pixmap.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&pixmap); painter.setPen(note()->textColor()); painter.translate(0, 0); richText.drawContents(&painter, QRectF(0, 0, pixmap.width(), pixmap.height())); painter.end(); return pixmap; } QPixmap ImageContent::feedbackPixmap(qreal width, qreal height) { if (width >= m_pixmapItem.pixmap().width() && height >= m_pixmapItem.pixmap().height()) { // Full size if (m_pixmapItem.pixmap().hasAlpha()) { QPixmap opaque(m_pixmapItem.pixmap().width(), m_pixmapItem.pixmap().height()); opaque.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&opaque); painter.drawPixmap(0, 0, m_pixmapItem.pixmap()); painter.end(); return opaque; } else { return m_pixmapItem.pixmap(); } } else { // Scaled down QImage imageToScale = m_pixmapItem.pixmap().toImage(); QPixmap pmScaled; pmScaled = QPixmap::fromImage(imageToScale.scaled(width, height, Qt::KeepAspectRatio)); if (pmScaled.hasAlpha()) { QPixmap opaque(pmScaled.width(), pmScaled.height()); opaque.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&opaque); painter.drawPixmap(0, 0, pmScaled); painter.end(); return opaque; } else { return pmScaled; } } } QPixmap AnimationContent::feedbackPixmap(qreal width, qreal height) { QPixmap pixmap = m_movie->currentPixmap(); if (width >= pixmap.width() && height >= pixmap.height()) // Full size return pixmap; else { // Scaled down QImage imageToScale = pixmap.toImage(); QPixmap pmScaled; pmScaled = QPixmap::fromImage(imageToScale.scaled(width, height, Qt::KeepAspectRatio)); return pmScaled; } } QPixmap LinkContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap CrossReferenceContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap ColorContent::feedbackPixmap(qreal width, qreal height) { // TODO: Duplicate code: make a rect() method! QRectF boundingRect = m_colorItem.boundingRect(); QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); QPixmap pixmap(qMin(width, boundingRect.width()), qMin(height, boundingRect.height())); pixmap.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&pixmap); m_colorItem.paint(&painter, nullptr, nullptr); //, pixmap.width(), pixmap.height(), palette, false, false, false); // We don't care of the three last boolean parameters. painter.end(); return pixmap; } QPixmap FileContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap LauncherContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap UnknownContent::feedbackPixmap(qreal width, qreal height) { QRectF boundingRect = m_unknownItem.boundingRect(); QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); QPixmap pixmap(qMin(width, boundingRect.width()), qMin(height, boundingRect.height())); QPainter painter(&pixmap); m_unknownItem.paint(&painter, nullptr, nullptr); //, pixmap.width() + 1, pixmap.height(), palette, false, false, false); // We don't care of the three last boolean parameters. painter.setPen(note()->backgroundColor().darker(FEEDBACK_DARKING)); painter.drawPoint(0, 0); painter.drawPoint(pixmap.width() - 1, 0); painter.drawPoint(0, pixmap.height() - 1); painter.drawPoint(pixmap.width() - 1, pixmap.height() - 1); painter.end(); return pixmap; } /** class TextContent: */ TextContent::TextContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, fileName) , m_graphicsTextItem(parent) { if (parent) { parent->addToGroup(&m_graphicsTextItem); m_graphicsTextItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(lazyLoad); } TextContent::~TextContent() { if (note()) note()->removeFromGroup(&m_graphicsTextItem); } qreal TextContent::setWidthAndGetHeight(qreal /*width*/) { return m_graphicsTextItem.boundingRect().height(); } bool TextContent::loadFromFile(bool lazyLoad) { DEBUG_WIN << "Loading TextContent From " + basket()->folderName() + fileName(); QString content; bool success = basket()->loadFromFile(fullPath(), &content); if (success) setText(content, lazyLoad); else { qDebug() << "FAILED TO LOAD TextContent: " << fullPath(); setText(QString(), lazyLoad); if (!QFile::exists(fullPath())) saveToFile(); // Reserve the fileName so no new note will have the same name! } return success; } bool TextContent::finishLazyLoad() { m_graphicsTextItem.setFont(note()->font()); contentChanged(m_graphicsTextItem.boundingRect().width() + 1); return true; } bool TextContent::saveToFile() { return basket()->saveToFile(fullPath(), text()); } QString TextContent::linkAt(const QPointF & /*pos*/) { return QString(); /* if (m_simpleRichText) return m_simpleRichText->documentLayout()->anchorAt(pos); else return QString(); // Lazy loaded*/ } QString TextContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening plain text..."); case OpenSeveral: return i18n("Opening plain texts..."); case OpenOneWith: return i18n("Opening plain text with..."); case OpenSeveralWith: return i18n("Opening plain texts with..."); case OpenOneWithDialog: return i18n("Open plain text with:"); case OpenSeveralWithDialog: return i18n("Open plain texts with:"); default: return QString(); } } void TextContent::setText(const QString &text, bool lazyLoad) { m_graphicsTextItem.setText(text); if (!lazyLoad) finishLazyLoad(); else contentChanged(m_graphicsTextItem.boundingRect().width()); } void TextContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString html = "" + Tools::tagCrossReferences(Tools::tagURLs(Tools::textToHTMLWithoutP(text().replace(QChar('\t'), " "))), false, exporter); // Don't collapse multiple spaces! exporter->stream << html.replace(" ", "  ").replace(QChar('\n'), '\n' + spaces.fill(' ', indent + 1)); } /** class HtmlContent: */ HtmlContent::HtmlContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, fileName) , m_simpleRichText(nullptr) , m_graphicsTextItem(parent) { if (parent) { parent->addToGroup(&m_graphicsTextItem); m_graphicsTextItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(lazyLoad); } HtmlContent::~HtmlContent() { if (note()) note()->removeFromGroup(&m_graphicsTextItem); delete m_simpleRichText; } qreal HtmlContent::setWidthAndGetHeight(qreal width) { width -= 1; m_graphicsTextItem.setTextWidth(width); return m_graphicsTextItem.boundingRect().height(); } bool HtmlContent::loadFromFile(bool lazyLoad) { DEBUG_WIN << "Loading HtmlContent From " + basket()->folderName() + fileName(); QString content; bool success = basket()->loadFromFile(fullPath(), &content); if (success) setHtml(content, lazyLoad); else { setHtml(QString(), lazyLoad); if (!QFile::exists(fullPath())) saveToFile(); // Reserve the fileName so no new note will have the same name! } return success; } bool HtmlContent::finishLazyLoad() { qreal width = m_graphicsTextItem.document()->idealWidth(); m_graphicsTextItem.setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsFocusable); m_graphicsTextItem.setTextInteractionFlags(Qt::TextEditorInteraction); /*QString css = ".cross_reference { display: block; width: 100%; text-decoration: none; color: #336600; }" "a:hover.cross_reference { text-decoration: underline; color: #ff8000; }"; m_graphicsTextItem.document()->setDefaultStyleSheet(css);*/ QString convert = Tools::tagURLs(m_html); if (note()->allowCrossReferences()) convert = Tools::tagCrossReferences(convert); m_graphicsTextItem.setHtml(convert); m_graphicsTextItem.setDefaultTextColor(note()->textColor()); m_graphicsTextItem.setFont(note()->font()); m_graphicsTextItem.setTextWidth(1); // We put a width of 1 pixel, so usedWidth() is equal to the minimum width int minWidth = m_graphicsTextItem.document()->idealWidth(); m_graphicsTextItem.setTextWidth(width); contentChanged(minWidth + 1); return true; } bool HtmlContent::saveToFile() { return basket()->saveToFile(fullPath(), html()); } QString HtmlContent::linkAt(const QPointF &pos) { return m_graphicsTextItem.document()->documentLayout()->anchorAt(pos); } QString HtmlContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening text..."); case OpenSeveral: return i18n("Opening texts..."); case OpenOneWith: return i18n("Opening text with..."); case OpenSeveralWith: return i18n("Opening texts with..."); case OpenOneWithDialog: return i18n("Open text with:"); case OpenSeveralWithDialog: return i18n("Open texts with:"); default: return QString(); } } void HtmlContent::setHtml(const QString &html, bool lazyLoad) { m_html = html; /* The code was commented, so now non-Latin text is stored directly in Unicode. * If testing doesn't show any bugs, this block should be deleted QRegExp rx("([^\\x00-\\x7f])"); while (m_html.contains(rx)) { m_html.replace( rx.cap().unicode()[0], QString("&#%1;").arg(rx.cap().unicode()[0].unicode()) ); }*/ m_textEquivalent = toText(QString()); // OPTIM_FILTER if (!lazyLoad) finishLazyLoad(); else contentChanged(10); } void HtmlContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString convert = Tools::tagURLs(html().replace("\t", " ")); if (note()->allowCrossReferences()) convert = Tools::tagCrossReferences(convert, false, exporter); exporter->stream << Tools::htmlToParagraph(convert).replace(" ", "  ").replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class ImageContent: */ ImageContent::ImageContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, fileName) , m_pixmapItem(parent) , m_format() { if (parent) { parent->addToGroup(&m_pixmapItem); m_pixmapItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(lazyLoad); } ImageContent::~ImageContent() { if (note()) note()->removeFromGroup(&m_pixmapItem); } qreal ImageContent::setWidthAndGetHeight(qreal width) { width -= 1; // Don't store width: we will get it on paint! if (width >= m_pixmapItem.pixmap().width()) // Full size { m_pixmapItem.setScale(1.0); return m_pixmapItem.boundingRect().height(); } else { // Scaled down qreal scaleFactor = width / m_pixmapItem.pixmap().width(); m_pixmapItem.setScale(scaleFactor); return m_pixmapItem.boundingRect().height() * scaleFactor; } } bool ImageContent::loadFromFile(bool lazyLoad) { if (lazyLoad) return true; else return finishLazyLoad(); } bool ImageContent::finishLazyLoad() { DEBUG_WIN << "Loading ImageContent From " + basket()->folderName() + fileName(); QByteArray content; QPixmap pixmap; if (basket()->loadFromFile(fullPath(), &content)) { QBuffer buffer(&content); buffer.open(QIODevice::ReadOnly); m_format = QImageReader::imageFormat(&buffer); // See QImageIO to know what formats can be supported. buffer.close(); if (!m_format.isNull()) { pixmap.loadFromData(content); setPixmap(pixmap); return true; } } qDebug() << "FAILED TO LOAD ImageContent: " << fullPath(); m_format = "PNG"; // If the image is set later, it should be saved without destruction, so we use PNG by default. pixmap = QPixmap(1, 1); // Create a 1x1 pixels image instead of an undefined one. pixmap.fill(); pixmap.setMask(pixmap.createHeuristicMask()); setPixmap(pixmap); if (!QFile::exists(fullPath())) saveToFile(); // Reserve the fileName so no new note will have the same name! return false; } bool ImageContent::saveToFile() { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); m_pixmapItem.pixmap().save(&buffer, m_format); return basket()->saveToFile(fullPath(), ba); } void ImageContent::toolTipInfos(QStringList *keys, QStringList *values) { keys->append(i18n("Size")); values->append(i18n("%1 by %2 pixels", QString::number(m_pixmapItem.pixmap().width()), QString::number(m_pixmapItem.pixmap().height()))); } QString ImageContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening image..."); case OpenSeveral: return i18n("Opening images..."); case OpenOneWith: return i18n("Opening image with..."); case OpenSeveralWith: return i18n("Opening images with..."); case OpenOneWithDialog: return i18n("Open image with:"); case OpenSeveralWithDialog: return i18n("Open images with:"); default: return QString(); } } void ImageContent::setPixmap(const QPixmap &pixmap) { m_pixmapItem.setPixmap(pixmap); // Since it's scaled, the height is always greater or equal to the size of the tag emblems (16) contentChanged(16 + 1); // TODO: always good? I don't think... } void ImageContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { qreal width = m_pixmapItem.pixmap().width(); qreal height = m_pixmapItem.pixmap().height(); qreal contentWidth = note()->width() - note()->contentX() - 1 - Note::NOTE_MARGIN; QString imageName = exporter->copyFile(fullPath(), /*createIt=*/true); if (contentWidth <= m_pixmapItem.pixmap().width()) { // Scaled down qreal scale = contentWidth / m_pixmapItem.pixmap().width(); width = m_pixmapItem.pixmap().width() * scale; height = m_pixmapItem.pixmap().height() * scale; exporter->stream << "dataFolderName << imageName << "\" title=\"" << i18n("Click for full size view") << "\">"; } exporter->stream << "dataFolderName << imageName << "\" width=\"" << width << "\" height=\"" << height << "\" alt=\"\">"; if (contentWidth <= m_pixmapItem.pixmap().width()) // Scaled down exporter->stream << ""; } /** class AnimationContent: */ AnimationContent::AnimationContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, fileName) , m_buffer(new QBuffer(this)) , m_movie(new QMovie(this)) , m_currentWidth(0) , m_graphicsPixmap(parent) { if (parent) { parent->addToGroup(&m_graphicsPixmap); m_graphicsPixmap.setPos(parent->contentX(), Note::NOTE_MARGIN); connect(parent->basket(), SIGNAL(activated()), m_movie, SLOT(start())); connect(parent->basket(), SIGNAL(closed()), m_movie, SLOT(stop())); } basket()->addWatchedFile(fullPath()); connect(m_movie, SIGNAL(resized(QSize)), this, SLOT(movieResized())); connect(m_movie, SIGNAL(frameChanged(int)), this, SLOT(movieFrameChanged())); loadFromFile(lazyLoad); } AnimationContent::~AnimationContent() { note()->removeFromGroup(&m_graphicsPixmap); } qreal AnimationContent::setWidthAndGetHeight(qreal width) { m_currentWidth = width; QPixmap pixmap = m_graphicsPixmap.pixmap(); if (pixmap.width() > m_currentWidth) { qreal scaleFactor = m_currentWidth / pixmap.width(); m_graphicsPixmap.setScale(scaleFactor); return pixmap.height() * scaleFactor; } else { m_graphicsPixmap.setScale(1.0); return pixmap.height(); } return 0; } bool AnimationContent::loadFromFile(bool lazyLoad) { if (lazyLoad) return true; else return finishLazyLoad(); } bool AnimationContent::finishLazyLoad() { QByteArray content; if (basket()->loadFromFile(fullPath(), &content)) { m_buffer->setData(content); startMovie(); contentChanged(16); return true; } m_buffer->setData(nullptr); return false; } bool AnimationContent::saveToFile() { // Impossible! return false; } QString AnimationContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening animation..."); case OpenSeveral: return i18n("Opening animations..."); case OpenOneWith: return i18n("Opening animation with..."); case OpenSeveralWith: return i18n("Opening animations with..."); case OpenOneWithDialog: return i18n("Open animation with:"); case OpenSeveralWithDialog: return i18n("Open animations with:"); default: return QString(); } } bool AnimationContent::startMovie() { if (m_buffer->data().isEmpty()) return false; m_movie->setDevice(m_buffer); m_movie->start(); return true; } void AnimationContent::movieUpdated() { m_graphicsPixmap.setPixmap(m_movie->currentPixmap()); } void AnimationContent::movieResized() { m_graphicsPixmap.setPixmap(m_movie->currentPixmap()); } void AnimationContent::movieFrameChanged() { m_graphicsPixmap.setPixmap(m_movie->currentPixmap()); } void AnimationContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { exporter->stream << QString("\"\"") .arg(exporter->dataFolderName + exporter->copyFile(fullPath(), /*createIt=*/true), QString::number(m_movie->currentPixmap().size().width()), QString::number(m_movie->currentPixmap().size().height())); } /** class FileContent: */ FileContent::FileContent(Note *parent, const QString &fileName) : NoteContent(parent, fileName) , m_linkDisplayItem(parent) , m_previewJob(nullptr) { basket()->addWatchedFile(fullPath()); setFileName(fileName); // FIXME: TO THAT HERE BECAUSE NoteContent() constructor seems to don't be able to call virtual methods??? if (parent) { parent->addToGroup(&m_linkDisplayItem); m_linkDisplayItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } FileContent::~FileContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); } qreal FileContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } bool FileContent::loadFromFile(bool /*lazyLoad*/) { setFileName(fileName()); // File changed: get new file preview! return true; } void FileContent::toolTipInfos(QStringList *keys, QStringList *values) { // Get the size of the file: uint size = QFileInfo(fullPath()).size(); QString humanFileSize = KIO::convertSize((KIO::filesize_t)size); keys->append(i18n("Size")); values->append(humanFileSize); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(QUrl::fromLocalFile(fullPath())); if (mime.isValid()) { keys->append(i18n("Type")); values->append(mime.comment()); } MetaDataExtractionResult result(fullPath(), mime.name()); KFileMetaData::ExtractorCollection extractorCollection; for (KFileMetaData::Extractor *ex : extractorCollection.fetchExtractors(mime.name())) { ex->extract(&result); auto groups = result.preferredGroups(); DEBUG_WIN << "Metadata Extractor result has " << QString::number(groups.count()) << " groups"; int i = 0; for (auto it = groups.begin(); i < 6 && it != groups.end(); ++it) { if (!it->second.isEmpty()) { keys->append(it->first); values->append(it->second); i++; } } } } int FileContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF FileContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString FileContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this file") : QString()); } Qt::CursorShape FileContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } int FileContent::xEditorIndent() { return m_linkDisplayItem.linkDisplay().iconButtonRect().width() + 2; } QString FileContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening file..."); case OpenSeveral: return i18n("Opening files..."); case OpenOneWith: return i18n("Opening file with..."); case OpenSeveralWith: return i18n("Opening files with..."); case OpenOneWithDialog: return i18n("Open file with:"); case OpenSeveralWithDialog: return i18n("Open files with:"); default: return QString(); } } void FileContent::setFileName(const QString &fileName) { NoteContent::setFileName(fileName); QUrl url = QUrl::fromLocalFile(fullPath()); if (linkLook()->previewEnabled()) m_linkDisplayItem.linkDisplay().setLink(fileName, NoteFactory::iconForURL(url), linkLook(), note()->font()); // FIXME: move iconForURL outside of NoteFactory !!!!! else m_linkDisplayItem.linkDisplay().setLink(fileName, NoteFactory::iconForURL(url), QPixmap(), linkLook(), note()->font()); startFetchingUrlPreview(); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void FileContent::linkLookChanged() { fontChanged(); // setFileName(fileName()); // startFetchingUrlPreview(); } void FileContent::newPreview(const KFileItem &, const QPixmap &preview) { LinkLook *linkLook = this->linkLook(); m_linkDisplayItem.linkDisplay().setLink(fileName(), NoteFactory::iconForURL(QUrl::fromLocalFile(fullPath())), (linkLook->previewEnabled() ? preview : QPixmap()), linkLook, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void FileContent::removePreview(const KFileItem &ki) { newPreview(ki, QPixmap()); } void FileContent::startFetchingUrlPreview() { /* KUrl url(fullPath()); LinkLook *linkLook = this->linkLook(); // delete m_previewJob; if (!url.isEmpty() && linkLook->previewSize() > 0) { QUrl filteredUrl = NoteFactory::filteredURL(url);//KURIFilter::self()->filteredURI(url); KUrl::List urlList; urlList.append(filteredUrl); m_previewJob = KIO::filePreview(urlList, linkLook->previewSize(), linkLook->previewSize(), linkLook->iconSize()); connect(m_previewJob, SIGNAL(gotPreview(const KFileItem&, const QPixmap&)), this, SLOT(newPreview(const KFileItem&, const QPixmap&))); connect(m_previewJob, SIGNAL(failed(const KFileItem&)), this, SLOT(removePreview(const KFileItem&))); } */ } void FileContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString fileName = exporter->copyFile(fullPath(), true); exporter->stream << m_linkDisplayItem.linkDisplay().toHtml(exporter, QUrl::fromLocalFile(exporter->dataFolderName + fileName), QString()).replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class SoundContent: */ SoundContent::SoundContent(Note *parent, const QString &fileName) : FileContent(parent, fileName) { setFileName(fileName); music = new Phonon::MediaObject(this); music->setCurrentSource(Phonon::MediaSource(fullPath())); Phonon::AudioOutput *audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this); Phonon::Path path = Phonon::createPath(music, audioOutput); connect(music, SIGNAL(stateChanged(Phonon::State, Phonon::State)), this, SLOT(stateChanged(Phonon::State, Phonon::State))); } void SoundContent::stateChanged(Phonon::State newState, Phonon::State oldState) { qDebug() << "stateChanged " << oldState << " to " << newState; } QString SoundContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this sound") : QString()); } void SoundContent::setHoveredZone(int oldZone, int newZone) { if (newZone == Note::Custom0 || newZone == Note::Content) { // Start the sound preview: if (oldZone != Note::Custom0 && oldZone != Note::Content) { // Don't restart if it was already in one of those zones if (music->state() == 1) { music->play(); } } } else { // Stop the sound preview, if it was started: if (music->state() != 1) { music->stop(); // delete music;//TODO implement this in slot connected with music alted signal // music = 0; } } } QString SoundContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening sound..."); case OpenSeveral: return i18n("Opening sounds..."); case OpenOneWith: return i18n("Opening sound with..."); case OpenSeveralWith: return i18n("Opening sounds with..."); case OpenOneWithDialog: return i18n("Open sound with:"); case OpenSeveralWithDialog: return i18n("Open sounds with:"); default: return QString(); } } /** class LinkContent: */ LinkContent::LinkContent(Note *parent, const QUrl &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon) : NoteContent(parent) , m_linkDisplayItem(parent) , m_access_manager(nullptr) , m_acceptingData(false) , m_previewJob(nullptr) { setLink(url, title, icon, autoTitle, autoIcon); if (parent) { parent->addToGroup(&m_linkDisplayItem); m_linkDisplayItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } LinkContent::~LinkContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); delete m_access_manager; } qreal LinkContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } void LinkContent::saveToNode(QXmlStreamWriter &stream) { stream.writeStartElement("content"); stream.writeAttribute("title", title()); stream.writeAttribute("icon", icon()); stream.writeAttribute("autoIcon", (autoIcon() ? "true" : "false")); stream.writeAttribute("autoTitle", (autoTitle() ? "true" : "false")); stream.writeCharacters(url().toDisplayString()); stream.writeEndElement(); } void LinkContent::toolTipInfos(QStringList *keys, QStringList *values) { keys->append(i18n("Target")); values->append(m_url.toDisplayString()); } int LinkContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF LinkContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString LinkContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this link") : QString()); } Qt::CursorShape LinkContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } QString LinkContent::statusBarMessage(int zone) { if (zone == Note::Custom0 || zone == Note::Content) return m_url.toDisplayString(); else return QString(); } QUrl LinkContent::urlToOpen(bool /*with*/) { return NoteFactory::filteredURL(url()); // KURIFilter::self()->filteredURI(url()); } QString LinkContent::messageWhenOpening(OpenMessage where) { if (url().isEmpty()) return i18n("Link have no URL to open."); switch (where) { case OpenOne: return i18n("Opening link target..."); case OpenSeveral: return i18n("Opening link targets..."); case OpenOneWith: return i18n("Opening link target with..."); case OpenSeveralWith: return i18n("Opening link targets with..."); case OpenOneWithDialog: return i18n("Open link target with:"); case OpenSeveralWithDialog: return i18n("Open link targets with:"); default: return QString(); } } void LinkContent::setLink(const QUrl &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon) { m_autoTitle = autoTitle; m_autoIcon = autoIcon; m_url = NoteFactory::filteredURL(url); // KURIFilter::self()->filteredURI(url); m_title = (autoTitle ? NoteFactory::titleForURL(m_url) : title); m_icon = (autoIcon ? NoteFactory::iconForURL(m_url) : icon); LinkLook *look = LinkLook::lookForURL(m_url); if (look->previewEnabled()) m_linkDisplayItem.linkDisplay().setLink(m_title, m_icon, look, note()->font()); else m_linkDisplayItem.linkDisplay().setLink(m_title, m_icon, QPixmap(), look, note()->font()); startFetchingUrlPreview(); if (autoTitle) startFetchingLinkTitle(); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void LinkContent::linkLookChanged() { fontChanged(); } void LinkContent::newPreview(const KFileItem &, const QPixmap &preview) { LinkLook *linkLook = LinkLook::lookForURL(url()); m_linkDisplayItem.linkDisplay().setLink(title(), icon(), (linkLook->previewEnabled() ? preview : QPixmap()), linkLook, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void LinkContent::removePreview(const KFileItem &ki) { newPreview(ki, QPixmap()); } // QHttp slots for getting link title void LinkContent::httpReadyRead() { if (!m_acceptingData) return; // Check for availability qint64 bytesAvailable = m_reply->bytesAvailable(); if (bytesAvailable <= 0) return; QByteArray buf = m_reply->read(bytesAvailable); m_httpBuff.append(buf); // Stop at 10k bytes if (m_httpBuff.length() > 10000) { m_acceptingData = false; m_reply->abort(); endFetchingLinkTitle(); } } void LinkContent::httpDone(QNetworkReply *reply) { if (m_acceptingData) { m_acceptingData = false; endFetchingLinkTitle(); } // If all done, close and delete the reply. reply->deleteLater(); } void LinkContent::startFetchingLinkTitle() { QUrl newUrl = this->url(); // If this is not an HTTP request, just ignore it. if (newUrl.scheme() == "http") { // If we have no access_manager, create one. if (m_access_manager == nullptr) { m_access_manager = new KIO::Integration::AccessManager(this); connect(m_access_manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(httpDone(QNetworkReply *))); } // If no explicit port, default to port 80. if (newUrl.port() == 0) newUrl.setPort(80); // If no path or query part, default to / if ((newUrl.path() + newUrl.query()).isEmpty()) newUrl = QUrl::fromLocalFile("/"); // Issue request m_reply = m_access_manager->get(QNetworkRequest(newUrl)); m_acceptingData = true; connect(m_reply, SIGNAL(readyRead()), this, SLOT(httpReadyRead())); } } // Code duplicated from FileContent::startFetchingUrlPreview() void LinkContent::startFetchingUrlPreview() { QUrl url = this->url(); LinkLook *linkLook = LinkLook::lookForURL(this->url()); // delete m_previewJob; if (!url.isEmpty() && linkLook->previewSize() > 0) { QUrl filteredUrl = NoteFactory::filteredURL(url); // KURIFilter::self()->filteredURI(url); QList urlList; urlList.append(filteredUrl); m_previewJob = KIO::filePreview(urlList, linkLook->previewSize(), linkLook->previewSize(), linkLook->iconSize()); connect(m_previewJob, SIGNAL(gotPreview(const KFileItem &, const QPixmap &)), this, SLOT(newPreview(const KFileItem &, const QPixmap &))); connect(m_previewJob, SIGNAL(failed(const KFileItem &)), this, SLOT(removePreview(const KFileItem &))); } } void LinkContent::endFetchingLinkTitle() { if (m_httpBuff.length() > 0) { decodeHtmlTitle(); m_httpBuff.clear(); } else DEBUG_WIN << "LinkContent: empty buffer on endFetchingLinkTitle for " + m_url.toString(); } void LinkContent::exportToHTML(HTMLExporter *exporter, int indent) { QString linkTitle = title(); // TODO: // // Append address (useful for print version of the page/basket): // if (exportData.formatForImpression && (!autoTitle() && title() != NoteFactory::titleForURL(url().toDisplayString()))) { // // The address is on a new line, unless title is empty (empty lines was replaced by  ): // if (linkTitle == " "/*" "*/) // linkTitle = url().toDisplayString()/*QString()*/; // else // linkTitle = linkTitle + " <" + url().toDisplayString() + ">"/*+ "
"*/; // //linkTitle += "" + url().toDisplayString() + ""; // } QUrl linkURL; /* QFileInfo fInfo(url().path()); // DEBUG_WIN << url().path() // << "IsFile:" + QString::number(fInfo.isFile()) // << "IsDir:" + QString::number(fInfo.isDir()); if (exportData.embedLinkedFiles && fInfo.isFile()) { // DEBUG_WIN << "Embed file"; linkURL = exportData.dataFolderName + BasketScene::copyFile(url().path(), exportData.dataFolderPath, true); } else if (exportData.embedLinkedFolders && fInfo.isDir()) { // DEBUG_WIN << "Embed folder"; linkURL = exportData.dataFolderName + BasketScene::copyFile(url().path(), exportData.dataFolderPath, true); } else { // DEBUG_WIN << "Embed LINK"; */ linkURL = url(); /* } */ QString spaces; exporter->stream << m_linkDisplayItem.linkDisplay().toHtml(exporter, linkURL, linkTitle).replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class CrossReferenceContent: */ CrossReferenceContent::CrossReferenceContent(Note *parent, const QUrl &url, const QString &title, const QString &icon) : NoteContent(parent) , m_linkDisplayItem(parent) { this->setCrossReference(url, title, icon); if (parent) parent->addToGroup(&m_linkDisplayItem); } CrossReferenceContent::~CrossReferenceContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); } qreal CrossReferenceContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } void CrossReferenceContent::saveToNode(QXmlStreamWriter &stream) { stream.writeStartElement("content"); stream.writeAttribute("title", title()); stream.writeAttribute("icon", icon()); stream.writeCharacters(url().toDisplayString()); stream.writeEndElement(); } void CrossReferenceContent::toolTipInfos(QStringList *keys, QStringList *values) { keys->append(i18n("Target")); values->append(m_url.toDisplayString()); } int CrossReferenceContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF CrossReferenceContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString CrossReferenceContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this link") : QString()); } Qt::CursorShape CrossReferenceContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } QString CrossReferenceContent::statusBarMessage(int zone) { if (zone == Note::Custom0 || zone == Note::Content) return i18n("Link to %1", this->title()); else return QString(); } QUrl CrossReferenceContent::urlToOpen(bool /*with*/) { return m_url; } QString CrossReferenceContent::messageWhenOpening(OpenMessage where) { if (url().isEmpty()) return i18n("Link has no basket to open."); switch (where) { case OpenOne: return i18n("Opening basket..."); default: return QString(); } } void CrossReferenceContent::setLink(const QUrl &url, const QString &title, const QString &icon) { this->setCrossReference(url, title, icon); } void CrossReferenceContent::setCrossReference(const QUrl &url, const QString &title, const QString &icon) { m_url = url; m_title = (title.isEmpty() ? url.url() : title); m_icon = icon; LinkLook *look = LinkLook::crossReferenceLook; m_linkDisplayItem.linkDisplay().setLink(m_title, m_icon, look, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void CrossReferenceContent::linkLookChanged() { fontChanged(); } void CrossReferenceContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { QString url = m_url.url(); QString title; if (url.startsWith(QLatin1String("basket://"))) url = url.mid(9, url.length() - 9); if (url.endsWith('/')) url = url.left(url.length() - 1); BasketScene *basket = Global::bnpView->basketForFolderName(url); if (!basket) title = "unknown basket"; else title = basket->basketName(); // if the basket we're trying to link to is the basket that was exported then // we have to use a special way to refer to it for the links. if (basket == exporter->exportedBasket) url = "../../" + exporter->fileName; else { // if we're in the exported basket then the links have to include // the sub directories. if (exporter->currentBasket == exporter->exportedBasket) url.prepend(exporter->basketsFolderName); url.append(".html"); } QString linkIcon = exporter->iconsFolderName + exporter->copyIcon(m_icon, LinkLook::crossReferenceLook->iconSize()); linkIcon = QString("\"\"").arg(linkIcon); exporter->stream << QString("%2 %3").arg(url, linkIcon, title); } /** class LauncherContent: */ LauncherContent::LauncherContent(Note *parent, const QString &fileName) : NoteContent(parent, fileName) , m_linkDisplayItem(parent) { basket()->addWatchedFile(fullPath()); loadFromFile(/*lazyLoad=*/false); if (parent) { parent->addToGroup(&m_linkDisplayItem); m_linkDisplayItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } LauncherContent::~LauncherContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); } qreal LauncherContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } bool LauncherContent::loadFromFile(bool /*lazyLoad*/) // TODO: saveToFile() ?? Is it possible? { DEBUG_WIN << "Loading LauncherContent From " + basket()->folderName() + fileName(); KService service(fullPath()); setLauncher(service.name(), service.icon(), service.exec()); return true; } void LauncherContent::toolTipInfos(QStringList *keys, QStringList *values) { KService service(fullPath()); QString exec = service.exec(); if (service.terminal()) exec = i18n("%1 (run in terminal)", exec); if (!service.comment().isEmpty() && service.comment() != service.name()) { keys->append(i18n("Comment")); values->append(service.comment()); } keys->append(i18n("Command")); values->append(exec); } int LauncherContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF LauncherContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString LauncherContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Launch this application") : QString()); } Qt::CursorShape LauncherContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } QUrl LauncherContent::urlToOpen(bool with) { if (KService(fullPath()).exec().isEmpty()) return QUrl(); return (with ? QUrl() : QUrl::fromLocalFile(fullPath())); // Can open the application, but not with another application :-) } QString LauncherContent::messageWhenOpening(OpenMessage where) { if (KService(fullPath()).exec().isEmpty()) return i18n("The launcher have no command to run."); switch (where) { case OpenOne: return i18n("Launching application..."); case OpenSeveral: return i18n("Launching applications..."); case OpenOneWith: case OpenSeveralWith: case OpenOneWithDialog: case OpenSeveralWithDialog: // TODO: "Open this application with this file as parameter"? default: return QString(); } } void LauncherContent::setLauncher(const QString &name, const QString &icon, const QString &exec) { m_name = name; m_icon = icon; m_exec = exec; m_linkDisplayItem.linkDisplay().setLink(name, icon, LinkLook::launcherLook, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void LauncherContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString fileName = exporter->copyFile(fullPath(), /*createIt=*/true); exporter->stream << m_linkDisplayItem.linkDisplay().toHtml(exporter, QUrl::fromLocalFile(exporter->dataFolderName + fileName), QString()).replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class ColorItem: */ const int ColorItem::RECT_MARGIN = 2; ColorItem::ColorItem(Note *parent, const QColor &color) : QGraphicsItem(parent) , m_note(parent) { setColor(color); } void ColorItem::setColor(const QColor &color) { m_color = color; m_textRect = QFontMetrics(m_note->font()).boundingRect(m_color.name()); } QRectF ColorItem::boundingRect() const { qreal rectHeight = (m_textRect.height() + 2) * 3 / 2; qreal rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. return QRectF(0, 0, rectWidth + RECT_MARGIN + m_textRect.width() + RECT_MARGIN, rectHeight); } void ColorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { QRectF boundingRect = this->boundingRect(); qreal rectHeight = (m_textRect.height() + 2) * 3 / 2; qreal rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. // FIXME: Duplicate from CommonColorSelector::drawColorRect: // Fill: painter->fillRect(1, 1, rectWidth - 2, rectHeight - 2, color()); // Stroke: QColor stroke = color().darker(125); painter->setPen(stroke); painter->drawLine(1, 0, rectWidth - 2, 0); painter->drawLine(0, 1, 0, rectHeight - 2); painter->drawLine(1, rectHeight - 1, rectWidth - 2, rectHeight - 1); painter->drawLine(rectWidth - 1, 1, rectWidth - 1, rectHeight - 2); // Round corners: painter->setPen(Tools::mixColor(color(), stroke)); painter->drawPoint(1, 1); painter->drawPoint(1, rectHeight - 2); painter->drawPoint(rectWidth - 2, rectHeight - 2); painter->drawPoint(rectWidth - 2, 1); // Draw the text: painter->setFont(m_note->font()); painter->setPen(m_note->palette().color(QPalette::Active, QPalette::WindowText)); painter->drawText(rectWidth + RECT_MARGIN, 0, m_textRect.width(), boundingRect.height(), Qt::AlignLeft | Qt::AlignVCenter, color().name()); } /** class ColorContent: */ ColorContent::ColorContent(Note *parent, const QColor &color) : NoteContent(parent) , m_colorItem(parent, color) { if (parent) { parent->addToGroup(&m_colorItem); m_colorItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } ColorContent::~ColorContent() { if (note()) note()->removeFromGroup(&m_colorItem); } qreal ColorContent::setWidthAndGetHeight(qreal /*width*/) // We do not need width because we can't word-break, and width is always >= minWidth() { return m_colorItem.boundingRect().height(); } void ColorContent::saveToNode(QXmlStreamWriter &stream) { stream.writeStartElement("content"); stream.writeCharacters(color().name()); stream.writeEndElement(); } void ColorContent::toolTipInfos(QStringList *keys, QStringList *values) { int hue, saturation, value; color().getHsv(&hue, &saturation, &value); keys->append(i18nc("RGB Colorspace: Red/Green/Blue", "RGB")); values->append(i18n("Red: %1, Green: %2, Blue: %3,", QString::number(color().red()), QString::number(color().green()), QString::number(color().blue()))); keys->append(i18nc("HSV Colorspace: Hue/Saturation/Value", "HSV")); values->append(i18n("Hue: %1, Saturation: %2, Value: %3,", QString::number(hue), QString::number(saturation), QString::number(value))); - static QString cssColors[] = {"aqua", "00ffff", "black", "000000", "blue", "0000ff", "fuchsia", "ff00ff", "gray", "808080", "green", "008000", "lime", "00ff00", "maroon", "800000", - "navy", "000080", "olive", "808000", "purple", "800080", "red", "ff0000", "silver", "c0c0c0", "teal", "008080", "white", "ffffff", "yellow", "ffff00"}; - - static QString cssExtendedColors[] = {"aliceblue", - "f0f8ff", - "antiquewhite", - "faebd7", - "aquamarine", - "7fffd4", - "azure", - "f0ffff", - "beige", - "f5f5dc", - "bisque", - "ffe4c4", - "blanchedalmond", - "ffebcd", - "blueviolet", - "8a2be2", - "brown", - "a52a2a", - "burlywood", - "deb887", - "cadetblue", - "5f9ea0", - "chartreuse", - "7fff00", - "chocolate", - "d2691e", - "coral", - "ff7f50", - "cornflowerblue", - "6495ed", - "cornsilk", - "fff8dc", - "crimson", - "dc1436", - "cyan", - "00ffff", - "darkblue", - "00008b", - "darkcyan", - "008b8b", - "darkgoldenrod", - "b8860b", - "darkgray", - "a9a9a9", - "darkgreen", - "006400", - "darkkhaki", - "bdb76b", - "darkmagenta", - "8b008b", - "darkolivegreen", - "556b2f", - "darkorange", - "ff8c00", - "darkorchid", - "9932cc", - "darkred", - "8b0000", - "darksalmon", - "e9967a", - "darkseagreen", - "8fbc8f", - "darkslateblue", - "483d8b", - "darkslategray", - "2f4f4f", - "darkturquoise", - "00ced1", - "darkviolet", - "9400d3", - "deeppink", - "ff1493", - "deepskyblue", - "00bfff", - "dimgray", - "696969", - "dodgerblue", - "1e90ff", - "firebrick", - "b22222", - "floralwhite", - "fffaf0", - "forestgreen", - "228b22", - "gainsboro", - "dcdcdc", - "ghostwhite", - "f8f8ff", - "gold", - "ffd700", - "goldenrod", - "daa520", - "greenyellow", - "adff2f", - "honeydew", - "f0fff0", - "hotpink", - "ff69b4", - "indianred", - "cd5c5c", - "indigo", - "4b0082", - "ivory", - "fffff0", - "khaki", - "f0e68c", - "lavender", - "e6e6fa", - "lavenderblush", - "fff0f5", - "lawngreen", - "7cfc00", - "lemonchiffon", - "fffacd", - "lightblue", - "add8e6", - "lightcoral", - "f08080", - "lightcyan", - "e0ffff", - "lightgoldenrodyellow", - "fafad2", - "lightgreen", - "90ee90", - "lightgrey", - "d3d3d3", - "lightpink", - "ffb6c1", - "lightsalmon", - "ffa07a", - "lightseagreen", - "20b2aa", - "lightskyblue", - "87cefa", - "lightslategray", - "778899", - "lightsteelblue", - "b0c4de", - "lightyellow", - "ffffe0", - "limegreen", - "32cd32", - "linen", - "faf0e6", - "magenta", - "ff00ff", - "mediumaquamarine", - "66cdaa", - "mediumblue", - "0000cd", - "mediumorchid", - "ba55d3", - "mediumpurple", - "9370db", - "mediumseagreen", - "3cb371", - "mediumslateblue", - "7b68ee", - "mediumspringgreen", - "00fa9a", - "mediumturquoise", - "48d1cc", - "mediumvioletred", - "c71585", - "midnightblue", - "191970", - "mintcream", - "f5fffa", - "mistyrose", - "ffe4e1", - "moccasin", - "ffe4b5", - "navajowhite", - "ffdead", - "oldlace", - "fdf5e6", - "olivedrab", - "6b8e23", - "orange", - "ffa500", - "orangered", - "ff4500", - "orchid", - "da70d6", - "palegoldenrod", - "eee8aa", - "palegreen", - "98fb98", - "paleturquoise", - "afeeee", - "palevioletred", - "db7093", - "papayawhip", - "ffefd5", - "peachpuff", - "ffdab9", - "peru", - "cd853f", - "pink", - "ffc0cb", - "plum", - "dda0dd", - "powderblue", - "b0e0e6", - "rosybrown", - "bc8f8f", - "royalblue", - "4169e1", - "saddlebrown", - "8b4513", - "salmon", - "fa8072", - "sandybrown", - "f4a460", - "seagreen", - "2e8b57", - "seashell", - "fff5ee", - "sienna", - "a0522d", - "skyblue", - "87ceeb", - "slateblue", - "6a5acd", - "slategray", - "708090", - "snow", - "fffafa", - "springgreen", - "00ff7f", - "steelblue", - "4682b4", - "tan", - "d2b48c", - "thistle", - "d8bfd8", - "tomato", - "ff6347", - "turquoise", - "40e0d0", - "violet", - "ee82ee", - "wheat", - "f5deb3", - "whitesmoke", - "f5f5f5", - "yellowgreen", - "9acd32"}; - - QString colorHex = color().name().mid(1); // Take the hexadecimal name of the color, without the '#'. - - bool cssColorFound = false; - for (int i = 0; i < 2 * 16; i += 2) { - if (colorHex == cssColors[i + 1]) { - keys->append(i18n("CSS Color Name")); - values->append(cssColors[i]); - cssColorFound = true; - break; - } + const QString colorName = Tools::cssColorName(color().name()); + if (!colorName.isEmpty()) { + keys->append(i18n("CSS Color Name")); + values->append(colorName); } - if (!cssColorFound) - for (int i = 0; i < 2 * 124; i += 2) { - if (colorHex == cssExtendedColors[i + 1]) { - keys->append(i18n("CSS Extended Color Name")); - values->append(cssExtendedColors[i]); - break; - } - } - keys->append(i18n("Is Web Color")); values->append(Tools::isWebColor(color()) ? i18n("Yes") : i18n("No")); } void ColorContent::setColor(const QColor &color) { m_colorItem.setColor(color); contentChanged(m_colorItem.boundingRect().width()); } void ColorContent::addAlternateDragObjects(QMimeData *dragObject) { dragObject->setColorData(color()); } void ColorContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { // FIXME: Duplicate from setColor(): TODO: rectSize() QRectF textRect = QFontMetrics(note()->font()).boundingRect(color().name()); int rectHeight = (textRect.height() + 2) * 3 / 2; int rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. QString fileName = /*Tools::fileNameForNewFile(*/ QString("color_%1.png").arg(color().name().toLower().mid(1)) /*, exportData.iconsFolderPath)*/; QString fullPath = exporter->iconsFolderPath + fileName; QPixmap colorIcon(rectWidth, rectHeight); QPainter painter(&colorIcon); painter.setBrush(color()); painter.drawRoundedRect(0, 0, rectWidth, rectHeight, 2, 2); colorIcon.save(fullPath, "PNG"); QString iconHtml = QString("\"\"").arg(exporter->iconsFolderName + fileName, QString::number(colorIcon.width()), QString::number(colorIcon.height())); exporter->stream << iconHtml + ' ' + color().name(); } /** class UnknownItem: */ const qreal UnknownItem::DECORATION_MARGIN = 2; UnknownItem::UnknownItem(Note *parent) : QGraphicsItem(parent) , m_note(parent) { } QRectF UnknownItem::boundingRect() const { return QRectF(0, 0, m_textRect.width() + 2 * DECORATION_MARGIN, m_textRect.height() + 2 * DECORATION_MARGIN); } void UnknownItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { QPalette palette = m_note->basket()->palette(); qreal width = boundingRect().width(); qreal height = boundingRect().height(); painter->setPen(palette.color(QPalette::Active, QPalette::WindowText)); // Stroke: QColor stroke = Tools::mixColor(palette.color(QPalette::Active, QPalette::Background), palette.color(QPalette::Active, QPalette::WindowText)); painter->setPen(stroke); painter->drawLine(1, 0, width - 2, 0); painter->drawLine(0, 1, 0, height - 2); painter->drawLine(1, height - 1, width - 2, height - 1); painter->drawLine(width - 1, 1, width - 1, height - 2); // Round corners: painter->setPen(Tools::mixColor(palette.color(QPalette::Active, QPalette::Background), stroke)); painter->drawPoint(1, 1); painter->drawPoint(1, height - 2); painter->drawPoint(width - 2, height - 2); painter->drawPoint(width - 2, 1); painter->setPen(palette.color(QPalette::Active, QPalette::WindowText)); painter->drawText(DECORATION_MARGIN, DECORATION_MARGIN, width - 2 * DECORATION_MARGIN, height - 2 * DECORATION_MARGIN, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_mimeTypes); } void UnknownItem::setMimeTypes(QString mimeTypes) { m_mimeTypes = mimeTypes; m_textRect = QFontMetrics(m_note->font()).boundingRect(0, 0, 1, 500000, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, m_mimeTypes); } void UnknownItem::setWidth(qreal width) { prepareGeometryChange(); m_textRect = QFontMetrics(m_note->font()).boundingRect(0, 0, width, 500000, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, m_mimeTypes); } /** class UnknownContent: */ UnknownContent::UnknownContent(Note *parent, const QString &fileName) : NoteContent(parent, fileName) , m_unknownItem(parent) { if (parent) { parent->addToGroup(&m_unknownItem); m_unknownItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(/*lazyLoad=*/false); } UnknownContent::~UnknownContent() { if (note()) note()->removeFromGroup(&m_unknownItem); } qreal UnknownContent::setWidthAndGetHeight(qreal width) { m_unknownItem.setWidth(width); return m_unknownItem.boundingRect().height(); } bool UnknownContent::loadFromFile(bool /*lazyLoad*/) { DEBUG_WIN << "Loading UnknownContent From " + basket()->folderName() + fileName(); QString mimeTypes; QFile file(fullPath()); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&file); QString line; // Get the MIME-types names: do { if (!stream.atEnd()) { line = stream.readLine(); if (!line.isEmpty()) { if (mimeTypes.isEmpty()) mimeTypes += line; else mimeTypes += QString("\n") + line; } } } while (!line.isEmpty() && !stream.atEnd()); file.close(); } m_unknownItem.setMimeTypes(mimeTypes); contentChanged(m_unknownItem.boundingRect().width() + 1); return true; } void UnknownContent::addAlternateDragObjects(QMimeData *dragObject) { QFile file(fullPath()); if (file.open(QIODevice::ReadOnly)) { QDataStream stream(&file); // Get the MIME types names: QStringList mimes; QString line; do { if (!stream.atEnd()) { stream >> line; if (!line.isEmpty()) mimes.append(line); } } while (!line.isEmpty() && !stream.atEnd()); // Add the streams: quint64 size; // TODO: It was quint32 in version 0.5.0 ! QByteArray *array; for (int i = 0; i < mimes.count(); ++i) { // Get the size: stream >> size; // Allocate memory to retrieve size bytes and store them: array = new QByteArray; array->resize(size); stream.readRawData(array->data(), size); // Creata and add the QDragObject: dragObject->setData(mimes.at(i).toLatin1(), *array); delete array; // FIXME: Should we? } file.close(); } } void UnknownContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; exporter->stream << "
" << mimeTypes().replace("\n", '\n' + spaces.fill(' ', indent + 1 + 1)) << "
"; } -void NoteFactory__loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad) -{ - if (lowerTypeName == "text") - new TextContent(parent, content.text(), lazyLoad); - else if (lowerTypeName == "html") - new HtmlContent(parent, content.text(), lazyLoad); - else if (lowerTypeName == "image") - new ImageContent(parent, content.text(), lazyLoad); - else if (lowerTypeName == "animation") - new AnimationContent(parent, content.text(), lazyLoad); - else if (lowerTypeName == "sound") - new SoundContent(parent, content.text()); - else if (lowerTypeName == "file") - new FileContent(parent, content.text()); - else if (lowerTypeName == "link") { - bool autoTitle = content.attribute("title") == content.text(); - bool autoIcon = content.attribute("icon") == NoteFactory::iconForURL(QUrl::fromUserInput(content.text())); - autoTitle = XMLWork::trueOrFalse(content.attribute("autoTitle"), autoTitle); - autoIcon = XMLWork::trueOrFalse(content.attribute("autoIcon"), autoIcon); - new LinkContent(parent, QUrl::fromUserInput(content.text()), content.attribute("title"), content.attribute("icon"), autoTitle, autoIcon); - } else if (lowerTypeName == "cross_reference") { - new CrossReferenceContent(parent, QUrl::fromUserInput(content.text()), content.attribute("title"), content.attribute("icon")); - } else if (lowerTypeName == "launcher") - new LauncherContent(parent, content.text()); - else if (lowerTypeName == "color") - new ColorContent(parent, QColor(content.text())); - else if (lowerTypeName == "unknown") - new UnknownContent(parent, content.text()); -} - void LinkContent::decodeHtmlTitle() { KEncodingProber prober; prober.feed(m_httpBuff); // Fallback scheme: KEncodingProber - QTextCodec::codecForHtml - UTF-8 QTextCodec *textCodec; if (prober.confidence() > 0.5) textCodec = QTextCodec::codecForName(prober.encoding()); else textCodec = QTextCodec::codecForHtml(m_httpBuff, QTextCodec::codecForName("utf-8")); QString httpBuff = textCodec->toUnicode(m_httpBuff.data(), m_httpBuff.size()); // todo: this should probably strip odd html tags like   etc QRegExp reg("[\\s]*( )?([^<]+)[\\s]*", Qt::CaseInsensitive); reg.setMinimal(true); // qDebug() << *m_httpBuff << " bytes: " << bytes_read; if (reg.indexIn(httpBuff) >= 0) { m_title = reg.cap(2); m_autoTitle = false; setEdited(); // refresh the title setLink(url(), title(), icon(), autoTitle(), autoIcon()); } } diff --git a/src/notecontent.h b/src/notecontent.h index 894fbe5..f7fbcfa 100644 --- a/src/notecontent.h +++ b/src/notecontent.h @@ -1,949 +1,947 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef NOTECONTENT_H #define NOTECONTENT_H #include #include #include #include #include #include #include "linklabel.h" class QDomElement; class QBuffer; class QColor; class QMimeData; class QMovie; class QPainter; class QPixmap; class QPoint; class QRect; class QString; class QStringList; class QTextDocument; class QWidget; class KFileItem; class QUrl; namespace KIO { class PreviewJob; } namespace Phonon { class MediaObject; } class BasketScene; class FilterData; class Note; /** * LinkDisplayItem is a QGraphicsItem using a LinkDisplay */ class LinkDisplayItem : public QGraphicsItem { public: explicit LinkDisplayItem(Note *parent) : m_note(parent) { } ~LinkDisplayItem() override { } QRectF boundingRect() const override; void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override; LinkDisplay &linkDisplay() { return m_linkDisplay; } private: LinkDisplay m_linkDisplay; Note *m_note; }; /** A list of numeric identifier for each note type. * Declare a variable with the type NoteType::Id and assign a value like NoteType::Text... * @author Sébastien Laoût */ namespace NoteType { enum Id { Group = 255, Text = 1, Html, Image, Animation, Sound, File, Link, CrossReference, Launcher, Color, Unknown }; // Always positive } /** Abstract base class for every content type of basket note. * It's a base class to represent those types: Text, Html, Image, Animation, Sound, File, Link, Launcher, Color, Unknown. * @author Sébastien Laoût */ class NoteContent { public: // Constructor and destructor: explicit NoteContent(Note *parent, const QString &fileName = QString()); /// << Constructor. Inherited notes should call it to initialize the parent note. virtual ~NoteContent() { } /// << Virtual destructor. Reimplement it if you should destroy some data your custom types. // Simple Abstract Generic Methods: virtual NoteType::Id type() const = 0; /// << @return the internal number that identify that note type. virtual QString typeName() const = 0; /// << @return the translated type name to display in the user interface. virtual QString lowerTypeName() const = 0; /// << @return the type name in lowercase without space, for eg. saving. virtual QString toText(const QString &cuttedFullPath); /// << @return a plain text equivalent of the content. virtual QString toHtml(const QString &imageName, const QString &cuttedFullPath) = 0; /// << @return an HTML text equivalent of the content. @param imageName Save image in this Qt resource. virtual QPixmap toPixmap() { return QPixmap(); } /// << @return an image equivalent of the content. virtual void toLink(QUrl *url, QString *title, const QString &cuttedFullPath); /// << Set the link to the content. By default, it set them to fullPath() if useFile(). virtual bool useFile() const = 0; /// << @return true if it use a file to store the content. virtual bool canBeSavedAs() const = 0; /// << @return true if the content can be saved as a file by the user. virtual QString saveAsFilters() const = 0; /// << @return the filters for the user to choose a file destination to save the note as. virtual bool match(const FilterData &data) = 0; /// << @return true if the content match the filter criteria. // Complex Abstract Generic Methods: virtual void exportToHTML(HTMLExporter *exporter, int indent) = 0; /// << Export the note in an HTML file. virtual QString cssClass() const = 0; /// << @return the CSS class of the note when exported to HTML virtual qreal setWidthAndGetHeight(qreal width) = 0; /// << Relayout content with @p width (never less than minWidth()). @return its new height. virtual bool loadFromFile(bool /*lazyLoad*/) { return false; } /// << Load the content from the file. The default implementation does nothing. @see fileName(). virtual bool finishLazyLoad() { return false; } /// << Load what was not loaded by loadFromFile() if it was lazy-loaded virtual bool saveToFile() { return false; } /// << Save the content to the file. The default implementation does nothing. @see fileName(). virtual QString linkAt(const QPointF & /*pos*/) { return QString(); } /// << @return the link anchor at position @p pos or QString() if there is no link. virtual void saveToNode(QXmlStreamWriter &stream); /// << Save the note in the basket XML file. By default it store the filename if a file is used. virtual void fontChanged() = 0; /// << If your content display textual data, called when the font have changed (from tags or basket font) virtual void linkLookChanged() { } /// << If your content use LinkDisplay with preview enabled, reload the preview (can have changed size) virtual QString editToolTipText() const = 0; /// << @return "Edit this [text|image|...]" to put in the tooltip for the note's content zone. virtual void toolTipInfos(QStringList * /*keys*/, QStringList * /*values*/) { } /// << Get "key: value" couples to put in the tooltip for the note's content zone. // Custom Zones: /// Implement this if you want to store custom data. virtual int zoneAt(const QPointF & /*pos*/) { return 0; } /// << If your note-type have custom zones, @return the zone at @p pos or 0 if it's not a custom zone! virtual QRectF zoneRect(int /*zone*/, const QPointF & /*pos*/); /// << Idem, @return the rect of the custom zone virtual QString zoneTip(int /*zone*/) { return QString(); } /// << Idem, @return the toolTip of the custom zone virtual Qt::CursorShape cursorFromZone(int /*zone*/) const { return Qt::ArrowCursor; } /// << Idem, @return the mouse cursor when it is over zone @p zone! virtual void setHoveredZone(int /*oldZone*/, int /*newZone*/) { } /// << If your note type need some feedback, you get notified of hovering changes here. virtual QString statusBarMessage(int /*zone*/) { return QString(); } /// << @return the statusBar message to show for zone @p zone, or QString() if nothing special have to be said. // Drag and Drop Content: virtual void serialize(QDataStream & /*stream*/) { } /// << Serialize the content in a QDragObject. If it consists of a file, it can be serialized for you. virtual bool shouldSerializeFile() { return useFile(); } /// << @return true if the dragging process should serialize the filename (and move the file if cutting). virtual void addAlternateDragObjects(QMimeData * /*dragObj*/) { } /// << If you offer more than toText/Html/Image/Link(), this will be called if this is the only selected. virtual QPixmap feedbackPixmap(qreal width, qreal height) = 0; /// << @return the pixmap to put under the cursor while dragging this object. virtual bool needSpaceForFeedbackPixmap() { return false; } /// << @return true if a space must be inserted before and after the DND feedback pixmap. // Content Edition: virtual int xEditorIndent() { return 0; } /// << If the editor should be indented (eg. to not cover an icon), return the number of pixels. // Open Content or File: virtual QUrl urlToOpen(bool /*with*/); /// << @return the URL to open the note, or an invalid QUrl if it's not openable. If @p with if false, it's a normal "Open". If it's true, it's for an "Open with..." action. The default /// implementation return the fullPath() if the note useFile() and nothing if not. enum OpenMessage { OpenOne, /// << Message to send to the statusbar when opening this note. OpenSeveral, /// << Message to send to the statusbar when opening several notes of this type. OpenOneWith, /// << Message to send to the statusbar when doing "Open With..." on this note. OpenSeveralWith, /// << Message to send to the statusbar when doing "Open With..." several notes of this type. OpenOneWithDialog, /// << Prompt-message of the "Open With..." dialog for this note. OpenSeveralWithDialog /// << Prompt-message of the "Open With..." dialog for several notes of this type. }; virtual QString messageWhenOpening(OpenMessage /*where*/) { return QString(); } /// << @return the message to display according to @p where or nothing if it can't be done. @see OpenMessage describing the nature of the message that should be returned... The default implementation return an empty string. NOTE: If /// urlToOpen() is invalid and messageWhenOpening() is not empty, then the user will be prompted to edit the note (with the message returned by messageWhenOpening()) for eg. being able to edit URL of a link if it's empty when opening /// it... virtual QString customOpenCommand() { return QString(); } /// << Reimplement this if your urlToOpen() should be opened with another application instead of the default KDE one. This choice should be left to the users in the setting (choice to use a custom app or not, and which app). // Common File Management: /// (and do save changes) and optionally hide the toolbar. virtual void setFileName(const QString &fileName); /// << Set the filename. Reimplement it if you eg. want to update the view when the filename is changed. bool trySetFileName(const QString &fileName); /// << Set the new filename and return true. Can fail and return false if a file with this fileName already exists. QString fullPath(); /// << Get the absolute path of the file where this content is stored on disk. QString fileName() const { return m_fileName; } /// << Get the file name where this content is stored (relative to the basket folder). @see fullPath(). qreal minWidth() const { return m_minWidth; } /// << Get the minimum width for this content. Note *note() { return m_note; } /// << Get the note managing this content. BasketScene *basket(); /// << Get the basket containing the note managing this content. virtual QGraphicsItem *graphicsItem() = 0; public: void setEdited(); /// << Mark the note as edited NOW: change the "last modification time and time" AND save the basket to XML file. protected: void contentChanged(qreal newMinWidth); /// << When the content has changed, inherited classes should call this to specify its new minimum size and trigger a basket relayout. private: Note *m_note; QString m_fileName; qreal m_minWidth; public: static const int FEEDBACK_DARKING; }; /** Real implementation of plain text notes: * @author Sébastien Laoût */ class TextContent : public NoteContent { public: // Constructor and destructor: TextContent(Note *parent, const QString &fileName, bool lazyLoad = false); ~TextContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toText(const QString & /*cuttedFullPath*/) override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool lazyLoad) override; bool finishLazyLoad() override; bool saveToFile() override; QString linkAt(const QPointF &pos) override; void fontChanged() override; QString editToolTipText() const override; // Drag and Drop Content: QPixmap feedbackPixmap(qreal width, qreal height) override; // Open Content or File: QString messageWhenOpening(OpenMessage where) override; // QString customOpenCommand(); // Content-Specific Methods: void setText(const QString &text, bool lazyLoad = false); /// << Change the text note-content and relayout the note. QString text() { return m_graphicsTextItem.text(); } /// << @return the text note-content. QGraphicsItem *graphicsItem() override { return &m_graphicsTextItem; } protected: // QString m_text; // QTextDocument *m_simpleRichText; QGraphicsSimpleTextItem m_graphicsTextItem; }; #include /** Real implementation of rich text (HTML) notes: * @author Sébastien Laoût */ class HtmlContent : public NoteContent { public: // Constructor and destructor: HtmlContent(Note *parent, const QString &fileName, bool lazyLoad = false); ~HtmlContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toText(const QString & /*cuttedFullPath*/) override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool lazyLoad) override; bool finishLazyLoad() override; bool saveToFile() override; QString linkAt(const QPointF &pos) override; void fontChanged() override; QString editToolTipText() const override; // Drag and Drop Content: QPixmap feedbackPixmap(qreal width, qreal height) override; // Open Content or File: QString messageWhenOpening(OpenMessage where) override; QString customOpenCommand() override; // Content-Specific Methods: void setHtml(const QString &html, bool lazyLoad = false); /// << Change the HTML note-content and relayout the note. QString html() { return m_html; } /// << @return the HTML note-content. QGraphicsItem *graphicsItem() override { return &m_graphicsTextItem; } protected: QString m_html; QString m_textEquivalent; // OPTIM_FILTER QTextDocument *m_simpleRichText; QGraphicsTextItem m_graphicsTextItem; }; /** Real implementation of image notes: * @author Sébastien Laoût */ class ImageContent : public NoteContent { public: // Constructor and destructor: ImageContent(Note *parent, const QString &fileName, bool lazyLoad = false); ~ImageContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; QPixmap toPixmap() override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool lazyLoad) override; bool finishLazyLoad() override; bool saveToFile() override; void fontChanged() override; QString editToolTipText() const override; void toolTipInfos(QStringList *keys, QStringList *values) override; // Drag and Drop Content: QPixmap feedbackPixmap(qreal width, qreal height) override; bool needSpaceForFeedbackPixmap() override { return true; } // Open Content or File: QString messageWhenOpening(OpenMessage where) override; QString customOpenCommand() override; // Content-Specific Methods: void setPixmap(const QPixmap &pixmap); /// << Change the pixmap note-content and relayout the note. QPixmap pixmap() { return m_pixmapItem.pixmap(); } /// << @return the pixmap note-content. QByteArray data(); QGraphicsItem *graphicsItem() override { return &m_pixmapItem; } protected: QGraphicsPixmapItem m_pixmapItem; QByteArray m_format; }; /** Real implementation of animated image (GIF, MNG) notes: * @author Sébastien Laoût */ class AnimationContent : public QObject, public NoteContent // QObject to be able to receive QMovie signals { Q_OBJECT public: // Constructor and destructor: AnimationContent(Note *parent, const QString &fileName, bool lazyLoad = false); ~AnimationContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; QPixmap toPixmap() override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; void fontChanged() override; QString editToolTipText() const override; // Drag and Drop Content: QPixmap feedbackPixmap(qreal width, qreal height) override; bool needSpaceForFeedbackPixmap() override { return true; } // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool lazyLoad) override; bool finishLazyLoad() override; bool saveToFile() override; // Open Content or File: QString messageWhenOpening(OpenMessage where) override; QString customOpenCommand() override; QGraphicsItem *graphicsItem() override { return &m_graphicsPixmap; } // Content-Specific Methods: bool startMovie(); protected slots: void movieUpdated(); void movieResized(); void movieFrameChanged(); protected: QBuffer *m_buffer; QMovie *m_movie; qreal m_currentWidth; QGraphicsPixmapItem m_graphicsPixmap; }; /** Real implementation of file notes: * @author Sébastien Laoût */ class FileContent : public QObject, public NoteContent { Q_OBJECT public: // Constructor and destructor: FileContent(Note *parent, const QString &fileName); ~FileContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool /*lazyLoad*/) override; void fontChanged() override; void linkLookChanged() override; QString editToolTipText() const override; void toolTipInfos(QStringList *keys, QStringList *values) override; // Drag and Drop Content: QPixmap feedbackPixmap(qreal width, qreal height) override; // Custom Zones: int zoneAt(const QPointF &pos) override; QRectF zoneRect(int zone, const QPointF & /*pos*/) override; QString zoneTip(int zone) override; Qt::CursorShape cursorFromZone(int zone) const override; // Content Edition: int xEditorIndent() override; // Open Content or File: QString messageWhenOpening(OpenMessage where) override; // Content-Specific Methods: void setFileName(const QString &fileName) override; /// << Reimplemented to be able to relayout the note. virtual LinkLook *linkLook() { return LinkLook::fileLook; } QGraphicsItem *graphicsItem() override { return &m_linkDisplayItem; } protected: LinkDisplayItem m_linkDisplayItem; // File Preview Management: protected slots: void newPreview(const KFileItem &, const QPixmap &preview); void removePreview(const KFileItem &); void startFetchingUrlPreview(); protected: KIO::PreviewJob *m_previewJob; }; /** Real implementation of sound notes: * @author Sébastien Laoût */ class SoundContent : public FileContent // A sound is a file with just a bit different user interaction { Q_OBJECT public: // Constructor and destructor: SoundContent(Note *parent, const QString &fileName); // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; QString editToolTipText() const override; // Complex Generic Methods: QString cssClass() const override; // Custom Zones: QString zoneTip(int zone) override; void setHoveredZone(int oldZone, int newZone) override; // Open Content or File: QString messageWhenOpening(OpenMessage where) override; QString customOpenCommand() override; // Content-Specific Methods: LinkLook *linkLook() override { return LinkLook::soundLook; } Phonon::MediaObject *music; private slots: void stateChanged(Phonon::State, Phonon::State); }; /** Real implementation of link notes: * @author Sébastien Laoût */ class LinkContent : public QObject, public NoteContent { Q_OBJECT public: // Constructor and destructor: LinkContent(Note *parent, const QUrl &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon); ~LinkContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toText(const QString & /*cuttedFullPath*/) override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; void toLink(QUrl *url, QString *title, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; void saveToNode(QXmlStreamWriter &stream) override; void fontChanged() override; void linkLookChanged() override; QString editToolTipText() const override; void toolTipInfos(QStringList *keys, QStringList *values) override; // Drag and Drop Content: void serialize(QDataStream &stream) override; QPixmap feedbackPixmap(qreal width, qreal height) override; // Custom Zones: int zoneAt(const QPointF &pos) override; QRectF zoneRect(int zone, const QPointF & /*pos*/) override; QString zoneTip(int zone) override; Qt::CursorShape cursorFromZone(int zone) const override; QString statusBarMessage(int zone) override; // Open Content or File: QUrl urlToOpen(bool /*with*/) override; QString messageWhenOpening(OpenMessage where) override; // Content-Specific Methods: void setLink(const QUrl &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon); /// << Change the link and relayout the note. QUrl url() { return m_url; } /// << @return the URL of the link note-content. QString title() { return m_title; } /// << @return the displayed title of the link note-content. QString icon() { return m_icon; } /// << @return the displayed icon of the link note-content. bool autoTitle() { return m_autoTitle; } /// << @return if the title is auto-computed from the URL. bool autoIcon() { return m_autoIcon; } /// << @return if the icon is auto-computed from the URL. void startFetchingLinkTitle(); QGraphicsItem *graphicsItem() override { return &m_linkDisplayItem; } protected: QUrl m_url; QString m_title; QString m_icon; bool m_autoTitle; bool m_autoIcon; LinkDisplayItem m_linkDisplayItem; KIO::Integration::AccessManager *m_access_manager; QNetworkReply *m_reply; QByteArray m_httpBuff; ///< Accumulator for downloaded HTTP data with yet unknown encoding bool m_acceptingData; ///< When false, don't accept any HTTP data // File Preview Management: protected slots: void httpReadyRead(); void httpDone(QNetworkReply *reply); void newPreview(const KFileItem &, const QPixmap &preview); void removePreview(const KFileItem &); void startFetchingUrlPreview(); protected: KIO::PreviewJob *m_previewJob; private: void decodeHtmlTitle(); ///< Detect encoding of \p m_httpBuff and extract the title from HTML void endFetchingLinkTitle(); ///< Extract title and clear http buffer }; /** Real implementation of cross reference notes: * Copied and modified from LinkContent. * @author Brian C. Milco */ class CrossReferenceContent : public QObject, public NoteContent { Q_OBJECT public: // Constructor and destructor: CrossReferenceContent(Note *parent, const QUrl &url, const QString &title, const QString &icon); ~CrossReferenceContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toText(const QString & /*cuttedFullPath*/) override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; void toLink(QUrl *url, QString *title, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal) override; void saveToNode(QXmlStreamWriter &stream) override; void fontChanged() override; void linkLookChanged() override; QString editToolTipText() const override; void toolTipInfos(QStringList *keys, QStringList *values) override; // Drag and Drop Content: void serialize(QDataStream &stream) override; QPixmap feedbackPixmap(qreal width, qreal height) override; // Custom Zones: int zoneAt(const QPointF &pos) override; QRectF zoneRect(int zone, const QPointF & /*pos*/) override; QString zoneTip(int zone) override; Qt::CursorShape cursorFromZone(int zone) const override; QString statusBarMessage(int zone) override; // Open Content or File: QUrl urlToOpen(bool /*with*/) override; QString messageWhenOpening(OpenMessage where) override; // Content-Specific Methods: void setLink(const QUrl &url, const QString &title, const QString &icon); /// << Change the link and relayout the note. void setCrossReference(const QUrl &url, const QString &title, const QString &icon); QUrl url() { return m_url; } /// << @return the URL of the link note-content. QString title() { return m_title; } /// << @return the displayed title of the link note-content. QString icon() { return m_icon; } /// << @return the displayed icon of the link note-content. QGraphicsItem *graphicsItem() override { return &m_linkDisplayItem; } protected: QUrl m_url; QString m_title; QString m_icon; LinkDisplayItem m_linkDisplayItem; }; /** Real implementation of launcher notes: * @author Sébastien Laoût */ class LauncherContent : public NoteContent { public: // Constructor and destructor: LauncherContent(Note *parent, const QString &fileName); ~LauncherContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; void toLink(QUrl *url, QString *title, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool /*lazyLoad*/) override; void fontChanged() override; QString editToolTipText() const override; void toolTipInfos(QStringList *keys, QStringList *values) override; // Drag and Drop Content: QPixmap feedbackPixmap(qreal width, qreal height) override; // Custom Zones: int zoneAt(const QPointF &pos) override; QRectF zoneRect(int zone, const QPointF & /*pos*/) override; QString zoneTip(int zone) override; Qt::CursorShape cursorFromZone(int zone) const override; // Open Content or File: QUrl urlToOpen(bool with) override; QString messageWhenOpening(OpenMessage where) override; // Content-Specific Methods: void setLauncher(const QString &name, const QString &icon, const QString &exec); /// << Change the launcher note-content and relayout the note. Normally called by loadFromFile (no save done). QString name() { return m_name; } /// << @return the URL of the launcher note-content. QString icon() { return m_icon; } /// << @return the displayed icon of the launcher note-content. QString exec() { return m_exec; } /// << @return the execute command line of the launcher note-content. // TODO: KService *service() ??? And store everything in thta service ? QGraphicsItem *graphicsItem() override { return &m_linkDisplayItem; } protected: QString m_name; // TODO: Store them in linkDisplay to gain place (idem for Link notes) QString m_icon; QString m_exec; LinkDisplayItem m_linkDisplayItem; }; /** * */ class ColorItem : public QGraphicsItem { public: ColorItem(Note *parent, const QColor &color); // virtual ~ColorItem(); virtual QColor color() { return m_color; } virtual void setColor(const QColor &color); QRectF boundingRect() const override; void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override; private: Note *m_note; QColor m_color; QRectF m_textRect; static const int RECT_MARGIN; }; /** Real implementation of color notes: * @author Sébastien Laoût */ class ColorContent : public NoteContent { public: // Constructor and destructor: ColorContent(Note *parent, const QColor &color); ~ColorContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toText(const QString & /*cuttedFullPath*/) override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; void saveToNode(QXmlStreamWriter &stream) override; void fontChanged() override; QString editToolTipText() const override; void toolTipInfos(QStringList *keys, QStringList *values) override; // Drag and Drop Content: void serialize(QDataStream &stream) override; QPixmap feedbackPixmap(qreal width, qreal height) override; bool needSpaceForFeedbackPixmap() override { return true; } void addAlternateDragObjects(QMimeData *dragObject) override; // Content-Specific Methods: void setColor(const QColor &color); /// << Change the color note-content and relayout the note. QColor color() { return m_colorItem.color(); } /// << @return the color note-content. QGraphicsItem *graphicsItem() override { return &m_colorItem; } protected: ColorItem m_colorItem; }; /** * */ class UnknownItem : public QGraphicsItem { public: UnknownItem(Note *parent); QRectF boundingRect() const override; void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override; virtual QString mimeTypes() { return m_mimeTypes; } virtual void setMimeTypes(QString mimeTypes); virtual void setWidth(qreal width); private: Note *m_note; QString m_mimeTypes; QRectF m_textRect; static const qreal DECORATION_MARGIN; }; /** Real implementation of unknown MIME-types dropped notes: * @author Sébastien Laoût */ class UnknownContent : public NoteContent { public: // Constructor and destructor: UnknownContent(Note *parent, const QString &fileName); ~UnknownContent() override; // Simple Generic Methods: NoteType::Id type() const override; QString typeName() const override; QString lowerTypeName() const override; QString toText(const QString & /*cuttedFullPath*/) override; QString toHtml(const QString &imageName, const QString &cuttedFullPath) override; void toLink(QUrl *url, QString *title, const QString &cuttedFullPath) override; bool useFile() const override; bool canBeSavedAs() const override; QString saveAsFilters() const override; bool match(const FilterData &data) override; // Complex Generic Methods: void exportToHTML(HTMLExporter *exporter, int indent) override; QString cssClass() const override; qreal setWidthAndGetHeight(qreal width) override; bool loadFromFile(bool /*lazyLoad*/) override; void fontChanged() override; QString editToolTipText() const override; // Drag and Drop Content: bool shouldSerializeFile() override { return false; } void addAlternateDragObjects(QMimeData *dragObject) override; QPixmap feedbackPixmap(qreal width, qreal height) override; bool needSpaceForFeedbackPixmap() override { return true; } // Open Content or File: QUrl urlToOpen(bool /*with*/) override { return QUrl(); } QGraphicsItem *graphicsItem() override { return &m_unknownItem; } // Content-Specific Methods: QString mimeTypes() { return m_unknownItem.mimeTypes(); } /// << @return the list of MIME types this note-content contains. private: UnknownItem m_unknownItem; }; -void NoteFactory__loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad); - #endif // NOTECONTENT_H diff --git a/src/notefactory.cpp b/src/notefactory.cpp index 9422eb4..1557d94 100644 --- a/src/notefactory.cpp +++ b/src/notefactory.cpp @@ -1,1041 +1,1074 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "notefactory.h" +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //For createHeuristicMask #include #include #include #include #include #include //For Qt::mightBeRichText(...) #include //For KGlobal::mainComponent().aboutData(...) #include #include #include #include #include #include #include #include #include "basketlistview.h" #include "basketscene.h" #include "file_mimetypes.h" #include "global.h" #include "note.h" #include "notedrag.h" #include "settings.h" #include "tools.h" #include "variouswidgets.h" //For IconSizeDialog +#include "xmlwork.h" #include "debugwindow.h" /** Create notes from scratch (just a content) */ Note *NoteFactory::createNoteText(const QString &text, BasketScene *parent, bool reallyPlainText /* = false*/) { Note *note = new Note(parent); if (reallyPlainText) { TextContent *content = new TextContent(note, createFileForNewNote(parent, "txt")); content->setText(text); content->saveToFile(); } else { HtmlContent *content = new HtmlContent(note, createFileForNewNote(parent, "html")); QString html = "" + Tools::textToHTMLWithoutP(text) + ""; content->setHtml(html); content->saveToFile(); } return note; } Note *NoteFactory::createNoteHtml(const QString &html, BasketScene *parent) { Note *note = new Note(parent); HtmlContent *content = new HtmlContent(note, createFileForNewNote(parent, "html")); content->setHtml(html); content->saveToFile(); return note; } Note *NoteFactory::createNoteLink(const QUrl &url, BasketScene *parent) { Note *note = new Note(parent); new LinkContent(note, url, titleForURL(url), iconForURL(url), /*autoTitle=*/true, /*autoIcon=*/true); return note; } Note *NoteFactory::createNoteLink(const QUrl &url, const QString &title, BasketScene *parent) { Note *note = new Note(parent); new LinkContent(note, url, title, iconForURL(url), /*autoTitle=*/false, /*autoIcon=*/true); return note; } Note *NoteFactory::createNoteCrossReference(const QUrl &url, BasketScene *parent) { Note *note = new Note(parent); new CrossReferenceContent(note, url, titleForURL(url), iconForURL(url)); return note; } Note *NoteFactory::createNoteCrossReference(const QUrl &url, const QString &title, BasketScene *parent) { Note *note = new Note(parent); new CrossReferenceContent(note, url, title, iconForURL(url)); return note; } Note *NoteFactory::createNoteCrossReference(const QUrl &url, const QString &title, const QString &icon, BasketScene *parent) { Note *note = new Note(parent); new CrossReferenceContent(note, url, title, icon); return note; } Note *NoteFactory::createNoteImage(const QPixmap &image, BasketScene *parent) { Note *note = new Note(parent); ImageContent *content = new ImageContent(note, createFileForNewNote(parent, "png")); content->setPixmap(image); content->saveToFile(); return note; } Note *NoteFactory::createNoteColor(const QColor &color, BasketScene *parent) { Note *note = new Note(parent); new ColorContent(note, color); return note; } /** Return a string list containing {url1, title1, url2, title2, url3, title3...} */ QStringList NoteFactory::textToURLList(const QString &text) { // List to return: QStringList list; // Split lines: QStringList texts = text.split('\n'); // For each lines: QStringList::iterator it; for (it = texts.begin(); it != texts.end(); ++it) { // Strip white spaces: (*it) = (*it).trimmed(); // Don't care of empty entries: if ((*it).isEmpty()) continue; // Compute lower case equivalent: QString ltext = (*it).toLower(); /* Search for mail address ("*@*.*" ; "*" can contain '_', '-', or '.') and add protocol to it */ QString mailExpString = "[\\w-\\.]+@[\\w-\\.]+\\.[\\w]+"; QRegExp mailExp("^" + mailExpString + '$'); if (mailExp.exactMatch(ltext)) { ltext.insert(0, "mailto:"); (*it).insert(0, "mailto:"); } // TODO: Recognize "" (link between '<' and '>') // TODO: Replace " at " by "@" and " dot " by "." to look for e-mail addresses /* Search for mail address like "Name " */ QRegExp namedMailExp("^([\\w\\s]+)\\s<(" + mailExpString + ")>$"); // namedMailExp.setCaseSensitive(true); // For the name to be keeped with uppercases // DOESN'T WORK ! if (namedMailExp.exactMatch(ltext)) { QString name = namedMailExp.cap(1); QString address = "mailto:" + namedMailExp.cap(2); // Threat it NOW, as it's an exception (it have a title): list.append(address); list.append(name); continue; } /* Search for an url and create an URL note */ if ((ltext.startsWith('/') && ltext[1] != '/' && ltext[1] != '*') || // Take files but not C/C++/... comments ! ltext.startsWith(QLatin1String("file:")) || ltext.startsWith(QLatin1String("http://")) || ltext.startsWith(QLatin1String("https://")) || ltext.startsWith(QLatin1String("www.")) || ltext.startsWith(QLatin1String("ftp.")) || ltext.startsWith(QLatin1String("ftp://")) || ltext.startsWith(QLatin1String("mailto:"))) { // First, correct the text to use the good format for the url if (ltext.startsWith('/')) (*it).insert(0, "file:"); if (ltext.startsWith(QLatin1String("www."))) (*it).insert(0, "http://"); if (ltext.startsWith(QLatin1String("ftp."))) (*it).insert(0, "ftp://"); // And create the Url note (or launcher if URL point a .desktop file) list.append(*it); list.append(QString()); // We don't have any title } else return QStringList(); // FAILED: treat the text as a text, and not as a URL list! } return list; } Note *NoteFactory::createNoteFromText(const QString &text, BasketScene *parent) { /* Search for a color (#RGB , #RRGGBB , #RRRGGGBBB , #RRRRGGGGBBBB) and create a color note */ QRegExp exp("^#(?:[a-fA-F\\d]{3}){1,4}$"); if (exp.exactMatch(text)) return createNoteColor(QColor(text), parent); /* Try to convert the text as a URL or a list of URLs */ QStringList uriList = textToURLList(text); if (!uriList.isEmpty()) { // TODO: This code is almost duplicated from fropURLs()! Note *note; Note *firstNote = nullptr; Note *lastInserted = nullptr; QStringList::iterator it; for (it = uriList.begin(); it != uriList.end(); ++it) { QString url = (*it); ++it; QString title = (*it); if (title.isEmpty()) note = createNoteLinkOrLauncher(QUrl::fromUserInput(url), parent); else note = createNoteLink(QUrl::fromUserInput(url), title, parent); // If we got a new note, insert it in a linked list (we will return the first note of that list): if (note) { // qDebug() << "Drop URL: " << (*it).toDisplayString(); if (!firstNote) firstNote = note; else { lastInserted->setNext(note); note->setPrev(lastInserted); } lastInserted = note; } } return firstNote; // It don't return ALL inserted notes ! } // QString newText = text.trimmed(); // The text for a new note, without useless spaces /* Else, it's a text or an HTML note, so, create it */ if (Qt::mightBeRichText(/*newT*/ text)) return createNoteHtml(/*newT*/ text, parent); else return createNoteText(/*newT*/ text, parent); } Note *NoteFactory::createNoteLauncher(const QUrl &url, BasketScene *parent) { if (url.isEmpty()) return createNoteLauncher(QString(), QString(), QString(), parent); else return copyFileAndLoad(url, parent); } Note *NoteFactory::createNoteLauncher(const QString &command, const QString &name, const QString &icon, BasketScene *parent) { QString fileName = createNoteLauncherFile(command, name, icon, parent); if (fileName.isEmpty()) return nullptr; else return loadFile(fileName, parent); } QString NoteFactory::createNoteLauncherFile(const QString &command, const QString &name, const QString &icon, BasketScene *parent) { QString content = QString( "[Desktop Entry]\n" "Exec=%1\n" "Name=%2\n" "Icon=%3\n" "Encoding=UTF-8\n" "Type=Application\n") .arg(command, name, icon.isEmpty() ? QString("exec") : icon); QString fileName = fileNameForNewNote(parent, "launcher.desktop"); QString fullPath = parent->fullPathForFileName(fileName); // parent->dontCareOfCreation(fullPath); QFile file(fullPath); if (file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); stream.setCodec("UTF-8"); stream << content; file.close(); return fileName; } else return QString(); } Note *NoteFactory::createNoteLinkOrLauncher(const QUrl &url, BasketScene *parent) { // IMPORTANT: we create the service ONLY if the extension is ".desktop". // Otherwise, KService take a long time to analyze all the file // and output such things to stdout: // "Invalid entry (missing '=') at /my/file.ogg:11984" // "Invalid entry (missing ']') at /my/file.ogg:11984"... KService::Ptr service; if (url.fileName().endsWith(QLatin1String(".desktop"))) service = new KService(url.path()); // If link point to a .desktop file then add a launcher, otherwise it's a link if (service && service->isValid()) return createNoteLauncher(url, parent); else return createNoteLink(url, parent); } bool NoteFactory::movingNotesInTheSameBasket(const QMimeData *source, BasketScene *parent, Qt::DropAction action) { if (NoteDrag::canDecode(source)) return action == Qt::MoveAction && NoteDrag::basketOf(source) == parent; else return false; } Note *NoteFactory::dropNote(const QMimeData *source, BasketScene *parent, bool fromDrop, Qt::DropAction action, Note * /*noteSource*/) { if (source == nullptr) { return nullptr; } Note *note = nullptr; QStringList formats = source->formats(); /* No data */ if (formats.size() == 0) { // TODO: add a parameter to say if it's from a clipboard paste, a selection paste, or a drop // To be able to say "The clipboard/selection/drop is empty". // KMessageBox::error(parent, i18n("There is no data to insert."), i18n("No Data")); return nullptr; } /* Debug */ if (Global::debugWindow) { *Global::debugWindow << "Drop :"; for (int i = 0; i < formats.size(); ++i) *Global::debugWindow << "\t[" + QString::number(i) + "] " + formats[i]; switch (action) { // The source want that we: case Qt::CopyAction: *Global::debugWindow << ">> Drop action: Copy"; break; case Qt::MoveAction: *Global::debugWindow << ">> Drop action: Move"; break; case Qt::LinkAction: *Global::debugWindow << ">> Drop action: Link"; break; default: *Global::debugWindow << ">> Drop action: Unknown"; // supported by Qt! } } /* Copy or move a Note */ if (NoteDrag::canDecode(source)) { bool moveFiles = fromDrop && action == Qt::MoveAction; bool moveNotes = moveFiles; return NoteDrag::decode(source, parent, moveFiles, moveNotes); // Filename will be kept } /* Else : Drop object to note */ QImage image = qvariant_cast(source->imageData()); if (!image.isNull()) return createNoteImage(QPixmap::fromImage(image), parent); if (source->hasColor()) { return createNoteColor(qvariant_cast(source->colorData()), parent); } // And then the hack (if provide color MIME type or a text that contains color), using createNote Color RegExp: QString hack; QRegExp exp("^#(?:[a-fA-F\\d]{3}){1,4}$"); hack = source->text(); if (source->hasFormat("application/x-color") || (!hack.isNull() && exp.exactMatch(hack))) { QColor color = qvariant_cast(source->colorData()); if (color.isValid()) return createNoteColor(color, parent); // if ( (note = createNoteColor(color, parent)) ) // return note; // // Theoretically it should be returned. If not, continue by dropping other things } QList urls = source->urls(); if (!urls.isEmpty()) { // If it's a Paste, we should know if files should be copied (copy&paste) or moved (cut&paste): if (!fromDrop && Tools::isAFileCut(source)) action = Qt::MoveAction; return dropURLs(urls, parent, action, fromDrop); } // FIXME: use dropURLs() also from Mozilla? /* * Mozilla's stuff sometimes uses utf-16-le - little-endian UTF-16. * * This has the property that for the ASCII subset case (And indeed, the * ISO-8859-1 subset, I think), if you treat it as a C-style string, * it'll come out to one character long in most cases, since it looks * like: * * "<\0H\0T\0M\0L\0>\0" * * A strlen() call on that will give you 1, which simply isn't correct. * That might, I suppose, be the answer, or something close. * * Also, Mozilla's drag/drop code predates the use of MIME types in XDnD * - hence it'll throw about STRING and UTF8_STRING quite happily, hence * the odd named types. * * Thanks to Dave Cridland for having said me that. */ if (source->hasFormat("text/x-moz-url")) { // FOR MOZILLA // Get the array and create a QChar array of 1/2 of the size QByteArray mozilla = source->data("text/x-moz-url"); QVector chars(mozilla.count() / 2); // A small debug work to know the value of each bytes if (Global::debugWindow) for (int i = 0; i < mozilla.count(); i++) *Global::debugWindow << QString("'") + QChar(mozilla[i]) + "' " + QString::number(int(mozilla[i])); // text/x-moz-url give the URL followed by the link title and separated by OxOA (10 decimal: new line?) uint size = 0; QChar *name = nullptr; // For each little endian mozilla chars, copy it to the array of QChars for (int i = 0; i < mozilla.count(); i += 2) { chars[i / 2] = QChar(mozilla[i], mozilla[i + 1]); if (mozilla.at(i) == 0x0A) { size = i / 2; name = &(chars[i / 2 + 1]); } } // Create a QString that take the address of the first QChar and a length if (name == nullptr) { // We haven't found name (FIXME: Is it possible ?) QString normalHtml(&(chars[0]), chars.size()); return createNoteLink(normalHtml, parent); } else { QString normalHtml(&(chars[0]), size); QString normalTitle(name, chars.size() - size - 1); return createNoteLink(normalHtml, normalTitle, parent); } } if (source->hasFormat("text/html")) { QString html; QString subtype("html"); // If the text/html comes from Mozilla or GNOME it can be UTF-16 encoded: we need ExtendedTextDrag to check that ExtendedTextDrag::decode(source, html, subtype); return createNoteHtml(html, parent); } QString text; // If the text/plain comes from GEdit or GNOME it can be empty: we need ExtendedTextDrag to check other MIME types if (ExtendedTextDrag::decode(source, text)) return createNoteFromText(text, parent); /* Create a cross reference note */ if (source->hasFormat(BasketTreeListView::TREE_ITEM_MIME_STRING)) { QByteArray data = source->data(BasketTreeListView::TREE_ITEM_MIME_STRING); QDataStream stream(&data, QIODevice::ReadOnly); QString basketName, folderName, icon; while (!stream.atEnd()) stream >> basketName >> folderName >> icon; return createNoteCrossReference(QUrl("basket://" + folderName), basketName, icon, parent); } /* Unsuccessful drop */ note = createNoteUnknown(source, parent); QString message = i18n( "

%1 doesn't support the data you've dropped.
" "It however created a generic note, allowing you to drag or copy it to an application that understand it.

" "

If you want the support of these data, please contact developer.

", QGuiApplication::applicationDisplayName()); KMessageBox::information(parent->graphicsView()->viewport(), message, i18n("Unsupported MIME Type(s)"), "unsupportedDropInfo", KMessageBox::AllowLink); return note; } Note *NoteFactory::createNoteUnknown(const QMimeData *source, BasketScene *parent /*, const QString &annotations*/) { // Save the MimeSource in a file: create and open the file: QString fileName = createFileForNewNote(parent, "unknown"); QFile file(parent->fullPath() + fileName); if (!file.open(QIODevice::WriteOnly)) return nullptr; QDataStream stream(&file); // Echo MIME types: QStringList formats = source->formats(); for (int i = 0; formats.size(); ++i) stream << QString(formats[i]); // Output the '\0'-terminated format name string // Echo end of MIME types list delimiter: stream << QString(); // Echo the length (in bytes) and then the data, and then same for next MIME type: for (int i = 0; formats.size(); ++i) { QByteArray data = source->data(formats[i]); stream << (quint32)data.count(); stream.writeRawData(data.data(), data.count()); } file.close(); Note *note = new Note(parent); new UnknownContent(note, fileName); return note; } Note *NoteFactory::dropURLs(QList urls, BasketScene *parent, Qt::DropAction action, bool fromDrop) { KModifierKeyInfo keyinfo; int shouldAsk = 0; // shouldAsk==0: don't ask ; shouldAsk==1: ask for "file" ; shouldAsk>=2: ask for "files" bool shiftPressed = keyinfo.isKeyPressed(Qt::Key_Shift); bool ctrlPressed = keyinfo.isKeyPressed(Qt::Key_Control); bool modified = fromDrop && (shiftPressed || ctrlPressed); if (modified) // Then no menu + modified action ; // action is already set: no work to do else if (fromDrop) { // Compute if user should be asked or not for (QList::iterator it = urls.begin(); it != urls.end(); ++it) if ((*it).scheme() != "mailto") { // Do not ask when dropping mail address :-) shouldAsk++; if (shouldAsk == 1 /*2*/) // Sufficient break; } if (shouldAsk) { QMenu menu(parent->graphicsView()); QList actList; actList << new QAction(QIcon::fromTheme("go-jump"), i18n("&Move Here\tShift"), &menu) << new QAction(QIcon::fromTheme("edit-copy"), i18n("&Copy Here\tCtrl"), &menu) << new QAction(QIcon::fromTheme("insert-link"), i18n("&Link Here\tCtrl+Shift"), &menu); foreach (QAction *a, actList) menu.addAction(a); menu.addSeparator(); menu.addAction(QIcon::fromTheme("dialog-cancel"), i18n("C&ancel\tEscape")); int id = actList.indexOf(menu.exec(QCursor::pos())); switch (id) { case 0: action = Qt::MoveAction; break; case 1: action = Qt::CopyAction; break; case 2: action = Qt::LinkAction; break; default: return nullptr; } modified = true; } } else { // fromPaste ; } /* Policy of drops of URL: * Email: [Modifier keys: Useless] + - Link mail address * Remote URL: [Modifier keys: {Copy,Link}] + - Download as Image, Animation and Launcher + - Link other URLs * Local URL: [Modifier keys: {Copy,Move,Link}] * - Copy as Image, Animation and Launcher [Modifier keys: {Copy,Move,Link}] * - Link folder [Modifier keys: Useless] * - Make Launcher of executable [Modifier keys: {Copy_exec,Move_exec,Link_Launcher}] * - Ask for file (if use want to copy and it is a sound: make Sound) * Policy of pastes of URL: [NO modifier keys] * - Same as drops * - But copy when ask should be done * - Unless cut-selection is true: move files instead * Policy of file created in the basket dir: [NO modifier keys] * - View as Image, Animation, Sound, Launcher * - View as File */ Note *note; Note *firstNote = nullptr; Note *lastInserted = nullptr; for (QList::iterator it = urls.begin(); it != urls.end(); ++it) { if (((*it).scheme() == "mailto") || (action == Qt::LinkAction)) note = createNoteLinkOrLauncher(*it, parent); // else if (!(*it).isLocalFile()) { // if (action != Qt::LinkAction && (maybeImageOrAnimation(*it)/* || maybeSound(*it)*/)) // note = copyFileAndLoad(*it, parent); // else // note = createNoteLinkOrLauncher(*it, parent); // } else { if (action == Qt::CopyAction) note = copyFileAndLoad(*it, parent); else if (action == Qt::MoveAction) note = moveFileAndLoad(*it, parent); else note = createNoteLinkOrLauncher(*it, parent); } // If we got a new note, insert it in a linked list (we will return the first note of that list): if (note) { DEBUG_WIN << "Drop URL: " + (*it).toDisplayString(); if (!firstNote) firstNote = note; else { lastInserted->setNext(note); note->setPrev(lastInserted); } lastInserted = note; } } return firstNote; } void NoteFactory::consumeContent(QDataStream &stream, NoteType::Id type) { if (type == NoteType::Link) { QUrl url; QString title, icon; quint64 autoTitle64, autoIcon64; stream >> url >> title >> icon >> autoTitle64 >> autoIcon64; } else if (type == NoteType::CrossReference) { QUrl url; QString title, icon; stream >> url >> title >> icon; } else if (type == NoteType::Color) { QColor color; stream >> color; } } Note *NoteFactory::decodeContent(QDataStream &stream, NoteType::Id type, BasketScene *parent) { /* if (type == NoteType::Text) { QString text; stream >> text; return NoteFactory::createNoteText(text, parent); } else if (type == NoteType::Html) { QString html; stream >> html; return NoteFactory::createNoteHtml(html, parent); } else if (type == NoteType::Image) { QPixmap pixmap; stream >> pixmap; return NoteFactory::createNoteImage(pixmap, parent); } else */ if (type == NoteType::Link) { QUrl url; QString title, icon; quint64 autoTitle64, autoIcon64; bool autoTitle, autoIcon; stream >> url >> title >> icon >> autoTitle64 >> autoIcon64; autoTitle = (bool)autoTitle64; autoIcon = (bool)autoIcon64; Note *note = NoteFactory::createNoteLink(url, parent); ((LinkContent *)(note->content()))->setLink(url, title, icon, autoTitle, autoIcon); return note; } else if (type == NoteType::CrossReference) { QUrl url; QString title, icon; stream >> url >> title >> icon; Note *note = NoteFactory::createNoteCrossReference(url, parent); ((CrossReferenceContent *)(note->content()))->setCrossReference(url, title, icon); return note; } else if (type == NoteType::Color) { QColor color; stream >> color; return NoteFactory::createNoteColor(color, parent); } else return nullptr; // NoteFactory::loadFile() is sufficient } bool NoteFactory::maybeText(const QMimeType &mimeType) { return mimeType.inherits(MimeTypes::TEXT); } bool NoteFactory::maybeHtml(const QMimeType &mimeType) { return mimeType.inherits(MimeTypes::HTML); } bool NoteFactory::maybeImage(const QMimeType &mimeType) { return mimeType.name().startsWith(MimeTypes::IMAGE); } bool NoteFactory::maybeAnimation(const QMimeType &mimeType) { return mimeType.inherits(MimeTypes::ANIMATION) || mimeType.name() == MimeTypes::ANIMATION_MNG; } bool NoteFactory::maybeSound(const QMimeType &mimeType) { return mimeType.name().startsWith(MimeTypes::AUDIO); } bool NoteFactory::maybeLauncher(const QMimeType &mimeType) { return mimeType.inherits(MimeTypes::LAUNCHER); } ////////////// NEW: Note *NoteFactory::copyFileAndLoad(const QUrl &url, BasketScene *parent) { QString fileName = fileNameForNewNote(parent, url.fileName()); QString fullPath = parent->fullPathForFileName(fileName); if (Global::debugWindow) *Global::debugWindow << "copyFileAndLoad: " + url.toDisplayString() + " to " + fullPath; // QString annotations = i18n("Original file: %1", url.toDisplayString()); // parent->dontCareOfCreation(fullPath); KIO::CopyJob *copyJob = KIO::copy(url, QUrl::fromLocalFile(fullPath), KIO::Overwrite | KIO::Resume); parent->connect(copyJob, &KIO::CopyJob::copyingDone, parent, &BasketScene::slotCopyingDone2); NoteType::Id type = typeForURL(url, parent); // Use the type of the original file because the target doesn't exist yet return loadFile(fileName, type, parent); } Note *NoteFactory::moveFileAndLoad(const QUrl &url, BasketScene *parent) { // Globally the same as copyFileAndLoad() but move instead of copy (KIO::move()) QString fileName = fileNameForNewNote(parent, url.fileName()); QString fullPath = parent->fullPathForFileName(fileName); if (Global::debugWindow) *Global::debugWindow << "moveFileAndLoad: " + url.toDisplayString() + " to " + fullPath; // QString annotations = i18n("Original file: %1", url.toDisplayString()); // parent->dontCareOfCreation(fullPath); KIO::CopyJob *copyJob = KIO::move(url, QUrl::fromLocalFile(fullPath), KIO::Overwrite | KIO::Resume); parent->connect(copyJob, &KIO::CopyJob::copyingDone, parent, &BasketScene::slotCopyingDone2); NoteType::Id type = typeForURL(url, parent); // Use the type of the original file because the target doesn't exist yet return loadFile(fileName, type, parent); } Note *NoteFactory::loadFile(const QString &fileName, BasketScene *parent) { // The file MUST exists QFileInfo file(QUrl::fromLocalFile(parent->fullPathForFileName(fileName)).path()); if (!file.exists()) return nullptr; NoteType::Id type = typeForURL(parent->fullPathForFileName(fileName), parent); Note *note = loadFile(fileName, type, parent); return note; } Note *NoteFactory::loadFile(const QString &fileName, NoteType::Id type, BasketScene *parent) { Note *note = new Note(parent); switch (type) { case NoteType::Text: new TextContent(note, fileName); break; case NoteType::Html: new HtmlContent(note, fileName); break; case NoteType::Image: new ImageContent(note, fileName); break; case NoteType::Animation: new AnimationContent(note, fileName); break; case NoteType::Sound: new SoundContent(note, fileName); break; case NoteType::File: new FileContent(note, fileName); break; case NoteType::Launcher: new LauncherContent(note, fileName); break; case NoteType::Unknown: new UnknownContent(note, fileName); break; default: case NoteType::Link: case NoteType::CrossReference: case NoteType::Color: return nullptr; } return note; } NoteType::Id NoteFactory::typeForURL(const QUrl &url, BasketScene * /*parent*/) { bool viewText = Settings::viewTextFileContent(); bool viewHTML = Settings::viewHtmlFileContent(); bool viewImage = Settings::viewImageFileContent(); bool viewSound = Settings::viewSoundFileContent(); QMimeDatabase db; QMimeType mimeType = db.mimeTypeForUrl(url); if (Global::debugWindow) { if (mimeType.isValid()) *Global::debugWindow << "typeForURL: " + url.toDisplayString() + " ; MIME type = " + mimeType.name(); else *Global::debugWindow << "typeForURL: mimeType is empty for " + url.toDisplayString(); } // Go from specific to more generic if (maybeLauncher(mimeType)) return NoteType::Launcher; else if (viewHTML && (maybeHtml(mimeType))) return NoteType::Html; else if (viewText && maybeText(mimeType)) return NoteType::Text; else if (viewImage && maybeAnimation(mimeType)) return NoteType::Animation; // See Note::movieStatus(int) else if (viewImage && maybeImage(mimeType)) return NoteType::Image; // for more explanations else if (viewSound && maybeSound(mimeType)) return NoteType::Sound; else return NoteType::File; } QString NoteFactory::fileNameForNewNote(BasketScene *parent, const QString &wantedName) { return Tools::fileNameForNewFile(wantedName, parent->fullPath()); } // Create a file to store a new note in Basket parent and with extension extension. // If wantedName is provided, the function will first try to use this file name, or derive it if it's impossible // (extension willn't be used for that case) QString NoteFactory::createFileForNewNote(BasketScene *parent, const QString &extension, const QString &wantedName) { QString fileName; QString fullName; if (wantedName.isEmpty()) { // TODO: fileNameForNewNote(parent, "note1."+extension); QDir dir; int nb = parent->count() + 1; QString time = QTime::currentTime().toString("hhmmss"); for (;; ++nb) { fileName = QString("note%1-%2.%3").arg(nb).arg(time).arg(extension); fullName = parent->fullPath() + fileName; dir = QDir(fullName); if (!dir.exists(fullName)) break; } } else { fileName = fileNameForNewNote(parent, wantedName); fullName = parent->fullPath() + fileName; } // Create the file // parent->dontCareOfCreation(fullName); QFile file(fullName); file.open(QIODevice::WriteOnly); file.close(); return fileName; } QUrl NoteFactory::filteredURL(const QUrl &url) { // KURIFilter::filteredURI() is slow if the URL contains only letters, digits and '-' or '+'. // So, we don't use that function is that case: bool isSlow = true; for (int i = 0; i < url.url().length(); ++i) { QChar c = url.url()[i]; if (!c.isLetterOrNumber() && c != '-' && c != '+') { isSlow = false; break; } } if (isSlow) return url; else { QStringList list; list << QLatin1String("kshorturifilter") << QLatin1String("kuriikwsfilter") << QLatin1String("kurisearchfilter") << QLatin1String("localdomainfilter") << QLatin1String("fixuphosturifilter"); return KUriFilter::self()->filteredUri(url, list); } } QString NoteFactory::titleForURL(const QUrl &url) { QString title = url.toDisplayString(); QString home = "file:" + QDir::homePath() + '/'; if (title.startsWith(QLatin1String("mailto:"))) return title.remove(0, 7); if (title.startsWith(home)) title = "~/" + title.remove(0, home.length()); if (title.startsWith(QLatin1String("file://"))) title = title.remove(0, 7); // 7 == QString("file://").length() - 1 else if (title.startsWith(QLatin1String("file:"))) title = title.remove(0, 5); // 5 == QString("file:").length() - 1 else if (title.startsWith(QLatin1String("http://www."))) title = title.remove(0, 11); // 11 == QString("http://www.").length() - 1 else if (title.startsWith(QLatin1String("https://www."))) title = title.remove(0, 12); // 12 == QString("https://www.").length() - 1 else if (title.startsWith(QLatin1String("http://"))) title = title.remove(0, 7); // 7 == QString("http://").length() - 1 else if (title.startsWith(QLatin1String("https://"))) title = title.remove(0, 8); // 8 == QString("https://").length() - 1 if (!url.isLocalFile()) { if (title.endsWith(QLatin1String("/index.html")) && title.length() > 11) title.truncate(title.length() - 11); // 11 == QString("/index.html").length() else if (title.endsWith(QLatin1String("/index.htm")) && title.length() > 10) title.truncate(title.length() - 10); // 10 == QString("/index.htm").length() else if (title.endsWith(QLatin1String("/index.xhtml")) && title.length() > 12) title.truncate(title.length() - 12); // 12 == QString("/index.xhtml").length() else if (title.endsWith(QLatin1String("/index.php")) && title.length() > 10) title.truncate(title.length() - 10); // 10 == QString("/index.php").length() else if (title.endsWith(QLatin1String("/index.asp")) && title.length() > 10) title.truncate(title.length() - 10); // 10 == QString("/index.asp").length() else if (title.endsWith(QLatin1String("/index.php3")) && title.length() > 11) title.truncate(title.length() - 11); // 11 == QString("/index.php3").length() else if (title.endsWith(QLatin1String("/index.php4")) && title.length() > 11) title.truncate(title.length() - 11); // 11 == QString("/index.php4").length() else if (title.endsWith(QLatin1String("/index.php5")) && title.length() > 11) title.truncate(title.length() - 11); // 11 == QString("/index.php5").length() } if (title.length() > 2 && title.endsWith('/')) // length > 2 because "/" and "~/" shouldn't be transformed to QString() and "~" title.truncate(title.length() - 1); // eg. transform "www.kde.org/" to "www.kde.org" return title; } QString NoteFactory::iconForURL(const QUrl &url) { if (url.scheme() == "mailto") { return QStringLiteral("message"); } // return KMimeType::iconNameForUrl(url.url()); return QString(); } // TODO: Can I add "autoTitle" and "autoIcon" entries to .desktop files? or just store them in basket, as now... /* Try our better to find an icon suited to the command line * eg. "/usr/bin/kwrite-3.2 ~/myfile.txt /home/other/file.xml" * will give the "kwrite" icon! */ QString NoteFactory::iconForCommand(const QString &command) { QString icon; // 1. Use first word as icon (typically the program without argument) icon = command.split(' ').first(); // 2. If the command is a full path, take only the program file name icon = icon.mid(icon.lastIndexOf('/') + 1); // strip path if given [But it doesn't care of such // "myprogram /my/path/argument" -> return "argument". Would // must first strip first word and then strip path... Useful ?? // 3. Use characters before any '-' (e.g. use "gimp" icon if run command is "gimp-1.3") if (!isIconExist(icon)) icon = icon.split('-').first(); // 4. If the icon still not findable, use a generic icon if (!isIconExist(icon)) icon = "exec"; return icon; } bool NoteFactory::isIconExist(const QString &icon) { return !KIconLoader::global()->loadIcon(icon, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, true).isNull(); } Note *NoteFactory::createEmptyNote(NoteType::Id type, BasketScene *parent) { QPixmap *pixmap; switch (type) { case NoteType::Text: return NoteFactory::createNoteText(QString(), parent, /*reallyPlainText=*/true); case NoteType::Html: return NoteFactory::createNoteHtml(QString(), parent); case NoteType::Image: pixmap = new QPixmap(QSize(Settings::defImageX(), Settings::defImageY())); pixmap->fill(); pixmap->setMask(pixmap->createHeuristicMask()); return NoteFactory::createNoteImage(*pixmap, parent); case NoteType::Link: return NoteFactory::createNoteLink(QUrl(), parent); case NoteType::CrossReference: return NoteFactory::createNoteCrossReference(QUrl(), parent); case NoteType::Launcher: return NoteFactory::createNoteLauncher(QUrl(), parent); case NoteType::Color: return NoteFactory::createNoteColor(Qt::black, parent); default: case NoteType::Animation: case NoteType::Sound: case NoteType::File: case NoteType::Unknown: return nullptr; // Not possible! } } Note *NoteFactory::importKMenuLauncher(BasketScene *parent) { QPointer dialog = new KOpenWithDialog(parent->graphicsView()->viewport()); dialog->setSaveNewApplications(true); // To create temp file, needed by createNoteLauncher() dialog->exec(); if (dialog->service()) { // * locateLocal() return a local file even if it is a system wide one (local one doesn't exists) // * desktopEntryPath() returns the full path for system wide resources, but relative path if in home QString serviceFilePath = dialog->service()->entryPath(); if (!serviceFilePath.startsWith('/')) serviceFilePath = dialog->service()->locateLocal(); return createNoteLauncher(QUrl::fromUserInput(serviceFilePath), parent); } return nullptr; } Note *NoteFactory::importIcon(BasketScene *parent) { QString iconName = KIconDialog::getIcon(KIconLoader::Desktop, KIconLoader::Application, false, Settings::defIconSize()); if (!iconName.isEmpty()) { QPointer dialog = new IconSizeDialog(i18n("Import Icon as Image"), i18n("Choose the size of the icon to import as an image:"), iconName, Settings::defIconSize(), nullptr); dialog->exec(); if (dialog->iconSize() > 0) { Settings::setDefIconSize(dialog->iconSize()); Settings::saveConfig(); return createNoteImage(QIcon::fromTheme(iconName).pixmap(dialog->iconSize()), parent); // TODO: wantedName = iconName ! } } return nullptr; } Note *NoteFactory::importFileContent(BasketScene *parent) { QUrl url = QFileDialog::getOpenFileUrl(parent->graphicsView(), i18n("Load File Content into a Note"), QUrl(), QString()); if (!url.isEmpty()) return copyFileAndLoad(url, parent); return nullptr; } + +void NoteFactory::loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad) +{ + if (lowerTypeName == "text") { + new TextContent(parent, content.text(), lazyLoad); + } else if (lowerTypeName == "html") { + new HtmlContent(parent, content.text(), lazyLoad); + } else if (lowerTypeName == "image") { + new ImageContent(parent, content.text(), lazyLoad); + } else if (lowerTypeName == "animation") { + new AnimationContent(parent, content.text(), lazyLoad); + } else if (lowerTypeName == "sound") { + new SoundContent(parent, content.text()); + } else if (lowerTypeName == "file") { + new FileContent(parent, content.text()); + } else if (lowerTypeName == "link") { + bool autoTitle = content.attribute("title") == content.text(); + bool autoIcon = content.attribute("icon") == NoteFactory::iconForURL(QUrl::fromUserInput(content.text())); + autoTitle = XMLWork::trueOrFalse(content.attribute("autoTitle"), autoTitle); + autoIcon = XMLWork::trueOrFalse(content.attribute("autoIcon"), autoIcon); + new LinkContent(parent, QUrl::fromUserInput(content.text()), content.attribute("title"), content.attribute("icon"), autoTitle, autoIcon); + } else if (lowerTypeName == "cross_reference") { + new CrossReferenceContent(parent, QUrl::fromUserInput(content.text()), content.attribute("title"), content.attribute("icon")); + } else if (lowerTypeName == "launcher") { + new LauncherContent(parent, content.text()); + } else if (lowerTypeName == "color") { + new ColorContent(parent, QColor(content.text())); + } else if (lowerTypeName == "unknown") { + new UnknownContent(parent, content.text()); + } +} diff --git a/src/notefactory.h b/src/notefactory.h index b71cfe6..d41ad49 100644 --- a/src/notefactory.h +++ b/src/notefactory.h @@ -1,84 +1,86 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef NOTEFACTORY_H #define NOTEFACTORY_H #include "notecontent.h" //For NoteType::Id class QColor; class QPixmap; class QString; class QStringList; class QUrl; class QMimeType; class BasketScene; class Note; /** Factory class to create (new, drop, paste) or load BasketIem, and eventually save them (?) * @author Sébastien Laoût */ namespace NoteFactory { /** Functions to create a new note from a content item. * Content, if any, is saved to file but the note is not inserted in the basket, and the basket is not saved. * Return 0 if the note has not been successfully created. * In some cases, the returned note can be a group containing several notes or the first note of a chained list. * The method BasketScene::TODO() can insert several grouped or chained notes without problem. */ Note *createNoteText(const QString &text, BasketScene *parent, bool reallyPlainText = false); Note *createNoteHtml(const QString &html, BasketScene *parent); Note *createNoteLink(const QUrl &url, BasketScene *parent); Note *createNoteLink(const QUrl &url, const QString &title, BasketScene *parent); Note *createNoteCrossReference(const QUrl &url, BasketScene *parent); Note *createNoteCrossReference(const QUrl &url, const QString &title, BasketScene *parent); Note *createNoteCrossReference(const QUrl &url, const QString &title, const QString &icon, BasketScene *parent); Note *createNoteImage(const QPixmap &image, BasketScene *parent); Note *createNoteColor(const QColor &color, BasketScene *parent); Note *createNoteFromText(const QString &content, BasketScene *parent); // Find automatically the type from the text meaning // TODO: Return Note::List? Note *createNoteLauncher(const QUrl &url, BasketScene *parent); Note *createNoteLauncher(const QString &command, const QString &name, const QString &icon, BasketScene *parent); Note *createNoteUnknown(const QMimeData *source, BasketScene *parent); /** Functions to create derived notes from a content */ Note *createNoteLinkOrLauncher(const QUrl &url, BasketScene *parent); Note *copyFileAndLoad(const QUrl &url, BasketScene *parent); Note *moveFileAndLoad(const QUrl &url, BasketScene *parent); Note *loadFile(const QString &fileName, BasketScene *parent); /// << Determine the content of the file (the file SHOULD exists) and return a note of the good type. Note *loadFile(const QString &fileName, NoteType::Id type, BasketScene *parent); /// << Create a note of type @p type. The file is not obliged to exist. /** Functions to create a new note from a drop or paste event */ Note *dropNote(const QMimeData *source, BasketScene *parent, bool fromDrop = false, Qt::DropAction action = Qt::CopyAction, Note *noteSource = nullptr); bool movingNotesInTheSameBasket(const QMimeData *source, BasketScene *parent, Qt::DropAction action); Note *dropURLs(QList urls, BasketScene *parent, Qt::DropAction action, bool fromDrop); Note *decodeContent(QDataStream &stream, NoteType::Id type, BasketScene *parent); /// << Decode the @p stream to a note or return 0 if a general loadFile() is sufficient. void consumeContent(QDataStream &stream, NoteType::Id type); /// << Decode the @p stream to a note or return 0 if a general loadFile() is sufficient. /** Functions to create a note file but not load it in a note object */ QString createNoteLauncherFile(const QString &command, const QString &name, const QString &icon, BasketScene *parent); /** Other useful functions */ NoteType::Id typeForURL(const QUrl &url, BasketScene *parent); bool maybeText(const QMimeType &mimeType); bool maybeHtml(const QMimeType &mimeType); bool maybeImage(const QMimeType &mimeType); bool maybeAnimation(const QMimeType &mimeType); bool maybeSound(const QMimeType &mimeType); bool maybeLauncher(const QMimeType &mimeType); QString fileNameForNewNote(BasketScene *parent, const QString &wantedName); QString createFileForNewNote(BasketScene *parent, const QString &extension, const QString &wantedName = QString()); QUrl filteredURL(const QUrl &url); QString titleForURL(const QUrl &url); QString iconForURL(const QUrl &url); QString iconForCommand(const QString &command); bool isIconExist(const QString &icon); QStringList textToURLList(const QString &text); // @Return { url1, title1, url2, title2, url3, title3... } /** Insert GUI menu */ Note *createEmptyNote(NoteType::Id type, BasketScene *parent); // Insert empty if of type Note::Type Note *importKMenuLauncher(BasketScene *parent); Note *importIcon(BasketScene *parent); Note *importFileContent(BasketScene *parent); + +void loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad); } #endif // NOTEFACTORY_H diff --git a/src/tools.cpp b/src/tools.cpp index e64c16d..d2e2ceb 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -1,769 +1,922 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "tools.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //For KIO::trash #include #include "config.h" #include "debugwindow.h" // cross reference #include "bnpview.h" #include "global.h" #include "htmlexporter.h" #include "linklabel.h" #include QVector StopWatch::starts; QVector StopWatch::totals; QVector StopWatch::counts; void StopWatch::start(int id) { if (id >= starts.size()) { totals.resize(id + 1); counts.resize(id + 1); for (int i = starts.size(); i <= id; i++) { totals[i] = 0; counts[i] = 0; } starts.resize(id + 1); } starts[id] = QTime::currentTime(); } void StopWatch::check(int id) { if (id >= starts.size()) return; double time = starts[id].msecsTo(QTime::currentTime()) / 1000.0; totals[id] += time; counts[id]++; qDebug() << Q_FUNC_INFO << "Timer_" << id << ": " << time << " s [" << counts[id] << " times, total: " << totals[id] << " s, average: " << totals[id] / counts[id] << " s]" << endl; } /** @namespace HTM * @brief HTML tags constants */ namespace HTM { static const char *BR = "
"; static const char *PAR = "

"; static const char *_PAR = "

"; // Styles static const char *FONT_FAMILY = "font-family: %1; "; static const char *FONT_STYLE = "font-style: %1; "; static const char *TEXT_DECORATION = "text-decoration: %1; "; static const char *ITALIC = "italic"; static const char *UNDERLINE = "underline"; static const char *LINE_THROUGH = "line-through"; // static const char* FONT_WEIGHT = "font-weight: %1; "; static const char *FONT_SIZE = "font-size: %1pt; "; static const char *COLOR = "color: %1; "; } QString Tools::textToHTML(const QString &text) { if (text.isEmpty()) return "

"; if (/*text.isEmpty() ||*/ text == " " || text == " ") return "

 

"; // convertFromPlainText() replace "\n\n" by "

\n

": we don't want that QString htmlString = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal); return htmlString.replace("

\n", "
\n
\n").replace("\n

", "\n"); // Don't replace first and last tags } QString Tools::textToHTMLWithoutP(const QString &text) { // textToHTML(text) return "

HTMLizedText

". We remove the strating "

" and ending

" QString HTMLizedText = textToHTML(text); return HTMLizedText.mid(3, HTMLizedText.length() - 3 - 4); } QString Tools::htmlToParagraph(const QString &html) { QString result = html; bool startedBySpan = false; // Remove the start tag, all the and the start // Because can contain style="..." parameter, we transform it to int pos = result.indexOf("\n", each tag can be separated by space characters (%s) // "

" can be omitted (eg. if the HTML doesn't contain paragraph but tables), as well as "" (optional) pos = result.indexOf(QRegExp("(?:(?:

[\\s\\n\\r\\t]*)*[\\s\\n\\r\\t]*)*", Qt::CaseInsensitive)); if (pos != -1) result = result.left(pos); if (startedBySpan) result += "
"; return result; } // The following is adapted from KStringHanlder::tagURLs // The adaptation lies in the change to urlEx // Thanks to Richard Heck QString Tools::tagURLs(const QString &text) { QRegExp urlEx(""); QString richText(text); int urlPos = 0; int urlLen; if ((urlPos = urlEx.indexIn(richText, urlPos)) >= 0) urlPos += urlEx.matchedLength(); else urlPos = 0; urlEx.setPattern("(www\\.(?!\\.)|(fish|(f|ht)tp(|s))://)[\\d\\w\\./,:_~\\?=&;#@\\-\\+\\%\\$]+[\\d\\w/]"); while ((urlPos = urlEx.indexIn(richText, urlPos)) >= 0) { urlLen = urlEx.matchedLength(); // if this match is already a link don't convert it. if (richText.mid(urlPos - 6, 6) == "href=\"") { urlPos += urlLen; continue; } QString href = richText.mid(urlPos, urlLen); // we handle basket links separately... if (href.contains("basket://")) { urlPos += urlLen; continue; } // Qt doesn't support (?<=pattern) so we do it here if ((urlPos > 0) && richText[urlPos - 1].isLetterOrNumber()) { urlPos++; continue; } // Don't use QString::arg since %01, %20, etc could be in the string QString anchor = "" + href + ""; richText.replace(urlPos, urlLen, anchor); urlPos += anchor.length(); } return richText; } QString Tools::tagCrossReferences(const QString &text, bool userLink, HTMLExporter *exporter) { QString richText(text); int urlPos = 0; int urlLen; QRegExp urlEx("\\[\\[(.+)\\]\\]"); urlEx.setMinimal(true); while ((urlPos = urlEx.indexIn(richText, urlPos)) >= 0) { urlLen = urlEx.matchedLength(); QString href = urlEx.cap(1); QStringList hrefParts = href.split('|'); QString anchor; if (exporter) // if we're exporting this basket to html. anchor = crossReferenceForHtml(hrefParts, exporter); else if (userLink) // the link is manually created (ie [[/top level/sub]] ) anchor = crossReferenceForConversion(hrefParts); else // otherwise it's a standard link (ie. [[basket://basket107]] ) anchor = crossReferenceForBasket(hrefParts); richText.replace(urlPos, urlLen, anchor); urlPos += anchor.length(); } return richText; } QString Tools::crossReferenceForBasket(QStringList linkParts) { QString basketLink = linkParts.first(); QString title; bool linkIsEmpty = false; if (basketLink == "basket://" || basketLink.isEmpty()) linkIsEmpty = true; title = linkParts.last().trimmed(); QString css = LinkLook::crossReferenceLook->toCSS("cross_reference", QColor()); QString classes = "cross_reference"; classes += (linkIsEmpty ? " xref_empty" : QString()); css += (linkIsEmpty ? " a.xref_empty { display: block; width: 100%; text-decoration: underline; color: #CC2200; }" " a:hover.xref_empty { color: #A55858; }" : QString()); QString anchor = "" + QUrl::fromPercentEncoding(title.toUtf8()) + ""; return anchor; } QString Tools::crossReferenceForHtml(QStringList linkParts, HTMLExporter *exporter) { QString basketLink = linkParts.first(); QString title; bool linkIsEmpty = false; if (basketLink == "basket://" || basketLink.isEmpty()) linkIsEmpty = true; title = linkParts.last().trimmed(); QString url; if (basketLink.startsWith(QLatin1String("basket://"))) url = basketLink.mid(9, basketLink.length() - 9); BasketScene *basket = Global::bnpView->basketForFolderName(url); // remove the trailing slash. url = url.left(url.length() - 1); // if the basket we're trying to link to is the basket that was exported then // we have to use a special way to refer to it for the links. if (basket == exporter->exportedBasket) url = "../../" + exporter->fileName; else { // if we're in the exported basket then the links have to include // the sub directories. if (exporter->currentBasket == exporter->exportedBasket) url.prepend(exporter->basketsFolderName); if (!url.isEmpty()) url.append(".html"); } QString classes = "cross_reference"; classes += (linkIsEmpty ? " xref_empty" : QString()); QString css = (linkIsEmpty ? " a.xref_empty { display: block; width: 100%; text-decoration: underline; color: #CC2200; }" " a:hover.xref_empty { color: #A55858; }" : QString()); QString anchor = "" + QUrl::fromPercentEncoding(title.toUtf8()) + ""; return anchor; } QString Tools::crossReferenceForConversion(QStringList linkParts) { QString basketLink = linkParts.first(); QString title; if (basketLink.startsWith(QLatin1String("basket://"))) return QString("[[%1|%2]]").arg(basketLink, linkParts.last()); if (basketLink.endsWith('/')) basketLink = basketLink.left(basketLink.length() - 1); QStringList pages = basketLink.split('/'); if (linkParts.count() <= 1) title = pages.last(); else title = linkParts.last().trimmed(); QString url = Global::bnpView->folderFromBasketNameLink(pages); url.prepend("basket://"); // if we don't change the link return it back exactly // as it came in because it may not be a link. if (url == "basket://" || url.isEmpty()) { return linkParts.join(QString()).prepend("[[").append("]]"); } else { return QString("[[%1|%2]]").arg(url, title); } } QString Tools::htmlToText(const QString &html) { QString text = htmlToParagraph(html); text.remove('\n'); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("
", " "); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", "\n"); text.replace("", " "); text.replace("", " "); text.replace("
", "\n"); text.replace("
", "\n"); text.replace("

", "\n"); // FIXME: Format tags better, if possible // TODO: Replace é and co. by their equivalent! // To manage tags: int pos = 0; int pos2; QString tag, tag3; // To manage lists: int deep = 0; // The deep of the current line in imbriqued lists QList ul; // true if current list is a
    one, false if it's an
      one QList lines; // The line number if it is an
        list // We're removing every other tags, or replace them in the case of li: while ((pos = text.indexOf("<"), pos) != -1) { // What is the current tag? tag = text.mid(pos + 1, 2); tag3 = text.mid(pos + 1, 3); // Lists work: if (tag == "ul") { deep++; ul.push_back(true); lines.push_back(-1); } else if (tag == "ol") { deep++; ul.push_back(false); lines.push_back(0); } else if (tag3 == "/ul" || tag3 == "/ol") { deep--; ul.pop_back(); lines.pop_back(); } // Where the tag closes? pos2 = text.indexOf(">"); if (pos2 != -1) { // Remove the tag: text.remove(pos, pos2 - pos + 1); // And replace li with "* ", "x. "... without forbidding to indent that: if (tag == "li") { // How many spaces before the line (indentation): QString spaces; for (int i = 1; i < deep; i++) spaces += QStringLiteral(" "); // The bullet or number of the line: QString bullet = "* "; if (ul.back() == false) { lines.push_back(lines.back() + 1); lines.pop_back(); bullet = QString::number(lines.back()) + ". "; } // Insertion: text.insert(pos, spaces + bullet); } if ((tag3 == "/ul" || tag3 == "/ol") && deep == 0) text.insert(pos, "\n"); // Empty line before and after a set of lists } ++pos; } text.replace(">", ">"); text.replace("<", "<"); text.replace(""", "\""); text.replace(" ", " "); text.replace("&", "&"); // CONVERT IN LAST!! // HtmlContent produces "\n" for empty note if (text == "\n") text = QString(); return text; } QString Tools::textDocumentToMinimalHTML(QTextDocument *document) { QString result = "\n" "\n"; QFont defaultFont; int fragCount, blockCount = 0; bool leadingBrNeeded = false; for (QTextBlock blockIt = document->begin(); blockIt != document->end(); blockIt = blockIt.next(), ++blockCount) { result += HTM::PAR; // Prepare to detect empty blocks fragCount = 0; for (QTextBlock::iterator subIt = blockIt.begin(); !(subIt.atEnd()); ++subIt, ++fragCount) { QTextFragment currentFragment = subIt.fragment(); if (currentFragment.isValid()) { // Dealing with need to add leading linebreak (see later for // further notes) if (leadingBrNeeded) { result += HTM::BR; leadingBrNeeded = false; } QTextCharFormat charFmt = currentFragment.charFormat(); const QColor &textColor = charFmt.foreground().color(); bool isTextBlack = (textColor == QColor() || textColor == QColor(Qt::black)); if (charFmt.font() == defaultFont && isTextBlack) { result += currentFragment.text().toHtmlEscaped(); continue; } // If we use charFmt.fontWeight, setting a tag overrides it and all characters become non-bold. // So we use instead bool bold = (charFmt.fontWeight() >= QFont::Bold); if (bold) result += ""; // Compose style string (font and color) result += "= QFont::Bold) ? QFont::Bold : QFont::Normal; result += QString(HTM::FONT_WEIGHT).arg(weight); }*/ if (charFmt.fontPointSize() != defaultFont.pointSize() && charFmt.fontPointSize() != 0) result += QString(HTM::FONT_SIZE).arg(charFmt.fontPointSize()); if (!isTextBlack) result += QString(HTM::COLOR).arg(textColor.name()); result += "\">" + currentFragment.text().toHtmlEscaped() + ""; if (bold) result += ""; } } // Detecting empty blocks (Qt4 fails to generate a fragment from an empty line) // Inserting a linebreak directly here seems to cause the renderer to render // two breaks, so have to append it to the contents of the previous paragraph... if (!fragCount) { // If the first fragment is an empty fragment, the linebreak must be // added to the next fragment otherwise you get the above double breaks if (!blockCount) leadingBrNeeded = true; // Deal with the problem only when the last block is not affected, // otherwise you get double breaks again... Blocks counted from 0 else if (blockCount != (document->blockCount() - 1)) { result.chop(7); result = result + HTM::BR + HTM::_PAR + HTM::PAR; } } result += HTM::_PAR; } result += ""; return result; } QString Tools::cssFontDefinition(const QFont &font, bool onlyFontFamily) { // The font definition: QString definition = font.key() + (font.italic() ? QStringLiteral("italic ") : QString()) + (font.bold() ? QStringLiteral("bold ") : QString()) + QString::number(QFontInfo(font).pixelSize()) + QStringLiteral("px "); // Then, try to match the font name with a standard CSS font family: QString genericFont; if (definition.contains("serif", Qt::CaseInsensitive) || definition.contains("roman", Qt::CaseInsensitive)) { genericFont = QStringLiteral("serif"); } // No "else if" because "sans serif" must be counted as "sans". So, the order between "serif" and "sans" is important if (definition.contains("sans", Qt::CaseInsensitive) || definition.contains("arial", Qt::CaseInsensitive) || definition.contains("helvetica", Qt::CaseInsensitive)) { genericFont = QStringLiteral("sans-serif"); } if (definition.contains("mono", Qt::CaseInsensitive) || definition.contains("courier", Qt::CaseInsensitive) || definition.contains("typewriter", Qt::CaseInsensitive) || definition.contains("console", Qt::CaseInsensitive) || definition.contains("terminal", Qt::CaseInsensitive) || definition.contains("news", Qt::CaseInsensitive)) { genericFont = QStringLiteral("monospace"); } // Eventually add the generic font family to the definition: QString fontDefinition = QStringLiteral("\"%1\"").arg(font.family()); if (!genericFont.isEmpty()) fontDefinition += ", " + genericFont; if (onlyFontFamily) return fontDefinition; return definition + fontDefinition; } QString Tools::stripEndWhiteSpaces(const QString &string) { uint length = string.length(); uint i; for (i = length; i > 0; --i) if (!string[i - 1].isSpace()) break; if (i == 0) return QString(); else return string.left(i); } +QString Tools::cssColorName(const QString& colorHex) +{ + static const QMap cssColors = { + {"#00ffff", "aqua" }, + {"#000000", "black" }, + {"#0000ff", "blue" }, + {"#ff00ff", "fuchsia" }, + {"#808080", "gray" }, + {"#008000", "green" }, + {"#00ff00", "lime" }, + {"#800000", "maroon" }, + {"#000080", "navy" }, + {"#808000", "olive" }, + {"#800080", "purple" }, + {"#ff0000", "red" }, + {"#c0c0c0", "silver" }, + {"#008080", "teal" }, + {"#ffffff", "white" }, + {"#ffff00", "yellow" }, + // CSS extended colors + {"#f0f8ff", "aliceblue" }, + {"#faebd7", "antiquewhite" }, + {"#7fffd4", "aquamarine" }, + {"#f0ffff", "azure" }, + {"#f5f5dc", "beige" }, + {"#ffe4c4", "bisque" }, + {"#ffebcd", "blanchedalmond" }, + {"#8a2be2", "blueviolet" }, + {"#a52a2a", "brown" }, + {"#deb887", "burlywood" }, + {"#5f9ea0", "cadetblue" }, + {"#7fff00", "chartreuse" }, + {"#d2691e", "chocolate" }, + {"#ff7f50", "coral" }, + {"#6495ed", "cornflowerblue" }, + {"#fff8dc", "cornsilk" }, + {"#dc1436", "crimson" }, + {"#00ffff", "cyan" }, + {"#00008b", "darkblue" }, + {"#008b8b", "darkcyan" }, + {"#b8860b", "darkgoldenrod" }, + {"#a9a9a9", "darkgray" }, + {"#006400", "darkgreen" }, + {"#bdb76b", "darkkhaki" }, + {"#8b008b", "darkmagenta" }, + {"#556b2f", "darkolivegreen" }, + {"#ff8c00", "darkorange" }, + {"#9932cc", "darkorchid" }, + {"#8b0000", "darkred" }, + {"#e9967a", "darksalmon" }, + {"#8fbc8f", "darkseagreen" }, + {"#483d8b", "darkslateblue" }, + {"#2f4f4f", "darkslategray" }, + {"#00ced1", "darkturquoise" }, + {"#9400d3", "darkviolet" }, + {"#ff1493", "deeppink" }, + {"#00bfff", "deepskyblue" }, + {"#696969", "dimgray" }, + {"#1e90ff", "dodgerblue" }, + {"#b22222", "firebrick" }, + {"#fffaf0", "floralwhite" }, + {"#228b22", "forestgreen" }, + {"#dcdcdc", "gainsboro" }, + {"#f8f8ff", "ghostwhite" }, + {"#ffd700", "gold" }, + {"#daa520", "goldenrod" }, + {"#adff2f", "greenyellow" }, + {"#f0fff0", "honeydew" }, + {"#ff69b4", "hotpink" }, + {"#cd5c5c", "indianred" }, + {"#4b0082", "indigo" }, + {"#fffff0", "ivory" }, + {"#f0e68c", "khaki" }, + {"#e6e6fa", "lavender" }, + {"#fff0f5", "lavenderblush" }, + {"#7cfc00", "lawngreen" }, + {"#fffacd", "lemonchiffon" }, + {"#add8e6", "lightblue" }, + {"#f08080", "lightcoral" }, + {"#e0ffff", "lightcyan" }, + {"#fafad2", "lightgoldenrodyellow" }, + {"#90ee90", "lightgreen" }, + {"#d3d3d3", "lightgrey" }, + {"#ffb6c1", "lightpink" }, + {"#ffa07a", "lightsalmon" }, + {"#20b2aa", "lightseagreen" }, + {"#87cefa", "lightskyblue" }, + {"#778899", "lightslategray" }, + {"#b0c4de", "lightsteelblue" }, + {"#ffffe0", "lightyellow" }, + {"#32cd32", "limegreen" }, + {"#faf0e6", "linen" }, + {"#ff00ff", "magenta" }, + {"#66cdaa", "mediumaquamarine" }, + {"#0000cd", "mediumblue" }, + {"#ba55d3", "mediumorchid" }, + {"#9370db", "mediumpurple" }, + {"#3cb371", "mediumseagreen" }, + {"#7b68ee", "mediumslateblue" }, + {"#00fa9a", "mediumspringgreen" }, + {"#48d1cc", "mediumturquoise" }, + {"#c71585", "mediumvioletred" }, + {"#191970", "midnightblue" }, + {"#f5fffa", "mintcream" }, + {"#ffe4e1", "mistyrose" }, + {"#ffe4b5", "moccasin" }, + {"#ffdead", "navajowhite" }, + {"#fdf5e6", "oldlace" }, + {"#6b8e23", "olivedrab" }, + {"#ffa500", "orange" }, + {"#ff4500", "orangered" }, + {"#da70d6", "orchid" }, + {"#eee8aa", "palegoldenrod" }, + {"#98fb98", "palegreen" }, + {"#afeeee", "paleturquoise" }, + {"#db7093", "palevioletred" }, + {"#ffefd5", "papayawhip" }, + {"#ffdab9", "peachpuff" }, + {"#cd853f", "peru" }, + {"#ffc0cb", "pink" }, + {"#dda0dd", "plum" }, + {"#b0e0e6", "powderblue" }, + {"#bc8f8f", "rosybrown" }, + {"#4169e1", "royalblue" }, + {"#8b4513", "saddlebrown" }, + {"#fa8072", "salmon" }, + {"#f4a460", "sandybrown" }, + {"#2e8b57", "seagreen" }, + {"#fff5ee", "seashell" }, + {"#a0522d", "sienna" }, + {"#87ceeb", "skyblue" }, + {"#6a5acd", "slateblue" }, + {"#708090", "slategray" }, + {"#fffafa", "snow" }, + {"#00ff7f", "springgreen" }, + {"#4682b4", "steelblue" }, + {"#d2b48c", "tan" }, + {"#d8bfd8", "thistle" }, + {"#ff6347", "tomato" }, + {"#40e0d0", "turquoise" }, + {"#ee82ee", "violet" }, + {"#f5deb3", "wheat" }, + {"#f5f5f5", "whitesmoke" }, + {"#9acd32", "yellowgreen" } }; + + return cssColors.value(colorHex, QString()); +} + + bool Tools::isWebColor(const QColor &color) { int r = color.red(); // The 216 web colors are those colors whose RGB (Red, Green, Blue) int g = color.green(); // values are all in the set (0, 51, 102, 153, 204, 255). int b = color.blue(); - return ((r == 0 || r == 51 || r == 102 || r == 153 || r == 204 || r == 255) && (g == 0 || g == 51 || g == 102 || g == 153 || g == 204 || g == 255) && (b == 0 || b == 51 || b == 102 || b == 153 || b == 204 || b == 255)); + return ((r == 0 || r == 51 || r == 102 || r == 153 || r == 204 || r == 255) && + (g == 0 || g == 51 || g == 102 || g == 153 || g == 204 || g == 255) && + (b == 0 || b == 51 || b == 102 || b == 153 || b == 204 || b == 255)); } QColor Tools::mixColor(const QColor &color1, const QColor &color2, const float ratio) { QColor mixedColor; - mixedColor.setRgb((color1.red() * ratio + color2.red()) / (1 + ratio), (color1.green() * ratio + color2.green()) / (1 + ratio), (color1.blue() * ratio + color2.blue()) / (1 + ratio)); + mixedColor.setRgb((color1.red() * ratio + color2.red()) / (1 + ratio), + (color1.green() * ratio + color2.green()) / (1 + ratio), + (color1.blue() * ratio + color2.blue()) / (1 + ratio)); return mixedColor; } bool Tools::tooDark(const QColor &color) { return color.value() < 175; } // TODO: Use it for all indentPixmap() QPixmap Tools::normalizePixmap(const QPixmap &pixmap, int width, int height) { if (height <= 0) height = width; if (pixmap.isNull() || (pixmap.width() == width && pixmap.height() == height)) return pixmap; return pixmap; } QPixmap Tools::indentPixmap(const QPixmap &source, int depth, int deltaX) { // Verify if it is possible: if (depth <= 0 || source.isNull()) return source; // Compute the number of pixels to indent: if (deltaX <= 0) deltaX = 2 * source.width() / 3; int indent = depth * deltaX; // Create the images: QImage resultImage(indent + source.width(), source.height(), QImage::Format_ARGB32); resultImage.setColorCount(32); QImage sourceImage = source.toImage(); // Clear the indent part (the left part) by making it fully transparent: uint *p; for (int row = 0; row < resultImage.height(); ++row) { for (int column = 0; column < resultImage.width(); ++column) { p = (uint *)resultImage.scanLine(row) + column; *p = 0; // qRgba(0, 0, 0, 0) } } // Copy the source image byte per byte to the right part: uint *q; for (int row = 0; row < sourceImage.height(); ++row) { for (int column = 0; column < sourceImage.width(); ++column) { p = (uint *)resultImage.scanLine(row) + indent + column; q = (uint *)sourceImage.scanLine(row) + column; *p = *q; } } // And return the result: QPixmap result = QPixmap::fromImage(resultImage); return result; } void Tools::deleteRecursively(const QString &folderOrFile) { if (folderOrFile.isEmpty()) return; QFileInfo fileInfo(folderOrFile); if (fileInfo.isDir()) { // Delete the child files: QDir dir(folderOrFile, QString(), QDir::Name | QDir::IgnoreCase, QDir::TypeMask | QDir::Hidden); QStringList list = dir.entryList(); for (QStringList::Iterator it = list.begin(); it != list.end(); ++it) if (*it != "." && *it != "..") deleteRecursively(folderOrFile + '/' + *it); // And then delete the folder: dir.rmdir(folderOrFile); } else // Delete the file: QFile::remove(folderOrFile); } void Tools::deleteMetadataRecursively(const QString &folderOrFile) { QFileInfo fileInfo(folderOrFile); if (fileInfo.isDir()) { // Delete Metadata of the child files: QDir dir(folderOrFile, QString(), QDir::Name | QDir::IgnoreCase, QDir::TypeMask | QDir::Hidden); QStringList list = dir.entryList(); for (QStringList::Iterator it = list.begin(); it != list.end(); ++it) if (*it != "." && *it != "..") deleteMetadataRecursively(folderOrFile + '/' + *it); } } void Tools::trashRecursively(const QString &folderOrFile) { if (folderOrFile.isEmpty()) return; KIO::trash(QUrl::fromLocalFile(folderOrFile), KIO::HideProgressInfo); } QString Tools::fileNameForNewFile(const QString &wantedName, const QString &destFolder) { QString fileName = wantedName; QString fullName = destFolder + fileName; QString extension = QString(); int number = 2; QDir dir; // First check if the file do not exists yet (simplier and more often case) dir = QDir(fullName); if (!dir.exists(fullName)) return fileName; // Find the file extension, if it exists : Split fileName in fileName and extension // Example : fileName == "note5-3.txt" => fileName = "note5-3" and extension = ".txt" int extIndex = fileName.lastIndexOf('.'); if (extIndex != -1 && extIndex != int(fileName.length() - 1)) { // Extension found and fileName do not ends with '.' ! extension = fileName.mid(extIndex); fileName.truncate(extIndex); } // else fileName = fileName and extension = QString() // Find the file number, if it exists : Split fileName in fileName and number // Example : fileName == "note5-3" => fileName = "note5" and number = 3 int extNumber = fileName.lastIndexOf('-'); if (extNumber != -1 && extNumber != int(fileName.length() - 1)) { // Number found and fileName do not ends with '-' ! bool isANumber; int theNumber = fileName.mid(extNumber + 1).toInt(&isANumber); if (isANumber) { number = theNumber; fileName.truncate(extNumber); } // else : } // else fileName = fileName and number = 2 (because if the file already exists, the genereated name is at last the 2nd) QString finalName; for (/*int number = 2*/;; ++number) { // TODO: FIXME: If overflow ??? finalName = fileName + '-' + QString::number(number) + extension; fullName = destFolder + finalName; dir = QDir(fullName); if (!dir.exists(fullName)) break; } return finalName; } qint64 Tools::computeSizeRecursively(const QString &path) { qint64 result = 0; QFileInfo file(path); result += file.size(); if (file.isDir()) { QFileInfoList children = QDir(path).entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden); foreach (const QFileInfo &child, children) result += computeSizeRecursively(child.absoluteFilePath()); } return result; } // TODO: Move it from NoteFactory /*QString NoteFactory::iconForURL(const QUrl &url) { QString icon = KMimeType::iconNameForUrl(url.url()); if ( url.scheme() == "mailto" ) icon = "message"; return icon; }*/ bool Tools::isAFileCut(const QMimeData *source) { if (source->hasFormat("application/x-kde-cutselection")) { QByteArray array = source->data("application/x-kde-cutselection"); return !array.isEmpty() && QByteArray(array.data(), array.size() + 1).at(0) == '1'; } else return false; } void Tools::printChildren(QObject *parent) { const QObjectList objs = parent->children(); QObject *obj; for (int i = 0; i < objs.size(); i++) { obj = objs.at(i); qDebug() << Q_FUNC_INFO << obj->metaObject()->className() << ": " << obj->objectName() << endl; } } QString Tools::makeStandardCaption(const QString &userCaption) { QString caption = QGuiApplication::applicationDisplayName(); if (!userCaption.isEmpty()) return userCaption + i18nc("Document/application separator in titlebar", " – ") + caption; else return caption; } QByteArray Tools::systemCodeset() { QByteArray codeset; #if HAVE_LANGINFO_H // Qt since 4.2 always returns 'System' as codecForLocale and libraries like for example // KEncodingFileDialog expects real encoding name. So on systems that have langinfo.h use // nl_langinfo instead, just like Qt compiled without iconv does. Windows already has its own // workaround codeset = nl_langinfo(CODESET); if ((codeset == "ANSI_X3.4-1968") || (codeset == "US-ASCII")) { // means ascii, "C"; QTextCodec doesn't know, so avoid warning codeset = "ISO-8859-1"; } #endif return codeset; } diff --git a/src/tools.h b/src/tools.h index 8f023e5..27188a0 100644 --- a/src/tools.h +++ b/src/tools.h @@ -1,114 +1,117 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef TOOLS_H #define TOOLS_H #include class QColor; class QFont; class QMimeData; class QObject; class QPixmap; class QString; class QStringList; class QTime; class QTextDocument; class HTMLExporter; class StopWatch { public: static void start(int id); static void check(int id); private: static QVector starts; static QVector totals; static QVector counts; }; /** Some useful functions for that application. * @author Sébastien Laoût */ namespace Tools { // Text <-> HTML Conversions and Tools: QString textToHTML(const QString &text); QString textToHTMLWithoutP(const QString &text); QString htmlToParagraph(const QString &html); QString htmlToText(const QString &html); QString textDocumentToMinimalHTML(QTextDocument *document); //!< Avoid unneeded spans and style attributes QString tagURLs(const QString &test); QString cssFontDefinition(const QFont &font, bool onlyFontFamily = false); // Cross Reference tools: QString tagCrossReferences(const QString &text, bool userLink = false, HTMLExporter *exporter = nullptr); // private functions: QString crossReferenceForBasket(QStringList linkParts); QString crossReferenceForHtml(QStringList linkParts, HTMLExporter *exporter); QString crossReferenceForConversion(QStringList linkParts); // String Manipulations: QString stripEndWhiteSpaces(const QString &string); QString makeStandardCaption(const QString &userCaption); //!< Replacement for KDialog::makeStandardCaption // Pixmap Manipulations: +/** @Return the CSS color name for the given @p colorHex in #rrggbb format, or empty string if none matches + */ +QString cssColorName(const QString &colorHex); /** @Return true if it is a Web color */ bool isWebColor(const QColor &color); /** @Return a color that is 50% of @p color1 and 50% of @p color2. */ QColor mixColor(const QColor &color1, const QColor &color2, const float ratio = 1); /** @Return true if the color is too dark to be darkened one more time. */ bool tooDark(const QColor &color); /** Make sure the @p pixmap is of the size (@p width, @p height) and @return a pixmap of this size. * If @p height <= 0, then width will be used to make the picture square. */ QPixmap normalizePixmap(const QPixmap &pixmap, int width, int height = 0); /** @Return the pixmap @p source with depth*deltaX transparent pixels added to the left.\n * If @p deltaX is <= 0, then an indent delta is computed depending on the @p source width. */ QPixmap indentPixmap(const QPixmap &source, int depth, int deltaX = 0); // File and Folder Manipulations: /** Delete the folder @p folderOrFile recursively (to remove sub-folders and child files too). */ void deleteRecursively(const QString &folderOrFile); /** Trash the folder @p folderOrFile recursively (to move sub-folders and child files to the Trash, too). */ void trashRecursively(const QString &folderOrFile); /** Delete the metadata of file or folder @p folderOrFile from Nepomuk, recursively. */ void deleteMetadataRecursively(const QString &folderOrFile); /** @Return a new filename that doesn't already exist in @p destFolder. * If @p wantedName already exist in @p destFolder, a dash and a number will be added before the extension. * Id there were already such a number in @p wantedName, it is incremented until a free filename is found. */ QString fileNameForNewFile(const QString &wantedName, const QString &destFolder); //! @returns Total size in bytes of all files and subdirectories qint64 computeSizeRecursively(const QString &path); // Other: // void iconForURL(const QUrl &url); /** @Return true if the source is from a file cutting in Konqueror. * @Return false if it was just a copy or if it was a drag. */ bool isAFileCut(const QMimeData *source); /// Implementation of system encoding detection from KDE 4 QByteArray systemCodeset(); // Debug void printChildren(QObject *parent); } #endif // TOOLS_H