diff --git a/src/backup.cpp b/src/backup.cpp index de98934..685fc8c 100644 --- a/src/backup.cpp +++ b/src/backup.cpp @@ -1,421 +1,421 @@ /** * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #include "backup.h" #include "formatimporter.h" // To move a folder #include "global.h" #include "settings.h" #include "tools.h" #include "variouswidgets.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // usleep() /** * Backups are wrapped in a .tar.gz, inside that folder name. * An archive is not a backup or is corrupted if data are not in that folder! */ const QString backupMagicFolder = "BasKet-Note-Pads_Backup"; /** class BackupDialog: */ BackupDialog::BackupDialog(QWidget *parent, const char *name) : QDialog(parent) { setObjectName(name); setModal(true); setWindowTitle(i18n("Backup & Restore")); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QWidget *page = new QWidget(this); QVBoxLayout *pageVBoxLayout = new QVBoxLayout(page); pageVBoxLayout->setMargin(0); mainLayout->addWidget(page); // pageVBoxLayout->setSpacing(spacingHint()); QString savesFolder = Global::savesFolder(); savesFolder = savesFolder.left(savesFolder.length() - 1); // savesFolder ends with "/" QGroupBox *folderGroup = new QGroupBox(i18n("Save Folder"), page); pageVBoxLayout->addWidget(folderGroup); mainLayout->addWidget(folderGroup); QVBoxLayout *folderGroupLayout = new QVBoxLayout; folderGroup->setLayout(folderGroupLayout); folderGroupLayout->addWidget(new QLabel("" + i18n("Your baskets are currently stored in that folder:
%1", savesFolder), folderGroup)); QWidget *folderWidget = new QWidget; folderGroupLayout->addWidget(folderWidget); QHBoxLayout *folderLayout = new QHBoxLayout(folderWidget); folderLayout->setContentsMargins(0, 0, 0, 0); QPushButton *moveFolder = new QPushButton(i18n("&Move to Another Folder..."), folderWidget); QPushButton *useFolder = new QPushButton(i18n("&Use Another Existing Folder..."), folderWidget); HelpLabel *helpLabel = new HelpLabel(i18n("Why to do that?"), i18n("

You can move the folder where %1 store your baskets to:

    " "
  • Store your baskets in a visible place in your home folder, like ~/Notes or ~/Baskets, so you can manually backup them when you want.
  • " "
  • Store your baskets on a server to share them between two computers.
    " "In this case, mount the shared-folder to the local file system and ask %1 to use that mount point.
    " "Warning: you should not run %1 at the same time on both computers, or you risk to loss data while the two applications are desynced.
  • " "

Please remember that you should not change the content of that folder manually (eg. adding a file in a basket folder will not add that file to the basket).

", QGuiApplication::applicationDisplayName()), folderWidget); folderLayout->addWidget(moveFolder); folderLayout->addWidget(useFolder); folderLayout->addWidget(helpLabel); folderLayout->addStretch(); connect(moveFolder, SIGNAL(clicked()), this, SLOT(moveToAnotherFolder())); connect(useFolder, SIGNAL(clicked()), this, SLOT(useAnotherExistingFolder())); QGroupBox *backupGroup = new QGroupBox(i18n("Backups"), page); pageVBoxLayout->addWidget(backupGroup); mainLayout->addWidget(backupGroup); QVBoxLayout *backupGroupLayout = new QVBoxLayout; backupGroup->setLayout(backupGroupLayout); QWidget *backupWidget = new QWidget; backupGroupLayout->addWidget(backupWidget); QHBoxLayout *backupLayout = new QHBoxLayout(backupWidget); backupLayout->setContentsMargins(0, 0, 0, 0); QPushButton *backupButton = new QPushButton(i18n("&Backup..."), backupWidget); QPushButton *restoreButton = new QPushButton(i18n("&Restore a Backup..."), backupWidget); m_lastBackup = new QLabel(QString(), backupWidget); backupLayout->addWidget(backupButton); backupLayout->addWidget(restoreButton); backupLayout->addWidget(m_lastBackup); backupLayout->addStretch(); connect(backupButton, SIGNAL(clicked()), this, SLOT(backup())); connect(restoreButton, SIGNAL(clicked()), this, SLOT(restore())); populateLastBackup(); (new QWidget(page))->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); mainLayout->addWidget(buttonBox); buttonBox->button(QDialogButtonBox::Close)->setDefault(true); } BackupDialog::~BackupDialog() { } void BackupDialog::populateLastBackup() { QString lastBackupText = i18n("Last backup: never"); if (Settings::lastBackup().isValid()) lastBackupText = i18n("Last backup: %1", Settings::lastBackup().toString(Qt::LocalDate)); m_lastBackup->setText(lastBackupText); } void BackupDialog::moveToAnotherFolder() { QUrl selectedURL = QFileDialog::getExistingDirectoryUrl(/*parent=*/nullptr, /*caption=*/i18n("Choose a Folder Where to Move Baskets"), /*startDir=*/Global::savesFolder()); if (!selectedURL.isEmpty()) { QString folder = selectedURL.path(); QDir dir(folder); // The folder should not exists, or be empty (because KDirSelectDialog will likely create it anyway): if (dir.exists()) { // Get the content of the folder: QStringList content = dir.entryList(); if (content.count() > 2) { // "." and ".." int result = KMessageBox::questionYesNo(nullptr, "" + i18n("The folder %1 is not empty. Do you want to override it?", folder), i18n("Override Folder?"), KGuiItem(i18n("&Override"), "document-save")); if (result == KMessageBox::No) return; } Tools::deleteRecursively(folder); } FormatImporter copier; copier.moveFolder(Global::savesFolder(), folder); Backup::setFolderAndRestart(folder, i18n("Your baskets have been successfully moved to %1. %2 is going to be restarted to take this change into account.")); } } void BackupDialog::useAnotherExistingFolder() { QUrl selectedURL = QFileDialog::getExistingDirectoryUrl(/*parent=*/nullptr, /*caption=*/i18n("Choose an Existing Folder to Store Baskets"), /*startDir=*/Global::savesFolder()); if (!selectedURL.isEmpty()) { Backup::setFolderAndRestart(selectedURL.path(), i18n("Your basket save folder has been successfully changed to %1. %2 is going to be restarted to take this change into account.")); } } void BackupDialog::backup() { QDir dir; // Compute a default file name & path (eg. "Baskets_2007-01-31.tar.gz"): KConfig *config = KSharedConfig::openConfig().data(); KConfigGroup configGroup(config, "Backups"); QString folder = configGroup.readEntry("lastFolder", QDir::homePath()) + '/'; QString fileName = i18nc("Backup filename (without extension), %1 is the date", "Baskets_%1", QDate::currentDate().toString(Qt::ISODate)); QString url = folder + fileName; // Ask a file name & path to the user: QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files"); QString destination = url; for (bool askAgain = true; askAgain;) { // Ask: destination = QFileDialog::getSaveFileName(nullptr, i18n("Backup Baskets"), destination, filter); // User canceled? if (destination.isEmpty()) return; // File already existing? Ask for overriding: if (dir.exists(destination)) { int result = KMessageBox::questionYesNoCancel( nullptr, "" + i18n("The file %1 already exists. Do you really want to override it?", QUrl::fromLocalFile(destination).fileName()), i18n("Override File?"), KGuiItem(i18n("&Override"), "document-save")); if (result == KMessageBox::Cancel) return; else if (result == KMessageBox::Yes) askAgain = false; } else askAgain = false; } QProgressDialog dialog; dialog.setWindowTitle(i18n("Backup Baskets")); dialog.setLabelText(i18n("Backing up baskets. Please wait...")); dialog.setModal(true); dialog.setCancelButton(nullptr); dialog.setAutoClose(true); dialog.setRange(0, 0 /*Busy/Undefined*/); dialog.setValue(0); dialog.show(); /* If needed, uncomment this and call similar code in other places below QProgressBar* progress = new QProgressBar(dialog); progress->setTextVisible(false); dialog.setBar(progress);*/ BackupThread thread(destination, Global::savesFolder()); thread.start(); while (thread.isRunning()) { dialog.setValue(dialog.value() + 1); // Or else, the animation is not played! qApp->processEvents(); usleep(300); // Not too long because if the backup process is finished, we wait for nothing } Settings::setLastBackup(QDate::currentDate()); Settings::saveConfig(); populateLastBackup(); } void BackupDialog::restore() { // Get last backup folder: KConfig *config = KSharedConfig::openConfig().data(); KConfigGroup configGroup(config, "Backups"); QString folder = configGroup.readEntry("lastFolder", QDir::homePath()) + '/'; // Ask a file name to the user: QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files"); QString path = QFileDialog::getOpenFileName(this, i18n("Open Basket Archive"), folder, filter); if (path.isEmpty()) // User has canceled return; // Before replacing the basket data folder with the backup content, we safely backup the current baskets to the home folder. // So if the backup is corrupted or something goes wrong while restoring (power cut...) the user will be able to restore the old working data: QString safetyPath = Backup::newSafetyFolder(); FormatImporter copier; copier.moveFolder(Global::savesFolder(), safetyPath); // Add the README file for user to cancel a bad restoration: QString readmePath = safetyPath + i18n("README.txt"); QFile file(readmePath); if (file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); stream << i18n("This is a safety copy of your baskets like they were before you started to restore the backup %1.", QUrl::fromLocalFile(path).fileName()) + "\n\n" << i18n("If the restoration was a success and you restored what you wanted to restore, you can remove this folder.") + "\n\n" << i18n("If something went wrong during the restoration process, you can re-use this folder to store your baskets and nothing will be lost.") + "\n\n" << i18n("Choose \"Basket\" -> \"Backup & Restore...\" -> \"Use Another Existing Folder...\" and select that folder.") + '\n'; file.close(); } QString message = "

" + i18n("Restoring %1. Please wait...", QUrl::fromLocalFile(path).fileName()) + "

" + i18n("If something goes wrong during the restoration process, read the file %1.", readmePath); QProgressDialog *dialog = new QProgressDialog(); dialog->setWindowTitle(i18n("Restore Baskets")); dialog->setLabelText(message); dialog->setModal(/*modal=*/true); dialog->setCancelButton(nullptr); dialog->setAutoClose(true); dialog->setRange(0, 0 /*Busy/Undefined*/); dialog->setValue(0); dialog->show(); // Uncompress: RestoreThread thread(path, Global::savesFolder()); thread.start(); while (thread.isRunning()) { dialog->setValue(dialog->value() + 1); // Or else, the animation is not played! qApp->processEvents(); usleep(300); // Not too long because if the restore process is finished, we wait for nothing } dialog->hide(); // The restore is finished, do not continue to show it while telling the user the application is going to be restarted delete dialog; // If we only hidden it, it reappeared just after having restored a small backup... Very strange. dialog = nullptr; // This was annoying since it is modal and the "BasKet Note Pads is going to be restarted" message was not reachable. // qApp->processEvents(); // Check for errors: if (!thread.success()) { // Restore the old baskets: QDir dir; dir.remove(readmePath); copier.moveFolder(safetyPath, Global::savesFolder()); // Tell the user: KMessageBox::error(nullptr, i18n("This archive is either not a backup of baskets or is corrupted. It cannot be imported. Your old baskets have been preserved instead."), i18n("Restore Error")); return; } - // Note: The safety backup is not removed now because the code can has been wrong, somehow, or the user perhapse restored an older backup by error... + // Note: The safety backup is not removed now because the code can has been wrong, somehow, or the user perhaps restored an older backup by error... // The restore process will not be called very often (it is possible it will only be called once or twice around the world during the next years). // So it is rare enough to force the user to remove the safety folder, but keep him in control and let him safely recover from restoration errors. Backup::setFolderAndRestart(Global::savesFolder() /*No change*/, i18n("Your backup has been successfully restored to %1. %2 is going to be restarted to take this change into account.")); } /** class Backup: */ QString Backup::binaryPath; void Backup::figureOutBinaryPath(const char *argv0, QApplication &app) { /* The application can be launched by two ways: - Globally (app.applicationFilePath() is good) - In KDevelop or with an absolute path (app.applicationFilePath() is wrong) This function is called at the very start of main() so that the current directory has not been changed yet. Command line (argv[0]) QDir(argv[0]).canonicalPath() app.applicationFilePath() ====================== ============================================= ========================= "basket" "" "/opt/kde3/bin/basket" "./src/.libs/basket" "/home/seb/prog/basket/debug/src/.lib/basket" "/opt/kde3/bin/basket" */ binaryPath = QDir(argv0).canonicalPath(); if (binaryPath.isEmpty()) binaryPath = app.applicationFilePath(); } void Backup::setFolderAndRestart(const QString &folder, const QString &message) { // Set the folder: Settings::setDataFolder(folder); Settings::saveConfig(); // Reassure the user that the application main window disappearance is not a crash, but a normal restart. // This is important for users to trust the application in such a critical phase and understands what's happening: KMessageBox::information(nullptr, "" + message.arg((folder.endsWith('/') ? folder.left(folder.length() - 1) : folder), QGuiApplication::applicationDisplayName()), i18n("Restart")); // Restart the application: KRun::runCommand(binaryPath, QCoreApplication::applicationName(), QCoreApplication::applicationName(), nullptr); exit(0); } QString Backup::newSafetyFolder() { QDir dir; QString fullPath; fullPath = QDir::homePath() + '/' + i18nc("Safety folder name before restoring a basket data archive", "Baskets Before Restoration") + '/'; if (!dir.exists(fullPath)) return fullPath; for (int i = 2;; ++i) { fullPath = QDir::homePath() + '/' + i18nc("Safety folder name before restoring a basket data archive", "Baskets Before Restoration (%1)", i) + '/'; if (!dir.exists(fullPath)) return fullPath; } return QString(); } /** class BackupThread: */ BackupThread::BackupThread(const QString &tarFile, const QString &folderToBackup) : m_tarFile(tarFile) , m_folderToBackup(folderToBackup) { } void BackupThread::run() { KTar tar(m_tarFile, "application/x-gzip"); tar.open(QIODevice::WriteOnly); tar.addLocalDirectory(m_folderToBackup, backupMagicFolder); // KArchive does not add hidden files. Basket description files (".basket") are hidden, we add them manually: QDir dir(m_folderToBackup + "baskets/"); QStringList baskets = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (QStringList::Iterator it = baskets.begin(); it != baskets.end(); ++it) { tar.addLocalFile(m_folderToBackup + "baskets/" + *it + "/.basket", backupMagicFolder + "/baskets/" + *it + "/.basket"); } // We finished: tar.close(); } /** class RestoreThread: */ RestoreThread::RestoreThread(const QString &tarFile, const QString &destFolder) : m_tarFile(tarFile) , m_destFolder(destFolder) { } void RestoreThread::run() { m_success = false; KTar tar(m_tarFile, "application/x-gzip"); tar.open(QIODevice::ReadOnly); if (tar.isOpen()) { const KArchiveDirectory *directory = tar.directory(); if (directory->entries().contains(backupMagicFolder)) { const KArchiveEntry *entry = directory->entry(backupMagicFolder); if (entry->isDirectory()) { ((const KArchiveDirectory *)entry)->copyTo(m_destFolder); m_success = true; } } tar.close(); } } diff --git a/src/basketscene.cpp b/src/basketscene.cpp index adfc6e6..f307541 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... 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_isSelecting = false; // Don't draw selection rectangle after 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 + // Need to do it 2 times, because it's wrong otherwise // (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/htmlexporter.cpp b/src/htmlexporter.cpp index bad8657..b8a4428 100644 --- a/src/htmlexporter.cpp +++ b/src/htmlexporter.cpp @@ -1,528 +1,528 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "htmlexporter.h" #include "basketlistview.h" #include "basketscene.h" #include "bnpview.h" #include "config.h" #include "linklabel.h" #include "note.h" #include "notecontent.h" #include "tools.h" #include #include #include #include #include #include #include //For KIO::copy #include //For KIO::file_copy #include #include #include #include #include #include #include #include #include #include HTMLExporter::HTMLExporter(BasketScene *basket) : dialog(new QProgressDialog()) { QDir dir; // Compute a default file name & path: KConfigGroup config = Global::config()->group("Export to HTML"); QString folder = config.readEntry("lastFolder", QDir::homePath()) + '/'; QString url = folder + QString(basket->basketName()).replace('/', '_') + ".html"; // Ask a file name & path to the user: QString filter = "*.html *.htm|" + i18n("HTML Documents") + "\n*|" + i18n("All Files"); QString destination = url; for (bool askAgain = true; askAgain;) { // Ask: destination = QFileDialog::getSaveFileName(nullptr, i18n("Export to HTML"), destination, filter); // User canceled? if (destination.isEmpty()) return; // File already existing? Ask for overriding: if (dir.exists(destination)) { int result = KMessageBox::questionYesNoCancel( nullptr, "" + i18n("The file %1 already exists. Do you really want to override it?", QUrl::fromLocalFile(destination).fileName()), i18n("Override File?"), KGuiItem(i18n("&Override"), "document-save")); if (result == KMessageBox::Cancel) return; else if (result == KMessageBox::Yes) askAgain = false; } else askAgain = false; } // Create the progress dialog that will always be shown during the export: dialog->setWindowTitle(i18n("Export to HTML")); dialog->setLabelText(i18n("Exporting to HTML. Please wait...")); dialog->setCancelButton(nullptr); dialog->setAutoClose(true); dialog->show(); // Remember the last folder used for HTML exploration: config.writeEntry("lastFolder", QUrl::fromLocalFile(destination).adjusted(QUrl::RemoveFilename).path()); config.sync(); prepareExport(basket, destination); exportBasket(basket, /*isSubBasketScene*/ false); dialog->setValue(dialog->value() + 1); // Finishing finished } HTMLExporter::~HTMLExporter() { } void HTMLExporter::prepareExport(BasketScene *basket, const QString &fullPath) { dialog->setRange(0, /*Preparation:*/ 1 + /*Finishing:*/ 1 + /*Basket:*/ 1 + /*SubBaskets:*/ Global::bnpView->basketCount(Global::bnpView->listViewItemForBasket(basket))); dialog->setValue(0); qApp->processEvents(); // Remember the file path chosen by the user: filePath = fullPath; fileName = QUrl::fromLocalFile(fullPath).fileName(); exportedBasket = basket; currentBasket = nullptr; BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); withBasketTree = (item->childCount() >= 0); // Create and empty the files folder: QString filesFolderPath = i18nc("HTML export folder (files)", "%1_files", filePath) + '/'; // eg.: "/home/seb/foo.html_files/" Tools::deleteRecursively(filesFolderPath); QDir dir; dir.mkdir(filesFolderPath); // Create sub-folders: iconsFolderPath = filesFolderPath + i18nc("HTML export folder (icons)", "icons") + '/'; // eg.: "/home/seb/foo.html_files/icons/" imagesFolderPath = filesFolderPath + i18nc("HTML export folder (images)", "images") + '/'; // eg.: "/home/seb/foo.html_files/images/" basketsFolderPath = filesFolderPath + i18nc("HTML export folder (baskets)", "baskets") + '/'; // eg.: "/home/seb/foo.html_files/baskets/" dir.mkdir(iconsFolderPath); dir.mkdir(imagesFolderPath); dir.mkdir(basketsFolderPath); dialog->setValue(dialog->value() + 1); // Preparation finished } void HTMLExporter::exportBasket(BasketScene *basket, bool isSubBasket) { if (!basket->isLoaded()) { basket->load(); } currentBasket = basket; // Compute the absolute & relative paths for this basket: filesFolderPath = i18nc("HTML export folder (files)", "%1_files", filePath) + '/'; if (isSubBasket) { basketFilePath = basketsFolderPath + basket->folderName().left(basket->folderName().length() - 1) + ".html"; filesFolderName = QStringLiteral("../"); dataFolderName = basket->folderName().left(basket->folderName().length() - 1) + '-' + i18nc("HTML export folder (data)", "data") + '/'; dataFolderPath = basketsFolderPath + dataFolderName; basketsFolderName = QString(); } else { basketFilePath = filePath; filesFolderName = i18nc("HTML export folder (files)", "%1_files", QUrl::fromLocalFile(filePath).fileName()) + '/'; dataFolderName = filesFolderName + i18nc("HTML export folder (data)", "data") + '/'; dataFolderPath = filesFolderPath + i18nc("HTML export folder (data)", "data") + '/'; basketsFolderName = filesFolderName + i18nc("HTML export folder (baskets)", "baskets") + '/'; } iconsFolderName = (isSubBasket ? QStringLiteral("../") : filesFolderName) + i18nc("HTML export folder (icons)", "icons") + QLatin1Char('/'); // eg.: "foo.html_files/icons/" or "../icons/" imagesFolderName = (isSubBasket ? QStringLiteral("../") : filesFolderName) + i18nc("HTML export folder (images)", "images") + QLatin1Char('/'); // eg.: "foo.html_files/images/" or "../images/" qDebug() << "Exporting ================================================"; qDebug() << " filePath:" << filePath; qDebug() << " basketFilePath:" << basketFilePath; qDebug() << " filesFolderPath:" << filesFolderPath; qDebug() << " filesFolderName:" << filesFolderName; qDebug() << " iconsFolderPath:" << iconsFolderPath; qDebug() << " iconsFolderName:" << iconsFolderName; qDebug() << " imagesFolderPath:" << imagesFolderPath; qDebug() << " imagesFolderName:" << imagesFolderName; qDebug() << " dataFolderPath:" << dataFolderPath; qDebug() << " dataFolderName:" << dataFolderName; qDebug() << " basketsFolderPath:" << basketsFolderPath; qDebug() << " basketsFolderName:" << basketsFolderName; // Create the data folder for this basket: QDir dir; dir.mkdir(dataFolderPath); backgroundColorName = basket->backgroundColor().name().toLower().mid(1); // Generate basket icons: QString basketIcon16 = iconsFolderName + copyIcon(basket->icon(), 16); QString basketIcon32 = iconsFolderName + copyIcon(basket->icon(), 32); // Generate the [+] image for groups: QPixmap expandGroup(Note::EXPANDER_WIDTH, Note::EXPANDER_HEIGHT); expandGroup.fill(basket->backgroundColor()); QPainter painter(&expandGroup); Note::drawExpander(&painter, 0, 0, basket->backgroundColor(), /*expand=*/true, basket); painter.end(); expandGroup.save(imagesFolderPath + "expand_group_" + backgroundColorName + ".png", "PNG"); // Generate the [-] image for groups: QPixmap foldGroup(Note::EXPANDER_WIDTH, Note::EXPANDER_HEIGHT); foldGroup.fill(basket->backgroundColor()); painter.begin(&foldGroup); Note::drawExpander(&painter, 0, 0, basket->backgroundColor(), /*expand=*/false, basket); painter.end(); foldGroup.save(imagesFolderPath + "fold_group_" + backgroundColorName + ".png", "PNG"); // Open the file to write: QFile file(basketFilePath); if (!file.open(QIODevice::WriteOnly)) return; stream.setDevice(&file); stream.setCodec("UTF-8"); // Output the header: QString borderColor = Tools::mixColor(basket->backgroundColor(), basket->textColor()).name(); stream << "\n" "\n" " \n" " \n" " \n" " \n" " " << Tools::textToHTMLWithoutP(basket->basketName()) << "\n" " \n"; // Create the column handle image: QPixmap columnHandle(Note::RESIZER_WIDTH, 50); painter.begin(&columnHandle); Note::drawInactiveResizer(&painter, 0, 0, columnHandle.height(), basket->backgroundColor(), /*column=*/true); painter.end(); columnHandle.save(imagesFolderPath + "column_handle_" + backgroundColorName + ".png", "PNG"); stream << " \n" " \n" "

\"\" " << Tools::textToHTMLWithoutP(basket->basketName()) << "

\n"; if (withBasketTree) writeBasketTree(basket); // If filtering, only export filtered notes, inform to the user: // TODO: Filtering tags too!! // TODO: Make sure only filtered notes are exported! // if (decoration()->filterData().isFiltering) // stream << // "

" << i18n("Notes matching the filter "%1":", Tools::textToHTMLWithoutP(decoration()->filterData().string)) << "

\n"; stream << "
\n"; if (basket->isColumnsLayout()) stream << " \n" " \n"; else stream << "
sceneRect().height() << "px; width: " << basket->sceneRect().width() << "px; min-width: 100%;\">\n"; for (Note *note = basket->firstNote(); note; note = note->next()) exportNote(note, /*indent=*/(basket->isFreeLayout() ? 4 : 5)); // Output the footer: if (basket->isColumnsLayout()) stream << "
\n" "
\n"; else stream << "
\n"; stream << QString( " \n" "

%1

\n") .arg(i18n("Made with %2 %3, a tool to take notes and keep information at hand.", KAboutData::applicationData().homepage(), QGuiApplication::applicationDisplayName(), VERSION)); stream << " \n" "\n"; file.close(); stream.setDevice(nullptr); dialog->setValue(dialog->value() + 1); // Basket exportation finished // Recursively export child baskets: BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); if (item->childCount() >= 0) { for (int i = 0; i < item->childCount(); i++) { exportBasket(((BasketListViewItem *)item->child(i))->basket(), /*isSubBasket=*/true); } } } void HTMLExporter::exportNote(Note *note, int indent) { QString spaces; if (note->isColumn()) { QString width; if (false /*TODO: DEBUG AND REENABLE: hasResizer()*/) { // As we cannot be precise in CSS (say eg. "width: 50%-40px;"), // we output a percentage that is approximately correct. // For instance, we compute the currently used percentage of width in the basket - // and try make make it the same on a 1024*768 display in a Web browser: + // and try make it the same on a 1024*768 display in a Web browser: int availableSpaceForColumnsInThisBasket = note->basket()->sceneRect().width() - (note->basket()->columnsCount() - 1) * Note::RESIZER_WIDTH; int availableSpaceForColumnsInBrowser = 1024 /* typical screen width */ - 25 /* window border and scrollbar width */ - 2 * 5 /* page margin */ - (note->basket()->columnsCount() - 1) * Note::RESIZER_WIDTH; if (availableSpaceForColumnsInThisBasket <= 0) availableSpaceForColumnsInThisBasket = 1; int widthValue = (int)(availableSpaceForColumnsInBrowser * (double)note->groupWidth() / availableSpaceForColumnsInThisBasket); if (widthValue <= 0) widthValue = 1; if (widthValue > 100) widthValue = 100; width = QString(" width=\"%1%\"").arg(QString::number(widthValue)); } stream << spaces.fill(' ', indent) << "\n"; // Export child notes: for (Note *child = note->firstChild(); child; child = child->next()) { stream << spaces.fill(' ', indent + 1); exportNote(child, indent + 1); stream << '\n'; } stream << spaces.fill(' ', indent) << "\n"; if (note->hasResizer()) stream << spaces.fill(' ', indent) << "\n"; return; } QString freeStyle; if (note->isFree()) freeStyle = " style=\"position: absolute; left: " + QString::number(note->x()) + "px; top: " + QString::number(note->y()) + "px; width: " + QString::number(note->groupWidth()) + "px\""; if (note->isGroup()) { stream << '\n' << spaces.fill(' ', indent) << "\n"; // Note content is expected to be on the same HTML line, but NOT groups int i = 0; for (Note *child = note->firstChild(); child; child = child->next()) { stream << spaces.fill(' ', indent); if (i == 0) stream << " isFolded() ? "expand_group_" : "fold_group_") << backgroundColorName << ".png" << "\" width=\"" << Note::EXPANDER_WIDTH << "\" height=\"" << Note::EXPANDER_HEIGHT << "\">\n"; else if (i == 1) stream << " countDirectChilds() << "\">\n"; else stream << " \n"; stream << spaces.fill(' ', indent) << " "; exportNote(child, indent + 3); stream << "\n" << spaces.fill(' ', indent) << " \n"; ++i; } stream << '\n' << spaces.fill(' ', indent) << "\n" /*<< spaces.fill(' ', indent - 1)*/; } else { // Additional class for the content (link, netword, color...): QString additionalClasses = note->content()->cssClass(); if (!additionalClasses.isEmpty()) additionalClasses = ' ' + additionalClasses; // Assign the style of each associated tags: for (State::List::Iterator it = note->states().begin(); it != note->states().end(); ++it) additionalClasses += " tag_" + (*it)->id(); // stream << spaces.fill(' ', indent); stream << ""; if (note->emblemsCount() > 0) { stream << ""; } stream << "
"; for (State::List::Iterator it = note->states().begin(); it != note->states().end(); ++it) if (!(*it)->emblem().isEmpty()) { int emblemSize = 16; QString iconFileName = copyIcon((*it)->emblem(), emblemSize); stream << "\""textEquivalent() << "\" title=\"" << (*it)->fullName() << "\">"; } stream << ""; note->content()->exportToHTML(this, indent); stream << "
"; } } void HTMLExporter::writeBasketTree(BasketScene *currentBasket) { stream << "
    \n"; writeBasketTree(currentBasket, exportedBasket, 3); stream << "
\n"; } void HTMLExporter::writeBasketTree(BasketScene *currentBasket, BasketScene *basket, int indent) { // Compute variable HTML code: QString spaces; QString cssClass = (basket == currentBasket ? QStringLiteral(" class=\"current\"") : QString()); QString link('#'); if (currentBasket != basket) { if (currentBasket == exportedBasket) { link = basketsFolderName + basket->folderName().left(basket->folderName().length() - 1) + ".html"; } else if (basket == exportedBasket) { link = "../../" + fileName; } else { link = basket->folderName().left(basket->folderName().length() - 1) + ".html"; } } QString spanStyle; if (basket->backgroundColorSetting().isValid() || basket->textColorSetting().isValid()) { spanStyle = " style=\"background-color: " + basket->backgroundColor().name() + "; color: " + basket->textColor().name() + "\""; } // Write the basket tree line: stream << spaces.fill(' ', indent) << "
  • " "basketName()) << "\">" "icon(), 16) << "\" width=\"16\" height=\"16\" alt=\"\">" << Tools::textToHTMLWithoutP(basket->basketName()) << ""; // Write the sub-baskets lines & end the current one: BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); if (item->childCount() >= 0) { stream << "\n" << spaces.fill(' ', indent) << "
      \n"; for (int i = 0; i < item->childCount(); i++) writeBasketTree(currentBasket, ((BasketListViewItem *)item->child(i))->basket(), indent + 2); stream << spaces.fill(' ', indent) << "
    \n" << spaces.fill(' ', indent) << "
  • \n"; } else { stream << "\n"; } } /** Save an icon to a folder. * If an icon with the same name already exist in the destination, * it is assumed the icon is already copied, so no action is took. * It is optimized so that you can have an empty folder receiving the icons * and call copyIcon() each time you encounter one during export process. */ QString HTMLExporter::copyIcon(const QString &iconName, int size) { if (iconName.isEmpty()) { return QString(); } // Sometimes icon can be "favicons/www.kde.org", we replace the '/' with a '_' QString fileName = iconName; // QString::replace() isn't const, so I must copy the string before fileName = "ico" + QString::number(size) + '_' + fileName.replace('/', '_') + ".png"; QString fullPath = iconsFolderPath + fileName; if (!QFile::exists(fullPath)) { QIcon::fromTheme(iconName).pixmap(size, KIconLoader::Desktop).save(fullPath, "PNG"); } return fileName; } /** Done: Sometimes we can call two times copyFile() with the same srcPath and dataFolderPath * (eg. when exporting basket to HTML with two links to same filename * (but not necesary same path, as in "/home/foo.txt" and "/foo.txt") ) * The first copy isn't yet started, so the dest file isn't created and this method * returns the same filename !!!!!!!!!!!!!!!!!!!! */ QString HTMLExporter::copyFile(const QString &srcPath, bool createIt) { QString fileName = Tools::fileNameForNewFile(QUrl::fromLocalFile(srcPath).fileName(), dataFolderPath); QString fullPath = dataFolderPath + fileName; if (!currentBasket->isEncrypted()) { if (createIt) { // We create the file to be sure another very near call to copyFile() willn't choose the same name: QFile file(QUrl::fromLocalFile(fullPath).path()); if (file.open(QIODevice::WriteOnly)) file.close(); - // And then we copy the file AND overwriting the file we juste created: + // And then we copy the file AND overwriting the file we just created: KIO::file_copy(QUrl::fromLocalFile(srcPath), QUrl::fromLocalFile(fullPath), 0666, KIO::HideProgressInfo | KIO::Resume | KIO::Overwrite); } else { /*KIO::CopyJob *copyJob = */ KIO::copy(QUrl::fromLocalFile(srcPath), QUrl::fromLocalFile(fullPath), KIO::DefaultFlags); // Do it as before } } else { QByteArray array; bool success = currentBasket->loadFromFile(srcPath, &array); if (success) { saveToFile(fullPath, array); } else { qDebug() << "Unable to load encrypted file " << srcPath; } } return fileName; } void HTMLExporter::saveToFile(const QString &fullPath, const QByteArray &array) { QFile file(QUrl::fromLocalFile(fullPath).path()); if (file.open(QIODevice::WriteOnly)) { file.write(array, array.size()); file.close(); } else { qDebug() << "Unable to open file for writing: " << fullPath; } } diff --git a/src/note.cpp b/src/note.cpp index 3902a79..fa66939 100644 --- a/src/note.cpp +++ b/src/note.cpp @@ -1,2524 +1,2524 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "note.h" #include #include #include #include //For KGLobal::locale( #include #include #include #include #include #include #include #include // sqrt() and pow() functions #include // rand() function #include "basketscene.h" #include "debugwindow.h" #include "filter.h" #include "notefactory.h" // For NoteFactory::filteredURL() #include "noteselection.h" #include "settings.h" #include "tag.h" #include "tools.h" /** class Note: */ #define FOR_EACH_CHILD(childVar) for (Note *childVar = firstChild(); childVar; childVar = childVar->next()) class NotePrivate { public: NotePrivate() : prev(nullptr) , next(nullptr) , width(-1) , height(Note::MIN_HEIGHT) { } Note *prev; Note *next; qreal width; qreal height; }; qreal Note::NOTE_MARGIN = 2; qreal Note::INSERTION_HEIGHT = 5; qreal Note::EXPANDER_WIDTH = 9; qreal Note::EXPANDER_HEIGHT = 9; qreal Note::GROUP_WIDTH = 2 * NOTE_MARGIN + EXPANDER_WIDTH; qreal Note::HANDLE_WIDTH = GROUP_WIDTH; qreal Note::RESIZER_WIDTH = GROUP_WIDTH; qreal Note::TAG_ARROW_WIDTH = 5; qreal Note::EMBLEM_SIZE = 16; qreal Note::MIN_HEIGHT = 2 * NOTE_MARGIN + EMBLEM_SIZE; Note::Note(BasketScene *parent) : d(new NotePrivate) , m_groupWidth(250) , m_isFolded(false) , m_firstChild(nullptr) , m_parentNote(nullptr) , m_basket(parent) , m_content(nullptr) , m_addedDate(QDateTime::currentDateTime()) , m_lastModificationDate(QDateTime::currentDateTime()) , m_computedAreas(false) , m_onTop(false) , m_hovered(false) , m_hoveredZone(Note::None) , m_focused(false) , m_selected(false) , m_wasInLastSelectionRect(false) , m_computedState() , m_emblemsCount(0) , m_haveInvisibleTags(false) , m_matching(true) { setHeight(MIN_HEIGHT); if (m_basket) { m_basket->addItem(this); } } Note::~Note() { if (m_basket) { if (m_content && m_content->graphicsItem()) { m_basket->removeItem(m_content->graphicsItem()); } m_basket->removeItem(this); } delete m_content; deleteChilds(); } void Note::setNext(Note *next) { d->next = next; } Note *Note::next() const { return d->next; } void Note::setPrev(Note *prev) { d->prev = prev; } Note *Note::prev() const { return d->prev; } qreal Note::bottom() const { return y() + height() - 1; } void Note::setParentBasket(BasketScene *basket) { if (m_basket) m_basket->removeItem(this); m_basket = basket; if (m_basket) m_basket->addItem(this); } QString Note::addedStringDate() { return m_addedDate.toString(); } QString Note::lastModificationStringDate() { return m_lastModificationDate.toString(); } QString Note::toText(const QString &cuttedFullPath) { if (content()) { // Convert note to text: QString text = content()->toText(cuttedFullPath); // If we should not export tags with the text, return immediately: if (!Settings::exportTextTags()) return text; // Compute the text equivalent of the tag states: QString firstLine; QString otherLines; for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { if (!(*it)->textEquivalent().isEmpty()) { firstLine += (*it)->textEquivalent() + ' '; if ((*it)->onAllTextLines()) otherLines += (*it)->textEquivalent() + ' '; } } // Merge the texts: if (firstLine.isEmpty()) return text; if (otherLines.isEmpty()) return firstLine + text; QStringList lines = text.split('\n'); QString result = firstLine + lines[0] + (lines.count() > 1 ? "\n" : QString()); for (int i = 1 /*Skip the first line*/; i < lines.count(); ++i) result += otherLines + lines[i] + (i < lines.count() - 1 ? "\n" : QString()); return result; } else return QString(); } bool Note::computeMatching(const FilterData &data) { // Groups are always matching: if (!content()) return true; // If we were editing this note and there is a save operation in the middle, then do not hide it suddenly: if (basket()->editedNote() == this) return true; bool matching; // First match tags (they are fast to compute): switch (data.tagFilterType) { default: case FilterData::DontCareTagsFilter: matching = true; break; case FilterData::NotTaggedFilter: matching = m_states.count() <= 0; break; case FilterData::TaggedFilter: matching = m_states.count() > 0; break; case FilterData::TagFilter: matching = hasTag(data.tag); break; case FilterData::StateFilter: matching = hasState(data.state); break; } // Don't try to match the content text if we are not matching now (the filter is of 'AND' type) or if we shouldn't try to match the string: if (matching && !data.string.isEmpty()) matching = content()->match(data); return matching; } int Note::newFilter(const FilterData &data) { bool wasMatching = matching(); m_matching = computeMatching(data); setOnTop(wasMatching && matching()); if (!matching()) { setSelected(false); hide(); } else if (!wasMatching) { show(); } int countMatches = (content() && matching() ? 1 : 0); FOR_EACH_CHILD(child) { countMatches += child->newFilter(data); } return countMatches; } void Note::deleteSelectedNotes(bool deleteFilesToo, QSet *notesToBeDeleted) { if (content()) { if (isSelected()) { basket()->unplugNote(this); if (deleteFilesToo && content()->useFile()) { Tools::deleteRecursively(fullPath()); // basket()->deleteFiles(fullPath()); // Also delete the folder if it's a folder } if (notesToBeDeleted) { notesToBeDeleted->insert(this); } } return; } bool isColumn = this->isColumn(); Note *child = firstChild(); Note *next; while (child) { next = child->next(); // If we delete 'child' on the next line, child->next() will be 0! child->deleteSelectedNotes(deleteFilesToo, notesToBeDeleted); child = next; } // if it remains at least two notes, the group must not be deleted if (!isColumn && !(firstChild() && firstChild()->next())) { if (notesToBeDeleted) { notesToBeDeleted->insert(this); } } } int Note::count() { if (content()) return 1; int count = 0; FOR_EACH_CHILD(child) { count += child->count(); } return count; } int Note::countDirectChilds() { int count = 0; FOR_EACH_CHILD(child) { ++count; } return count; } QString Note::fullPath() { if (content()) return basket()->fullPath() + content()->fileName(); else return QString(); } /*void Note::update() { update(0,0,boundingRect().width,boundingRect().height); }*/ void Note::setFocused(bool focused) { if (m_focused == focused) return; m_focused = focused; unbufferize(); update(); // FIXME: ??? } void Note::setSelected(bool selected) { if (isGroup()) selected = false; // A group cannot be selected! if (m_selected == selected) return; if (!selected && basket()->editedNote() == this) { // basket()->closeEditor(); return; // To avoid a bug that would count 2 less selected notes instead of 1 less! Because m_selected is modified only below. } if (selected) basket()->addSelectedNote(); else basket()->removeSelectedNote(); m_selected = selected; unbufferize(); update(); // FIXME: ??? } void Note::resetWasInLastSelectionRect() { m_wasInLastSelectionRect = false; FOR_EACH_CHILD(child) { child->resetWasInLastSelectionRect(); } } void Note::finishLazyLoad() { if (content()) content()->finishLazyLoad(); FOR_EACH_CHILD(child) { child->finishLazyLoad(); } } void Note::selectIn(const QRectF &rect, bool invertSelection, bool unselectOthers /*= true*/) { // QRect myRect(x(), y(), width(), height()); // bool intersects = myRect.intersects(rect); // Only intersects with visible areas. // If the note is not visible, the user don't think it will be selected while selecting the note(s) that hide this, so act like the user think: bool intersects = false; for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &r = *it; if (r.intersects(rect)) { intersects = true; break; } } bool toSelect = intersects || (!unselectOthers && isSelected()); if (invertSelection) { toSelect = (m_wasInLastSelectionRect == intersects) ? isSelected() : !isSelected(); } setSelected(toSelect); m_wasInLastSelectionRect = intersects; Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->selectIn(rect, invertSelection, unselectOthers); else child->setSelectedRecursively(false); child = child->next(); first = false; } } bool Note::allSelected() { if (isGroup()) { Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) if (!child->allSelected()) return false; ; child = child->next(); first = false; } return true; } else return isSelected(); } void Note::setSelectedRecursively(bool selected) { setSelected(selected && matching()); FOR_EACH_CHILD(child) { child->setSelectedRecursively(selected); } } void Note::invertSelectionRecursively() { if (content()) setSelected(!isSelected() && matching()); FOR_EACH_CHILD(child) { child->invertSelectionRecursively(); } } void Note::unselectAllBut(Note *toSelect) { if (this == toSelect) setSelectedRecursively(true); else { setSelected(false); Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->unselectAllBut(toSelect); else child->setSelectedRecursively(false); child = child->next(); first = false; } } } void Note::invertSelectionOf(Note *toSelect) { if (this == toSelect) setSelectedRecursively(!isSelected()); else { Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->invertSelectionOf(toSelect); child = child->next(); first = false; } } } Note *Note::theSelectedNote() { if (!isGroup() && isSelected()) return this; Note *selectedOne; Note *child = firstChild(); while (child) { selectedOne = child->theSelectedNote(); if (selectedOne) return selectedOne; child = child->next(); } return nullptr; } NoteSelection *Note::selectedNotes() { if (content()) { if (isSelected()) return new NoteSelection(this); else return nullptr; } NoteSelection *selection = new NoteSelection(this); FOR_EACH_CHILD(child) { selection->append(child->selectedNotes()); } if (selection->firstChild) { if (selection->firstChild->next) return selection; else { // If 'selection' is a group with only one content, return directly that content: NoteSelection *reducedSelection = selection->firstChild; // delete selection; // TODO: Cut all connections of 'selection' before deleting it! for (NoteSelection *node = reducedSelection; node; node = node->next) node->parent = nullptr; return reducedSelection; } } else { delete selection; return nullptr; } } bool Note::isAfter(Note *note) { if (this == nullptr || note == nullptr) return true; Note *next = this; while (next) { if (next == note) return false; next = next->nextInStack(); } return true; } bool Note::containsNote(Note *note) { // if (this == note) // return true; while (note) if (note == this) return true; else note = note->parentNote(); // FOR_EACH_CHILD (child) // if (child->containsNote(note)) // return true; return false; } Note *Note::firstRealChild() { Note *child = m_firstChild; while (child) { if (!child->isGroup() /*&& child->matching()*/) return child; child = child->firstChild(); } // Empty group: return nullptr; } Note *Note::lastRealChild() { Note *child = lastChild(); while (child) { if (child->content()) return child; Note *possibleChild = child->lastRealChild(); if (possibleChild && possibleChild->content()) return possibleChild; child = child->prev(); } return nullptr; } Note *Note::lastChild() { Note *child = m_firstChild; while (child && child->next()) child = child->next(); return child; } Note *Note::lastSibling() { Note *last = this; while (last && last->next()) last = last->next(); return last; } qreal Note::yExpander() { Note *child = firstRealChild(); if (child && !child->isShown()) child = child->nextShownInStack(); // FIXME: Restrict scope to 'this' if (child) return (child->boundingRect().height() - EXPANDER_HEIGHT) / 2; else // Groups always have at least 2 notes, except for columns which can have no child (but should exists anyway): return 0; } bool Note::isFree() const { return parentNote() == nullptr && basket() && basket()->isFreeLayout(); } bool Note::isColumn() const { return parentNote() == nullptr && basket() && basket()->isColumnsLayout(); } bool Note::hasResizer() const { // "isFree" || "isColumn but not the last" return parentNote() == nullptr && ((basket() && basket()->isFreeLayout()) || d->next != nullptr); } qreal Note::resizerHeight() const { return (isColumn() ? basket()->sceneRect().height() : d->height); } void Note::setHoveredZone(Zone zone) // TODO: Remove setHovered(bool) and assume it is hovered if zone != None !!!!!!! { if (m_hoveredZone != zone) { if (content()) content()->setHoveredZone(m_hoveredZone, zone); m_hoveredZone = zone; unbufferize(); } } Note::Zone Note::zoneAt(const QPointF &pos, bool toAdd) { // Keep the resizer highlighted when resizong, even if the cursor is over another note: if (basket()->resizingNote() == this) return Resizer; // When dropping/pasting something on a column resizer, add it at the bottom of the column, and don't group it with the whole column: if (toAdd && isColumn() && hasResizer()) { qreal right = rightLimit() - x(); if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= 0) && (pos.y() < resizerHeight())) // Code copied from below return BottomColumn; } // Below a column: if (isColumn()) { if (pos.y() >= height() && pos.x() < rightLimit() - x()) return BottomColumn; } // If toAdd, return only TopInsert, TopGroup, BottomInsert or BottomGroup // (by spanning those areas in 4 equal rectangles in the note): if (toAdd) { if (!isFree() && !Settings::groupOnInsertionLine()) { if (pos.y() < height() / 2) return TopInsert; else return BottomInsert; } if (isColumn() && pos.y() >= height()) return BottomGroup; if (pos.y() < height() / 2) if (pos.x() < width() / 2 && !isFree()) return TopInsert; else return TopGroup; else if (pos.x() < width() / 2 && !isFree()) return BottomInsert; else return BottomGroup; } // If in the resizer: if (hasResizer()) { qreal right = rightLimit() - x(); if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= 0) && (pos.y() < resizerHeight())) return Resizer; } // If isGroup, return only Group, GroupExpander, TopInsert or BottomInsert: if (isGroup()) { if (pos.y() < INSERTION_HEIGHT) { if (isFree()) return TopGroup; else return TopInsert; } if (pos.y() >= height() - INSERTION_HEIGHT) { if (isFree()) return BottomGroup; else return BottomInsert; } if (pos.x() >= NOTE_MARGIN && pos.x() < NOTE_MARGIN + EXPANDER_WIDTH) { qreal yExp = yExpander(); if (pos.y() >= yExp && pos.y() < yExp + EXPANDER_HEIGHT) return GroupExpander; } if (pos.x() < width()) return Group; else return Note::None; } // Else, it's a normal note: if (pos.x() < HANDLE_WIDTH) return Handle; if (pos.y() < INSERTION_HEIGHT) { if ((!isFree() && !Settings::groupOnInsertionLine()) || (pos.x() < width() / 2 && !isFree())) return TopInsert; else return TopGroup; } if (pos.y() >= height() - INSERTION_HEIGHT) { if ((!isFree() && !Settings::groupOnInsertionLine()) || (pos.x() < width() / 2 && !isFree())) return BottomInsert; else return BottomGroup; } for (int i = 0; i < m_emblemsCount; i++) { if (pos.x() >= HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * i && pos.x() < HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * i + NOTE_MARGIN + EMBLEM_SIZE) return (Zone)(Emblem0 + i); } if (pos.x() < HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * m_emblemsCount + NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN) return TagsArrow; if (!linkAt(pos).isEmpty()) return Link; int customZone = content()->zoneAt(pos - QPointF(contentX(), NOTE_MARGIN)); if (customZone) return (Note::Zone)customZone; return Content; } QString Note::linkAt(const QPointF &pos) { QString link = m_content->linkAt(pos - QPointF(contentX(), NOTE_MARGIN)); if (link.isEmpty() || link.startsWith(QLatin1String("basket://"))) return link; else return NoteFactory::filteredURL(QUrl::fromUserInput(link)).toDisplayString(); // KURIFilter::self()->filteredURI(link); } qreal Note::contentX() const { return HANDLE_WIDTH + NOTE_MARGIN + (EMBLEM_SIZE + NOTE_MARGIN) * m_emblemsCount + TAG_ARROW_WIDTH + NOTE_MARGIN; } QRectF Note::zoneRect(Note::Zone zone, const QPointF &pos) { if (zone >= Emblem0) return QRect(HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * (zone - Emblem0), INSERTION_HEIGHT, NOTE_MARGIN + EMBLEM_SIZE, height() - 2 * INSERTION_HEIGHT); qreal yExp; qreal right; qreal xGroup = (isFree() ? (isGroup() ? 0 : GROUP_WIDTH) : width() / 2); QRectF rect; qreal insertSplit = (Settings::groupOnInsertionLine() ? 2 : 1); switch (zone) { case Note::Handle: return QRectF(0, 0, HANDLE_WIDTH, d->height); case Note::Group: yExp = yExpander(); if (pos.y() < yExp) { return QRectF(0, INSERTION_HEIGHT, d->width, yExp - INSERTION_HEIGHT); } if (pos.y() > yExp + EXPANDER_HEIGHT) { return QRectF(0, yExp + EXPANDER_HEIGHT, d->width, d->height - yExp - EXPANDER_HEIGHT - INSERTION_HEIGHT); } if (pos.x() < NOTE_MARGIN) { return QRectF(0, 0, NOTE_MARGIN, d->height); } else { return QRectF(d->width - NOTE_MARGIN, 0, NOTE_MARGIN, d->height); } case Note::TagsArrow: return QRectF(HANDLE_WIDTH + (NOTE_MARGIN + EMBLEM_SIZE) * m_emblemsCount, INSERTION_HEIGHT, NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN, d->height - 2 * INSERTION_HEIGHT); case Note::Custom0: case Note::Content: rect = content()->zoneRect(zone, pos - QPointF(contentX(), NOTE_MARGIN)); rect.translate(contentX(), NOTE_MARGIN); return rect.intersected(QRectF(contentX(), INSERTION_HEIGHT, d->width - contentX(), d->height - 2 * INSERTION_HEIGHT)); // Only IN contentRect case Note::GroupExpander: return QRectF(NOTE_MARGIN, yExpander(), EXPANDER_WIDTH, EXPANDER_HEIGHT); case Note::Resizer: right = rightLimit(); return QRectF(right - x(), 0, RESIZER_WIDTH, resizerHeight()); case Note::Link: case Note::TopInsert: if (isGroup()) return QRectF(0, 0, d->width, INSERTION_HEIGHT); else return QRectF(HANDLE_WIDTH, 0, d->width / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); case Note::TopGroup: return QRectF(xGroup, 0, d->width - xGroup, INSERTION_HEIGHT); case Note::BottomInsert: if (isGroup()) return QRectF(0, d->height - INSERTION_HEIGHT, d->width, INSERTION_HEIGHT); else return QRectF(HANDLE_WIDTH, d->height - INSERTION_HEIGHT, d->width / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); case Note::BottomGroup: return QRectF(xGroup, d->height - INSERTION_HEIGHT, d->width - xGroup, INSERTION_HEIGHT); case Note::BottomColumn: return QRectF(0, d->height, rightLimit() - x(), basket()->sceneRect().height() - d->height); case Note::None: return QRectF(/*0, 0, -1, -1*/); default: return QRectF(/*0, 0, -1, -1*/); } } Qt::CursorShape Note::cursorFromZone(Zone zone) const { switch (zone) { case Note::Handle: case Note::Group: return Qt::SizeAllCursor; break; case Note::Resizer: if (isColumn()) { return Qt::SplitHCursor; } else { return Qt::SizeHorCursor; } break; case Note::Custom0: return m_content->cursorFromZone(zone); break; case Note::Link: case Note::TagsArrow: case Note::GroupExpander: return Qt::PointingHandCursor; break; case Note::Content: return Qt::IBeamCursor; break; case Note::TopInsert: case Note::TopGroup: case Note::BottomInsert: case Note::BottomGroup: case Note::BottomColumn: return Qt::CrossCursor; break; case Note::None: return Qt::ArrowCursor; break; default: State *state = stateForEmblemNumber(zone - Emblem0); if (state && state->parentTag()->states().count() > 1) return Qt::PointingHandCursor; else return Qt::ArrowCursor; } } qreal Note::height() const { return d->height; } void Note::setHeight(qreal height) { setInitialHeight(height); } void Note::setInitialHeight(qreal height) { prepareGeometryChange(); d->height = height; } void Note::unsetWidth() { prepareGeometryChange(); d->width = 0; unbufferize(); FOR_EACH_CHILD(child) child->unsetWidth(); } qreal Note::width() const { return (isGroup() ? (isColumn() ? 0 : GROUP_WIDTH) : d->width); } void Note::requestRelayout() { prepareGeometryChange(); d->width = 0; unbufferize(); basket()->relayoutNotes(); // TODO: A signal that will relayout ONCE and DELAYED if called several times } void Note::setWidth(qreal width) // TODO: inline ? { if (d->width != width) setWidthForceRelayout(width); } void Note::setWidthForceRelayout(qreal width) { prepareGeometryChange(); unbufferize(); d->width = (width < minWidth() ? minWidth() : width); int contentWidth = width - contentX() - NOTE_MARGIN; if (m_content) { ///// FIXME: is this OK? if (contentWidth < 1) contentWidth = 1; if (contentWidth < m_content->minWidth()) contentWidth = m_content->minWidth(); setHeight(m_content->setWidthAndGetHeight(contentWidth /* < 1 ? 1 : contentWidth*/) + 2 * NOTE_MARGIN); if (d->height < 3 * INSERTION_HEIGHT) // Assure a minimal size... setHeight(3 * INSERTION_HEIGHT); } } qreal Note::minWidth() const { if (m_content) return contentX() + m_content->minWidth() + NOTE_MARGIN; else return GROUP_WIDTH; ///// FIXME: is this OK? } qreal Note::minRight() { if (isGroup()) { qreal right = x() + width(); Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) right = qMax(right, child->minRight()); child = child->next(); first = false; } if (isColumn()) { qreal minColumnRight = x() + 2 * HANDLE_WIDTH; if (right < minColumnRight) return minColumnRight; } return right; } else return x() + minWidth(); } bool Note::toggleFolded() { // Close the editor if it was editing a note that we are about to hide after collapsing: if (!m_isFolded && basket() && basket()->isDuringEdit()) { if (containsNote(basket()->editedNote()) && firstRealChild() != basket()->editedNote()) basket()->closeEditor(); } // Important to close the editor FIRST, because else, the last edited note would not show during folding animation (don't ask me why ;-) ): m_isFolded = !m_isFolded; unbufferize(); return true; } Note *Note::noteAt(QPointF pos) { if (matching() && hasResizer()) { int right = rightLimit(); // TODO: This code is duplicated 3 times: !!!! if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= y()) && (pos.y() < y() + resizerHeight())) { if (!m_computedAreas) recomputeAreas(); for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &rect = *it; if (rect.contains(pos.x(), pos.y())) return this; } } } if (isGroup()) { if ((pos.x() >= x()) && (pos.x() < x() + width()) && (pos.y() >= y()) && (pos.y() < y() + d->height)) { if (!m_computedAreas) recomputeAreas(); for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &rect = *it; if (rect.contains(pos.x(), pos.y())) return this; } return nullptr; } Note *child = firstChild(); Note *found; bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) { found = child->noteAt(pos); if (found) return found; } child = child->next(); first = false; } } else if (matching() && pos.y() >= y() && pos.y() < y() + d->height && pos.x() >= x() && pos.x() < x() + d->width) { if (!m_computedAreas) recomputeAreas(); for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF &rect = *it; if (rect.contains(pos.x(), pos.y())) return this; } return nullptr; } return nullptr; } QRectF Note::boundingRect() const { if (hasResizer()) { return QRectF(0, 0, rightLimit() - x() + RESIZER_WIDTH, resizerHeight()); } return QRectF(0, 0, width(), height()); } QRectF Note::resizerRect() { return QRectF(rightLimit(), y(), RESIZER_WIDTH, resizerHeight()); } bool Note::showSubNotes() { return !m_isFolded || basket()->isFiltering(); } void Note::relayoutAt(qreal ax, qreal ay) { if (!matching()) return; m_computedAreas = false; m_areas.clear(); - // Don't relayout free notes one under the other, because by definition they are freely positionned! + // Don't relayout free notes one under the other, because by definition they are freely positioned! if (isFree()) { ax = x(); ay = y(); // If it's a column, it always have the same "fixed" position (no animation): } else if (isColumn()) { ax = (prev() ? prev()->rightLimit() + RESIZER_WIDTH : 0); ay = 0; setX(ax); setY(ay); // But relayout others vertically if they are inside such primary groups or if it is a "normal" basket: } else { setX(ax); setY(ay); } // Then, relayout sub-notes (only the first, if the group is folded) and so, assign an height to the group: if (isGroup()) { qreal h = 0; Note *child = firstChild(); bool first = true; while (child) { if (child->matching() && (!m_isFolded || first || basket()->isFiltering())) { // Don't use showSubNotes() but use !m_isFolded because we don't want a relayout for the animated collapsing notes child->relayoutAt(ax + width(), ay + h); h += child->height(); if (!child->isVisible()) child->show(); } else { // In case the user collapse a group, then move it and then expand it: child->setXRecursively(x() + width()); // notes SHOULD have a good X coordinate, and not the old one! if (child->isVisible()) child->hideRecursively(); } // For future animation when re-match, but on bottom of already matched notes! // Find parent primary note and set the Y to THAT y: // if (!child->matching()) // child->setY(parentPrimaryNote()->y()); child = child->next(); first = false; } if (height() != h || d->height != h) { unbufferize(); /*if (animate) addAnimation(0, 0, h - height()); else {*/ setHeight(h); unbufferize(); //} } } else { // If rightLimit is exceeded, set the top-level right limit!!! // and NEED RELAYOUT setWidth(finalRightLimit() - x()); } // Set the basket area limits (but not for child notes: no need, because they will look for their parent note): if (!parentNote()) { if (basket()->tmpWidth < finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0)) basket()->tmpWidth = finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0); if (basket()->tmpHeight < y() + height()) basket()->tmpHeight = y() + height(); // However, if the note exceed the allowed size, let it! : } else if (!isGroup()) { if (basket()->tmpWidth < x() + width() + (hasResizer() ? RESIZER_WIDTH : 0)) basket()->tmpWidth = x() + width() + (hasResizer() ? RESIZER_WIDTH : 0); if (basket()->tmpHeight < y() + height()) basket()->tmpHeight = y() + height(); } } void Note::setXRecursively(qreal x) { setX(x); FOR_EACH_CHILD(child) child->setXRecursively(x + width()); } void Note::setYRecursively(qreal y) { setY(y); FOR_EACH_CHILD(child) child->setYRecursively(y); } void Note::hideRecursively() { hide(); FOR_EACH_CHILD(child) child->hideRecursively(); } void Note::setGroupWidth(qreal width) { m_groupWidth = width; } qreal Note::groupWidth() const { if (hasResizer()) return m_groupWidth; else return rightLimit() - x(); } qreal Note::rightLimit() const { if (isColumn() && d->next == nullptr) // The last column return qMax((x() + minWidth()), (qreal)basket()->graphicsView()->viewport()->width()); else if (parentNote()) return parentNote()->rightLimit(); else return x() + m_groupWidth; } qreal Note::finalRightLimit() const { if (isColumn() && d->next == nullptr) // The last column return qMax(x() + minWidth(), (qreal)basket()->graphicsView()->viewport()->width()); else if (parentNote()) return parentNote()->finalRightLimit(); else return x() + m_groupWidth; } void Note::drawExpander(QPainter *painter, qreal x, qreal y, const QColor &background, bool expand, BasketScene *basket) { QStyleOption opt; opt.state = (expand ? QStyle::State_On : QStyle::State_Off); opt.rect = QRect(x, y, 9, 9); opt.palette = basket->palette(); opt.palette.setColor(QPalette::Base, background); painter->fillRect(opt.rect, background); QStyle *style = basket->style(); if (!expand) { style->drawPrimitive(QStyle::PE_IndicatorArrowDown, &opt, painter, basket->graphicsView()->viewport()); } else { style->drawPrimitive(QStyle::PE_IndicatorArrowRight, &opt, painter, basket->graphicsView()->viewport()); } } QColor expanderBackground(qreal height, qreal y, const QColor &foreground) { // We will divide height per two, subtract one and use that below a division bar: // To avoid division by zero error, height should be bigger than 3. // And to avoid y errors or if y is on the borders, we return the border color: the background color. if (height <= 3 || y <= 0 || y >= height - 1) return foreground; const QColor dark = foreground.darker(110); // 1/1.1 of brightness const QColor light = foreground.lighter(150); // 50% brighter qreal h1, h2, s1, s2, v1, v2; int ng; if (y <= (height - 2) / 2) { light.getHsvF(&h1, &s1, &v1); dark.getHsvF(&h2, &s2, &v2); ng = (height - 2) / 2; y -= 1; } else { dark.getHsvF(&h1, &s1, &v1); foreground.getHsvF(&h2, &s2, &v2); ng = (height - 2) - (height - 2) / 2; y -= 1 + (height - 2) / 2; } return QColor::fromHsvF(h1 + ((h2 - h1) * y) / (ng - 1), s1 + ((s2 - s1) * y) / (ng - 1), v1 + ((v2 - v1) * y) / (ng - 1)); } void Note::drawHandle(QPainter *painter, qreal x, qreal y, qreal width, qreal height, const QColor &background, const QColor &foreground, const QColor &lightForeground) { const QPen backgroundPen(background); const QPen foregroundPen(foreground); // Draw the surrounding rectangle: painter->setPen(foregroundPen); painter->drawLine(0, 0, width - 1, 0); painter->drawLine(0, 0, 0, height - 1); painter->drawLine(0, height - 1, width - 1, height - 1); // Draw the gradients: painter->fillRect(1 + x, 1 + y, width - 2, height - 2, lightForeground); // Round the top corner with background color: painter->setPen(backgroundPen); painter->drawLine(0, 0, 0, 3); painter->drawLine(1, 0, 3, 0); painter->drawPoint(1, 1); // Round the bottom corner with background color: painter->drawLine(0, height - 1, 0, height - 4); painter->drawLine(1, height - 1, 3, height - 1); painter->drawPoint(1, height - 2); // Surrounding line of the rounded top-left corner: painter->setPen(foregroundPen); painter->drawLine(1, 2, 1, 3); painter->drawLine(2, 1, 3, 1); // Surrounding line of the rounded bottom-left corner: painter->drawLine(1, height - 3, 1, height - 4); painter->drawLine(2, height - 2, 3, height - 2); // Anti-aliased rounded top corner (1/2): painter->setPen(lightForeground); painter->drawPoint(0, 3); painter->drawPoint(3, 0); painter->drawPoint(2, 2); // Anti-aliased rounded bottom corner: painter->drawPoint(0, height - 4); painter->drawPoint(3, height - 1); painter->drawPoint(2, height - 3); // Draw the grips: const qreal middleHeight = (height - 1) / 2; const qreal middleWidth = width / 2; painter->fillRect(middleWidth - 2, middleHeight, 2, 2, foreground); painter->fillRect(middleWidth + 2, middleHeight, 2, 2, foreground); painter->fillRect(middleWidth - 2, middleHeight - 4, 2, 2, foreground); painter->fillRect(middleWidth + 2, middleHeight - 4, 2, 2, foreground); painter->fillRect(middleWidth - 2, middleHeight + 4, 2, 2, foreground); painter->fillRect(middleWidth + 2, middleHeight + 4, 2, 2, foreground); } void Note::drawResizer(QPainter *painter, qreal x, qreal y, qreal width, qreal height, const QColor &background, const QColor &foreground, bool rounded) { const QPen backgroundPen(background); const QPen foregroundPen(foreground); const QColor lightForeground = Tools::mixColor(background, foreground, 2); // Draw the surrounding rectangle: painter->setPen(foregroundPen); painter->fillRect(0, 0, width, height, lightForeground); painter->drawLine(0, 0, width - 2, 0); painter->drawLine(0, height - 1, width - 2, height - 1); painter->drawLine(width - 1, 2, width - 1, height - 2); if (isColumn()) { painter->drawLine(0, 2, 0, height - 2); } if (rounded) { // Round the top corner with background color: painter->setPen(backgroundPen); painter->drawLine(width - 1, 0, width - 3, 0); painter->drawLine(width - 1, 1, width - 1, 2); painter->drawPoint(width - 2, 1); // Round the bottom corner with background color: painter->drawLine(width - 1, height - 1, width - 1, height - 4); painter->drawLine(width - 2, height - 1, width - 4, height - 1); painter->drawPoint(width - 2, height - 2); // Surrounding line of the rounded top-left corner: painter->setPen(foregroundPen); painter->drawLine(width - 2, 2, width - 2, 3); painter->drawLine(width - 3, 1, width - 4, 1); // Surrounding line of the rounded bottom-left corner: painter->drawLine(width - 2, height - 3, width - 2, height - 4); painter->drawLine(width - 3, height - 2, width - 4, height - 2); // Anti-aliased rounded top corner (1/2): painter->setPen(Tools::mixColor(foreground, background, 2)); painter->drawPoint(width - 1, 3); painter->drawPoint(width - 4, 0); // Anti-aliased rounded bottom corner: painter->drawPoint(width - 1, height - 4); painter->drawPoint(width - 4, height - 1); // Anti-aliased rounded top corner (2/2): painter->setPen(foreground); painter->drawPoint(width - 3, 2); // Anti-aliased rounded bottom corner (2/2): painter->drawPoint(width - 3, height - 3); } // Draw the arrows: qreal xArrow = 2; qreal hMargin = 9; int countArrows = (height >= hMargin * 4 + 6 * 3 ? 3 : (height >= hMargin * 3 + 6 * 2 ? 2 : 1)); for (int i = 0; i < countArrows; ++i) { qreal yArrow; switch (countArrows) { default: case 1: yArrow = (height - 6) / 2; break; case 2: yArrow = (i == 1 ? hMargin : height - hMargin - 6); break; case 3: yArrow = (i == 1 ? hMargin : (i == 2 ? (height - 6) / 2 : height - hMargin - 6)); break; } /// Dark color: painter->setPen(foreground); // Left arrow: painter->drawLine(xArrow, yArrow + 2, xArrow + 2, yArrow); painter->drawLine(xArrow, yArrow + 2, xArrow + 2, yArrow + 4); // Right arrow: painter->drawLine(width - 1 - xArrow, yArrow + 2, width - 1 - xArrow - 2, yArrow); painter->drawLine(width - 1 - xArrow, yArrow + 2, width - 1 - xArrow - 2, yArrow + 4); /// Light color: painter->setPen(background); // Left arrow: painter->drawLine(xArrow, yArrow + 2 + 1, xArrow + 2, yArrow + 1); painter->drawLine(xArrow, yArrow + 2 + 1, xArrow + 2, yArrow + 4 + 1); // Right arrow: painter->drawLine(width - 1 - xArrow, yArrow + 2 + 1, width - 1 - xArrow - 2, yArrow + 1); painter->drawLine(width - 1 - xArrow, yArrow + 2 + 1, width - 1 - xArrow - 2, yArrow + 4 + 1); } } void Note::drawInactiveResizer(QPainter *painter, qreal x, qreal y, qreal height, const QColor &background, bool column) { // If background color is too dark, we compute a lighter color instead of a darker: QColor darkBgColor = (Tools::tooDark(background) ? background.lighter(120) : background.darker(105)); painter->fillRect(x, y, RESIZER_WIDTH, height, Tools::mixColor(background, darkBgColor, 2)); } QPalette Note::palette() const { return (m_basket ? m_basket->palette() : qApp->palette()); } /* type: 1: topLeft * 2: bottomLeft * 3: topRight * 4: bottomRight * 5: fourCorners * 6: noteInsideAndOutsideCorners * (x,y) relate to the painter origin * (width,height) only used for 5:fourCorners type */ void Note::drawRoundings(QPainter *painter, qreal x, qreal y, int type, qreal width, qreal height) { qreal right; switch (type) { case 1: x += this->x(); y += this->y(); basket()->blendBackground(*painter, QRectF(x, y, 4, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 1, 2, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 3, 1, 1), this->x(), this->y()); break; case 2: x += this->x(); y += this->y(); basket()->blendBackground(*painter, QRectF(x, y - 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 1, 2, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + 2, 4, 1), this->x(), this->y()); break; case 3: right = rightLimit(); x += right; y += this->y(); basket()->blendBackground(*painter, QRectF(x - 1, y, 4, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 1, y + 1, 2, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 2, y + 2, 1, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 2, y + 3, 1, 1), right, this->y()); break; case 4: right = rightLimit(); x += right; y += this->y(); basket()->blendBackground(*painter, QRectF(x + 2, y - 1, 1, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 2, y, 1, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x + 1, y + 1, 2, 1), right, this->y()); basket()->blendBackground(*painter, QRectF(x - 1, y + 2, 4, 1), right, this->y()); break; case 5: // First make sure the corners are white (depending on the widget style): painter->setPen(basket()->backgroundColor()); painter->drawPoint(x, y); painter->drawPoint(x + width - 1, y); painter->drawPoint(x + width - 1, y + height - 1); painter->drawPoint(x, y + height - 1); // And then blend corners: x += this->x(); y += this->y(); basket()->blendBackground(*painter, QRectF(x, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 1, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x, y + height - 1, 1, 1), this->x(), this->y()); break; case 6: x += this->x(); y += this->y(); // if (!isSelected()) { // Inside left corners: basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH + 1, y + 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH, y + 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH + 1, y + height - 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + HANDLE_WIDTH, y + height - 3, 1, 1), this->x(), this->y()); // Inside right corners: basket()->blendBackground(*painter, QRectF(x + width - 4, y + 1, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 3, y + 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 4, y + height - 2, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 3, y + height - 3, 1, 1), this->x(), this->y()); //} // Outside right corners: basket()->blendBackground(*painter, QRectF(x + width - 1, y, 1, 1), this->x(), this->y()); basket()->blendBackground(*painter, QRectF(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); break; } } /// Blank Spaces Drawing: void Note::setOnTop(bool onTop) { setZValue(onTop ? 100 : 0); m_onTop = onTop; Note *note = firstChild(); while (note) { note->setOnTop(onTop); note = note->next(); } } void substractRectOnAreas(const QRectF &rectToSubstract, QList &areas, bool andRemove) { for (int i = 0; i < areas.size();) { QRectF &rect = areas[i]; // Split the rectangle if it intersects with rectToSubstract: if (rect.intersects(rectToSubstract)) { // Create the top rectangle: if (rectToSubstract.top() > rect.top()) { areas.insert(i++, QRectF(rect.left(), rect.top(), rect.width(), rectToSubstract.top() - rect.top())); rect.setTop(rectToSubstract.top()); } // Create the bottom rectangle: if (rectToSubstract.bottom() < rect.bottom()) { areas.insert(i++, QRectF(rect.left(), rectToSubstract.bottom(), rect.width(), rect.bottom() - rectToSubstract.bottom())); rect.setBottom(rectToSubstract.bottom()); } // Create the left rectangle: if (rectToSubstract.left() > rect.left()) { areas.insert(i++, QRectF(rect.left(), rect.top(), rectToSubstract.left() - rect.left(), rect.height())); rect.setLeft(rectToSubstract.left()); } // Create the right rectangle: if (rectToSubstract.right() < rect.right()) { areas.insert(i++, QRectF(rectToSubstract.right(), rect.top(), rect.right() - rectToSubstract.right(), rect.height())); rect.setRight(rectToSubstract.right()); } // Remove the rectangle if it's entirely contained: if (andRemove && rectToSubstract.contains(rect)) areas.removeAt(i); else ++i; } else ++i; } } void Note::recomputeAreas() { // Initialize the areas with the note rectangle(s): m_areas.clear(); m_areas.append(visibleRect()); if (hasResizer()) m_areas.append(resizerRect()); // Cut the areas where other notes are on top of this note: Note *note = basket()->firstNote(); bool noteIsAfterThis = false; while (note) { noteIsAfterThis = recomputeAreas(note, noteIsAfterThis); note = note->next(); } } bool Note::recomputeAreas(Note *note, bool noteIsAfterThis) { if (note == this) noteIsAfterThis = true; // Only compute overlapping of notes AFTER this, or ON TOP this: // else if ( note->matching() && noteIsAfterThis && (!isOnTop() || (isOnTop() && note->isOnTop())) || (!isOnTop() && note->isOnTop()) ) { else if (note->matching() && noteIsAfterThis && ((!(isOnTop() || isEditing()) || ((isOnTop() || isEditing()) && (note->isOnTop() || note->isEditing()))) || (!(isOnTop() || isEditing()) && (note->isOnTop() || note->isEditing())))) { // if (!(isSelected() && !note->isSelected())) { // FIXME: FIXME: FIXME: FIXME: This last condition was added LATE, so we should look if it's ALWAYS good: substractRectOnAreas(note->visibleRect(), m_areas, true); if (note->hasResizer()) substractRectOnAreas(note->resizerRect(), m_areas, true); //} } if (note->isGroup()) { Note *child = note->firstChild(); bool first = true; while (child) { if ((note->showSubNotes() || first) && note->matching()) noteIsAfterThis = recomputeAreas(child, noteIsAfterThis); child = child->next(); first = false; } } return noteIsAfterThis; } bool Note::isEditing() { return basket()->editedNote() == this; } /* Drawing policy: * ============== * - Draw the note on a pixmap and then draw the pixmap on screen is faster and * flicker-free, rather than drawing directly on screen * - The next time the pixmap can be directly redrawn on screen without * (relatively low, for small texts) time-consuming text-drawing * - To keep memory footprint low, we can destruct the bufferPixmap because * redrawing it offscreen and applying it onscreen is nearly as fast as just * drawing the pixmap onscreen * - But as drawing the pixmap offscreen is little time consuming we can keep * last visible notes buffered and then the redraw of the entire window is * INSTANTANEOUS * - We keep buffered note/group draws BUT NOT the resizer: such objects are * small and fast to draw, so we don't complexify code for that */ void Note::draw(QPainter *painter, const QRectF & /*clipRect*/) { if (!matching()) return; /** Compute visible areas: */ if (!m_computedAreas) recomputeAreas(); if (m_areas.isEmpty()) return; /** Directly draw pixmap on screen if it is already buffered: */ if (isBufferized()) { drawBufferOnScreen(painter, m_bufferedPixmap); return; } /** If pixmap is Null (size 0), no point in painting: **/ if (!width() || !height()) return; /** Initialise buffer painter: */ m_bufferedPixmap = QPixmap(width(), height()); Q_ASSERT(!m_bufferedPixmap.isNull()); QPainter painter2(&m_bufferedPixmap); /** Initialise colors: */ QColor baseColor(basket()->backgroundColor()); QColor highColor(palette().color(QPalette::Highlight)); QColor midColor = Tools::mixColor(baseColor, highColor, 2); /** Initialise brushes and pens: */ QBrush baseBrush(baseColor); QBrush highBrush(highColor); QPen basePen(baseColor); QPen highPen(highColor); QPen midPen(midColor); /** Figure out the state of the note: */ bool hovered = m_hovered && m_hoveredZone != TopInsert && m_hoveredZone != BottomInsert && m_hoveredZone != Resizer; /** And then draw the group: */ if (isGroup()) { // Draw background or handle: if (hovered) { drawHandle(&painter2, 0, 0, width(), height(), baseColor, highColor, midColor); drawRoundings(&painter2, 0, 0, /*type=*/1); drawRoundings(&painter2, 0, height() - 3, /*type=*/2); } else { painter2.fillRect(0, 0, width(), height(), baseBrush); basket()->blendBackground(painter2, boundingRect().translated(x(), y()), -1, -1, /*opaque=*/true); } // Draw expander: qreal yExp = yExpander(); drawExpander(&painter2, NOTE_MARGIN, yExp, hovered ? midColor : baseColor, m_isFolded, basket()); // Draw expander rounded edges: if (hovered) { QColor color1 = expanderBackground(height(), yExp, highColor); QColor color2 = expanderBackground(height(), yExp + EXPANDER_HEIGHT - 1, highColor); painter2.setPen(color1); painter2.drawPoint(NOTE_MARGIN, yExp); painter2.drawPoint(NOTE_MARGIN + 9 - 1, yExp); painter2.setPen(color2); painter2.drawPoint(NOTE_MARGIN, yExp + 9 - 1); painter2.drawPoint(NOTE_MARGIN + 9 - 1, yExp + 9 - 1); } else drawRoundings(&painter2, NOTE_MARGIN, yExp, /*type=*/5, 9, 9); // Draw on screen: painter2.end(); drawBufferOnScreen(painter, m_bufferedPixmap); return; } /** Or draw the note: */ // What are the background colors: QColor background; if (m_computedState.backgroundColor().isValid()) { background = m_computedState.backgroundColor(); } else { background = basket()->backgroundColor(); } if (!hovered && !isSelected()) { // Draw background: painter2.fillRect(0, 0, width(), height(), background); basket()->blendBackground(painter2, boundingRect().translated(x(), y())); } else { // Draw selection background: painter2.fillRect(0, 0, width(), height(), midColor); // Top/Bottom lines: painter2.setPen(highPen); painter2.drawLine(0, height() - 1, width(), height() - 1); painter2.drawLine(0, 0, width(), 0); // The handle: drawHandle(&painter2, 0, 0, HANDLE_WIDTH, height(), baseColor, highColor, midColor); drawRoundings(&painter2, 0, 0, /*type=*/1); drawRoundings(&painter2, 0, height() - 3, /*type=*/2); painter2.setPen(midColor); drawRoundings(&painter2, 0, 0, /*type=*/6, width(), height()); } // Draw the Emblems: qreal yIcon = (height() - EMBLEM_SIZE) / 2; qreal xIcon = HANDLE_WIDTH + NOTE_MARGIN; for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { if (!(*it)->emblem().isEmpty()) { QPixmap stateEmblem = KIconLoader::global()->loadIcon((*it)->emblem(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, false); painter2.drawPixmap(xIcon, yIcon, stateEmblem); xIcon += NOTE_MARGIN + EMBLEM_SIZE; } } // Determine the colors (for the richText drawing) and the text color (for the tags arrow too): QPalette notePalette(basket()->palette()); notePalette.setColor(QPalette::Text, (m_computedState.textColor().isValid() ? m_computedState.textColor() : basket()->textColor())); notePalette.setColor(QPalette::Background, background); if (isSelected()) notePalette.setColor(QPalette::Text, palette().color(QPalette::HighlightedText)); // Draw the Tags Arrow: if (hovered) { QColor textColor = notePalette.color(QPalette::Text); QColor light = Tools::mixColor(textColor, background); QColor mid = Tools::mixColor(textColor, light); painter2.setPen(light); // QPen(basket()->colorGroup().darker().lighter(150))); painter2.drawLine(xIcon, yIcon + 6, xIcon + 4, yIcon + 6); painter2.setPen(mid); // QPen(basket()->colorGroup().darker())); painter2.drawLine(xIcon + 1, yIcon + 7, xIcon + 3, yIcon + 7); painter2.setPen(textColor); // QPen(basket()->colorGroup().foreground())); painter2.drawPoint(xIcon + 2, yIcon + 8); } else if (m_haveInvisibleTags) { painter2.setPen(notePalette.color(QPalette::Text) /*QPen(basket()->colorGroup().foreground())*/); painter2.drawPoint(xIcon, yIcon + 7); painter2.drawPoint(xIcon + 2, yIcon + 7); painter2.drawPoint(xIcon + 4, yIcon + 7); } // Draw on screen: painter2.end(); drawBufferOnScreen(painter, m_bufferedPixmap); } void Note::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { if (!m_basket->isLoaded()) return; if (boundingRect().width() <= 0.1 || boundingRect().height() <= 0.1) return; draw(painter, boundingRect()); if (hasResizer()) { qreal right = rightLimit() - x(); QRectF resizerRect(0, 0, RESIZER_WIDTH, resizerHeight()); // Prepare to draw the resizer: QPixmap pixmap(RESIZER_WIDTH, resizerHeight()); QPainter painter2(&pixmap); // Draw gradient or resizer: if ((m_hovered && m_hoveredZone == Resizer) || ((m_hovered || isSelected()) && !isColumn())) { QColor baseColor(basket()->backgroundColor()); QColor highColor(palette().color(QPalette::Highlight)); drawResizer(&painter2, 0, 0, RESIZER_WIDTH, resizerHeight(), baseColor, highColor, /*rounded=*/!isColumn()); if (!isColumn()) { drawRoundings(&painter2, RESIZER_WIDTH - 3, 0, /*type=*/3); drawRoundings(&painter2, RESIZER_WIDTH - 3, resizerHeight() - 3, /*type=*/4); } } else { drawInactiveResizer(&painter2, /*x=*/0, /*y=*/0, /*height=*/resizerHeight(), basket()->backgroundColor(), isColumn()); resizerRect.translate(rightLimit(), y()); basket()->blendBackground(painter2, resizerRect); } // Draw on screen: painter2.end(); painter->drawPixmap(right, 0, pixmap); } } void Note::drawBufferOnScreen(QPainter *painter, const QPixmap &contentPixmap) { for (QList::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { QRectF rect = (*it).translated(-x(), -y()); if (rect.x() >= width()) // It's a rect of the resizer, don't draw it! continue; painter->drawPixmap(rect.x(), rect.y(), contentPixmap, rect.x(), rect.y(), rect.width(), rect.height()); } } void Note::setContent(NoteContent *content) { m_content = content; } /*const */ State::List &Note::states() const { return (State::List &)m_states; } void Note::addState(State *state, bool orReplace) { if (!content()) return; Tag *tag = state->parentTag(); State::List::iterator itStates = m_states.begin(); // Browse all tags, see if the note has it, increment itSates if yes, and then insert the state at this position... // For each existing tags: for (Tag::List::iterator it = Tag::all.begin(); it != Tag::all.end(); ++it) { // If the current tag isn't the one to assign or the current one on the note, go to the next tag: if (*it != tag && itStates != m_states.end() && *it != (*itStates)->parentTag()) continue; // We found the tag to insert: if (*it == tag) { // And the note already have the tag: if (itStates != m_states.end() && *it == (*itStates)->parentTag()) { // We replace the state if wanted: if (orReplace) { itStates = m_states.insert(itStates, state); ++itStates; m_states.erase(itStates); recomputeStyle(); } } else { m_states.insert(itStates, state); recomputeStyle(); } return; } // The note has this tag: if (itStates != m_states.end() && *it == (*itStates)->parentTag()) ++itStates; } } QFont Note::font() { return m_computedState.font(basket()->QGraphicsScene::font()); } QColor Note::backgroundColor() { if (m_computedState.backgroundColor().isValid()) return m_computedState.backgroundColor(); else return basket()->backgroundColor(); } QColor Note::textColor() { if (m_computedState.textColor().isValid()) return m_computedState.textColor(); else return basket()->textColor(); } void Note::recomputeStyle() { State::merge(m_states, &m_computedState, &m_emblemsCount, &m_haveInvisibleTags, basket()->backgroundColor()); // unsetWidth(); if (content()) { if (content()->graphicsItem()) content()->graphicsItem()->setPos(contentX(), NOTE_MARGIN); content()->fontChanged(); } // requestRelayout(); // TODO! } void Note::recomputeAllStyles() { if (content()) // We do the merge ourself, without calling recomputeStyle(), so there is no infinite recursion: // State::merge(m_states, &m_computedState, &m_emblemsCount, &m_haveInvisibleTags, basket()->backgroundColor()); recomputeStyle(); else if (isGroup()) FOR_EACH_CHILD(child) child->recomputeAllStyles(); } bool Note::removedStates(const QList &deletedStates) { bool modifiedBasket = false; if (!states().isEmpty()) { for (QList::const_iterator it = deletedStates.begin(); it != deletedStates.end(); ++it) if (hasState(*it)) { removeState(*it); modifiedBasket = true; } } FOR_EACH_CHILD(child) if (child->removedStates(deletedStates)) modifiedBasket = true; return modifiedBasket; } void Note::addTag(Tag *tag) { addState(tag->states().first(), /*but do not replace:*/ false); } void Note::removeState(State *state) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if (*it == state) { m_states.erase(it); recomputeStyle(); return; } } void Note::removeTag(Tag *tag) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if ((*it)->parentTag() == tag) { m_states.erase(it); recomputeStyle(); return; } } void Note::removeAllTags() { m_states.clear(); recomputeStyle(); } void Note::addTagToSelectedNotes(Tag *tag) { if (content() && isSelected()) addTag(tag); FOR_EACH_CHILD(child) child->addTagToSelectedNotes(tag); } void Note::removeTagFromSelectedNotes(Tag *tag) { if (content() && isSelected()) { if (hasTag(tag)) setWidth(0); removeTag(tag); } FOR_EACH_CHILD(child) child->removeTagFromSelectedNotes(tag); } void Note::removeAllTagsFromSelectedNotes() { if (content() && isSelected()) { if (m_states.count() > 0) setWidth(0); removeAllTags(); } FOR_EACH_CHILD(child) child->removeAllTagsFromSelectedNotes(); } void Note::addStateToSelectedNotes(State *state, bool orReplace) { if (content() && isSelected()) addState(state, orReplace); FOR_EACH_CHILD(child) child->addStateToSelectedNotes(state, orReplace); // TODO: BasketScene::addStateToSelectedNotes() does not have orReplace } void Note::changeStateOfSelectedNotes(State *state) { if (content() && isSelected() && hasTag(state->parentTag())) addState(state); FOR_EACH_CHILD(child) child->changeStateOfSelectedNotes(state); } bool Note::selectedNotesHaveTags() { if (content() && isSelected() && m_states.count() > 0) return true; FOR_EACH_CHILD(child) if (child->selectedNotesHaveTags()) return true; return false; } bool Note::hasState(State *state) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if (*it == state) return true; return false; } bool Note::hasTag(Tag *tag) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if ((*it)->parentTag() == tag) return true; return false; } State *Note::stateOfTag(Tag *tag) { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if ((*it)->parentTag() == tag) return *it; return nullptr; } bool Note::allowCrossReferences() { for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) if (!(*it)->allowCrossReferences()) return false; return true; } State *Note::stateForEmblemNumber(int number) const { int i = -1; for (State::List::const_iterator it = m_states.begin(); it != m_states.end(); ++it) if (!(*it)->emblem().isEmpty()) { ++i; if (i == number) return *it; } return nullptr; } bool Note::stateForTagFromSelectedNotes(Tag *tag, State **state) { if (content() && isSelected()) { // What state is the tag on this note? State *stateOfTag = this->stateOfTag(tag); // This tag is not assigned to this note, the action will assign it, then: if (stateOfTag == nullptr) *state = nullptr; else { // Take the LOWEST state of all the selected notes: // Say the two selected notes have the state "Done" and "To Do". // The first note set *state to "Done". // When reaching the second note, we should recognize "To Do" is first in the tag states, then take it // Because pressing the tag shortcut key should first change state before removing the tag! if (*state == nullptr) *state = stateOfTag; else { bool stateIsFirst = true; for (State *nextState = stateOfTag->nextState(); nextState; nextState = nextState->nextState(/*cycle=*/false)) if (nextState == *state) stateIsFirst = false; if (!stateIsFirst) *state = stateOfTag; } } return true; // We encountered a selected note } bool encounteredSelectedNote = false; FOR_EACH_CHILD(child) { bool encountered = child->stateForTagFromSelectedNotes(tag, state); if (encountered && *state == nullptr) return true; if (encountered) encounteredSelectedNote = true; } return encounteredSelectedNote; } void Note::inheritTagsOf(Note *note) { if (!note || !content()) return; for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) if ((*it)->parentTag() && (*it)->parentTag()->inheritedBySiblings()) addTag((*it)->parentTag()); } void Note::unbufferizeAll() { unbufferize(); if (isGroup()) { Note *child = firstChild(); while (child) { child->unbufferizeAll(); child = child->next(); } } } QRectF Note::visibleRect() { QList areas; areas.append(QRectF(x(), y(), width(), height())); // When we are folding a parent group, if this note is bigger than the first real note of the group, cut the top of this: /*Note *parent = parentNote(); while (parent) { if (parent->expandingOrCollapsing()) substractRectOnAreas(QRect(x(), parent->y() - height(), width(), height()), areas, true); parent = parent->parentNote(); }*/ if (areas.count() > 0) return areas.first(); else return QRectF(); } void Note::recomputeBlankRects(QList &blankAreas) { if (!matching()) return; // visibleRect() instead of rect() because if we are folding/expanding a smaller parent group, then some part is hidden! // But anyway, a resizer is always a primary note and is never hidden by a parent group, so no visibleResizerRect() method! substractRectOnAreas(visibleRect(), blankAreas, true); if (hasResizer()) substractRectOnAreas(resizerRect(), blankAreas, true); if (isGroup()) { Note *child = firstChild(); bool first = true; while (child) { if ((showSubNotes() || first) && child->matching()) child->recomputeBlankRects(blankAreas); child = child->next(); first = false; } } } void Note::linkLookChanged() { if (isGroup()) { Note *child = firstChild(); while (child) { child->linkLookChanged(); child = child->next(); } } else content()->linkLookChanged(); } Note *Note::noteForFullPath(const QString &path) { if (content() && fullPath() == path) return this; Note *child = firstChild(); Note *found; while (child) { found = child->noteForFullPath(path); if (found) return found; child = child->next(); } return nullptr; } void Note::listUsedTags(QList &list) { for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { Tag *tag = (*it)->parentTag(); if (!list.contains(tag)) list.append(tag); } FOR_EACH_CHILD(child) child->listUsedTags(list); } void Note::usedStates(QList &states) { if (content()) for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) if (!states.contains(*it)) states.append(*it); FOR_EACH_CHILD(child) child->usedStates(states); } Note *Note::nextInStack() { // First, search in the children: if (firstChild()) { if (firstChild()->content()) return firstChild(); else return firstChild()->nextInStack(); } // Then, in the next: if (next()) { if (next()->content()) return next(); else return next()->nextInStack(); } // And finally, in the parent: Note *note = parentNote(); while (note) if (note->next()) if (note->next()->content()) return note->next(); else return note->next()->nextInStack(); else note = note->parentNote(); // Not found: return nullptr; } Note *Note::prevInStack() { // First, search in the previous: if (prev() && prev()->content()) return prev(); // Else, it's a group, get the last item in that group: if (prev()) { Note *note = prev()->lastRealChild(); if (note) return note; } if (parentNote()) return parentNote()->prevInStack(); else return nullptr; } Note *Note::nextShownInStack() { Note *next = nextInStack(); while (next && !next->isShown()) next = next->nextInStack(); return next; } Note *Note::prevShownInStack() { Note *prev = prevInStack(); while (prev && !prev->isShown()) prev = prev->prevInStack(); return prev; } bool Note::isShown() { // First, the easy one: groups are always shown: if (isGroup()) return true; // And another easy part: non-matching notes are hidden: if (!matching()) return false; if (basket()->isFiltering()) // And isMatching() because of the line above! return true; // So, here we go to the complex case: if the note is inside a collapsed group: Note *group = parentNote(); Note *child = this; while (group) { if (group->isFolded() && group->firstChild() != child) return false; child = group; group = group->parentNote(); } return true; } void Note::debug() { qDebug() << "Note@" << (quint64)this; if (!this) { qDebug(); return; } if (isColumn()) qDebug() << ": Column"; else if (isGroup()) qDebug() << ": Group"; else qDebug() << ": Content[" << content()->lowerTypeName() << "]: " << toText(QString()); qDebug(); } Note *Note::firstSelected() { if (isSelected()) return this; Note *first = nullptr; FOR_EACH_CHILD(child) { first = child->firstSelected(); if (first) break; } return first; } Note *Note::lastSelected() { if (isSelected()) return this; Note *last = nullptr, *tmp = nullptr; FOR_EACH_CHILD(child) { tmp = child->lastSelected(); if (tmp) last = tmp; } return last; } Note *Note::selectedGroup() { if (isGroup() && allSelected() && count() == basket()->countSelecteds()) return this; FOR_EACH_CHILD(child) { Note *selectedGroup = child->selectedGroup(); if (selectedGroup) return selectedGroup; } return nullptr; } void Note::groupIn(Note *group) { if (this == group) return; if (allSelected() && !isColumn()) { basket()->unplugNote(this); basket()->insertNote(this, group, Note::BottomColumn, QPointF(), /*animateNewPosition=*/true); } else { Note *next; Note *child = firstChild(); while (child) { next = child->next(); child->groupIn(group); child = next; } } } bool Note::tryExpandParent() { Note *parent = parentNote(); Note *child = this; while (parent) { if (parent->firstChild() != child) return false; if (parent->isColumn()) return false; if (parent->isFolded()) { parent->toggleFolded(); basket()->relayoutNotes(); return true; } child = parent; parent = parent->parentNote(); } return false; } bool Note::tryFoldParent() // TODO: withCtrl ? withShift ? { Note *parent = parentNote(); Note *child = this; while (parent) { if (parent->firstChild() != child) return false; if (parent->isColumn()) return false; if (!parent->isFolded()) { parent->toggleFolded(); basket()->relayoutNotes(); return true; } child = parent; parent = parent->parentNote(); } return false; } qreal Note::distanceOnLeftRight(Note *note, int side) { if (side == BasketScene::RIGHT_SIDE) { // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (x() > note->x() || finalRightLimit() > note->finalRightLimit()) return -1; } else { /*LEFT_SIDE:*/ // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (x() < note->x() || finalRightLimit() < note->finalRightLimit()) return -1; } if (x() == note->x() && finalRightLimit() == note->finalRightLimit()) return -1; qreal thisCenterX = x() + (side == BasketScene::LEFT_SIDE ? width() : /*RIGHT_SIDE:*/ 0); qreal thisCenterY = y() + height() / 2; qreal noteCenterX = note->x() + note->width() / 2; qreal noteCenterY = note->y() + note->height() / 2; if (thisCenterY > note->bottom()) noteCenterY = note->bottom(); else if (thisCenterY < note->y()) noteCenterY = note->y(); else noteCenterY = thisCenterY; qreal angle = 0; if (noteCenterX - thisCenterX != 0) angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); if (angle < 0) angle = -angle; return sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle; } qreal Note::distanceOnTopBottom(Note *note, int side) { if (side == BasketScene::BOTTOM_SIDE) { // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (y() > note->y() || bottom() > note->bottom()) return -1; } else { /*TOP_SIDE:*/ // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: if (y() < note->y() || bottom() < note->bottom()) return -1; } if (y() == note->y() && bottom() == note->bottom()) return -1; qreal thisCenterX = x() + width() / 2; qreal thisCenterY = y() + (side == BasketScene::TOP_SIDE ? height() : /*BOTTOM_SIDE:*/ 0); qreal noteCenterX = note->x() + note->width() / 2; qreal noteCenterY = note->y() + note->height() / 2; if (thisCenterX > note->finalRightLimit()) noteCenterX = note->finalRightLimit(); else if (thisCenterX < note->x()) noteCenterX = note->x(); else noteCenterX = thisCenterX; qreal angle = 0; if (noteCenterX - thisCenterX != 0) angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); if (angle < 0) angle = -angle; return sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle; } Note *Note::parentPrimaryNote() { Note *primary = this; while (primary->parentNote()) primary = primary->parentNote(); return primary; } void Note::deleteChilds() { Note *child = firstChild(); while (child) { Note *tmp = child->next(); delete child; child = tmp; } } bool Note::saveAgain() { bool result = true; if (content()) { if (!content()->saveToFile()) result = false; } FOR_EACH_CHILD(child) { if (!child->saveAgain()) result = false; } if (!result) { DEBUG_WIN << QString("Note::saveAgain returned false for %1:%2").arg((content() != nullptr) ? content()->typeName() : "null", toText(QString())); } return result; } bool Note::convertTexts() { bool convertedNotes = false; if (content() && content()->lowerTypeName() == "text") { QString text = ((TextContent *)content())->text(); QString html = "" + Tools::textToHTMLWithoutP(text) + ""; basket()->saveToFile(fullPath(), html); setContent(new HtmlContent(this, content()->fileName())); convertedNotes = true; } FOR_EACH_CHILD(child) if (child->convertTexts()) convertedNotes = true; return convertedNotes; } /* vim: set et sts=4 sw=4 ts=16 tw=78 : */ /* kate: indent-width 4; replace-tabs on; */ diff --git a/src/noteedit.cpp b/src/noteedit.cpp index f2973e9..e0244d4 100644 --- a/src/noteedit.cpp +++ b/src/noteedit.cpp @@ -1,1322 +1,1322 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "noteedit.h" #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 #include #include "basketlistview.h" #include "basketscene.h" #include "focusedwidgets.h" #include "icon_names.h" #include "note.h" #include "notecontent.h" #include "notefactory.h" #include "settings.h" #include "tools.h" #include "variouswidgets.h" /** class NoteEditor: */ NoteEditor::NoteEditor(NoteContent *noteContent) { m_isEmpty = false; m_canceled = false; m_widget = nullptr; m_textEdit = nullptr; m_lineEdit = nullptr; m_noteContent = noteContent; } NoteEditor::~NoteEditor() { delete m_widget; } Note *NoteEditor::note() { return m_noteContent->note(); } void NoteEditor::setCursorTo(const QPointF &pos) { // clicked comes from the QMouseEvent, which is in item's coordinate system. if (m_textEdit) { QPointF currentPos = note()->mapFromScene(pos); QPointF deltaPos = m_textEdit->pos() - note()->pos(); m_textEdit->setTextCursor(m_textEdit->cursorForPosition((currentPos - deltaPos).toPoint())); } } void NoteEditor::startSelection(const QPointF &pos) { if (m_textEdit) { QPointF currentPos = note()->mapFromScene(pos); QPointF deltaPos = m_textEdit->pos() - note()->pos(); m_textEdit->setTextCursor(m_textEdit->cursorForPosition((currentPos - deltaPos).toPoint())); } } void NoteEditor::updateSelection(const QPointF &pos) { if (m_textEdit) { QPointF currentPos = note()->mapFromScene(pos); QPointF deltaPos = m_textEdit->pos() - note()->pos(); QTextCursor cursor = m_textEdit->cursorForPosition((currentPos - deltaPos).toPoint()); QTextCursor currentCursor = m_textEdit->textCursor(); // select the text currentCursor.setPosition(cursor.position(), QTextCursor::KeepAnchor); // update the cursor m_textEdit->setTextCursor(currentCursor); } } void NoteEditor::endSelection(const QPointF & /*pos*/) { // For TextEdit inside GraphicsScene selectionChanged() is only generated for the first selected char - // thus we need to call it manually after selection is finished if (FocusedTextEdit *textEdit = dynamic_cast(m_textEdit)) textEdit->onSelectionChanged(); } void NoteEditor::paste(const QPointF &pos, QClipboard::Mode mode) { if (FocusedTextEdit *textEdit = dynamic_cast(m_textEdit)) { setCursorTo(pos); textEdit->paste(mode); } } void NoteEditor::connectActions(BasketScene *scene) { if (m_textEdit) { connect(m_textEdit, SIGNAL(textChanged()), scene, SLOT(selectionChangedInEditor())); connect(m_textEdit, SIGNAL(textChanged()), scene, SLOT(contentChangedInEditor())); connect(m_textEdit, SIGNAL(textChanged()), scene, SLOT(placeEditorAndEnsureVisible())); connect(m_textEdit, SIGNAL(selectionChanged()), scene, SLOT(selectionChangedInEditor())); } else if (m_lineEdit) { connect(m_lineEdit, SIGNAL(textChanged(const QString &)), scene, SLOT(selectionChangedInEditor())); connect(m_lineEdit, SIGNAL(textChanged(const QString &)), scene, SLOT(contentChangedInEditor())); connect(m_lineEdit, SIGNAL(selectionChanged()), scene, SLOT(selectionChangedInEditor())); } } NoteEditor *NoteEditor::editNoteContent(NoteContent *noteContent, QWidget *parent) { TextContent *textContent = dynamic_cast(noteContent); if (textContent) return new TextEditor(textContent, parent); HtmlContent *htmlContent = dynamic_cast(noteContent); if (htmlContent) return new HtmlEditor(htmlContent, parent); ImageContent *imageContent = dynamic_cast(noteContent); if (imageContent) return new ImageEditor(imageContent, parent); AnimationContent *animationContent = dynamic_cast(noteContent); if (animationContent) return new AnimationEditor(animationContent, parent); FileContent *fileContent = dynamic_cast(noteContent); // Same for SoundContent if (fileContent) return new FileEditor(fileContent, parent); LinkContent *linkContent = dynamic_cast(noteContent); if (linkContent) return new LinkEditor(linkContent, parent); CrossReferenceContent *crossReferenceContent = dynamic_cast(noteContent); if (crossReferenceContent) return new CrossReferenceEditor(crossReferenceContent, parent); LauncherContent *launcherContent = dynamic_cast(noteContent); if (launcherContent) return new LauncherEditor(launcherContent, parent); ColorContent *colorContent = dynamic_cast(noteContent); if (colorContent) return new ColorEditor(colorContent, parent); UnknownContent *unknownContent = dynamic_cast(noteContent); if (unknownContent) return new UnknownEditor(unknownContent, parent); return nullptr; } void NoteEditor::setInlineEditor(QWidget *inlineEditor) { if (!m_widget) { m_widget = new QGraphicsProxyWidget(); } m_widget->setWidget(inlineEditor); m_widget->setZValue(500); // m_widget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); m_widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_textEdit = nullptr; m_lineEdit = nullptr; KTextEdit *textEdit = dynamic_cast(inlineEditor); if (textEdit) { m_textEdit = textEdit; } else { QLineEdit *lineEdit = dynamic_cast(inlineEditor); if (lineEdit) { m_lineEdit = lineEdit; } } } /** class TextEditor: */ TextEditor::TextEditor(TextContent *textContent, QWidget *parent) : NoteEditor(textContent) , m_textContent(textContent) { FocusedTextEdit *textEdit = new FocusedTextEdit(/*disableUpdatesOnKeyPress=*/true, parent); textEdit->setLineWidth(0); textEdit->setMidLineWidth(0); textEdit->setFrameStyle(QFrame::Box); QPalette palette; palette.setColor(textEdit->backgroundRole(), note()->backgroundColor()); palette.setColor(textEdit->foregroundRole(), note()->textColor()); textEdit->setPalette(palette); textEdit->setFont(note()->font()); textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); if (Settings::spellCheckTextNotes()) textEdit->setCheckSpellingEnabled(true); textEdit->setPlainText(m_textContent->text()); // Not sure if the following comment is still true - // FIXME: Sometimes, the cursor flicker at ends before being positionned where clicked (because qApp->processEvents() I think) + // FIXME: Sometimes, the cursor flicker at ends before being positioned where clicked (because qApp->processEvents() I think) textEdit->moveCursor(QTextCursor::End); textEdit->verticalScrollBar()->setCursor(Qt::ArrowCursor); setInlineEditor(textEdit); connect(textEdit, SIGNAL(escapePressed()), this, SIGNAL(askValidation())); connect(textEdit, SIGNAL(mouseEntered()), this, SIGNAL(mouseEnteredEditorWidget())); connect(textEdit, SIGNAL(cursorPositionChanged()), textContent->note()->basket(), SLOT(editorCursorPositionChanged())); // In case it is a very big note, the top is displayed and Enter is pressed: the cursor is on bottom, we should enure it visible: QTimer::singleShot(0, textContent->note()->basket(), SLOT(editorCursorPositionChanged())); } TextEditor::~TextEditor() { delete graphicsWidget()->widget(); // TODO: delete that in validate(), so we can remove one method } void TextEditor::autoSave(bool toFileToo) { bool autoSpellCheck = true; if (toFileToo) { if (Settings::spellCheckTextNotes() != textEdit()->checkSpellingEnabled()) { Settings::setSpellCheckTextNotes(textEdit()->checkSpellingEnabled()); Settings::saveConfig(); } autoSpellCheck = textEdit()->checkSpellingEnabled(); textEdit()->setCheckSpellingEnabled(false); } m_textContent->setText(textEdit()->toPlainText()); if (toFileToo) { m_textContent->saveToFile(); m_textContent->setEdited(); textEdit()->setCheckSpellingEnabled(autoSpellCheck); } } void TextEditor::validate() { if (Settings::spellCheckTextNotes() != textEdit()->checkSpellingEnabled()) { Settings::setSpellCheckTextNotes(textEdit()->checkSpellingEnabled()); Settings::saveConfig(); } textEdit()->setCheckSpellingEnabled(false); if (textEdit()->document()->isEmpty()) setEmpty(); m_textContent->setText(textEdit()->toPlainText()); m_textContent->saveToFile(); m_textContent->setEdited(); note()->setWidth(0); } /** class HtmlEditor: */ HtmlEditor::HtmlEditor(HtmlContent *htmlContent, QWidget *parent) : NoteEditor(htmlContent) , m_htmlContent(htmlContent) { FocusedTextEdit *textEdit = new FocusedTextEdit(/*disableUpdatesOnKeyPress=*/true, parent); textEdit->setLineWidth(0); textEdit->setMidLineWidth(0); textEdit->setFrameStyle(QFrame::Box); textEdit->setAutoFormatting(Settings::autoBullet() ? QTextEdit::AutoAll : QTextEdit::AutoNone); QPalette palette; palette.setColor(textEdit->backgroundRole(), note()->backgroundColor()); palette.setColor(textEdit->foregroundRole(), note()->textColor()); textEdit->setPalette(palette); textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); textEdit->setHtml(Tools::tagCrossReferences(m_htmlContent->html(), /*userLink=*/true)); textEdit->moveCursor(QTextCursor::End); textEdit->verticalScrollBar()->setCursor(Qt::ArrowCursor); setInlineEditor(textEdit); connect(textEdit, SIGNAL(mouseEntered()), this, SIGNAL(mouseEnteredEditorWidget())); connect(textEdit, SIGNAL(escapePressed()), this, SIGNAL(askValidation())); connect(InlineEditors::instance()->richTextFont, SIGNAL(currentFontChanged(const QFont &)), this, SLOT(onFontSelectionChanged(const QFont &))); connect(InlineEditors::instance()->richTextFontSize, SIGNAL(sizeChanged(qreal)), textEdit, SLOT(setFontPointSize(qreal))); connect(InlineEditors::instance()->richTextColor, SIGNAL(activated(const QColor &)), textEdit, SLOT(setTextColor(const QColor &))); connect(InlineEditors::instance()->focusWidgetFilter, SIGNAL(escapePressed()), textEdit, SLOT(setFocus())); connect(InlineEditors::instance()->focusWidgetFilter, SIGNAL(returnPressed()), textEdit, SLOT(setFocus())); connect(InlineEditors::instance()->richTextFont, SIGNAL(activated(int)), textEdit, SLOT(setFocus())); connect(InlineEditors::instance()->richTextFontSize, SIGNAL(activated(int)), textEdit, SLOT(setFocus())); connect(textEdit, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionChanged())); connect(textEdit, SIGNAL(currentCharFormatChanged(const QTextCharFormat &)), this, SLOT(charFormatChanged(const QTextCharFormat &))); // connect( textEdit, SIGNAL(currentVerticalAlignmentChanged(VerticalAlignment)), this, SLOT(slotVerticalAlignmentChanged()) ); connect(InlineEditors::instance()->richTextBold, SIGNAL(triggered(bool)), this, SLOT(setBold(bool))); connect(InlineEditors::instance()->richTextItalic, SIGNAL(triggered(bool)), textEdit, SLOT(setFontItalic(bool))); connect(InlineEditors::instance()->richTextUnderline, SIGNAL(triggered(bool)), textEdit, SLOT(setFontUnderline(bool))); connect(InlineEditors::instance()->richTextLeft, SIGNAL(triggered()), this, SLOT(setLeft())); connect(InlineEditors::instance()->richTextCenter, SIGNAL(triggered()), this, SLOT(setCentered())); connect(InlineEditors::instance()->richTextRight, SIGNAL(triggered()), this, SLOT(setRight())); connect(InlineEditors::instance()->richTextJustified, SIGNAL(triggered()), this, SLOT(setBlock())); // InlineEditors::instance()->richTextToolBar()->show(); cursorPositionChanged(); charFormatChanged(textEdit->currentCharFormat()); // QTimer::singleShot( 0, this, SLOT(cursorPositionChanged()) ); InlineEditors::instance()->enableRichTextToolBar(); connect(InlineEditors::instance()->richTextUndo, SIGNAL(triggered()), textEdit, SLOT(undo())); connect(InlineEditors::instance()->richTextRedo, SIGNAL(triggered()), textEdit, SLOT(redo())); connect(textEdit, SIGNAL(undoAvailable(bool)), InlineEditors::instance()->richTextUndo, SLOT(setEnabled(bool))); connect(textEdit, SIGNAL(redoAvailable(bool)), InlineEditors::instance()->richTextRedo, SLOT(setEnabled(bool))); connect(textEdit, SIGNAL(textChanged()), this, SLOT(editTextChanged())); InlineEditors::instance()->richTextUndo->setEnabled(false); InlineEditors::instance()->richTextRedo->setEnabled(false); connect(textEdit, SIGNAL(cursorPositionChanged()), htmlContent->note()->basket(), SLOT(editorCursorPositionChanged())); // In case it is a very big note, the top is displayed and Enter is pressed: the cursor is on bottom, we should enure it visible: QTimer::singleShot(0, htmlContent->note()->basket(), SLOT(editorCursorPositionChanged())); } void HtmlEditor::cursorPositionChanged() { InlineEditors::instance()->richTextFont->setCurrentFont(textEdit()->currentFont().family()); if (InlineEditors::instance()->richTextColor->color() != textEdit()->textColor()) InlineEditors::instance()->richTextColor->setColor(textEdit()->textColor()); InlineEditors::instance()->richTextBold->setChecked((textEdit()->fontWeight() >= QFont::Bold)); InlineEditors::instance()->richTextItalic->setChecked(textEdit()->fontItalic()); InlineEditors::instance()->richTextUnderline->setChecked(textEdit()->fontUnderline()); switch (textEdit()->alignment()) { default: case 1 /*Qt::AlignLeft*/: InlineEditors::instance()->richTextLeft->setChecked(true); break; case 2 /*Qt::AlignRight*/: InlineEditors::instance()->richTextRight->setChecked(true); break; case 4 /*Qt::AlignHCenter*/: InlineEditors::instance()->richTextCenter->setChecked(true); break; case 8 /*Qt::AlignJustify*/: InlineEditors::instance()->richTextJustified->setChecked(true); break; } } void HtmlEditor::editTextChanged() { // The following is a workaround for an apparent Qt bug. // When I start typing in a textEdit, the undo&redo actions are not enabled until I click // or move the cursor - probably, the signal undoAvailable() is not emitted. // So, I had to intervene and do that manually. InlineEditors::instance()->richTextUndo->setEnabled(textEdit()->document()->isUndoAvailable()); InlineEditors::instance()->richTextRedo->setEnabled(textEdit()->document()->isRedoAvailable()); } void HtmlEditor::charFormatChanged(const QTextCharFormat &format) { InlineEditors::instance()->richTextFontSize->setFontSize(format.font().pointSize()); } /*void HtmlEditor::slotVerticalAlignmentChanged(QTextEdit::VerticalAlignment align) { QTextEdit::VerticalAlignment align = textEdit() switch (align) { case KTextEdit::AlignSuperScript: InlineEditors::instance()->richTextSuper->setChecked(true); InlineEditors::instance()->richTextSub->setChecked(false); break; case KTextEdit::AlignSubScript: InlineEditors::instance()->richTextSuper->setChecked(false); InlineEditors::instance()->richTextSub->setChecked(true); break; default: InlineEditors::instance()->richTextSuper->setChecked(false); InlineEditors::instance()->richTextSub->setChecked(false); } NoteHtmlEditor::buttonToggled(int id) : case 106: if (isChecked && m_toolbar->isButtonOn(107)) m_toolbar->setButton(107, false); m_text->setVerticalAlignment(isChecked ? KTextEdit::AlignSuperScript : KTextEdit::AlignNormal); break; case 107: if (isChecked && m_toolbar->isButtonOn(106)) m_toolbar->setButton(106, false); m_text->setVerticalAlignment(isChecked ? KTextEdit::AlignSubScript : KTextEdit::AlignNormal); break; }*/ void HtmlEditor::setLeft() { textEdit()->setAlignment(Qt::AlignLeft); } void HtmlEditor::setRight() { textEdit()->setAlignment(Qt::AlignRight); } void HtmlEditor::setCentered() { textEdit()->setAlignment(Qt::AlignHCenter); } void HtmlEditor::setBlock() { textEdit()->setAlignment(Qt::AlignJustify); } void HtmlEditor::onFontSelectionChanged(const QFont &font) { // Change font family only textEdit()->setFontFamily(font.family()); InlineEditors::instance()->richTextFont->clearFocus(); // textEdit()->setFocus(); } void HtmlEditor::setBold(bool isChecked) { qWarning() << "setBold " << isChecked; textEdit()->setFontWeight(isChecked ? QFont::Bold : QFont::Normal); } HtmlEditor::~HtmlEditor() { // delete graphicsWidget()->widget(); } void HtmlEditor::autoSave(bool toFileToo) { m_htmlContent->setHtml(textEdit()->document()->toHtml("utf-8")); if (toFileToo) { m_htmlContent->saveToFile(); m_htmlContent->setEdited(); } } void HtmlEditor::validate() { if (Tools::htmlToText(textEdit()->toHtml()).isEmpty()) setEmpty(); QString convert = textEdit()->document()->toHtml("utf-8"); if (note()->allowCrossReferences()) convert = Tools::tagCrossReferences(convert, /*userLink=*/true); m_htmlContent->setHtml(convert); m_htmlContent->saveToFile(); m_htmlContent->setEdited(); disconnect(); graphicsWidget()->disconnect(); if (InlineEditors::instance()) { InlineEditors::instance()->disableRichTextToolBar(); // if (InlineEditors::instance()->richTextToolBar()) // InlineEditors::instance()->richTextToolBar()->hide(); } if (graphicsWidget()) { note()->setZValue(1); delete graphicsWidget()->widget(); setInlineEditor(nullptr); } } /** class ImageEditor: */ ImageEditor::ImageEditor(ImageContent *imageContent, QWidget *parent) : NoteEditor(imageContent) { int choice = KMessageBox::questionYesNoCancel(parent, i18n("Images can not be edited here at the moment (the next version of BasKet Note Pads will include an image editor).\n" "Do you want to open it with an application that understand it?"), i18n("Edit Image Note"), KStandardGuiItem::open(), KGuiItem(i18n("Load From &File..."), IconNames::DOCUMENT_IMPORT), KStandardGuiItem::cancel()); switch (choice) { case (KMessageBox::Yes): note()->basket()->noteOpen(note()); break; case (KMessageBox::No): // Load from file cancel(); Global::bnpView->insertWizard(3); // 3 maps to m_actLoadFile break; case (KMessageBox::Cancel): cancel(); break; } } /** class AnimationEditor: */ AnimationEditor::AnimationEditor(AnimationContent *animationContent, QWidget *parent) : NoteEditor(animationContent) { int choice = KMessageBox::questionYesNo(parent, i18n("This animated image can not be edited here.\n" "Do you want to open it with an application that understands it?"), i18n("Edit Animation Note"), KStandardGuiItem::open(), KStandardGuiItem::cancel()); if (choice == KMessageBox::Yes) note()->basket()->noteOpen(note()); } /** class FileEditor: */ FileEditor::FileEditor(FileContent *fileContent, QWidget *parent) : NoteEditor(fileContent) , m_fileContent(fileContent) { QLineEdit *lineEdit = new QLineEdit(parent); FocusWidgetFilter *filter = new FocusWidgetFilter(lineEdit); QPalette palette; palette.setColor(lineEdit->backgroundRole(), note()->backgroundColor()); palette.setColor(lineEdit->foregroundRole(), note()->textColor()); lineEdit->setPalette(palette); lineEdit->setFont(note()->font()); lineEdit->setText(m_fileContent->fileName()); lineEdit->selectAll(); setInlineEditor(lineEdit); connect(filter, SIGNAL(returnPressed()), this, SIGNAL(askValidation())); connect(filter, SIGNAL(escapePressed()), this, SIGNAL(askValidation())); connect(filter, SIGNAL(mouseEntered()), this, SIGNAL(mouseEnteredEditorWidget())); } FileEditor::~FileEditor() { delete graphicsWidget()->widget(); } void FileEditor::autoSave(bool toFileToo) { // FIXME: How to detect cancel? if (toFileToo && !lineEdit()->text().isEmpty() && m_fileContent->trySetFileName(lineEdit()->text())) { m_fileContent->setFileName(lineEdit()->text()); m_fileContent->setEdited(); } } void FileEditor::validate() { autoSave(/*toFileToo=*/true); } /** class LinkEditor: */ LinkEditor::LinkEditor(LinkContent *linkContent, QWidget *parent) : NoteEditor(linkContent) { QPointer dialog = new LinkEditDialog(linkContent, parent); if (dialog->exec() == QDialog::Rejected) cancel(); if (linkContent->url().isEmpty() && linkContent->title().isEmpty()) setEmpty(); } /** class CrossReferenceEditor: */ CrossReferenceEditor::CrossReferenceEditor(CrossReferenceContent *crossReferenceContent, QWidget *parent) : NoteEditor(crossReferenceContent) { QPointer dialog = new CrossReferenceEditDialog(crossReferenceContent, parent); if (dialog->exec() == QDialog::Rejected) cancel(); if (crossReferenceContent->url().isEmpty() && crossReferenceContent->title().isEmpty()) setEmpty(); } /** class LauncherEditor: */ LauncherEditor::LauncherEditor(LauncherContent *launcherContent, QWidget *parent) : NoteEditor(launcherContent) { QPointer dialog = new LauncherEditDialog(launcherContent, parent); if (dialog->exec() == QDialog::Rejected) cancel(); if (launcherContent->name().isEmpty() && launcherContent->exec().isEmpty()) setEmpty(); } /** class ColorEditor: */ ColorEditor::ColorEditor(ColorContent *colorContent, QWidget *parent) : NoteEditor(colorContent) { QPointer dialog = new QColorDialog(parent); dialog->setCurrentColor(colorContent->color()); dialog->setWindowTitle(i18n("Edit Color Note")); // dialog->setButtons(QDialog::Ok | QDialog::Cancel); if (dialog->exec() == QDialog::Accepted) { if (dialog->currentColor() != colorContent->color()) { colorContent->setColor(dialog->currentColor()); colorContent->setEdited(); } } else cancel(); /* This code don't allow to set a caption to the dialog: QColor color = colorContent()->color(); color = QColorDialog::getColor(parent)==QDialog::Accepted&&color!=m_color); if ( color.isValid() ) { colorContent()->setColor(color); setEdited(); }*/ } /** class UnknownEditor: */ UnknownEditor::UnknownEditor(UnknownContent *unknownContent, QWidget *parent) : NoteEditor(unknownContent) { KMessageBox::information(parent, i18n("The type of this note is unknown and can not be edited here.\n" "You however can drag or copy the note into an application that understands it."), i18n("Edit Unknown Note")); } /*********************************************************************/ /** class LinkEditDialog: */ LinkEditDialog::LinkEditDialog(LinkContent *contentNote, QWidget *parent /*, QKeyEvent *ke*/) : QDialog(parent) , m_noteContent(contentNote) { // QDialog options setWindowTitle(i18n("Edit Link Note")); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); setObjectName("EditLink"); setModal(true); QWidget *page = new QWidget(this); mainLayout->addWidget(page); // QGridLayout *layout = new QGridLayout(page, /*nRows=*/4, /*nCols=*/2, /*margin=*/0, spacingHint()); QGridLayout *layout = new QGridLayout(page); mainLayout->addLayout(layout); QWidget *wid1 = new QWidget(page); mainLayout->addWidget(wid1); QHBoxLayout *titleLay = new QHBoxLayout(wid1); m_title = new QLineEdit(m_noteContent->title(), wid1); m_autoTitle = new QPushButton(i18n("Auto"), wid1); m_autoTitle->setCheckable(true); m_autoTitle->setChecked(m_noteContent->autoTitle()); titleLay->addWidget(m_title); titleLay->addWidget(m_autoTitle); QWidget *wid = new QWidget(page); mainLayout->addWidget(wid); QHBoxLayout *hLay = new QHBoxLayout(wid); m_icon = new KIconButton(wid); QLabel *label3 = new QLabel(page); mainLayout->addWidget(label3); label3->setText(i18n("&Icon:")); label3->setBuddy(m_icon); if (m_noteContent->url().isEmpty()) { m_url = new KUrlRequester(QUrl(QString()), wid); m_url->setMode(KFile::File | KFile::ExistingOnly); } else { m_url = new KUrlRequester(m_noteContent->url().toDisplayString(), wid); m_url->setMode(KFile::File | KFile::ExistingOnly); } if (m_noteContent->title().isEmpty()) { m_title->setText(QString()); } else { m_title->setText(m_noteContent->title()); } QUrl filteredURL = NoteFactory::filteredURL(QUrl::fromUserInput(m_url->lineEdit()->text())); // KURIFilter::self()->filteredURI(KUrl(m_url->lineEdit()->text())); m_icon->setIconType(KIconLoader::NoGroup, KIconLoader::MimeType); m_icon->setIconSize(LinkLook::lookForURL(filteredURL)->iconSize()); m_autoIcon = new QPushButton(i18n("Auto"), wid); // Create before to know size here: /* Icon button: */ m_icon->setIcon(m_noteContent->icon()); int minSize = m_autoIcon->sizeHint().height(); // Make the icon button at least the same height than the other buttons for a better alignment (nicer to the eyes): if (m_icon->sizeHint().height() < minSize) m_icon->setFixedSize(minSize, minSize); else m_icon->setFixedSize(m_icon->sizeHint().height(), m_icon->sizeHint().height()); // Make it square /* Auto button: */ m_autoIcon->setCheckable(true); m_autoIcon->setChecked(m_noteContent->autoIcon()); hLay->addWidget(m_icon); hLay->addWidget(m_autoIcon); hLay->addStretch(); m_url->lineEdit()->setMinimumWidth(m_url->lineEdit()->fontMetrics().maxWidth() * 20); m_title->setMinimumWidth(m_title->fontMetrics().maxWidth() * 20); // m_url->setShowLocalProtocol(true); QLabel *label1 = new QLabel(page); mainLayout->addWidget(label1); label1->setText(i18n("Ta&rget:")); label1->setBuddy(m_url); QLabel *label2 = new QLabel(page); mainLayout->addWidget(label2); label2->setText(i18n("&Title:")); label2->setBuddy(m_title); layout->addWidget(label1, 0, 0, Qt::AlignVCenter); layout->addWidget(label2, 1, 0, Qt::AlignVCenter); layout->addWidget(label3, 2, 0, Qt::AlignVCenter); layout->addWidget(m_url, 0, 1, Qt::AlignVCenter); layout->addWidget(wid1, 1, 1, Qt::AlignVCenter); layout->addWidget(wid, 2, 1, Qt::AlignVCenter); m_isAutoModified = false; connect(m_url, SIGNAL(textChanged(const QString &)), this, SLOT(urlChanged(const QString &))); connect(m_title, SIGNAL(textChanged(const QString &)), this, SLOT(doNotAutoTitle(const QString &))); connect(m_icon, SIGNAL(iconChanged(QString)), this, SLOT(doNotAutoIcon(QString))); connect(m_autoTitle, SIGNAL(clicked()), this, SLOT(guessTitle())); connect(m_autoIcon, SIGNAL(clicked()), this, SLOT(guessIcon())); QWidget *stretchWidget = new QWidget(page); mainLayout->addWidget(stretchWidget); QSizePolicy policy(QSizePolicy::Fixed, QSizePolicy::Expanding); policy.setHorizontalStretch(1); policy.setVerticalStretch(255); stretchWidget->setSizePolicy(policy); // Make it fill ALL vertical space layout->addWidget(stretchWidget, 3, 1, Qt::AlignVCenter); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(okButton, SIGNAL(clicked()), SLOT(slotOk())); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); mainLayout->addWidget(buttonBox); // urlChanged(QString()); // if (ke) // qApp->postEvent(m_url->lineEdit(), ke); } LinkEditDialog::~LinkEditDialog() { } void LinkEditDialog::ensurePolished() { QDialog::ensurePolished(); if (m_url->lineEdit()->text().isEmpty()) { m_url->setFocus(); m_url->lineEdit()->end(false); } else { m_title->setFocus(); m_title->end(false); } } void LinkEditDialog::urlChanged(const QString &) { m_isAutoModified = true; // guessTitle(); // guessIcon(); // Optimization (filter only once): QUrl filteredURL = NoteFactory::filteredURL(m_url->url()); // KURIFilter::self()->filteredURI(KUrl(m_url->url())); if (m_autoIcon->isChecked()) m_icon->setIcon(NoteFactory::iconForURL(filteredURL)); if (m_autoTitle->isChecked()) { m_title->setText(NoteFactory::titleForURL(filteredURL)); m_autoTitle->setChecked(true); // Because the setText() will disable it! } } void LinkEditDialog::doNotAutoTitle(const QString &) { if (m_isAutoModified) m_isAutoModified = false; else m_autoTitle->setChecked(false); } void LinkEditDialog::doNotAutoIcon(QString) { m_autoIcon->setChecked(false); } void LinkEditDialog::guessIcon() { if (m_autoIcon->isChecked()) { QUrl filteredURL = NoteFactory::filteredURL(m_url->url()); // KURIFilter::self()->filteredURI(KUrl(m_url->url())); m_icon->setIcon(NoteFactory::iconForURL(filteredURL)); } } void LinkEditDialog::guessTitle() { if (m_autoTitle->isChecked()) { QUrl filteredURL = NoteFactory::filteredURL(m_url->url()); // KURIFilter::self()->filteredURI(KUrl(m_url->url())); m_title->setText(NoteFactory::titleForURL(filteredURL)); m_autoTitle->setChecked(true); // Because the setText() will disable it! } } void LinkEditDialog::slotOk() { QUrl filteredURL = NoteFactory::filteredURL(m_url->url()); // KURIFilter::self()->filteredURI(KUrl(m_url->url())); m_noteContent->setLink(filteredURL, m_title->text(), m_icon->icon(), m_autoTitle->isChecked(), m_autoIcon->isChecked()); m_noteContent->setEdited(); /* Change icon size if link look have changed */ LinkLook *linkLook = LinkLook::lookForURL(filteredURL); QString icon = m_icon->icon(); // When we change size, icon isn't changed and keep it's old size m_icon->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); // Reset size policy m_icon->setIconSize(linkLook->iconSize()); // So I store it's name and reload it after size change ! m_icon->setIcon(icon); int minSize = m_autoIcon->sizeHint().height(); // Make the icon button at least the same height than the other buttons for a better alignment (nicer to the eyes): if (m_icon->sizeHint().height() < minSize) m_icon->setFixedSize(minSize, minSize); else m_icon->setFixedSize(m_icon->sizeHint().height(), m_icon->sizeHint().height()); // Make it square } /** class CrossReferenceEditDialog: */ CrossReferenceEditDialog::CrossReferenceEditDialog(CrossReferenceContent *contentNote, QWidget *parent /*, QKeyEvent *ke*/) : QDialog(parent) , m_noteContent(contentNote) { QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); // QDialog options setWindowTitle(i18n("Edit Cross Reference")); QWidget *page = new QWidget(this); mainLayout->addWidget(page); QWidget *wid = new QWidget(page); mainLayout->addWidget(wid); QGridLayout *layout = new QGridLayout(page); mainLayout->addLayout(layout); m_targetBasket = new KComboBox(wid); this->generateBasketList(m_targetBasket); if (m_noteContent->url().isEmpty()) { BasketListViewItem *item = Global::bnpView->topLevelItem(0); m_noteContent->setCrossReference(QUrl::fromUserInput(item->data(0, Qt::UserRole).toString()), m_targetBasket->currentText(), "edit-copy"); this->urlChanged(0); } else { QString url = m_noteContent->url().url(); // cannot use findData because I'm using a StringList and I don't have the second // piece of data to make find work. for (int i = 0; i < m_targetBasket->count(); ++i) { if (url == m_targetBasket->itemData(i, Qt::UserRole).toStringList().first()) { m_targetBasket->setCurrentIndex(i); break; } } } QLabel *label1 = new QLabel(page); mainLayout->addWidget(label1); label1->setText(i18n("Ta&rget:")); label1->setBuddy(m_targetBasket); layout->addWidget(label1, 0, 0, Qt::AlignVCenter); layout->addWidget(m_targetBasket, 0, 1, Qt::AlignVCenter); connect(m_targetBasket, SIGNAL(activated(int)), this, SLOT(urlChanged(int))); QWidget *stretchWidget = new QWidget(page); mainLayout->addWidget(stretchWidget); QSizePolicy policy(QSizePolicy::Fixed, QSizePolicy::Expanding); policy.setHorizontalStretch(1); policy.setVerticalStretch(255); stretchWidget->setSizePolicy(policy); // Make it fill ALL vertical space layout->addWidget(stretchWidget, 3, 1, Qt::AlignVCenter); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); mainLayout->addWidget(buttonBox); setObjectName("EditCrossReference"); setModal(true); connect(okButton, SIGNAL(clicked()), SLOT(slotOk())); } CrossReferenceEditDialog::~CrossReferenceEditDialog() { } void CrossReferenceEditDialog::urlChanged(const int index) { if (m_targetBasket) m_noteContent->setCrossReference( QUrl::fromUserInput(m_targetBasket->itemData(index, Qt::UserRole).toStringList().first()), m_targetBasket->currentText().trimmed(), m_targetBasket->itemData(index, Qt::UserRole).toStringList().last()); } void CrossReferenceEditDialog::slotOk() { m_noteContent->setEdited(); } void CrossReferenceEditDialog::generateBasketList(KComboBox *targetList, BasketListViewItem *item, int indent) { if (!item) { // include ALL top level items and their children. for (int i = 0; i < Global::bnpView->topLevelItemCount(); ++i) this->generateBasketList(targetList, Global::bnpView->topLevelItem(i)); } else { BasketScene *bv = item->basket(); // TODO: add some fancy deco stuff to make it look like a tree list. QString pad; QString text = item->text(0); // user text text.prepend(pad.fill(' ', indent * 2)); // create the link text QString link = "basket://"; link.append(bv->folderName().toLower()); // unique ref. QStringList data; data.append(link); data.append(bv->icon()); targetList->addItem(item->icon(0), text, QVariant(data)); int subBasketCount = item->childCount(); if (subBasketCount > 0) { indent++; for (int i = 0; i < subBasketCount; ++i) { this->generateBasketList(targetList, (BasketListViewItem *)item->child(i), indent); } } } } /** class LauncherEditDialog: */ LauncherEditDialog::LauncherEditDialog(LauncherContent *contentNote, QWidget *parent) : QDialog(parent) , m_noteContent(contentNote) { QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); // QDialog options setWindowTitle(i18n("Edit Launcher Note")); setObjectName("EditLauncher"); setModal(true); QWidget *page = new QWidget(this); mainLayout->addWidget(page); // QGridLayout *layout = new QGridLayout(page, /*nRows=*/4, /*nCols=*/2, /*margin=*/0, spacingHint()); QGridLayout *layout = new QGridLayout(page); mainLayout->addLayout(layout); KService service(contentNote->fullPath()); m_command = new RunCommandRequester(service.exec(), i18n("Choose a command to run:"), page); mainLayout->addWidget(m_command); m_name = new QLineEdit(service.name(), page); mainLayout->addWidget(m_name); QWidget *wid = new QWidget(page); mainLayout->addWidget(wid); QHBoxLayout *hLay = new QHBoxLayout(wid); m_icon = new KIconButton(wid); QLabel *label = new QLabel(page); mainLayout->addWidget(label); label->setText(i18n("&Icon:")); label->setBuddy(m_icon); m_icon->setIconType(KIconLoader::NoGroup, KIconLoader::Application); m_icon->setIconSize(LinkLook::launcherLook->iconSize()); QPushButton *guessButton = new QPushButton(i18n("&Guess"), wid); /* Icon button: */ m_icon->setIcon(service.icon()); int minSize = guessButton->sizeHint().height(); // Make the icon button at least the same height than the other buttons for a better alignment (nicer to the eyes): if (m_icon->sizeHint().height() < minSize) m_icon->setFixedSize(minSize, minSize); else m_icon->setFixedSize(m_icon->sizeHint().height(), m_icon->sizeHint().height()); // Make it square /* Guess button: */ hLay->addWidget(m_icon); hLay->addWidget(guessButton); hLay->addStretch(); m_command->lineEdit()->setMinimumWidth(m_command->lineEdit()->fontMetrics().maxWidth() * 20); QLabel *label1 = new QLabel(page); mainLayout->addWidget(label1); label1->setText(i18n("Comman&d:")); label1->setBuddy(m_command->lineEdit()); QLabel *label2 = new QLabel(page); mainLayout->addWidget(label2); label2->setText(i18n("&Name:")); label2->setBuddy(m_name); layout->addWidget(label1, 0, 0, Qt::AlignVCenter); layout->addWidget(label2, 1, 0, Qt::AlignVCenter); layout->addWidget(label, 2, 0, Qt::AlignVCenter); layout->addWidget(m_command, 0, 1, Qt::AlignVCenter); layout->addWidget(m_name, 1, 1, Qt::AlignVCenter); layout->addWidget(wid, 2, 1, Qt::AlignVCenter); QWidget *stretchWidget = new QWidget(page); mainLayout->addWidget(stretchWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(okButton, SIGNAL(clicked()), SLOT(slotOk())); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); mainLayout->addWidget(buttonBox); QSizePolicy policy(QSizePolicy::Fixed, QSizePolicy::Expanding); policy.setHorizontalStretch(1); policy.setVerticalStretch(255); stretchWidget->setSizePolicy(policy); // Make it fill ALL vertical space layout->addWidget(stretchWidget, 3, 1, Qt::AlignVCenter); connect(guessButton, SIGNAL(clicked()), this, SLOT(guessIcon())); } LauncherEditDialog::~LauncherEditDialog() { } void LauncherEditDialog::ensurePolished() { QDialog::ensurePolished(); if (m_command->runCommand().isEmpty()) { m_command->lineEdit()->setFocus(); m_command->lineEdit()->end(false); } else { m_name->setFocus(); m_name->end(false); } } void LauncherEditDialog::slotOk() { // TODO: Remember if a string has been modified AND IS DIFFERENT FROM THE // ORIGINAL! KDesktopFile dtFile(m_noteContent->fullPath()); KConfigGroup grp = dtFile.desktopGroup(); grp.writeEntry("Exec", m_command->runCommand()); grp.writeEntry("Name", m_name->text()); grp.writeEntry("Icon", m_icon->icon()); // Just for faster feedback: conf object will save to disk (and then // m_note->loadContent() called) m_noteContent->setLauncher(m_name->text(), m_icon->icon(), m_command->runCommand()); m_noteContent->setEdited(); } void LauncherEditDialog::guessIcon() { m_icon->setIcon(NoteFactory::iconForCommand(m_command->runCommand())); } /** class InlineEditors: */ InlineEditors::InlineEditors() { } InlineEditors::~InlineEditors() { } InlineEditors *InlineEditors::instance() { static InlineEditors *instance = nullptr; if (!instance) instance = new InlineEditors(); return instance; } void InlineEditors::initToolBars(KActionCollection *ac) { QFont defaultFont; QColor textColor = (Global::bnpView && Global::bnpView->currentBasket() ? Global::bnpView->currentBasket()->textColor() : palette().color(QPalette::Text)); // NOTE: currently it is NULL since initToolBars is called early. Could use different way to get MainWindow pointer from main KMainWindow *parent = Global::activeMainWindow(); // Init the RichTextEditor Toolbar: richTextFont = new QFontComboBox(Global::activeMainWindow()); focusWidgetFilter = new FocusWidgetFilter(richTextFont); richTextFont->setFixedWidth(richTextFont->sizeHint().width() * 2 / 3); richTextFont->setCurrentFont(defaultFont.family()); QWidgetAction *action = new QWidgetAction(parent); ac->addAction("richtext_font", action); action->setDefaultWidget(richTextFont); action->setText(i18n("Font")); ac->setDefaultShortcut(action, Qt::Key_F6); richTextFontSize = new FontSizeCombo(/*rw=*/true, Global::activeMainWindow()); richTextFontSize->setFontSize(defaultFont.pointSize()); action = new QWidgetAction(parent); ac->addAction("richtext_font_size", action); action->setDefaultWidget(richTextFontSize); action->setText(i18n("Font Size")); ac->setDefaultShortcut(action, Qt::Key_F7); richTextColor = new KColorCombo(Global::activeMainWindow()); richTextColor->installEventFilter(focusWidgetFilter); richTextColor->setFixedWidth(richTextColor->sizeHint().height() * 2); richTextColor->setColor(textColor); action = new QWidgetAction(parent); ac->addAction("richtext_color", action); action->setDefaultWidget(richTextColor); action->setText(i18n("Color")); KToggleAction *ta = nullptr; ta = new KToggleAction(ac); ac->addAction("richtext_bold", ta); ta->setText(i18n("Bold")); ta->setIcon(QIcon::fromTheme("format-text-bold")); ac->setDefaultShortcut(ta, QKeySequence("Ctrl+B")); richTextBold = ta; ta = new KToggleAction(ac); ac->addAction("richtext_italic", ta); ta->setText(i18n("Italic")); ta->setIcon(QIcon::fromTheme("format-text-italic")); ac->setDefaultShortcut(ta, QKeySequence("Ctrl+I")); richTextItalic = ta; ta = new KToggleAction(ac); ac->addAction("richtext_underline", ta); ta->setText(i18n("Underline")); ta->setIcon(QIcon::fromTheme("format-text-underline")); ac->setDefaultShortcut(ta, QKeySequence("Ctrl+U")); richTextUnderline = ta; #if 0 ta = new KToggleAction(ac); ac->addAction("richtext_super", ta); ta->setText(i18n("Superscript")); ta->setIcon(QIcon::fromTheme("text_super")); richTextSuper = ta; ta = new KToggleAction(ac); ac->addAction("richtext_sub", ta); ta->setText(i18n("Subscript")); ta->setIcon(QIcon::fromTheme("text_sub")); richTextSub = ta; #endif ta = new KToggleAction(ac); ac->addAction("richtext_left", ta); ta->setText(i18n("Align Left")); ta->setIcon(QIcon::fromTheme("format-justify-left")); richTextLeft = ta; ta = new KToggleAction(ac); ac->addAction("richtext_center", ta); ta->setText(i18n("Centered")); ta->setIcon(QIcon::fromTheme("format-justify-center")); richTextCenter = ta; ta = new KToggleAction(ac); ac->addAction("richtext_right", ta); ta->setText(i18n("Align Right")); ta->setIcon(QIcon::fromTheme("format-justify-right")); richTextRight = ta; ta = new KToggleAction(ac); ac->addAction("richtext_block", ta); ta->setText(i18n("Justified")); ta->setIcon(QIcon::fromTheme("format-justify-fill")); richTextJustified = ta; QActionGroup *alignmentGroup = new QActionGroup(this); alignmentGroup->addAction(richTextLeft); alignmentGroup->addAction(richTextCenter); alignmentGroup->addAction(richTextRight); alignmentGroup->addAction(richTextJustified); ta = new KToggleAction(ac); ac->addAction("richtext_undo", ta); ta->setText(i18n("Undo")); ta->setIcon(QIcon::fromTheme("edit-undo")); richTextUndo = ta; ta = new KToggleAction(ac); ac->addAction("richtext_redo", ta); ta->setText(i18n("Redo")); ta->setIcon(QIcon::fromTheme("edit-redo")); richTextRedo = ta; disableRichTextToolBar(); } KToolBar *InlineEditors::richTextToolBar() { if (Global::activeMainWindow()) { Global::activeMainWindow()->toolBar(); // Make sure we create the main toolbar FIRST, so it will be on top of the edit toolbar! return Global::activeMainWindow()->toolBar("richTextEditToolBar"); } else return nullptr; } void InlineEditors::enableRichTextToolBar() { richTextFont->setEnabled(true); richTextFontSize->setEnabled(true); richTextColor->setEnabled(true); richTextBold->setEnabled(true); richTextItalic->setEnabled(true); richTextUnderline->setEnabled(true); richTextLeft->setEnabled(true); richTextCenter->setEnabled(true); richTextRight->setEnabled(true); richTextJustified->setEnabled(true); richTextUndo->setEnabled(true); richTextRedo->setEnabled(true); } void InlineEditors::disableRichTextToolBar() { disconnect(richTextFont); disconnect(richTextFontSize); disconnect(richTextColor); disconnect(richTextBold); disconnect(richTextItalic); disconnect(richTextUnderline); disconnect(richTextLeft); disconnect(richTextCenter); disconnect(richTextRight); disconnect(richTextJustified); disconnect(richTextUndo); disconnect(richTextRedo); richTextFont->setEnabled(false); richTextFontSize->setEnabled(false); richTextColor->setEnabled(false); richTextBold->setEnabled(false); richTextItalic->setEnabled(false); richTextUnderline->setEnabled(false); richTextLeft->setEnabled(false); richTextCenter->setEnabled(false); richTextRight->setEnabled(false); richTextJustified->setEnabled(false); richTextUndo->setEnabled(false); richTextRedo->setEnabled(false); // Return to a "proper" state: QFont defaultFont; QColor textColor = (Global::bnpView && Global::bnpView->currentBasket() ? Global::bnpView->currentBasket()->textColor() : palette().color(QPalette::Text)); richTextFont->setCurrentFont(defaultFont.family()); richTextFontSize->setFontSize(defaultFont.pointSize()); richTextColor->setColor(textColor); richTextBold->setChecked(false); richTextItalic->setChecked(false); richTextUnderline->setChecked(false); richTextLeft->setChecked(false); richTextCenter->setChecked(false); richTextRight->setChecked(false); richTextJustified->setChecked(false); } QPalette InlineEditors::palette() const { return qApp->palette(); } diff --git a/src/softwareimporters.h b/src/softwareimporters.h index c2b72f3..8b25d0c 100644 --- a/src/softwareimporters.h +++ b/src/softwareimporters.h @@ -1,77 +1,77 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef SOFTWAREIMPORTERS_H #define SOFTWAREIMPORTERS_H #include class QString; class QGroupBox; class QDomElement; class QRadioButton; class KTextEdit; class QVBoxLayout; class BasketScene; class Note; /** The dialog to ask how to import hierarchical data. * @author Sébastien Laoût */ class TreeImportDialog : public QDialog { Q_OBJECT public: explicit TreeImportDialog(QWidget *parent = nullptr); ~TreeImportDialog() override; int choice(); private: QGroupBox *m_choices; QVBoxLayout *m_choiceLayout; QRadioButton *m_hierarchy_choice; QRadioButton *m_separate_baskets_choice; QRadioButton *m_one_basket_choice; }; /** The dialog to ask how to import text files. * @author Sébastien Laoût */ class TextFileImportDialog : public QDialog { Q_OBJECT public: explicit TextFileImportDialog(QWidget *parent = nullptr); ~TextFileImportDialog() override; QString separator(); protected slots: void customSeparatorChanged(); private: QGroupBox *m_choices; QVBoxLayout *m_choiceLayout; QRadioButton *m_emptyline_choice; QRadioButton *m_newline_choice; QRadioButton *m_dash_choice; QRadioButton *m_star_choice; QRadioButton *m_all_in_one_choice; QRadioButton *m_anotherSeparator; KTextEdit *m_customSeparator; }; -/** Functions that import data from other softwares. +/** Functions that import data from other software. * @author Sébastien Laoût */ namespace SoftwareImporters { void finishImport(BasketScene *basket); // The importers in themselves: void importTextFile(); } #endif // SOFTWAREIMPORTERS_H diff --git a/src/tools.cpp b/src/tools.cpp index d2e2ceb..7300a93 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -1,922 +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)); } 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)); 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) + // First check if the file do not exists yet (simpler 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) + } // else fileName = fileName and number = 2 (because if the file already exists, the generated 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; }