diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa1f0f6..8fc9a07 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,139 +1,140 @@ include_directories(${GPGME_INCLUDES}) ########### next target ############### set(libbasket_SRCS aboutdata.cpp archive.cpp backgroundmanager.cpp backup.cpp basketfactory.cpp basketlistview.cpp basketproperties.cpp basketscene.cpp basketstatusbar.cpp basketview.cpp bnpview.cpp colorpicker.cpp + common.cpp crashhandler.cpp debugwindow.cpp decoratedbasket.cpp diskerrordialog.cpp file_metadata.cpp filter.cpp focusedwidgets.cpp formatimporter.cpp global.cpp gitwrapper.cpp htmlexporter.cpp history.cpp kcolorcombo2.cpp kgpgme.cpp linklabel.cpp newbasketdialog.cpp note.cpp notecontent.cpp notedrag.cpp noteedit.cpp notefactory.cpp noteselection.cpp password.cpp regiongrabber.cpp settings.cpp settings_versionsync.cpp softwareimporters.cpp systemtray.cpp tag.cpp tagsedit.cpp transparentwidget.cpp tools.cpp variouswidgets.cpp xmlwork.cpp ) set(PIMO_CPP "") # One of the generated files ki18n_wrap_ui(basket_FORM_HDRS passwordlayout.ui basketproperties.ui settings_versionsync.ui) qt5_add_dbus_adaptor(libbasket_SRCS org.kde.basket.BNPView.xml bnpview.h BNPView) qt5_add_resources(basket_RESOURCES ../basket.qrc) add_library(LibBasket SHARED ${libbasket_SRCS} ${basket_FORM_HDRS} ${basket_RESOURCES}) target_link_libraries(LibBasket ${PHONON_LIBRARY} ${GPGME_VANILLA_LIBRARIES} KF5::Archive KF5::ConfigWidgets KF5::CoreAddons KF5::Crash KF5::DBusAddons KF5::FileMetaData KF5::GlobalAccel KF5::GuiAddons KF5::I18n KF5::IconThemes KF5::KCMUtils KF5::KIOWidgets KF5::Notifications KF5::Parts KF5::TextWidgets KF5::WindowSystem KF5::XmlGui ) set_target_properties(LibBasket PROPERTIES VERSION ${Qt5Core_VERSION} SOVERSION ${Qt5Core_VERSION_MAJOR} ) install(TARGETS LibBasket DESTINATION ${LIB_INSTALL_DIR}) # Add unit tests after all variables have been set. # If we save target_link_libraries to a variable, we can reuse it too if (BUILD_TESTING) add_subdirectory(tests) endif () ########### next target ############### set(basket_SRCS main.cpp mainwindow.cpp application.cpp) add_executable(basket ${basket_SRCS}) target_link_libraries(basket LibBasket) if (LIBGIT2_FOUND) target_link_libraries(LibBasket ${LIBGIT2_LIBRARIES}) target_link_libraries(basket ${LIBGIT2_LIBRARIES}) endif() install(TARGETS basket DESTINATION ${BIN_INSTALL_DIR}) ########### next target ############### set(kcm_basket_PART_SRCS kcm_basket.cpp) add_library(kcm_basket MODULE ${kcm_basket_PART_SRCS}) target_link_libraries(kcm_basket LibBasket) install(TARGETS kcm_basket DESTINATION ${PLUGIN_INSTALL_DIR}) set(DESKTOP_FILES basket_config_general.desktop basket_config_baskets.desktop basket_config_new_notes.desktop basket_config_notes_appearance.desktop basket_config_apps.desktop basket_config_version_sync.desktop ) install(FILES org.kde.basket.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) install(FILES org.kde.basket.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES ${DESKTOP_FILES} DESTINATION ${SERVICES_INSTALL_DIR}) install(FILES basketui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/basket) diff --git a/src/archive.cpp b/src/archive.cpp index 2daacd9..f0973ba 100644 --- a/src/archive.cpp +++ b/src/archive.cpp @@ -1,625 +1,626 @@ /** * SPDX-FileCopyrightText: (C) 2006 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #include "archive.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //For Global::MainWindow() #include #include #include "backgroundmanager.h" #include "basketfactory.h" #include "basketlistview.h" #include "basketscene.h" #include "bnpview.h" +#include "common.h" #include "formatimporter.h" #include "global.h" #include "tag.h" #include "tools.h" #include "xmlwork.h" void Archive::save(BasketScene *basket, bool withSubBaskets, const QString &destination) { QDir dir; QProgressDialog dialog; dialog.setWindowTitle(i18n("Save as Basket Archive")); dialog.setLabelText(i18n("Saving as basket archive. Please wait...")); dialog.setCancelButton(nullptr); dialog.setAutoClose(true); dialog.setRange(0, /*Preparation:*/ 1 + /*Finishing:*/ 1 + /*Basket:*/ 1 + /*SubBaskets:*/ (withSubBaskets ? Global::bnpView->basketCount(Global::bnpView->listViewItemForBasket(basket)) : 0)); dialog.setValue(0); dialog.show(); // Create the temporary folder: QString tempFolder = Global::savesFolder() + "temp-archive/"; dir.mkdir(tempFolder); // Create the temporary archive file: QString tempDestination = tempFolder + "temp-archive.tar.gz"; KTar tar(tempDestination, "application/x-gzip"); tar.open(QIODevice::WriteOnly); tar.writeDir(QStringLiteral("baskets"), QString(), QString()); dialog.setValue(dialog.value() + 1); // Preparation finished qDebug() << "Preparation finished out of " << dialog.maximum(); // Copy the baskets data into the archive: QStringList backgrounds; Archive::saveBasketToArchive(basket, withSubBaskets, &tar, backgrounds, tempFolder, &dialog); // Create a Small baskets.xml Document: QString data; QXmlStreamWriter stream(&data); XMLWork::setupXmlStream(stream, "basketTree"); Global::bnpView->saveSubHierarchy(Global::bnpView->listViewItemForBasket(basket), stream, withSubBaskets); stream.writeEndElement(); stream.writeEndDocument(); - BasketScene::safelySaveToFile(tempFolder + "baskets.xml", data); + FileStorage::safelySaveToFile(tempFolder + "baskets.xml", data); tar.addLocalFile(tempFolder + "baskets.xml", "baskets/baskets.xml"); dir.remove(tempFolder + "baskets.xml"); // Save a Small tags.xml Document: QList tags; listUsedTags(basket, withSubBaskets, tags); Tag::saveTagsTo(tags, tempFolder + "tags.xml"); tar.addLocalFile(tempFolder + "tags.xml", "tags.xml"); dir.remove(tempFolder + "tags.xml"); // Save Tag Emblems (in case they are loaded on a computer that do not have those icons): QString tempIconFile = tempFolder + "icon.png"; for (Tag::List::iterator it = tags.begin(); it != tags.end(); ++it) { State::List states = (*it)->states(); for (State::List::iterator it2 = states.begin(); it2 != states.end(); ++it2) { State *state = (*it2); QPixmap icon = KIconLoader::global()->loadIcon(state->emblem(), KIconLoader::Small, 16, KIconLoader::DefaultState, QStringList(), nullptr, true); if (!icon.isNull()) { icon.save(tempIconFile, "PNG"); QString iconFileName = state->emblem().replace('/', '_'); tar.addLocalFile(tempIconFile, "tag-emblems/" + iconFileName); } } } dir.remove(tempIconFile); // Finish Tar.Gz Exportation: tar.close(); // Computing the File Preview: BasketScene *previewBasket = basket; // FIXME: Use the first non-empty basket! // QPixmap previewPixmap(previewBasket->visibleWidth(), previewBasket->visibleHeight()); QPixmap previewPixmap(previewBasket->width(), previewBasket->height()); QPainter painter(&previewPixmap); // Save old state, and make the look clean ("smile, you are filmed!"): NoteSelection *selection = previewBasket->selectedNotes(); previewBasket->unselectAll(); Note *focusedNote = previewBasket->focusedNote(); previewBasket->setFocusedNote(nullptr); previewBasket->doHoverEffects(nullptr, Note::None); // Take the screenshot: previewBasket->render(&painter); // Go back to the old look: previewBasket->selectSelection(selection); previewBasket->setFocusedNote(focusedNote); previewBasket->doHoverEffects(); // End and save our splandid painting: painter.end(); QImage previewImage = previewPixmap.toImage(); const int PREVIEW_SIZE = 256; previewImage = previewImage.scaled(PREVIEW_SIZE, PREVIEW_SIZE, Qt::KeepAspectRatio); previewImage.save(tempFolder + "preview.png", "PNG"); // Finally Save to the Real Destination file: QFile file(destination); if (file.open(QIODevice::WriteOnly)) { ulong previewSize = QFile(tempFolder + "preview.png").size(); ulong archiveSize = QFile(tempDestination).size(); QTextStream stream(&file); stream.setCodec("ISO-8859-1"); stream << "BasKetNP:archive\n" << "version:0.6.1\n" // << "read-compatible:0.6.1\n" // << "write-compatible:0.6.1\n" << "preview*:" << previewSize << "\n"; stream.flush(); // Copy the Preview File: const unsigned long BUFFER_SIZE = 1024; char *buffer = new char[BUFFER_SIZE]; long sizeRead; QFile previewFile(tempFolder + "preview.png"); if (previewFile.open(QIODevice::ReadOnly)) { while ((sizeRead = previewFile.read(buffer, BUFFER_SIZE)) > 0) file.write(buffer, sizeRead); } stream << "archive*:" << archiveSize << "\n"; stream.flush(); // Copy the Archive File: QFile archiveFile(tempDestination); if (archiveFile.open(QIODevice::ReadOnly)) { while ((sizeRead = archiveFile.read(buffer, BUFFER_SIZE)) > 0) file.write(buffer, sizeRead); } // Clean Up: delete[] buffer; buffer = nullptr; file.close(); } dialog.setValue(dialog.value() + 1); // Finishing finished qDebug() << "Finishing finished"; // Clean Up Everything: dir.remove(tempFolder + "preview.png"); dir.remove(tempDestination); dir.rmdir(tempFolder); } void Archive::saveBasketToArchive(BasketScene *basket, bool recursive, KTar *tar, QStringList &backgrounds, const QString &tempFolder, QProgressDialog *progress) { // Basket need to be loaded for tags exportation. // We load it NOW so that the progress bar really reflect the state of the exportation: if (!basket->isLoaded()) { basket->load(); } QDir dir; // Save basket data: tar->addLocalDirectory(basket->fullPath(), "baskets/" + basket->folderName()); // Save basket icon: QString tempIconFile = tempFolder + "icon.png"; if (!basket->icon().isEmpty() && basket->icon() != "basket") { QPixmap icon = KIconLoader::global()->loadIcon(basket->icon(), KIconLoader::Small, 16, KIconLoader::DefaultState, QStringList(), /*path_store=*/nullptr, /*canReturnNull=*/true); if (!icon.isNull()) { icon.save(tempIconFile, "PNG"); QString iconFileName = basket->icon().replace('/', '_'); tar->addLocalFile(tempIconFile, "basket-icons/" + iconFileName); } } // Save basket background image: QString imageName = basket->backgroundImageName(); if (!basket->backgroundImageName().isEmpty() && !backgrounds.contains(imageName)) { QString backgroundPath = Global::backgroundManager->pathForImageName(imageName); if (!backgroundPath.isEmpty()) { // Save the background image: tar->addLocalFile(backgroundPath, "backgrounds/" + imageName); // Save the preview image: QString previewPath = Global::backgroundManager->previewPathForImageName(imageName); if (!previewPath.isEmpty()) tar->addLocalFile(previewPath, "backgrounds/previews/" + imageName); // Save the configuration file: QString configPath = backgroundPath + ".config"; if (dir.exists(configPath)) tar->addLocalFile(configPath, "backgrounds/" + imageName + ".config"); } backgrounds.append(imageName); } progress->setValue(progress->value() + 1); // Basket exportation finished qDebug() << basket->basketName() << " finished"; // Recursively save child baskets: BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); if (recursive) { for (int i = 0; i < item->childCount(); i++) { saveBasketToArchive(((BasketListViewItem *)item->child(i))->basket(), recursive, tar, backgrounds, tempFolder, progress); } } } void Archive::listUsedTags(BasketScene *basket, bool recursive, QList &list) { basket->listUsedTags(list); BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); if (recursive) { for (int i = 0; i < item->childCount(); i++) { listUsedTags(((BasketListViewItem *)item->child(i))->basket(), recursive, list); } } } void Archive::open(const QString &path) { // Create the temporary folder: QString tempFolder = Global::savesFolder() + "temp-archive/"; QDir dir; dir.mkdir(tempFolder); const qint64 BUFFER_SIZE = 1024; QFile file(path); if (file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); stream.setCodec("ISO-8859-1"); QString line = stream.readLine(); if (line != "BasKetNP:archive") { KMessageBox::error(nullptr, i18n("This file is not a basket archive."), i18n("Basket Archive Error")); file.close(); Tools::deleteRecursively(tempFolder); return; } QString version; QStringList readCompatibleVersions; QStringList writeCompatibleVersions; while (!stream.atEnd()) { // Get Key/Value Pair From the Line to Read: line = stream.readLine(); int index = line.indexOf(':'); QString key; QString value; if (index >= 0) { key = line.left(index); value = line.right(line.length() - index - 1); } else { key = line; value = QString(); } if (key == "version") { version = value; } else if (key == "read-compatible") { readCompatibleVersions = value.split(';'); } else if (key == "write-compatible") { writeCompatibleVersions = value.split(';'); } else if (key == "preview*") { bool ok; qint64 size = value.toULong(&ok); if (!ok) { KMessageBox::error(nullptr, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); file.close(); Tools::deleteRecursively(tempFolder); return; } // Get the preview file: // FIXME: We do not need the preview for now // QFile previewFile(tempFolder + "preview.png"); // if (previewFile.open(QIODevice::WriteOnly)) { stream.seek(stream.pos() + size); } else if (key == "archive*") { if (version != "0.6.1" && readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) { KMessageBox::information(nullptr, i18n("This file was created with a recent version of %1. " "It can be opened but not every information will be available to you. " "For instance, some notes may be missing because they are of a type only available in new versions. " "When saving the file back, consider to save it to another file, to preserve the original one.", QGuiApplication::applicationDisplayName()), i18n("Basket Archive Error")); } if (version != "0.6.1" && !readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) { KMessageBox::error( nullptr, i18n("This file was created with a recent version of %1. Please upgrade to a newer version to be able to open that file.", QGuiApplication::applicationDisplayName()), i18n("Basket Archive Error")); file.close(); Tools::deleteRecursively(tempFolder); return; } bool ok; qint64 size = value.toULong(&ok); if (!ok) { KMessageBox::error(nullptr, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); file.close(); Tools::deleteRecursively(tempFolder); return; } if (Global::activeMainWindow()) { Global::activeMainWindow()->raise(); } // Get the archive file: QString tempArchive = tempFolder + "temp-archive.tar.gz"; QFile archiveFile(tempArchive); file.seek(stream.pos()); if (archiveFile.open(QIODevice::WriteOnly)) { char *buffer = new char[BUFFER_SIZE]; qint64 sizeRead; while ((sizeRead = file.read(buffer, qMin(BUFFER_SIZE, size))) > 0) { archiveFile.write(buffer, sizeRead); size -= sizeRead; } archiveFile.close(); delete[] buffer; // Extract the Archive: QString extractionFolder = tempFolder + "extraction/"; QDir dir; dir.mkdir(extractionFolder); KTar tar(tempArchive, "application/x-gzip"); tar.open(QIODevice::ReadOnly); tar.directory()->copyTo(extractionFolder); tar.close(); // Import the Tags: importTagEmblems(extractionFolder); // Import and rename tag emblems BEFORE loading them! QMap mergedStates = Tag::loadTags(extractionFolder + "tags.xml"); if (mergedStates.count() > 0) { Tag::saveTags(); } // Import the Background Images: importArchivedBackgroundImages(extractionFolder); // Import the Baskets: renameBasketFolders(extractionFolder, mergedStates); stream.seek(file.pos()); } } else if (key.endsWith('*')) { // We do not know what it is, but we should read the embedded-file in order to discard it: bool ok; qint64 size = value.toULong(&ok); if (!ok) { KMessageBox::error(nullptr, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); file.close(); Tools::deleteRecursively(tempFolder); return; } // Get the archive file: char *buffer = new char[BUFFER_SIZE]; qint64 sizeRead; while ((sizeRead = file.read(buffer, qMin(BUFFER_SIZE, size))) > 0) { size -= sizeRead; } delete[] buffer; } else { // We do not know what it is, and we do not care. } // Analyze the Value, if Understood: } file.close(); } Tools::deleteRecursively(tempFolder); } /** * When opening a basket archive that come from another computer, * it can contains tags that use icons (emblems) that are not present on that computer. * Fortunately, basket archives contains a copy of every used icons. * This method check for every emblems and import the missing ones. * It also modify the tags.xml copy for the emblems to point to the absolute path of the imported icons. */ void Archive::importTagEmblems(const QString &extractionFolder) { QDomDocument *document = XMLWork::openFile("basketTags", extractionFolder + "tags.xml"); if (document == nullptr) return; QDomElement docElem = document->documentElement(); QDir dir; dir.mkdir(Global::savesFolder() + "tag-emblems/"); FormatImporter copier; // Only used to copy files synchronously QDomNode node = docElem.firstChild(); while (!node.isNull()) { QDomElement element = node.toElement(); if ((!element.isNull()) && element.tagName() == "tag") { QDomNode subNode = element.firstChild(); while (!subNode.isNull()) { QDomElement subElement = subNode.toElement(); if ((!subElement.isNull()) && subElement.tagName() == "state") { QString emblemName = XMLWork::getElementText(subElement, "emblem"); if (!emblemName.isEmpty()) { QPixmap emblem = KIconLoader::global()->loadIcon(emblemName, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true); // The icon does not exists on that computer, import it: if (emblem.isNull()) { // Of the emblem path was eg. "/home/seb/emblem.png", it was exported as "tag-emblems/_home_seb_emblem.png". // So we need to copy that image to "~/.local/share/basket/tag-emblems/emblem.png": int slashIndex = emblemName.lastIndexOf('/'); QString emblemFileName = (slashIndex < 0 ? emblemName : emblemName.right(slashIndex - 2)); QString source = extractionFolder + "tag-emblems/" + emblemName.replace('/', '_'); QString destination = Global::savesFolder() + "tag-emblems/" + emblemFileName; if (!dir.exists(destination) && dir.exists(source)) copier.copyFolder(source, destination); // Replace the emblem path in the tags.xml copy: QDomElement emblemElement = XMLWork::getElement(subElement, "emblem"); subElement.removeChild(emblemElement); XMLWork::addElement(*document, subElement, "emblem", destination); } } } subNode = subNode.nextSibling(); } } node = node.nextSibling(); } - BasketScene::safelySaveToFile(extractionFolder + "tags.xml", document->toString()); + FileStorage::safelySaveToFile(extractionFolder + "tags.xml", document->toString()); } void Archive::importArchivedBackgroundImages(const QString &extractionFolder) { FormatImporter copier; // Only used to copy files synchronously QString destFolder = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/basket/backgrounds/"; QDir().mkpath(destFolder); // does not exist at the first run when addWelcomeBaskets is called QDir dir(extractionFolder + "backgrounds/", /*nameFilder=*/"*.png", /*sortSpec=*/QDir::Name | QDir::IgnoreCase, /*filterSpec=*/QDir::Files | QDir::NoSymLinks); QStringList files = dir.entryList(); for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) { QString image = *it; if (!Global::backgroundManager->exists(image)) { // Copy images: QString imageSource = extractionFolder + "backgrounds/" + image; QString imageDest = destFolder + image; copier.copyFolder(imageSource, imageDest); // Copy configuration file: QString configSource = extractionFolder + "backgrounds/" + image + ".config"; QString configDest = destFolder + image; if (dir.exists(configSource)) copier.copyFolder(configSource, configDest); // Copy preview: QString previewSource = extractionFolder + "backgrounds/previews/" + image; QString previewDest = destFolder + "previews/" + image; if (dir.exists(previewSource)) { dir.mkdir(destFolder + "previews/"); // Make sure the folder exists! copier.copyFolder(previewSource, previewDest); } // Append image to database: Global::backgroundManager->addImage(imageDest); } } } void Archive::renameBasketFolders(const QString &extractionFolder, QMap &mergedStates) { QDomDocument *doc = XMLWork::openFile("basketTree", extractionFolder + "baskets/baskets.xml"); if (doc != nullptr) { QMap folderMap; QDomElement docElem = doc->documentElement(); QDomNode node = docElem.firstChild(); renameBasketFolder(extractionFolder, node, folderMap, mergedStates); loadExtractedBaskets(extractionFolder, node, folderMap, nullptr); } } void Archive::renameBasketFolder(const QString &extractionFolder, QDomNode &basketNode, QMap &folderMap, QMap &mergedStates) { QDomNode n = basketNode; while (!n.isNull()) { QDomElement element = n.toElement(); if ((!element.isNull()) && element.tagName() == "basket") { QString folderName = element.attribute("folderName"); if (!folderName.isEmpty()) { // Find a folder name: QString newFolderName = BasketFactory::newFolderName(); folderMap[folderName] = newFolderName; // Reserve the folder name: QDir dir; dir.mkdir(Global::basketsFolder() + newFolderName); // Rename the merged tag ids: // if (mergedStates.count() > 0) { renameMergedStatesAndBasketIcon(extractionFolder + "baskets/" + folderName + ".basket", mergedStates, extractionFolder); // } // Child baskets: QDomNode node = element.firstChild(); renameBasketFolder(extractionFolder, node, folderMap, mergedStates); } } n = n.nextSibling(); } } void Archive::renameMergedStatesAndBasketIcon(const QString &fullPath, QMap &mergedStates, const QString &extractionFolder) { QDomDocument *doc = XMLWork::openFile("basket", fullPath); if (doc == nullptr) return; QDomElement docElem = doc->documentElement(); QDomElement properties = XMLWork::getElement(docElem, "properties"); importBasketIcon(properties, extractionFolder); QDomElement notes = XMLWork::getElement(docElem, "notes"); if (mergedStates.count() > 0) renameMergedStates(notes, mergedStates); - BasketScene::safelySaveToFile(fullPath, /*"\n" + */ doc->toString()); + FileStorage::safelySaveToFile(fullPath, /*"\n" + */ doc->toString()); } void Archive::importBasketIcon(QDomElement properties, const QString &extractionFolder) { QString iconName = XMLWork::getElementText(properties, "icon"); if (!iconName.isEmpty() && iconName != "basket") { QPixmap icon = KIconLoader::global()->loadIcon(iconName, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true); // The icon does not exists on that computer, import it: if (icon.isNull()) { QDir dir; dir.mkdir(Global::savesFolder() + "basket-icons/"); FormatImporter copier; // Only used to copy files synchronously // Of the icon path was eg. "/home/seb/icon.png", it was exported as "basket-icons/_home_seb_icon.png". // So we need to copy that image to "~/.local/share/basket/basket-icons/icon.png": int slashIndex = iconName.lastIndexOf('/'); QString iconFileName = (slashIndex < 0 ? iconName : iconName.right(slashIndex - 2)); QString source = extractionFolder + "basket-icons/" + iconName.replace('/', '_'); QString destination = Global::savesFolder() + "basket-icons/" + iconFileName; if (!dir.exists(destination)) copier.copyFolder(source, destination); // Replace the emblem path in the tags.xml copy: QDomElement iconElement = XMLWork::getElement(properties, "icon"); properties.removeChild(iconElement); QDomDocument document = properties.ownerDocument(); XMLWork::addElement(document, properties, "icon", destination); } } } void Archive::renameMergedStates(QDomNode notes, QMap &mergedStates) { QDomNode n = notes.firstChild(); while (!n.isNull()) { QDomElement element = n.toElement(); if (!element.isNull()) { if (element.tagName() == "group") { renameMergedStates(n, mergedStates); } else if (element.tagName() == "note") { QString tags = XMLWork::getElementText(element, "tags"); if (!tags.isEmpty()) { QStringList tagNames = tags.split(';'); for (QStringList::Iterator it = tagNames.begin(); it != tagNames.end(); ++it) { QString &tag = *it; if (mergedStates.contains(tag)) { tag = mergedStates[tag]; } } QString newTags = tagNames.join(";"); QDomElement tagsElement = XMLWork::getElement(element, "tags"); element.removeChild(tagsElement); QDomDocument document = element.ownerDocument(); XMLWork::addElement(document, element, "tags", newTags); } } } n = n.nextSibling(); } } void Archive::loadExtractedBaskets(const QString &extractionFolder, QDomNode &basketNode, QMap &folderMap, BasketScene *parent) { bool basketSetAsCurrent = (parent != nullptr); QDomNode n = basketNode; while (!n.isNull()) { QDomElement element = n.toElement(); if ((!element.isNull()) && element.tagName() == "basket") { QString folderName = element.attribute("folderName"); if (!folderName.isEmpty()) { // Move the basket folder to its destination, while renaming it uniquely: QString newFolderName = folderMap[folderName]; FormatImporter copier; // The folder has been "reserved" by creating it. Avoid asking the user to override: QDir dir; dir.rmdir(Global::basketsFolder() + newFolderName); copier.moveFolder(extractionFolder + "baskets/" + folderName, Global::basketsFolder() + newFolderName); // Append and load the basket in the tree: BasketScene *basket = Global::bnpView->loadBasket(newFolderName); BasketListViewItem *basketItem = Global::bnpView->appendBasket(basket, (basket && parent ? Global::bnpView->listViewItemForBasket(parent) : nullptr)); basketItem->setExpanded(!XMLWork::trueOrFalse(element.attribute("folded", "false"), false)); QDomElement properties = XMLWork::getElement(element, "properties"); importBasketIcon(properties, extractionFolder); // Rename the icon fileName if necessary basket->loadProperties(properties); // Open the first basket of the archive: if (!basketSetAsCurrent) { Global::bnpView->setCurrentBasket(basket); basketSetAsCurrent = true; } QDomNode node = element.firstChild(); loadExtractedBaskets(extractionFolder, node, folderMap, basket); } } n = n.nextSibling(); } } diff --git a/src/basketscene.cpp b/src/basketscene.cpp index 60fd69f..2b8f9d7 100644 --- a/src/basketscene.cpp +++ b/src/basketscene.cpp @@ -1,5192 +1,5035 @@ /** * 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 "common.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::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) { Q_EMIT postMessage(i18n("The new note does not match the filter and is hidden.")); } else if (founds == count - 1) { Q_EMIT postMessage(i18n("A new note does not match the filter and is hidden.")); } else if (founds > 0) { Q_EMIT postMessage(i18n("Some new notes do not match the filter and are hidden.")); } else { Q_EMIT 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); } Q_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); } } 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); 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); else m_firstNote = column; lastInsertedColumn = column; } // Reinsert the old notes in the first column: insertNote(notes, /*clicked=*/firstNote(), /*zone=*/Note::BottomColumn); 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); 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)) { + if (!FileStorage::saveToFile(fullPath() + ".basket", data, isEncrypted())) { 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)) { + if (FileStorage::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() { Q_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, &QAction::triggered, this, &BasketScene::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, &KDirWatch::dirty, this, &BasketScene::watchedFileModified); //connect(m_watcher, &KDirWatch::deleted, this, &BasketScene::watchedFileDeleted); connect(&m_watcherTimer, &QTimer::timeout, this, &BasketScene::updateModifiedNotes); // Various Connections: connect(&m_autoScrollSelectionTimer, &QTimer::timeout, this, &BasketScene::doAutoScrollSelection); connect(&m_timerCountsChanged, &QTimer::timeout, this, &BasketScene::countsChangedTimeOut); connect(&m_inactivityAutoSaveTimer, &QTimer::timeout, this, &BasketScene::inactivityAutoSaveTimeout); connect(&m_inactivityAutoLockTimer, &QTimer::timeout, this, &BasketScene::inactivityAutoLockTimeout); #ifdef HAVE_LIBGPGME m_gpg = new KGpgMe(); #endif m_locked = isFileEncrypted(); // setup the delayed commit timer m_commitdelay.setSingleShot(true); connect(&m_commitdelay, &QTimer::timeout, this, &BasketScene::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, &QMenu::aboutToHide, this, &BasketScene::delayedCancelInsertPopupMenu); connect(&menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); connect(&menu, &QMenu::aboutToHide, this, &BasketScene::disableNextClick); connect(&menu, &QMenu::aboutToHide, this, &BasketScene::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, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); connect(menu, &QMenu::aboutToHide, this, &BasketScene::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, &QMenu::aboutToHide, this, &BasketScene::delayedCancelInsertPopupMenu); connect(menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); connect(menu, &QMenu::aboutToHide, this, &BasketScene::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, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); connect(menu, &QMenu::aboutToHide, this, &BasketScene::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) { 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())); // 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; Q_EMIT resetStatusBarText(); doHoverEffects(); } void BasketScene::dropEvent(QGraphicsSceneDragDropEvent *event) { QPointF pos = event->scenePos(); qDebug() << "Drop Event at position " << pos.x() << ":" << pos.y(); m_isDuringDrag = false; Q_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); // 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 link; 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://")) { Q_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 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 // Q_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) Q_EMIT setStatusBarText(m_hoveredNote->linkAt(pos - QPoint(m_hoveredNote->x(), m_hoveredNote->y()))); else if (m_hoveredNote->content()) Q_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(); Q_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; Q_EMIT resetStatusBarText(); Q_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, &QButton::clicked, this, &BasketScene::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, &QMenu::triggered, this, &BasketScene::toggledStateInMenu); connect(&menu, &QMenu::aboutToHide, this, &BasketScene::unlockHovering); connect(&menu, &QMenu::aboutToHide, this, &BasketScene::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 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(), &KTextEdit::selectionChanged, this, &BasketScene::selectionChangedInEditor); disconnect(m_editor->textEdit(), &KTextEdit::textChanged, this, &BasketScene::selectionChangedInEditor); disconnect(m_editor->textEdit(), &KTextEdit::textChanged, this, &BasketScene::contentChangedInEditor); } else if (m_editor->lineEdit()) { disconnect(m_editor->lineEdit(), &QLineEdit::selectionChanged, this, &BasketScene::selectionChangedInEditor); disconnect(m_editor->lineEdit(), &QLineEdit::textChanged, this, &BasketScene::selectionChangedInEditor); disconnect(m_editor->lineEdit(), &QLineEdit::textChanged, this, &BasketScene::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()*/); Q_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, &NoteEditor::askValidation, this, &BasketScene::closeEditorDelayed); connect(m_editor, &NoteEditor::mouseEnteredEditorWidget, this, &BasketScene::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() Q_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: Q_EMIT postMessage(i18np("Copied note to clipboard.", "Copied notes to clipboard.", countCopied)); break; case CutToClipboard: Q_EMIT postMessage(i18np("Cut note to clipboard.", "Cut notes to clipboard.", countCopied)); break; case CopyToSelection: Q_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()) Q_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 { Q_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://")) { Q_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()) { Q_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)) Q_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())); } else { insertNote(group, first, Note::TopInsert); } // 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); // 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); Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); insertNote(fakeNote, group, Note::BottomColumn); 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); 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); else insertNote(fakeNote, firstNote(), Note::BottomColumn); } else { // TODO: Also allow to move notes on top of a group!!!!!!! insertNote(fakeNote, nullptr, Note::BottomInsert); } 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); else { // TODO: Also allow to move notes on top of a group!!!!!!! insertNote(fakeNote, nullptr, Note::BottomInsert); } 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)); // 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()) { Q_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/basketscene.h b/src/basketscene.h index 306a597..7531c76 100644 --- a/src/basketscene.h +++ b/src/basketscene.h @@ -1,723 +1,717 @@ /** * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef BASKET_H #define BASKET_H #include #include #include #include #include #include #include #include "config.h" #include "note.h" // For Note::Zone class QFrame; class QPixmap; class QPushButton; class QDomDocument; class QDomElement; class QContextMenuEvent; class QDragLeaveEvent; class QDragEnterEvent; class QDragMoveEvent; class QDropEvent; class QEvent; class QFocusEvent; class QHelpEvent; class QKeyEvent; class QMouseEvent; class QResizeEvent; class QWheelEvent; class QAction; class KDirWatch; class QKeySequence; class QUrl; namespace KIO { class Job; } class DecoratedBasket; class Note; class NoteEditor; class Tag; class TransparentWidget; #ifdef HAVE_LIBGPGME class KGpgMe; #endif /** * @author Sébastien Laoût */ class BasketScene : public QGraphicsScene { Q_OBJECT public: enum EncryptionTypes { NoEncryption = 0, PasswordEncryption = 1, PrivateKeyEncryption = 2 }; public: /// CONSTRUCTOR AND DESTRUCTOR: BasketScene(QWidget *parent, const QString &folderName); ~BasketScene() override; /// USER INTERACTION: private: bool m_noActionOnMouseRelease; bool m_ignoreCloseEditorOnNextMouseRelease; QPointF m_pressPos; bool m_canDrag; public: void drawBackground(QPainter *painter, const QRectF &rect) override; void drawForeground(QPainter *painter, const QRectF &rect) override; void enterEvent(QEvent *); void leaveEvent(QEvent *); void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; void clickedToInsert(QGraphicsSceneMouseEvent *event, Note *clicked = nullptr, int zone = 0); private Q_SLOTS: void setFocusIfNotInPopupMenu(); Q_SIGNALS: void crossReference(QString link); /// LAYOUT: private: Note *m_firstNote; int m_columnsCount; bool m_mindMap; Note *m_resizingNote; int m_pickedResizer; Note *m_movingNote; QPoint m_pickedHandle; QSet m_notesToBeDeleted; public: qreal tmpWidth; qreal tmpHeight; public: void unsetNotesWidth(); void relayoutNotes(); Note *noteAt(QPointF pos); inline Note *firstNote() { return m_firstNote; } inline int columnsCount() { return m_columnsCount; } inline bool isColumnsLayout() { return m_columnsCount > 0; } inline bool isFreeLayout() { return m_columnsCount <= 0; } inline bool isMindMap() { return isFreeLayout() && m_mindMap; } Note *resizingNote() { return m_resizingNote; } void deleteNotes(); Note *lastNote(); void setDisposition(int disposition, int columnCount); void equalizeColumnSizes(); /// NOTES INSERTION AND REMOVAL: public: /// The following methods assume that the note(s) to insert already all have 'this' as the parent basket: void appendNoteIn(Note *note, Note *in); /// << Add @p note (and the next linked notes) as the last note(s) of the group @p in. void appendNoteAfter(Note *note, Note *after); /// << Add @p note (and the next linked notes) just after (just below) the note @p after. void appendNoteBefore(Note *note, Note *before); /// << Add @p note (and the next linked notes) just before (just above) the note @p before. void groupNoteAfter(Note *note, Note *with); /// << Add a group at @p with place, move @p with in it, and add @p note (and the next linked notes) just after the group. void groupNoteBefore(Note *note, Note *with); /// << Add a group at @p with place, move @p with in it, and add @p note (and the next linked notes) just before the group. void unplugNote(Note *note); /// << Unplug @p note (and its child notes) from the basket (and also decrease counts...). /// << After that, you should delete the notes yourself. Do not call prepend/append/group... functions two times: unplug and ok void ungroupNote(Note *group); /// << Unplug @p group but put child notes at its place. /// And this one do almost all the above methods depending on the context: void insertNote(Note *note, Note *clicked, int zone, const QPointF &pos = QPointF()); void insertCreatedNote(Note *note); /// And working with selections: void unplugSelection(NoteSelection *selection); void insertSelection(NoteSelection *selection, Note *after); void selectSelection(NoteSelection *selection); protected Q_SLOTS: void doCleanUp(); private: void preparePlug(Note *note); private: Note *m_clickedToInsert; int m_zoneToInsert; QPointF m_posToInsert; Note *m_savedClickedToInsert; int m_savedZoneToInsert; QPointF m_savedPosToInsert; bool m_isInsertPopupMenu; QAction *m_insertMenuTitle; public: void saveInsertionData(); void restoreInsertionData(); void resetInsertionData(); public Q_SLOTS: void insertEmptyNote(int type); void insertWizard(int type); void insertColor(const QColor &color); void insertImage(const QPixmap &image); void pasteNote(QClipboard::Mode mode = QClipboard::Clipboard); void delayedCancelInsertPopupMenu(); void setInsertPopupMenu() { m_isInsertPopupMenu = true; } void cancelInsertPopupMenu() { m_isInsertPopupMenu = false; } private Q_SLOTS: void hideInsertPopupMenu(); void timeoutHideInsertPopupMenu(); /// TOOL TIPS: protected: void helpEvent(QGraphicsSceneHelpEvent *event) override; /// LOAD AND SAVE: private: bool m_loaded; bool m_loadingLaunched; bool m_locked; bool m_shouldConvertPlainTextNotes; QFrame *m_decryptBox; QPushButton *m_button; int m_encryptionType; QString m_encryptionKey; #ifdef HAVE_LIBGPGME KGpgMe *m_gpg; #endif QTimer m_inactivityAutoLockTimer; QTimer m_commitdelay; void enableActions(); private Q_SLOTS: void loadNotes(const QDomElement ¬es, Note *parent); void saveNotes(QXmlStreamWriter &stream, Note *parent); void unlock(); protected Q_SLOTS: void inactivityAutoLockTimeout(); public Q_SLOTS: void load(); void loadProperties(const QDomElement &properties); void saveProperties(QXmlStreamWriter &stream); bool save(); void commitEdit(); void reload(); public: bool isEncrypted(); bool isFileEncrypted(); bool isLocked() { return m_locked; }; void lock(); bool isLoaded() { return m_loaded; }; bool loadingLaunched() { return m_loadingLaunched; }; - bool loadFromFile(const QString &fullPath, QString *string); - bool loadFromFile(const QString &fullPath, QByteArray *array); - bool saveToFile(const QString &fullPath, const QString &string); - bool saveToFile(const QString &fullPath, const QByteArray &array); //[Encrypt and] save binary content - static bool safelySaveToFile(const QString &fullPath, const QByteArray &array, unsigned long length); - static bool safelySaveToFile(const QString &fullPath, const QString &string); - bool setProtection(int type, QString key); int encryptionType() { return m_encryptionType; }; QString encryptionKey() { return m_encryptionKey; }; + bool setProtection(int type, QString key); bool saveAgain(); /// BACKGROUND: private: QColor m_backgroundColorSetting; QString m_backgroundImageName; QPixmap *m_backgroundPixmap; QPixmap *m_opaqueBackgroundPixmap; QPixmap *m_selectedBackgroundPixmap; bool m_backgroundTiled; QColor m_textColorSetting; public: inline bool hasBackgroundImage() { return m_backgroundPixmap != nullptr; } inline const QPixmap *backgroundPixmap() { return m_backgroundPixmap; } inline bool isTiledBackground() { return m_backgroundTiled; } inline QString backgroundImageName() { return m_backgroundImageName; } inline QColor backgroundColorSetting() { return m_backgroundColorSetting; } inline QColor textColorSetting() { return m_textColorSetting; } QColor backgroundColor() const; QColor textColor() const; void setAppearance(const QString &icon, const QString &name, const QString &backgroundImage, const QColor &backgroundColor, const QColor &textColor); void blendBackground(QPainter &painter, const QRectF &rect, qreal xPainter = -1, qreal yPainter = -1, bool opaque = false, QPixmap *bg = nullptr); void blendBackground(QPainter &painter, const QRectF &rect, bool opaque, QPixmap *bg); void unbufferizeAll(); void subscribeBackgroundImages(); void unsubscribeBackgroundImages(); /// KEYBOARD SHORTCUT: public: QAction *m_action; private: int m_shortcutAction; private Q_SLOTS: void activatedShortcut(); public: QKeySequence shortcut() { return m_action->shortcut(); } int shortcutAction() { return m_shortcutAction; } void setShortcut(QKeySequence shortcut, int action); /// USER INTERACTION: private: Note *m_hoveredNote; int m_hoveredZone; bool m_lockedHovering; bool m_underMouse; QRectF m_inserterRect; bool m_inserterShown; bool m_inserterSplit; bool m_inserterTop; bool m_inserterGroup; void placeInserter(Note *note, int zone); void removeInserter(); public: // bool inserterShown() { return m_inserterShown; } bool inserterSplit() { return m_inserterSplit; } bool inserterGroup() { return m_inserterGroup; } public Q_SLOTS: void doHoverEffects(Note *note, Note::Zone zone, const QPointF &pos = QPointF(0, 0)); /// << @p pos is optional and only used to show the link target in the statusbar void doHoverEffects(const QPointF &pos); void doHoverEffects(); // The same, but using the current cursor position void mouseEnteredEditorWidget(); public: void popupTagsMenu(Note *note); void popupEmblemMenu(Note *note, int emblemNumber); void addTagToSelectedNotes(Tag *tag); void removeTagFromSelectedNotes(Tag *tag); void removeAllTagsFromSelectedNotes(); void addStateToSelectedNotes(State *state); void changeStateOfSelectedNotes(State *state); bool selectedNotesHaveTags(); const QRectF &inserterRect() { return m_inserterRect; } bool inserterShown() { return m_inserterShown; } void drawInserter(QPainter &painter, qreal xPainter, qreal yPainter); DecoratedBasket *decoration(); State *stateForTagFromSelectedNotes(Tag *tag); public Q_SLOTS: void activatedTagShortcut(Tag *tag); void recomputeAllStyles(); void removedStates(const QList &deletedStates); void toggledTagInMenu(QAction *act); void toggledStateInMenu(QAction *act); void unlockHovering(); void disableNextClick(); public: Note *m_tagPopupNote; private: Tag *m_tagPopup; QTime m_lastDisableClick; /// SELECTION: private: bool m_isSelecting; bool m_selectionStarted; bool m_selectionInvert; QPointF m_selectionBeginPoint; QPointF m_selectionEndPoint; QRectF m_selectionRect; QTimer m_autoScrollSelectionTimer; void stopAutoScrollSelection(); private Q_SLOTS: void doAutoScrollSelection(); public: inline bool isSelecting() { return m_isSelecting; } inline const QRectF &selectionRect() { return m_selectionRect; } void selectNotesIn(const QRectF &rect, bool invertSelection, bool unselectOthers = true); void resetWasInLastSelectionRect(); void selectAll(); void unselectAll(); void invertSelection(); void unselectAllBut(Note *toSelect); void invertSelectionOf(Note *toSelect); QColor selectionRectInsideColor(); Note *theSelectedNote(); NoteSelection *selectedNotes(); /// BLANK SPACES DRAWING: private: QList m_blankAreas; void recomputeBlankRects(); QWidget *m_cornerWidget; /// COMMUNICATION WITH ITS CONTAINER: Q_SIGNALS: void postMessage(const QString &message); /// << Post a temporary message in the statusBar. void setStatusBarText(const QString &message); /// << Set the permanent statusBar text or reset it if message isEmpty(). void resetStatusBarText(); /// << Equivalent to setStatusBarText(QString()). void propertiesChanged(BasketScene *basket); void countsChanged(BasketScene *basket); public Q_SLOTS: void linkLookChanged(); void signalCountsChanged(); private: QTimer m_timerCountsChanged; private Q_SLOTS: void countsChangedTimeOut(); /// NOTES COUNTING: public: void addSelectedNote() { ++m_countSelecteds; signalCountsChanged(); } void removeSelectedNote() { --m_countSelecteds; signalCountsChanged(); } void resetSelectedNote() { m_countSelecteds = 0; signalCountsChanged(); } // FIXME: Useful ??? int count() { return m_count; } int countFounds() { return m_countFounds; } int countSelecteds() { return m_countSelecteds; } private: int m_count; int m_countFounds; int m_countSelecteds; /// PROPERTIES: public: QString basketName() { return m_basketName; } QString icon() { return m_icon; } QString folderName() { return m_folderName; } QString fullPath(); QString fullPathForFileName(const QString &fileName); // Full path of an [existing or not] note in this basket static QString fullPathForFolderName(const QString &folderName); private: QString m_basketName; QString m_icon; QString m_folderName; /// ACTIONS ON SELECTED NOTES FROM THE INTERFACE: public Q_SLOTS: void noteEdit(Note *note = nullptr, bool justAdded = false, const QPointF &clickedPoint = QPointF()); void showEditedNoteWhileFiltering(); void noteDelete(); void noteDeleteWithoutConfirmation(bool deleteFilesToo = true); void noteCopy(); void noteCut(); void noteOpen(Note *note = nullptr); void noteOpenWith(Note *note = nullptr); void noteSaveAs(); void noteGroup(); void noteUngroup(); void noteMoveOnTop(); void noteMoveOnBottom(); void noteMoveNoteUp(); void noteMoveNoteDown(); void moveSelectionTo(Note *here, bool below); public: enum CopyMode { CopyToClipboard, CopyToSelection, CutToClipboard }; void doCopy(CopyMode copyMode); bool selectionIsOneGroup(); Note *selectedGroup(); Note *firstSelected(); Note *lastSelected(); /// NOTES EDITION: private: NoteEditor *m_editor; // QWidget *m_rightEditorBorder; TransparentWidget *m_leftEditorBorder; TransparentWidget *m_rightEditorBorder; bool m_redirectEditActions; bool m_editorTrackMouseEvent; qreal m_editorWidth; qreal m_editorHeight; QTimer m_inactivityAutoSaveTimer; bool m_doNotCloseEditor; QTextCursor m_textCursor; public: bool isDuringEdit() { return m_editor; } bool redirectEditActions() { return m_redirectEditActions; } bool hasTextInEditor(); bool hasSelectedTextInEditor(); bool selectedAllTextInEditor(); Note *editedNote(); protected Q_SLOTS: void selectionChangedInEditor(); void contentChangedInEditor(); void inactivityAutoSaveTimeout(); public Q_SLOTS: void editorCursorPositionChanged(); private: qreal m_editorX; qreal m_editorY; public Q_SLOTS: void placeEditor(bool andEnsureVisible = false); void placeEditorAndEnsureVisible(); bool closeEditor(bool deleteEmptyNote = true); void closeEditorDelayed(); void updateEditorAppearance(); void editorPropertiesChanged(); void openBasket(); void closeBasket(); /// FILTERING: public Q_SLOTS: void newFilter(const FilterData &data, bool andEnsureVisible = true); void filterAgain(bool andEnsureVisible = true); void filterAgainDelayed(); bool isFiltering(); /// DRAG AND DROP: private: bool m_isDuringDrag; QList m_draggedNotes; public: static void acceptDropEvent(QGraphicsSceneDragDropEvent *event, bool preCond = true); void dropEvent(QGraphicsSceneDragDropEvent *event) override; void blindDrop(QGraphicsSceneDragDropEvent *event); void blindDrop(const QMimeData *mimeData, Qt::DropAction dropAction, QObject *source); bool isDuringDrag() { return m_isDuringDrag; } QList draggedNotes() { return m_draggedNotes; } protected: void dragEnterEvent(QGraphicsSceneDragDropEvent *) override; void dragMoveEvent(QGraphicsSceneDragDropEvent *event) override; void dragLeaveEvent(QGraphicsSceneDragDropEvent *) override; public Q_SLOTS: void slotCopyingDone2(KIO::Job *job, const QUrl &from, const QUrl &to); public: Note *noteForFullPath(const QString &path); /// EXPORTATION: public: QList usedStates(); public: void listUsedTags(QList &list); /// MANAGE FOCUS: private: Note *m_focusedNote; public: void setFocusedNote(Note *note); void focusANote(); void focusANonSelectedNoteAbove(bool inSameColumn); void focusANonSelectedNoteBelow(bool inSameColumn); void focusANonSelectedNoteBelowOrThenAbove(); void focusANonSelectedNoteAboveOrThenBelow(); Note *focusedNote() { return m_focusedNote; } Note *firstNoteInStack(); Note *lastNoteInStack(); Note *firstNoteShownInStack(); Note *lastNoteShownInStack(); void selectRange(Note *start, Note *end, bool unselectOthers = true); /// FIXME: Not really a focus related method! void ensureNoteVisible(Note *note); void keyPressEvent(QKeyEvent *event) override; void focusInEvent(QFocusEvent *) override; void focusOutEvent(QFocusEvent *) override; QRectF noteVisibleRect(Note *note); // clipped global (desktop as origin) rectangle Note *firstNoteInGroup(); Note *noteOnHome(); Note *noteOnEnd(); enum NoteOn { LEFT_SIDE = 1, RIGHT_SIDE, TOP_SIDE, BOTTOM_SIDE }; Note *noteOn(NoteOn side); /// REIMPLEMENTED: public: void deleteFiles(); bool convertTexts(); public: void wheelEvent(QGraphicsSceneWheelEvent *event) override; public: Note *m_startOfShiftSelectionNote; /// THE NEW FILE WATCHER: private: KDirWatch *m_watcher; QTimer m_watcherTimer; QList m_modifiedFiles; public: void addWatchedFile(const QString &fullPath); void removeWatchedFile(const QString &fullPath); private Q_SLOTS: void watchedFileModified(const QString &fullPath); void watchedFileDeleted(const QString &fullPath); void updateModifiedNotes(); /// FROM OLD ARCHITECTURE ********************** public Q_SLOTS: void showFrameInsertTo() { } void resetInsertTo() { } void computeInsertPlace(const QPointF & /*cursorPosition*/) { } public: friend class SystemTray; /// SPEED OPTIMIZATION private: bool m_finishLoadOnFirstShow; bool m_relayoutOnNextShow; public: void aboutToBeActivated(); QGraphicsView *graphicsView() { return m_view; } private: QGraphicsView *m_view; }; #endif // BASKET_H diff --git a/src/bnpview.cpp b/src/bnpview.cpp index 3579076..a011562 100644 --- a/src/bnpview.cpp +++ b/src/bnpview.cpp @@ -1,2839 +1,2840 @@ /** * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #include "bnpview.h" +#include "common.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 #include #ifndef BASKET_USE_DRKONQI #include #endif // BASKET_USE_DRKONQI #include #include // usleep #include "archive.h" #include "backgroundmanager.h" #include "backup.h" #include "basketfactory.h" #include "basketlistview.h" #include "basketproperties.h" #include "basketscene.h" #include "basketstatusbar.h" #include "colorpicker.h" #include "crashhandler.h" #include "debugwindow.h" #include "decoratedbasket.h" #include "formatimporter.h" #include "gitwrapper.h" #include "history.h" #include "htmlexporter.h" #include "icon_names.h" #include "newbasketdialog.h" #include "notedrag.h" #include "noteedit.h" // To launch InlineEditors::initToolBars() #include "notefactory.h" #include "password.h" #include "regiongrabber.h" #include "settings.h" #include "softwareimporters.h" #include "tools.h" #include "xmlwork.h" #include #include #include #include //#include "bnpviewadaptor.h" /** class BNPView: */ const int BNPView::c_delayTooltipTime = 275; BNPView::BNPView(QWidget *parent, const char *name, KXMLGUIClient *aGUIClient, KActionCollection *actionCollection, BasketStatusBar *bar) : QSplitter(Qt::Horizontal, parent) , m_actLockBasket(nullptr) , m_actPassBasket(nullptr) , m_loading(true) , m_newBasketPopup(false) , m_firstShow(true) , m_colorPicker(new DesktopColorPicker()) , m_regionGrabber(nullptr) , m_passiveDroppedSelection(nullptr) , m_actionCollection(actionCollection) , m_guiClient(aGUIClient) , m_statusbar(bar) , m_tryHideTimer(nullptr) , m_hideTimer(nullptr) { // new BNPViewAdaptor(this); QDBusConnection dbus = QDBusConnection::sessionBus(); dbus.registerObject("/BNPView", this); setObjectName(name); /* Settings */ Settings::loadConfig(); Global::bnpView = this; // Needed when loading the baskets: Global::backgroundManager = new BackgroundManager(); setupGlobalShortcuts(); m_history = new QUndoStack(this); initialize(); QTimer::singleShot(0, this, SLOT(lateInit())); } BNPView::~BNPView() { int treeWidth = Global::bnpView->sizes()[Settings::treeOnLeft() ? 0 : 1]; Settings::setBasketTreeWidth(treeWidth); if (currentBasket() && currentBasket()->isDuringEdit()) currentBasket()->closeEditor(); Settings::saveConfig(); Global::bnpView = nullptr; delete Global::systemTray; Global::systemTray = nullptr; delete m_statusbar; delete m_history; m_history = nullptr; NoteDrag::createAndEmptyCuttingTmpFolder(); // Clean the temporary folder we used } void BNPView::lateInit() { /* InlineEditors* instance = InlineEditors::instance(); if(instance) { KToolBar* toolbar = instance->richTextToolBar(); if(toolbar) toolbar->hide(); } */ // If the main window is hidden when session is saved, Container::queryClose() // isn't called and the last value would be kept Settings::setStartDocked(true); Settings::saveConfig(); /* System tray icon */ Global::systemTray = new SystemTray(Global::activeMainWindow()); Global::systemTray->setIconByName(":/images/22-apps-basket"); connect(Global::systemTray, &SystemTray::showPart, this, &BNPView::showPart); /*if (Settings::useSystray()) Global::systemTray->show();*/ // Load baskets DEBUG_WIN << "Baskets are loaded from " + Global::basketsFolder(); NoteDrag::createAndEmptyCuttingTmpFolder(); // If last exec hasn't done it: clean the temporary folder we will use Tag::loadTags(); // Tags should be ready before loading baskets, but tags need the mainContainer to be ready to create KActions! load(); // If no basket has been found, try to import from an older version, if (topLevelItemCount() <= 0) { QDir dir; dir.mkdir(Global::basketsFolder()); if (FormatImporter::shouldImportBaskets()) { FormatImporter::importBaskets(); load(); } if (topLevelItemCount() <= 0) { // Create first basket: BasketFactory::newBasket(QString(), i18n("General")); GitWrapper::commitBasket(currentBasket()); GitWrapper::commitTagsXml(); } } // Load the Welcome Baskets if it is the First Time: if (!Settings::welcomeBasketsAdded()) { addWelcomeBaskets(); Settings::setWelcomeBasketsAdded(true); Settings::saveConfig(); } m_tryHideTimer = new QTimer(this); m_hideTimer = new QTimer(this); connect(m_tryHideTimer, &QTimer::timeout, this, &BNPView::timeoutTryHide); connect(m_hideTimer, &QTimer::timeout, this, &BNPView::timeoutHide); // Preload every baskets for instant filtering: /*StopWatch::start(100); QListViewItemIterator it(m_tree); while (it.current()) { BasketListViewItem *item = ((BasketListViewItem*)it.current()); item->basket()->load(); qApp->processEvents(); ++it; } StopWatch::check(100);*/ } void BNPView::addWelcomeBaskets() { // Possible paths where to find the welcome basket archive, trying the translated one, and falling back to the English one: QStringList possiblePaths; if (QString(Tools::systemCodeset()) == QString("UTF-8")) { // Welcome baskets are encoded in UTF-8. If the system is not, then use the English version: QString lang = QLocale().languageToString(QLocale().language()); possiblePaths.append(QStandardPaths::locate(QStandardPaths::GenericDataLocation, "basket/welcome/Welcome_" + lang + ".baskets")); possiblePaths.append(QStandardPaths::locate(QStandardPaths::GenericDataLocation, "basket/welcome/Welcome_" + lang.split('_')[0] + ".baskets")); } possiblePaths.append(QStandardPaths::locate(QStandardPaths::GenericDataLocation, "basket/welcome/Welcome_en_US.baskets")); // Take the first EXISTING basket archive found: QDir dir; QString path; for (QStringList::Iterator it = possiblePaths.begin(); it != possiblePaths.end(); ++it) { if (dir.exists(*it)) { path = *it; break; } } // Extract: if (!path.isEmpty()) Archive::open(path); } void BNPView::onFirstShow() { // In late init, because we need qApp->mainWidget() to be set! connectTagsMenu(); m_statusbar->setupStatusBar(); int treeWidth = Settings::basketTreeWidth(); if (treeWidth < 0) treeWidth = m_tree->fontMetrics().maxWidth() * 11; QList splitterSizes; splitterSizes.append(treeWidth); setSizes(splitterSizes); } void BNPView::setupGlobalShortcuts() { KActionCollection *ac = new KActionCollection(this); QAction *a = nullptr; // Ctrl+Shift+W only works when started standalone: QWidget *basketMainWindow = qobject_cast(Global::bnpView->parent()); int modifier = Qt::CTRL + Qt::ALT + Qt::SHIFT; if (basketMainWindow) { a = ac->addAction("global_show_hide_main_window", Global::systemTray, SLOT(toggleActive())); a->setText(i18n("Show/hide main window")); a->setStatusTip( i18n("Allows you to show main Window if it is hidden, and to hide " "it if it is shown.")); KGlobalAccel::self()->setGlobalShortcut(a, (QKeySequence(modifier + Qt::Key_W))); } a = ac->addAction("global_paste", Global::bnpView, SLOT(globalPasteInCurrentBasket())); a->setText(i18n("Paste clipboard contents in current basket")); a->setStatusTip( i18n("Allows you to paste clipboard contents in the current basket " "without having to open the main window.")); KGlobalAccel::self()->setGlobalShortcut(a, QKeySequence(modifier + Qt::Key_V)); a = ac->addAction("global_show_current_basket", Global::bnpView, SLOT(showPassiveContentForced())); a->setText(i18n("Show current basket name")); a->setStatusTip( i18n("Allows you to know basket is current without opening " "the main window.")); a = ac->addAction("global_paste_selection", Global::bnpView, SLOT(pasteSelInCurrentBasket())); a->setText(i18n("Paste selection in current basket")); a->setStatusTip( i18n("Allows you to paste clipboard selection in the current basket " "without having to open the main window.")); KGlobalAccel::self()->setGlobalShortcut(a, (QKeySequence(Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_S))); a = ac->addAction("global_new_basket", Global::bnpView, SLOT(askNewBasket())); a->setText(i18n("Create a new basket")); a->setStatusTip( i18n("Allows you to create a new basket without having to open the " "main window (you then can use the other global shortcuts to add " "a note, paste clipboard or paste selection in this new basket).")); a = ac->addAction("global_previous_basket", Global::bnpView, SLOT(goToPreviousBasket())); a->setText(i18n("Go to previous basket")); a->setStatusTip( i18n("Allows you to change current basket to the previous one without " "having to open the main window.")); a = ac->addAction("global_next_basket", Global::bnpView, SLOT(goToNextBasket())); a->setText(i18n("Go to next basket")); a->setStatusTip( i18n("Allows you to change current basket to the next one " "without having to open the main window.")); a = ac->addAction("global_note_add_html", Global::bnpView, SLOT(addNoteHtml())); a->setText(i18n("Insert text note")); a->setStatusTip( i18n("Add a text note to the current basket without having to open " "the main window.")); KGlobalAccel::self()->setGlobalShortcut(a, (QKeySequence(modifier + Qt::Key_T))); a = ac->addAction("global_note_add_image", Global::bnpView, SLOT(addNoteImage())); a->setText(i18n("Insert image note")); a->setStatusTip( i18n("Add an image note to the current basket without having to open " "the main window.")); a = ac->addAction("global_note_add_link", Global::bnpView, SLOT(addNoteLink())); a->setText(i18n("Insert link note")); a->setStatusTip( i18n("Add a link note to the current basket without having " "to open the main window.")); a = ac->addAction("global_note_add_color", Global::bnpView, SLOT(addNoteColor())); a->setText(i18n("Insert color note")); a->setStatusTip( i18n("Add a color note to the current basket without having to open " "the main window.")); a = ac->addAction("global_note_pick_color", Global::bnpView, SLOT(slotColorFromScreenGlobal())); a->setText(i18n("Pick color from screen")); a->setStatusTip( i18n("Add a color note picked from one pixel on screen to the current " "basket without " "having to open the main window.")); a = ac->addAction("global_note_grab_screenshot", Global::bnpView, SLOT(grabScreenshotGlobal())); a->setText(i18n("Grab screen zone")); a->setStatusTip( i18n("Grab a screen zone as an image in the current basket without " "having to open the main window.")); #if 0 a = ac->addAction("global_note_add_text", Global::bnpView, SLOT(addNoteText())); a->setText(i18n("Insert plain text note")); a->setStatusTip( i18n("Add a plain text note to the current basket without having to " "open the main window.")); #endif } void BNPView::initialize() { /// Configure the List View Columns: m_tree = new BasketTreeListView(this); m_tree->setHeaderLabel(i18n("Baskets")); m_tree->setSortingEnabled(false /*Disabled*/); m_tree->setRootIsDecorated(true); m_tree->setLineWidth(1); m_tree->setMidLineWidth(0); m_tree->setFocusPolicy(Qt::NoFocus); /// Configure the List View Drag and Drop: m_tree->setDragEnabled(true); m_tree->setDragDropMode(QAbstractItemView::DragDrop); m_tree->setAcceptDrops(true); m_tree->viewport()->setAcceptDrops(true); /// Configure the Splitter: m_stack = new QStackedWidget(this); setOpaqueResize(true); setCollapsible(indexOf(m_tree), true); setCollapsible(indexOf(m_stack), false); setStretchFactor(indexOf(m_tree), 0); setStretchFactor(indexOf(m_stack), 1); /// Configure the List View Signals: connect(m_tree, &BasketTreeListView::itemActivated, this, &BNPView::slotPressed); connect(m_tree, &BasketTreeListView::itemPressed, this, &BNPView::slotPressed); connect(m_tree, &BasketTreeListView::itemClicked, this, &BNPView::slotPressed); connect(m_tree, &BasketTreeListView::itemExpanded, this, &BNPView::needSave); connect(m_tree, &BasketTreeListView::itemCollapsed, this, &BNPView::needSave); connect(m_tree, &BasketTreeListView::contextMenuRequested, this, &BNPView::slotContextMenu); connect(m_tree, &BasketTreeListView::itemDoubleClicked, this, &BNPView::slotShowProperties); connect(m_tree, &BasketTreeListView::itemExpanded, this, &BNPView::basketChanged); connect(m_tree, &BasketTreeListView::itemCollapsed, this, &BNPView::basketChanged); connect(this, &BNPView::basketChanged, this, &BNPView::slotBasketChanged); connect(m_history, &QUndoStack::canRedoChanged, this, &BNPView::canUndoRedoChanged); connect(m_history, &QUndoStack::canUndoChanged, this, &BNPView::canUndoRedoChanged); setupActions(); /// What's This Help for the tree: m_tree->setWhatsThis( i18n("

Basket Tree

" "Here is the list of your baskets. " "You can organize your data by putting them in different baskets. " "You can group baskets by subject by creating new baskets inside others. " "You can browse between them by clicking a basket to open it, or reorganize them using drag and drop.")); setTreePlacement(Settings::treeOnLeft()); } void BNPView::setupActions() { QAction *a = nullptr; KActionCollection *ac = actionCollection(); a = ac->addAction("basket_export_basket_archive", this, SLOT(saveAsArchive())); a->setText(i18n("&Basket Archive...")); a->setIcon(QIcon::fromTheme("baskets")); a->setShortcut(0); m_actSaveAsArchive = a; a = ac->addAction("basket_import_basket_archive", this, SLOT(openArchive())); a->setText(i18n("&Basket Archive...")); a->setIcon(QIcon::fromTheme("baskets")); a->setShortcut(0); m_actOpenArchive = a; a = ac->addAction("window_hide", this, SLOT(hideOnEscape())); a->setText(i18n("&Hide Window")); m_actionCollection->setDefaultShortcut(a, KStandardShortcut::Close); m_actHideWindow = a; m_actHideWindow->setEnabled(Settings::useSystray()); // Init here ! a = ac->addAction("basket_export_html", this, SLOT(exportToHTML())); a->setText(i18n("&HTML Web Page...")); a->setIcon(QIcon::fromTheme("text-html")); a->setShortcut(0); m_actExportToHtml = a; a = ac->addAction("basket_import_text_file", this, &BNPView::importTextFile); a->setText(i18n("Text &File...")); a->setIcon(QIcon::fromTheme("text-plain")); a->setShortcut(0); a = ac->addAction("basket_backup_restore", this, SLOT(backupRestore())); a->setText(i18n("&Backup && Restore...")); a->setShortcut(0); a = ac->addAction("check_cleanup", this, SLOT(checkCleanup())); a->setText(i18n("&Check && Cleanup...")); a->setShortcut(0); if (Global::commandLineOpts->isSet("debug")) { a->setEnabled(true); } else { a->setEnabled(false); } /** Note : ****************************************************************/ a = ac->addAction("edit_delete", this, SLOT(delNote())); a->setText(i18n("D&elete")); a->setIcon(QIcon::fromTheme("edit-delete")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Delete")); m_actDelNote = a; m_actCutNote = ac->addAction(KStandardAction::Cut, this, SLOT(cutNote())); m_actCopyNote = ac->addAction(KStandardAction::Copy, this, SLOT(copyNote())); m_actSelectAll = ac->addAction(KStandardAction::SelectAll, this, SLOT(slotSelectAll())); m_actSelectAll->setStatusTip(i18n("Selects all notes")); a = ac->addAction("edit_unselect_all", this, SLOT(slotUnselectAll())); a->setText(i18n("U&nselect All")); m_actUnselectAll = a; m_actUnselectAll->setStatusTip(i18n("Unselects all selected notes")); a = ac->addAction("edit_invert_selection", this, SLOT(slotInvertSelection())); a->setText(i18n("&Invert Selection")); m_actionCollection->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Asterisk); m_actInvertSelection = a; m_actInvertSelection->setStatusTip(i18n("Inverts the current selection of notes")); a = ac->addAction("note_edit", this, SLOT(editNote())); a->setText(i18nc("Verb; not Menu", "&Edit...")); // a->setIcon(QIcon::fromTheme("edit")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Return")); m_actEditNote = a; m_actOpenNote = ac->addAction(KStandardAction::Open, "note_open", this, SLOT(openNote())); m_actOpenNote->setIcon(QIcon::fromTheme("window-new")); m_actOpenNote->setText(i18n("&Open")); m_actionCollection->setDefaultShortcut(m_actOpenNote, QKeySequence("F9")); a = ac->addAction("note_open_with", this, SLOT(openNoteWith())); a->setText(i18n("Open &With...")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Shift+F9")); m_actOpenNoteWith = a; m_actSaveNoteAs = ac->addAction(KStandardAction::SaveAs, "note_save_to_file", this, SLOT(saveNoteAs())); m_actSaveNoteAs->setText(i18n("&Save to File...")); m_actionCollection->setDefaultShortcut(m_actSaveNoteAs, QKeySequence("F10")); a = ac->addAction("note_group", this, SLOT(noteGroup())); a->setText(i18n("&Group")); a->setIcon(QIcon::fromTheme("mail-attachment")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+G")); m_actGroup = a; a = ac->addAction("note_ungroup", this, SLOT(noteUngroup())); a->setText(i18n("U&ngroup")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Shift+G")); m_actUngroup = a; a = ac->addAction("note_move_top", this, SLOT(moveOnTop())); a->setText(i18n("Move on &Top")); a->setIcon(QIcon::fromTheme("arrow-up-double")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Shift+Home")); m_actMoveOnTop = a; a = ac->addAction("note_move_up", this, SLOT(moveNoteUp())); a->setText(i18n("Move &Up")); a->setIcon(QIcon::fromTheme("arrow-up")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Shift+Up")); m_actMoveNoteUp = a; a = ac->addAction("note_move_down", this, SLOT(moveNoteDown())); a->setText(i18n("Move &Down")); a->setIcon(QIcon::fromTheme("arrow-down")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Shift+Down")); m_actMoveNoteDown = a; a = ac->addAction("note_move_bottom", this, SLOT(moveOnBottom())); a->setText(i18n("Move on &Bottom")); a->setIcon(QIcon::fromTheme("arrow-down-double")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Shift+End")); m_actMoveOnBottom = a; m_actPaste = ac->addAction(KStandardAction::Paste, this, SLOT(pasteInCurrentBasket())); /** Insert : **************************************************************/ #if 0 a = ac->addAction("insert_text"); a->setText(i18n("Plai&n Text")); a->setIcon(QIcon::fromTheme("text")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+T")); m_actInsertText = a; #endif a = ac->addAction("insert_html"); a->setText(i18n("&Text")); a->setIcon(QIcon::fromTheme("text-html")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Insert")); m_actInsertHtml = a; a = ac->addAction("insert_link"); a->setText(i18n("&Link")); a->setIcon(QIcon::fromTheme(IconNames::LINK)); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Y")); m_actInsertLink = a; a = ac->addAction("insert_cross_reference"); a->setText(i18n("Cross &Reference")); a->setIcon(QIcon::fromTheme(IconNames::CROSS_REF)); m_actInsertCrossReference = a; a = ac->addAction("insert_image"); a->setText(i18n("&Image")); a->setIcon(QIcon::fromTheme(IconNames::IMAGE)); m_actInsertImage = a; a = ac->addAction("insert_color"); a->setText(i18n("&Color")); a->setIcon(QIcon::fromTheme(IconNames::COLOR)); m_actInsertColor = a; a = ac->addAction("insert_launcher"); a->setText(i18n("L&auncher")); a->setIcon(QIcon::fromTheme(IconNames::LAUNCH)); m_actInsertLauncher = a; a = ac->addAction("insert_kmenu"); a->setText(i18n("Import Launcher for &desktop application...")); a->setIcon(QIcon::fromTheme(IconNames::KMENU)); m_actImportKMenu = a; a = ac->addAction("insert_icon"); a->setText(i18n("Im&port Icon...")); a->setIcon(QIcon::fromTheme(IconNames::ICONS)); m_actImportIcon = a; a = ac->addAction("insert_from_file"); a->setText(i18n("Load From &File...")); a->setIcon(QIcon::fromTheme(IconNames::DOCUMENT_IMPORT)); m_actLoadFile = a; // connect( m_actInsertText, QAction::triggered, this, [this] () { insertEmpty(NoteType::Text); }); connect(m_actInsertHtml, &QAction::triggered, this, [this] () { insertEmpty(NoteType::Html); }); connect(m_actInsertImage, &QAction::triggered, this, [this] () { insertEmpty(NoteType::Image); }); connect(m_actInsertLink, &QAction::triggered, this, [this] () { insertEmpty(NoteType::Link); }); connect(m_actInsertCrossReference, &QAction::triggered, this, [this] () { insertEmpty(NoteType::CrossReference); }); connect(m_actInsertColor, &QAction::triggered, this, [this] () { insertEmpty(NoteType::Color); }); connect(m_actInsertLauncher, &QAction::triggered, this, [this] () { insertEmpty(NoteType::Launcher); }); connect(m_actImportKMenu, &QAction::triggered, this, [this] () { insertWizard(1); }); connect(m_actImportIcon, &QAction::triggered, this, [this] () { insertWizard(2); }); connect(m_actLoadFile, &QAction::triggered, this, [this] () { insertWizard(3); }); a = ac->addAction("insert_screen_color", this, &BNPView::slotColorFromScreen); a->setText(i18n("C&olor from Screen")); a->setIcon(QIcon::fromTheme("kcolorchooser")); m_actColorPicker = a; connect(m_colorPicker.get(), &DesktopColorPicker::pickedColor, this, &BNPView::colorPicked); a = ac->addAction("insert_screen_capture", this, SLOT(grabScreenshot())); a->setText(i18n("Grab Screen &Zone")); a->setIcon(QIcon::fromTheme("ksnapshot")); m_actGrabScreenshot = a; //connect(m_actGrabScreenshot, SIGNAL(regionGrabbed(const QPixmap&)), this, SLOT(screenshotGrabbed(const QPixmap&))); //connect(m_colorPicker, SIGNAL(canceledPick()), this, SLOT(colorPickingCanceled())); // m_insertActions.append( m_actInsertText ); m_insertActions.append(m_actInsertHtml); m_insertActions.append(m_actInsertLink); m_insertActions.append(m_actInsertCrossReference); m_insertActions.append(m_actInsertImage); m_insertActions.append(m_actInsertColor); m_insertActions.append(m_actImportKMenu); m_insertActions.append(m_actInsertLauncher); m_insertActions.append(m_actImportIcon); m_insertActions.append(m_actLoadFile); m_insertActions.append(m_actColorPicker); m_insertActions.append(m_actGrabScreenshot); /** Basket : **************************************************************/ // At this stage, main.cpp has not set qApp->mainWidget(), so Global::runInsideKontact() // returns true. We do it ourself: bool runInsideKontact = true; QWidget *parentWidget = (QWidget *)parent(); while (parentWidget) { if (parentWidget->inherits("MainWindow")) runInsideKontact = false; parentWidget = (QWidget *)parentWidget->parent(); } // Use the "basket" icon in Kontact so it is consistent with the Kontact "New..." icon a = ac->addAction("basket_new", this, SLOT(askNewBasket())); a->setText(i18n("&New Basket...")); a->setIcon(QIcon::fromTheme((runInsideKontact ? "basket" : "document-new"))); m_actionCollection->setDefaultShortcuts(a, KStandardShortcut::shortcut(KStandardShortcut::New)); actNewBasket = a; a = ac->addAction("basket_new_sub", this, SLOT(askNewSubBasket())); a->setText(i18n("New &Sub-Basket...")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+Shift+N")); actNewSubBasket = a; a = ac->addAction("basket_new_sibling", this, SLOT(askNewSiblingBasket())); a->setText(i18n("New Si&bling Basket...")); actNewSiblingBasket = a; KActionMenu *newBasketMenu = new KActionMenu(i18n("&New"), ac); newBasketMenu->setIcon(QIcon::fromTheme("document-new")); ac->addAction("basket_new_menu", newBasketMenu); newBasketMenu->addAction(actNewBasket); newBasketMenu->addAction(actNewSubBasket); newBasketMenu->addAction(actNewSiblingBasket); connect(newBasketMenu, SIGNAL(triggered()), this, SLOT(askNewBasket())); a = ac->addAction("basket_properties", this, SLOT(propBasket())); a->setText(i18n("&Properties...")); a->setIcon(QIcon::fromTheme("document-properties")); m_actionCollection->setDefaultShortcut(a, QKeySequence("F2")); m_actPropBasket = a; a = ac->addAction("basket_sort_children_asc", this, SLOT(sortChildrenAsc())); a->setText(i18n("Sort Children Ascending")); a->setIcon(QIcon::fromTheme("view-sort-ascending")); m_actSortChildrenAsc = a; a = ac->addAction("basket_sort_children_desc", this, SLOT(sortChildrenDesc())); a->setText(i18n("Sort Children Descending")); a->setIcon(QIcon::fromTheme("view-sort-descending")); m_actSortChildrenDesc = a; a = ac->addAction("basket_sort_siblings_asc", this, SLOT(sortSiblingsAsc())); a->setText(i18n("Sort Siblings Ascending")); a->setIcon(QIcon::fromTheme("view-sort-ascending")); m_actSortSiblingsAsc = a; a = ac->addAction("basket_sort_siblings_desc", this, SLOT(sortSiblingsDesc())); a->setText(i18n("Sort Siblings Descending")); a->setIcon(QIcon::fromTheme("view-sort-descending")); m_actSortSiblingsDesc = a; a = ac->addAction("basket_remove", this, SLOT(delBasket())); a->setText(i18nc("Remove Basket", "&Remove")); a->setShortcut(0); m_actDelBasket = a; #ifdef HAVE_LIBGPGME a = ac->addAction("basket_password", this, SLOT(password())); a->setText(i18nc("Password protection", "Pass&word...")); a->setShortcut(0); m_actPassBasket = a; a = ac->addAction("basket_lock", this, SLOT(lockBasket())); a->setText(i18nc("Lock Basket", "&Lock")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+L")); m_actLockBasket = a; #endif /** Edit : ****************************************************************/ // m_actUndo = KStandardAction::undo( this, SLOT(undo()), actionCollection() ); // m_actUndo->setEnabled(false); // Not yet implemented ! // m_actRedo = KStandardAction::redo( this, SLOT(redo()), actionCollection() ); // m_actRedo->setEnabled(false); // Not yet implemented ! KToggleAction *toggleAct = nullptr; toggleAct = new KToggleAction(i18n("&Filter"), ac); ac->addAction("edit_filter", toggleAct); toggleAct->setIcon(QIcon::fromTheme("view-filter")); m_actionCollection->setDefaultShortcuts(toggleAct, KStandardShortcut::shortcut(KStandardShortcut::Find)); m_actShowFilter = toggleAct; connect(m_actShowFilter, SIGNAL(toggled(bool)), this, SLOT(showHideFilterBar(bool))); toggleAct = new KToggleAction(ac); ac->addAction("edit_filter_all_baskets", toggleAct); toggleAct->setText(i18n("&Search All")); toggleAct->setIcon(QIcon::fromTheme("edit-find")); m_actionCollection->setDefaultShortcut(toggleAct, QKeySequence("Ctrl+Shift+F")); m_actFilterAllBaskets = toggleAct; connect(m_actFilterAllBaskets, &KToggleAction::toggled, this, &BNPView::toggleFilterAllBaskets); a = ac->addAction("edit_filter_reset", this, SLOT(slotResetFilter())); a->setText(i18n("&Reset Filter")); a->setIcon(QIcon::fromTheme("edit-clear-locationbar-rtl")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Ctrl+R")); m_actResetFilter = a; /** Go : ******************************************************************/ a = ac->addAction("go_basket_previous", this, SLOT(goToPreviousBasket())); a->setText(i18n("&Previous Basket")); a->setIcon(QIcon::fromTheme("go-previous")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Alt+Left")); m_actPreviousBasket = a; a = ac->addAction("go_basket_next", this, SLOT(goToNextBasket())); a->setText(i18n("&Next Basket")); a->setIcon(QIcon::fromTheme("go-next")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Alt+Right")); m_actNextBasket = a; a = ac->addAction("go_basket_fold", this, SLOT(foldBasket())); a->setText(i18n("&Fold Basket")); a->setIcon(QIcon::fromTheme("go-up")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Alt+Up")); m_actFoldBasket = a; a = ac->addAction("go_basket_expand", this, SLOT(expandBasket())); a->setText(i18n("&Expand Basket")); a->setIcon(QIcon::fromTheme("go-down")); m_actionCollection->setDefaultShortcut(a, QKeySequence("Alt+Down")); m_actExpandBasket = a; #if 0 // FOR_BETA_PURPOSE: a = ac->addAction("beta_convert_texts", this, SLOT(convertTexts())); a->setText(i18n("Convert text notes to rich text notes")); a->setIcon(QIcon::fromTheme("run-build-file")); m_convertTexts = a; #endif InlineEditors::instance()->initToolBars(actionCollection()); /** Help : ****************************************************************/ a = ac->addAction("help_welcome_baskets", this, SLOT(addWelcomeBaskets())); a->setText(i18n("&Welcome Baskets")); } BasketListViewItem *BNPView::topLevelItem(int i) { return (BasketListViewItem *)m_tree->topLevelItem(i); } void BNPView::slotShowProperties(QTreeWidgetItem *item) { if (item) propBasket(); } void BNPView::slotContextMenu(const QPoint &pos) { QTreeWidgetItem *item; item = m_tree->itemAt(pos); QString menuName; if (item) { BasketScene *basket = ((BasketListViewItem *)item)->basket(); setCurrentBasket(basket); menuName = "basket_popup"; } else { menuName = "tab_bar_popup"; /* * "File -> New" create a new basket with the same parent basket as the current one. * But when invoked when right-clicking the empty area at the bottom of the basket tree, * it is obvious the user want to create a new basket at the bottom of the tree (with no parent). * So we set a temporary variable during the time the popup menu is shown, * so the slot askNewBasket() will do the right thing: */ setNewBasketPopup(); } QMenu *menu = popupMenu(menuName); connect(menu, &QMenu::aboutToHide, this, &BNPView::aboutToHideNewBasketPopup); menu->exec(m_tree->mapToGlobal(pos)); } /* this happens every time we switch the basket (but not if we tell the user we save the stuff */ void BNPView::save() { DEBUG_WIN << "Basket Tree: Saving..."; QString data; QXmlStreamWriter stream(&data); XMLWork::setupXmlStream(stream, "basketTree"); // Save Basket Tree: save(m_tree, nullptr, stream); stream.writeEndElement(); stream.writeEndDocument(); // Write to Disk: - BasketScene::safelySaveToFile(Global::basketsFolder() + "baskets.xml", data); + FileStorage::safelySaveToFile(Global::basketsFolder() + "baskets.xml", data); GitWrapper::commitBasketView(); } void BNPView::save(QTreeWidget *listView, QTreeWidgetItem *item, QXmlStreamWriter &stream) { if (item == nullptr) { if (listView == nullptr) { // This should not happen: we call either save(listView, 0) or save(0, item) DEBUG_WIN << "BNPView::save error: listView=NULL and item=NULL"; return; } // For each basket: for (int i = 0; i < listView->topLevelItemCount(); i++) { item = listView->topLevelItem(i); save(nullptr, item, stream); } } else { saveSubHierarchy(item, stream, true); } } void BNPView::writeBasketElement(QTreeWidgetItem *item, QXmlStreamWriter &stream) { BasketScene *basket = ((BasketListViewItem *)item)->basket(); // Save Attributes: stream.writeAttribute("folderName", basket->folderName()); if (item->childCount() >= 0) // If it can be expanded/folded: stream.writeAttribute("folded", XMLWork::trueOrFalse(!item->isExpanded())); if (((BasketListViewItem *)item)->isCurrentBasket()) stream.writeAttribute("lastOpened", "true"); basket->saveProperties(stream); } void BNPView::saveSubHierarchy(QTreeWidgetItem *item, QXmlStreamWriter &stream, bool recursive) { stream.writeStartElement("basket"); writeBasketElement(item, stream); // create root if (recursive) { for (int i = 0; i < item->childCount(); i++) { saveSubHierarchy(item->child(i), stream, true); } } stream.writeEndElement(); } void BNPView::load() { QScopedPointer doc(XMLWork::openFile("basketTree", Global::basketsFolder() + "baskets.xml")); // BEGIN Compatibility with 0.6.0 Pre-Alpha versions: if (!doc) doc.reset(XMLWork::openFile("basketsTree", Global::basketsFolder() + "baskets.xml")); // END if (doc != nullptr) { QDomElement docElem = doc->documentElement(); load(nullptr, docElem); } m_loading = false; } void BNPView::load(QTreeWidgetItem *item, const QDomElement &baskets) { QDomNode n = baskets.firstChild(); while (!n.isNull()) { QDomElement element = n.toElement(); if ((!element.isNull()) && element.tagName() == "basket") { QString folderName = element.attribute("folderName"); if (!folderName.isEmpty()) { BasketScene *basket = loadBasket(folderName); BasketListViewItem *basketItem = appendBasket(basket, item); basketItem->setExpanded(!XMLWork::trueOrFalse(element.attribute("folded", "false"), false)); basket->loadProperties(XMLWork::getElement(element, "properties")); if (XMLWork::trueOrFalse(element.attribute("lastOpened", element.attribute("lastOpened", "false")), false)) // Compat with 0.6.0-Alphas setCurrentBasket(basket); // Load Sub-baskets: load(basketItem, element); } } n = n.nextSibling(); } } BasketScene *BNPView::loadBasket(const QString &folderName) { if (folderName.isEmpty()) return nullptr; DecoratedBasket *decoBasket = new DecoratedBasket(m_stack, folderName); BasketScene *basket = decoBasket->basket(); m_stack->addWidget(decoBasket); connect(basket, &BasketScene::countsChanged, this, &BNPView::countsChanged); // Important: Create listViewItem and connect signal BEFORE loadProperties(), so we get the listViewItem updated without extra work: connect(basket, &BasketScene::propertiesChanged, this, &BNPView::updateBasketListViewItem); connect(basket->decoration()->filterBar(), &FilterBar::newFilter, this, &BNPView::newFilterFromFilterBar); connect(basket, &BasketScene::crossReference, this, &BNPView::loadCrossReference); return basket; } int BNPView::basketCount(QTreeWidgetItem *parent) { int count = 1; if (parent == nullptr) return 0; for (int i = 0; i < parent->childCount(); i++) { count += basketCount(parent->child(i)); } return count; } bool BNPView::canFold() { BasketListViewItem *item = listViewItemForBasket(currentBasket()); if (!item) return false; return (item->childCount() > 0 && item->isExpanded()); } bool BNPView::canExpand() { BasketListViewItem *item = listViewItemForBasket(currentBasket()); if (!item) return false; return (item->childCount() > 0 && !item->isExpanded()); } BasketListViewItem *BNPView::appendBasket(BasketScene *basket, QTreeWidgetItem *parentItem) { BasketListViewItem *newBasketItem; if (parentItem) newBasketItem = new BasketListViewItem(parentItem, parentItem->child(parentItem->childCount() - 1), basket); else { newBasketItem = new BasketListViewItem(m_tree, m_tree->topLevelItem(m_tree->topLevelItemCount() - 1), basket); } return newBasketItem; } void BNPView::loadNewBasket(const QString &folderName, const QDomElement &properties, BasketScene *parent) { BasketScene *basket = loadBasket(folderName); appendBasket(basket, (basket ? listViewItemForBasket(parent) : nullptr)); basket->loadProperties(properties); setCurrentBasketInHistory(basket); // save(); } int BNPView::topLevelItemCount() { return m_tree->topLevelItemCount(); } void BNPView::goToPreviousBasket() { if (m_history->canUndo()) m_history->undo(); } void BNPView::goToNextBasket() { if (m_history->canRedo()) m_history->redo(); } void BNPView::foldBasket() { BasketListViewItem *item = listViewItemForBasket(currentBasket()); if (item && item->childCount() <= 0) item->setExpanded(false); // If Alt+Left is hit and there is nothing to close, make sure the focus will go to the parent basket QKeyEvent *keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Left, nullptr, nullptr); QApplication::postEvent(m_tree, keyEvent); } void BNPView::expandBasket() { QKeyEvent *keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Right, nullptr, nullptr); QApplication::postEvent(m_tree, keyEvent); } void BNPView::closeAllEditors() { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = (BasketListViewItem *)(*it); item->basket()->closeEditor(); ++it; } } bool BNPView::convertTexts() { bool convertedNotes = false; QProgressDialog dialog; dialog.setWindowTitle(i18n("Plain Text Notes Conversion")); dialog.setLabelText(i18n("Converting plain text notes to rich text ones...")); dialog.setModal(true); dialog.setRange(0, basketCount()); dialog.show(); // setMinimumDuration(50/*ms*/); QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = (BasketListViewItem *)(*it); if (item->basket()->convertTexts()) convertedNotes = true; dialog.setValue(dialog.value() + 1); if (dialog.wasCanceled()) break; ++it; } return convertedNotes; } void BNPView::toggleFilterAllBaskets(bool doFilter) { // If the filter isn't already showing, we make sure it does. if (doFilter) m_actShowFilter->setChecked(true); // currentBasket()->decoration()->filterBar()->setFilterAll(doFilter); if (doFilter) currentBasket()->decoration()->filterBar()->setEditFocus(); // Filter every baskets: newFilter(); } /** This function can be called recursively because we call qApp->processEvents(). * If this function is called whereas another "instance" is running, * this new "instance" leave and set up a flag that is read by the first "instance" * to know it should re-begin the work. * PS: Yes, that's a very lame pseudo-threading but that works, and it's programmer-efforts cheap :-) */ void BNPView::newFilter() { static bool alreadyEntered = false; static bool shouldRestart = false; if (alreadyEntered) { shouldRestart = true; return; } alreadyEntered = true; shouldRestart = false; BasketScene *current = currentBasket(); const FilterData &filterData = current->decoration()->filterBar()->filterData(); // Set the filter data for every other baskets, or reset the filter for every other baskets if we just disabled the filterInAllBaskets: QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); if (item->basket() != current) { if (isFilteringAllBaskets()) item->basket()->decoration()->filterBar()->setFilterData(filterData); // Set the new FilterData for every other baskets else item->basket()->decoration()->filterBar()->setFilterData(FilterData()); // We just disabled the global filtering: remove the FilterData } ++it; } // Show/hide the "little filter icons" (during basket load) // or the "little numbers" (to show number of found notes in the baskets) is the tree: qApp->processEvents(); // Load every baskets for filtering, if they are not already loaded, and if necessary: if (filterData.isFiltering) { BasketScene *current = currentBasket(); QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); if (item->basket() != current) { BasketScene *basket = item->basket(); if (!basket->loadingLaunched() && !basket->isLocked()) basket->load(); basket->filterAgain(); qApp->processEvents(); if (shouldRestart) { alreadyEntered = false; shouldRestart = false; newFilter(); return; } } ++it; } } // qApp->processEvents(); m_tree->viewport()->update(); // to see the "little numbers" alreadyEntered = false; shouldRestart = false; } void BNPView::newFilterFromFilterBar() { if (isFilteringAllBaskets()) QTimer::singleShot(0, this, SLOT(newFilter())); // Keep time for the QLineEdit to display the filtered character and refresh correctly! } bool BNPView::isFilteringAllBaskets() { return m_actFilterAllBaskets->isChecked(); } BasketListViewItem *BNPView::listViewItemForBasket(BasketScene *basket) { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); if (item->basket() == basket) return item; ++it; } return nullptr; } BasketScene *BNPView::currentBasket() { DecoratedBasket *decoBasket = (DecoratedBasket *)m_stack->currentWidget(); if (decoBasket) return decoBasket->basket(); else return nullptr; } BasketScene *BNPView::parentBasketOf(BasketScene *basket) { BasketListViewItem *item = (BasketListViewItem *)(listViewItemForBasket(basket)->parent()); if (item) return item->basket(); else return nullptr; } void BNPView::setCurrentBasketInHistory(BasketScene *basket) { if (!basket) return; if (currentBasket() == basket) return; m_history->push(new HistorySetBasket(basket)); } void BNPView::setCurrentBasket(BasketScene *basket) { if (currentBasket() == basket) return; if (currentBasket()) currentBasket()->closeBasket(); if (basket) basket->aboutToBeActivated(); BasketListViewItem *item = listViewItemForBasket(basket); if (item) { m_tree->setCurrentItem(item); item->ensureVisible(); m_stack->setCurrentWidget(basket->decoration()); // If the window has changed size, only the current basket receive the event, // the others will receive ony one just before they are shown. // But this triggers unwanted animations, so we eliminate it: basket->relayoutNotes(); basket->openBasket(); setWindowTitle(item->basket()->basketName()); countsChanged(basket); updateStatusBarHint(); if (Global::systemTray) Global::systemTray->updateDisplay(); m_tree->scrollToItem(m_tree->currentItem()); item->basket()->setFocus(); } m_tree->viewport()->update(); Q_EMIT basketChanged(); } void BNPView::removeBasket(BasketScene *basket) { if (basket->isDuringEdit()) basket->closeEditor(); // Find a new basket to switch to and select it. // Strategy: get the next sibling, or the previous one if not found. // If there is no such one, get the parent basket: BasketListViewItem *basketItem = listViewItemForBasket(basket); BasketListViewItem *nextBasketItem = (BasketListViewItem *)(m_tree->itemBelow(basketItem)); if (!nextBasketItem) nextBasketItem = (BasketListViewItem *)m_tree->itemAbove(basketItem); if (!nextBasketItem) nextBasketItem = (BasketListViewItem *)(basketItem->parent()); if (nextBasketItem) setCurrentBasketInHistory(nextBasketItem->basket()); // Remove from the view: basket->unsubscribeBackgroundImages(); m_stack->removeWidget(basket->decoration()); // delete basket->decoration(); delete basketItem; // delete basket; // If there is no basket anymore, add a new one: if (!nextBasketItem) { BasketFactory::newBasket(QString(), i18n("General")); } else { // No need to save two times if we add a basket save(); } } void BNPView::setTreePlacement(bool onLeft) { if (onLeft) insertWidget(0, m_tree); else addWidget(m_tree); // updateGeometry(); qApp->postEvent(this, new QResizeEvent(size(), size())); } void BNPView::relayoutAllBaskets() { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); // item->basket()->unbufferizeAll(); item->basket()->unsetNotesWidth(); item->basket()->relayoutNotes(); ++it; } } void BNPView::recomputeAllStyles() { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); item->basket()->recomputeAllStyles(); item->basket()->unsetNotesWidth(); item->basket()->relayoutNotes(); ++it; } } void BNPView::removedStates(const QList &deletedStates) { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); item->basket()->removedStates(deletedStates); ++it; } } void BNPView::linkLookChanged() { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); item->basket()->linkLookChanged(); ++it; } } void BNPView::filterPlacementChanged(bool onTop) { QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = static_cast(*it); DecoratedBasket *decoration = static_cast(item->basket()->parent()); decoration->setFilterBarPosition(onTop); ++it; } } void BNPView::updateBasketListViewItem(BasketScene *basket) { BasketListViewItem *item = listViewItemForBasket(basket); if (item) item->setup(); if (basket == currentBasket()) { setWindowTitle(basket->basketName()); if (Global::systemTray) Global::systemTray->updateDisplay(); } // Don't save if we are loading! if (!m_loading) save(); } void BNPView::needSave(QTreeWidgetItem *) { if (!m_loading) // A basket has been collapsed/expanded or a new one is select: this is not urgent: QTimer::singleShot(500 /*ms*/, this, SLOT(save())); } void BNPView::slotPressed(QTreeWidgetItem *item, int column) { Q_UNUSED(column); BasketScene *basket = currentBasket(); if (basket == nullptr) return; // Impossible to Select no Basket: if (!item) m_tree->setCurrentItem(listViewItemForBasket(basket), true); else if (dynamic_cast(item) != nullptr && currentBasket() != ((BasketListViewItem *)item)->basket()) { setCurrentBasketInHistory(((BasketListViewItem *)item)->basket()); needSave(nullptr); } basket->graphicsView()->viewport()->setFocus(); } DecoratedBasket *BNPView::currentDecoratedBasket() { if (currentBasket()) return currentBasket()->decoration(); else return nullptr; } // Redirected actions : void BNPView::exportToHTML() { HTMLExporter exporter(currentBasket()); } void BNPView::editNote() { currentBasket()->noteEdit(); } void BNPView::cutNote() { currentBasket()->noteCut(); } void BNPView::copyNote() { currentBasket()->noteCopy(); } void BNPView::delNote() { currentBasket()->noteDelete(); } void BNPView::openNote() { currentBasket()->noteOpen(); } void BNPView::openNoteWith() { currentBasket()->noteOpenWith(); } void BNPView::saveNoteAs() { currentBasket()->noteSaveAs(); } void BNPView::noteGroup() { currentBasket()->noteGroup(); } void BNPView::noteUngroup() { currentBasket()->noteUngroup(); } void BNPView::moveOnTop() { currentBasket()->noteMoveOnTop(); } void BNPView::moveOnBottom() { currentBasket()->noteMoveOnBottom(); } void BNPView::moveNoteUp() { currentBasket()->noteMoveNoteUp(); } void BNPView::moveNoteDown() { currentBasket()->noteMoveNoteDown(); } void BNPView::slotSelectAll() { currentBasket()->selectAll(); } void BNPView::slotUnselectAll() { currentBasket()->unselectAll(); } void BNPView::slotInvertSelection() { currentBasket()->invertSelection(); } void BNPView::slotResetFilter() { currentDecoratedBasket()->resetFilter(); } void BNPView::importTextFile() { SoftwareImporters::importTextFile(); } void BNPView::backupRestore() { BackupDialog dialog; dialog.exec(); } void checkNote(Note *note, QList &fileList) { while (note) { note->finishLazyLoad(); if (note->isGroup()) { checkNote(note->firstChild(), fileList); } else if (note->content()->useFile()) { QString noteFileName = note->basket()->folderName() + note->content()->fileName(); int basketFileIndex = fileList.indexOf(noteFileName); if (basketFileIndex < 0) { DEBUG_WIN << "" + noteFileName + " NOT FOUND!"; } else { fileList.removeAt(basketFileIndex); } } note = note->next(); } } void checkBasket(BasketListViewItem *item, QList &dirList, QList &fileList) { BasketScene *basket = ((BasketListViewItem *)item)->basket(); QString basketFolderName = basket->folderName(); int basketFolderIndex = dirList.indexOf(basket->folderName()); if (basketFolderIndex < 0) { DEBUG_WIN << "" + basketFolderName + " NOT FOUND!"; } else { dirList.removeAt(basketFolderIndex); } int basketFileIndex = fileList.indexOf(basket->folderName() + ".basket"); if (basketFileIndex < 0) { DEBUG_WIN << ".basket file of " + basketFolderName + ".basket NOT FOUND!"; } else { fileList.removeAt(basketFileIndex); } if (!basket->loadingLaunched() && !basket->isLocked()) { basket->load(); } DEBUG_WIN << "\t********************************************************************************"; DEBUG_WIN << basket->basketName() << "(" << basketFolderName << ") loaded."; Note *note = basket->firstNote(); if (!note) { DEBUG_WIN << "\tHas NO notes!"; } else { checkNote(note, fileList); } basket->save(); qApp->processEvents(QEventLoop::ExcludeUserInputEvents, 100); for (int i = 0; i < item->childCount(); i++) { checkBasket((BasketListViewItem *)item->child(i), dirList, fileList); } if (basket != Global::bnpView->currentBasket()) { DEBUG_WIN << basket->basketName() << "(" << basketFolderName << ") unloading..."; DEBUG_WIN << "\t********************************************************************************"; basket->unbufferizeAll(); } else { DEBUG_WIN << basket->basketName() << "(" << basketFolderName << ") is the current basket, not unloading."; DEBUG_WIN << "\t********************************************************************************"; } qApp->processEvents(QEventLoop::ExcludeUserInputEvents, 100); } void BNPView::checkCleanup() { DEBUG_WIN << "Starting the check, cleanup and reindexing... (" + Global::basketsFolder() + ')'; QList dirList; QList fileList; QString topDirEntry; QString subDirEntry; QFileInfo fileInfo; QDir topDir(Global::basketsFolder(), QString(), QDir::Name | QDir::IgnoreCase, QDir::TypeMask | QDir::Hidden); Q_FOREACH (topDirEntry, topDir.entryList()) { if (topDirEntry != QLatin1String(".") && topDirEntry != QLatin1String("..")) { fileInfo.setFile(Global::basketsFolder() + '/' + topDirEntry); if (fileInfo.isDir()) { dirList << topDirEntry + '/'; QDir basketDir(Global::basketsFolder() + '/' + topDirEntry, QString(), QDir::Name | QDir::IgnoreCase, QDir::TypeMask | QDir::Hidden); Q_FOREACH (subDirEntry, basketDir.entryList()) { if (subDirEntry != "." && subDirEntry != "..") { fileList << topDirEntry + '/' + subDirEntry; } } } else if (topDirEntry != "." && topDirEntry != ".." && topDirEntry != "baskets.xml") { fileList << topDirEntry; } } } DEBUG_WIN << "Directories found: " + QString::number(dirList.count()); DEBUG_WIN << "Files found: " + QString::number(fileList.count()); DEBUG_WIN << "Checking Baskets:"; for (int i = 0; i < topLevelItemCount(); i++) { checkBasket(topLevelItem(i), dirList, fileList); } DEBUG_WIN << "Baskets checked."; DEBUG_WIN << "Directories remaining (not in any basket): " + QString::number(dirList.count()); DEBUG_WIN << "Files remaining (not in any basket): " + QString::number(fileList.count()); Q_FOREACH (topDirEntry, dirList) { DEBUG_WIN << "" + topDirEntry + " does not belong to any basket!"; // Tools::deleteRecursively(Global::basketsFolder() + '/' + topDirEntry); // DEBUG_WIN << "\t" + topDirEntry + " removed!"; Tools::trashRecursively(Global::basketsFolder() + "/" + topDirEntry); DEBUG_WIN << "\t" + topDirEntry + " trashed!"; Q_FOREACH (subDirEntry, fileList) { fileInfo.setFile(Global::basketsFolder() + '/' + subDirEntry); if (!fileInfo.isFile()) { fileList.removeAll(subDirEntry); DEBUG_WIN << "\t\t" + subDirEntry + " already removed!"; } } } Q_FOREACH (subDirEntry, fileList) { DEBUG_WIN << "" + subDirEntry + " does not belong to any note!"; // Tools::deleteRecursively(Global::basketsFolder() + '/' + subDirEntry); // DEBUG_WIN << "\t" + subDirEntry + " removed!"; Tools::trashRecursively(Global::basketsFolder() + '/' + subDirEntry); DEBUG_WIN << "\t" + subDirEntry + " trashed!"; } DEBUG_WIN << "Check, cleanup and reindexing completed"; } void BNPView::countsChanged(BasketScene *basket) { if (basket == currentBasket()) notesStateChanged(); } void BNPView::notesStateChanged() { BasketScene *basket = currentBasket(); // Update statusbar message : if (currentBasket()->isLocked()) setSelectionStatus(i18n("Locked")); else if (!basket->isLoaded()) setSelectionStatus(i18n("Loading...")); else if (basket->count() == 0) setSelectionStatus(i18n("No notes")); else { QString count = i18np("%1 note", "%1 notes", basket->count()); QString selecteds = i18np("%1 selected", "%1 selected", basket->countSelecteds()); QString showns = (currentDecoratedBasket()->filterData().isFiltering ? i18n("all matches") : i18n("no filter")); if (basket->countFounds() != basket->count()) showns = i18np("%1 match", "%1 matches", basket->countFounds()); setSelectionStatus(i18nc("e.g. '18 notes, 10 matches, 5 selected'", "%1, %2, %3", count, showns, selecteds)); } if (currentBasket()->redirectEditActions()) { m_actSelectAll->setEnabled(!currentBasket()->selectedAllTextInEditor()); m_actUnselectAll->setEnabled(currentBasket()->hasSelectedTextInEditor()); } else { m_actSelectAll->setEnabled(basket->countSelecteds() < basket->countFounds()); m_actUnselectAll->setEnabled(basket->countSelecteds() > 0); } m_actInvertSelection->setEnabled(basket->countFounds() > 0); updateNotesActions(); } void BNPView::updateNotesActions() { bool isLocked = currentBasket()->isLocked(); bool oneSelected = currentBasket()->countSelecteds() == 1; bool oneOrSeveralSelected = currentBasket()->countSelecteds() >= 1; bool severalSelected = currentBasket()->countSelecteds() >= 2; // FIXME: m_actCheckNotes is also modified in void BNPView::areSelectedNotesCheckedChanged(bool checked) // bool BasketScene::areSelectedNotesChecked() should return false if bool BasketScene::showCheckBoxes() is false // m_actCheckNotes->setChecked( oneOrSeveralSelected && // currentBasket()->areSelectedNotesChecked() && // currentBasket()->showCheckBoxes() ); Note *selectedGroup = (severalSelected ? currentBasket()->selectedGroup() : nullptr); m_actEditNote->setEnabled(!isLocked && oneSelected && !currentBasket()->isDuringEdit()); if (currentBasket()->redirectEditActions()) { m_actCutNote->setEnabled(currentBasket()->hasSelectedTextInEditor()); m_actCopyNote->setEnabled(currentBasket()->hasSelectedTextInEditor()); m_actPaste->setEnabled(true); m_actDelNote->setEnabled(currentBasket()->hasSelectedTextInEditor()); } else { m_actCutNote->setEnabled(!isLocked && oneOrSeveralSelected); m_actCopyNote->setEnabled(oneOrSeveralSelected); m_actPaste->setEnabled(!isLocked); m_actDelNote->setEnabled(!isLocked && oneOrSeveralSelected); } m_actOpenNote->setEnabled(oneOrSeveralSelected); m_actOpenNoteWith->setEnabled(oneSelected); // TODO: oneOrSeveralSelected IF SAME TYPE m_actSaveNoteAs->setEnabled(oneSelected); // IDEM? m_actGroup->setEnabled(!isLocked && severalSelected && (!selectedGroup || selectedGroup->isColumn())); m_actUngroup->setEnabled(!isLocked && selectedGroup && !selectedGroup->isColumn()); m_actMoveOnTop->setEnabled(!isLocked && oneOrSeveralSelected && !currentBasket()->isFreeLayout()); m_actMoveNoteUp->setEnabled(!isLocked && oneOrSeveralSelected); // TODO: Disable when unavailable! m_actMoveNoteDown->setEnabled(!isLocked && oneOrSeveralSelected); m_actMoveOnBottom->setEnabled(!isLocked && oneOrSeveralSelected && !currentBasket()->isFreeLayout()); for (QList::const_iterator action = m_insertActions.constBegin(); action != m_insertActions.constEnd(); ++action) (*action)->setEnabled(!isLocked); // From the old Note::contextMenuEvent(...) : /* if (useFile() || m_type == Link) { m_type == Link ? i18n("&Open target") : i18n("&Open") m_type == Link ? i18n("Open target &with...") : i18n("Open &with...") m_type == Link ? i18n("&Save target as...") : i18n("&Save a copy as...") // If useFile() there is always a file to open / open with / save, but : if (m_type == Link) { if (url().toDisplayString().isEmpty() && runCommand().isEmpty()) // no URL nor runCommand : popupMenu->setItemEnabled(7, false); // no possible Open ! if (url().toDisplayString().isEmpty()) // no URL : popupMenu->setItemEnabled(8, false); // no possible Open with ! if (url().toDisplayString().isEmpty() || url().path().endsWith("/")) // no URL or target a folder : popupMenu->setItemEnabled(9, false); // not possible to save target file } } else if (m_type != Color) { popupMenu->insertSeparator(); popupMenu->insertItem( QIcon::fromTheme("document-save-as"), i18n("&Save a copy as..."), this, SLOT(slotSaveAs()), 0, 10 ); }*/ } // BEGIN Color picker (code from KColorEdit): /* Activate the mode */ void BNPView::slotColorFromScreen(bool global) { m_colorPickWasGlobal = global; currentBasket()->saveInsertionData(); m_colorPicker->pickColor(); } void BNPView::slotColorFromScreenGlobal() { slotColorFromScreen(true); } void BNPView::colorPicked(const QColor &color) { if (!currentBasket()->isLoaded()) { showPassiveLoading(currentBasket()); currentBasket()->load(); } currentBasket()->insertColor(color); if (Settings::usePassivePopup()) { showPassiveDropped(i18n("Picked color to basket %1")); } } void BNPView::slotConvertTexts() { /* int result = KMessageBox::questionYesNoCancel( this, i18n( "

This will convert every text notes into rich text notes.
" "The content of the notes will not change and you will be able to apply formatting to those notes.

" "

This process cannot be reverted back: you will not be able to convert the rich text notes to plain text ones later.

" "

As a beta-tester, you are strongly encouraged to do the convert process because it is to test if plain text notes are still needed.
" "If nobody complain about not having plain text notes anymore, then the final version is likely to not support plain text notes anymore.

" "

Which basket notes do you want to convert?

" ), i18n("Convert Text Notes"), KGuiItem(i18n("Only in the Current Basket")), KGuiItem(i18n("In Every Baskets")) ); if (result == KMessageBox::Cancel) return; */ bool conversionsDone; // if (result == KMessageBox::Yes) // conversionsDone = currentBasket()->convertTexts(); // else conversionsDone = convertTexts(); if (conversionsDone) KMessageBox::information(this, i18n("The plain text notes have been converted to rich text."), i18n("Conversion Finished")); else KMessageBox::information(this, i18n("There are no plain text notes to convert."), i18n("Conversion Finished")); } QMenu *BNPView::popupMenu(const QString &menuName) { QMenu *menu = nullptr; if (m_guiClient) { qDebug() << "m_guiClient"; KXMLGUIFactory *factory = m_guiClient->factory(); if (factory) { menu = (QMenu *)factory->container(menuName, m_guiClient); } } if (menu == nullptr) { QString basketDataPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/basket/"; KMessageBox::error(this, i18n("

The file basketui.rc seems to not exist or is too old.
" "%1 cannot run without it and will stop.

" "

Please check your installation of %2.

" "

If you do not have administrator access to install the application " "system wide, you can copy the file basketui.rc from the installation " "archive to the folder %4.

" "

As last resort, if you are sure the application is correctly installed " "but you had a preview version of it, try to remove the " "file %5basketui.rc

", QGuiApplication::applicationDisplayName(), QGuiApplication::applicationDisplayName(), basketDataPath, basketDataPath, basketDataPath), i18n("Resource not Found"), KMessageBox::AllowLink); exit(1); // We SHOULD exit right now and aboard everything because the caller except menu != 0 to not crash. } return menu; } void BNPView::showHideFilterBar(bool show, bool switchFocus) { // if (show != m_actShowFilter->isChecked()) // m_actShowFilter->setChecked(show); m_actShowFilter->setChecked(show); currentDecoratedBasket()->setFilterBarVisible(show, switchFocus); if (!show) currentDecoratedBasket()->resetFilter(); } void BNPView::insertEmpty(int type) { if (currentBasket()->isLocked()) { showPassiveImpossible(i18n("Cannot add note.")); return; } currentBasket()->insertEmptyNote(type); } void BNPView::insertWizard(int type) { if (currentBasket()->isLocked()) { showPassiveImpossible(i18n("Cannot add note.")); return; } currentBasket()->insertWizard(type); } // BEGIN Screen Grabbing: void BNPView::grabScreenshot(bool global) { if (m_regionGrabber) { KWindowSystem::activateWindow(m_regionGrabber->winId()); return; } // Delay before to take a screenshot because if we hide the main window OR the systray popup menu, // we should wait the windows below to be repainted!!! // A special case is where the action is triggered with the global keyboard shortcut. // In this case, global is true, and we don't wait. // In the future, if global is also defined for other cases, check for // enum QAction::ActivationReason { UnknownActivation, EmulatedActivation, AccelActivation, PopupMenuActivation, ToolBarActivation }; int delay = (isMainWindowActive() ? 500 : (global /*qApp->activePopupWidget()*/ ? 0 : 200)); m_colorPickWasGlobal = global; hideMainWindow(); currentBasket()->saveInsertionData(); usleep(delay * 1000); m_regionGrabber = new RegionGrabber; connect(m_regionGrabber, &RegionGrabber::regionGrabbed, this, &BNPView::screenshotGrabbed); } void BNPView::hideMainWindow() { if (isMainWindowActive()) { if (Global::activeMainWindow()) { m_HiddenMainWindow = Global::activeMainWindow(); m_HiddenMainWindow->hide(); } m_colorPickWasShown = true; } else m_colorPickWasShown = false; } void BNPView::grabScreenshotGlobal() { grabScreenshot(true); } void BNPView::screenshotGrabbed(const QPixmap &pixmap) { delete m_regionGrabber; m_regionGrabber = nullptr; // Cancelled (pressed Escape): if (pixmap.isNull()) { if (m_colorPickWasShown) showMainWindow(); return; } if (!currentBasket()->isLoaded()) { showPassiveLoading(currentBasket()); currentBasket()->load(); } currentBasket()->insertImage(pixmap); if (m_colorPickWasShown) showMainWindow(); if (Settings::usePassivePopup()) showPassiveDropped(i18n("Grabbed screen zone to basket %1")); } BasketScene *BNPView::basketForFolderName(const QString &folderName) { /* QPtrList basketsList = listBaskets(); BasketScene *basket; for (basket = basketsList.first(); basket; basket = basketsList.next()) if (basket->folderName() == folderName) return basket; */ QString name = folderName; if (!name.endsWith('/')) name += '/'; QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); if (item->basket()->folderName() == name) return item->basket(); ++it; } return nullptr; } Note *BNPView::noteForFileName(const QString &fileName, BasketScene &basket, Note *note) { if (!note) note = basket.firstNote(); if (note->fullPath().endsWith(fileName)) return note; Note *child = note->firstChild(); Note *found; while (child) { found = noteForFileName(fileName, basket, child); if (found) return found; child = child->next(); } return nullptr; } void BNPView::setFiltering(bool filtering) { m_actShowFilter->setChecked(filtering); m_actResetFilter->setEnabled(filtering); if (!filtering) m_actFilterAllBaskets->setEnabled(false); } void BNPView::undo() { // TODO } void BNPView::redo() { // TODO } void BNPView::pasteToBasket(int /*index*/, QClipboard::Mode /*mode*/) { // TODO: REMOVE! // basketAt(index)->pasteNote(mode); } void BNPView::propBasket() { BasketPropertiesDialog dialog(currentBasket(), this); dialog.exec(); } void BNPView::delBasket() { // DecoratedBasket *decoBasket = currentDecoratedBasket(); BasketScene *basket = currentBasket(); int really = KMessageBox::questionYesNo(this, i18n("Do you really want to remove the basket %1 and its contents?", Tools::textToHTMLWithoutP(basket->basketName())), i18n("Remove Basket"), KGuiItem(i18n("&Remove Basket"), "edit-delete"), KStandardGuiItem::cancel()); if (really == KMessageBox::No) return; QStringList basketsList = listViewItemForBasket(basket)->childNamesTree(0); if (basketsList.count() > 0) { int deleteChilds = KMessageBox::questionYesNoList(this, i18n("%1 has the following children baskets.
Do you want to remove them too?
", Tools::textToHTMLWithoutP(basket->basketName())), basketsList, i18n("Remove Children Baskets"), KGuiItem(i18n("&Remove Children Baskets"), "edit-delete")); if (deleteChilds == KMessageBox::No) return; } QString basketFolderName = basket->folderName(); doBasketDeletion(basket); GitWrapper::commitDeleteBasket(basketFolderName); } void BNPView::doBasketDeletion(BasketScene *basket) { basket->closeEditor(); QTreeWidgetItem *basketItem = listViewItemForBasket(basket); for (int i = 0; i < basketItem->childCount(); i++) { // First delete the child baskets: doBasketDeletion(((BasketListViewItem *)basketItem->child(i))->basket()); } // Then, basket have no child anymore, delete it: DecoratedBasket *decoBasket = basket->decoration(); basket->deleteFiles(); removeBasket(basket); // Remove the action to avoid keyboard-shortcut clashes: delete basket->m_action; // FIXME: It's quick&dirty. In the future, the Basket should be deleted, and then the QAction deleted in the Basket destructor. delete decoBasket; // delete basket; } void BNPView::password() { #ifdef HAVE_LIBGPGME QPointer dlg = new PasswordDlg(qApp->activeWindow()); BasketScene *cur = currentBasket(); dlg->setType(cur->encryptionType()); dlg->setKey(cur->encryptionKey()); if (dlg->exec()) { cur->setProtection(dlg->type(), dlg->key()); if (cur->encryptionType() != BasketScene::NoEncryption) { // Clear metadata Tools::deleteMetadataRecursively(cur->fullPath()); cur->lock(); } } #endif } void BNPView::lockBasket() { #ifdef HAVE_LIBGPGME BasketScene *cur = currentBasket(); cur->lock(); #endif } void BNPView::saveAsArchive() { BasketScene *basket = currentBasket(); QDir dir; KConfigGroup config = KSharedConfig::openConfig()->group("Basket Archive"); QString folder = config.readEntry("lastFolder", QDir::homePath()) + "/"; QString url = folder + QString(basket->basketName()).replace('/', '_') + ".baskets"; QString filter = "*.baskets|" + i18n("Basket Archives") + "\n*|" + i18n("All Files"); QString destination = url; for (bool askAgain = true; askAgain;) { destination = QFileDialog::getSaveFileName(nullptr, i18n("Save as Basket Archive"), destination, filter); if (destination.isEmpty()) // User canceled return; if (dir.exists(destination)) { int result = KMessageBox::questionYesNoCancel( this, "" + 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; } bool withSubBaskets = true; // KMessageBox::questionYesNo(this, i18n("Do you want to export sub-baskets too?"), i18n("Save as Basket Archive")) == KMessageBox::Yes; config.writeEntry("lastFolder", QUrl::fromLocalFile(destination).adjusted(QUrl::RemoveFilename).path()); config.sync(); Archive::save(basket, withSubBaskets, destination); } QString BNPView::s_fileToOpen; void BNPView::delayedOpenArchive() { Archive::open(s_fileToOpen); } QString BNPView::s_basketToOpen; void BNPView::delayedOpenBasket() { BasketScene *bv = this->basketForFolderName(s_basketToOpen); this->setCurrentBasketInHistory(bv); } void BNPView::openArchive() { QString filter = QStringLiteral("*.baskets|") + i18n("Basket Archives") + QStringLiteral("\n*|") + i18n("All Files"); QString path = QFileDialog::getOpenFileName(this, i18n("Open Basket Archive"), QString(), filter); if (!path.isEmpty()) { // User has not canceled Archive::open(path); } } void BNPView::activatedTagShortcut() { Tag *tag = Tag::tagForKAction((QAction *)sender()); currentBasket()->activatedTagShortcut(tag); } void BNPView::slotBasketChanged() { m_actFoldBasket->setEnabled(canFold()); m_actExpandBasket->setEnabled(canExpand()); if (currentBasket()->decoration()->filterData().isFiltering) currentBasket()->decoration()->filterBar()->show(); // especially important for Filter all setFiltering(currentBasket() && currentBasket()->decoration()->filterData().isFiltering); this->canUndoRedoChanged(); } void BNPView::canUndoRedoChanged() { if (m_history) { m_actPreviousBasket->setEnabled(m_history->canUndo()); m_actNextBasket->setEnabled(m_history->canRedo()); } } void BNPView::currentBasketChanged() { } void BNPView::isLockedChanged() { bool isLocked = currentBasket()->isLocked(); setLockStatus(isLocked); // m_actLockBasket->setChecked(isLocked); m_actPropBasket->setEnabled(!isLocked); m_actDelBasket->setEnabled(!isLocked); updateNotesActions(); } void BNPView::askNewBasket() { askNewBasket(nullptr, nullptr); GitWrapper::commitCreateBasket(); } void BNPView::askNewBasket(BasketScene *parent, BasketScene *pickProperties) { NewBasketDefaultProperties properties; if (pickProperties) { properties.icon = pickProperties->icon(); properties.backgroundImage = pickProperties->backgroundImageName(); properties.backgroundColor = pickProperties->backgroundColorSetting(); properties.textColor = pickProperties->textColorSetting(); properties.freeLayout = pickProperties->isFreeLayout(); properties.columnCount = pickProperties->columnsCount(); } NewBasketDialog(parent, properties, this).exec(); } void BNPView::askNewSubBasket() { askNewBasket(/*parent=*/currentBasket(), /*pickPropertiesOf=*/currentBasket()); } void BNPView::askNewSiblingBasket() { askNewBasket(/*parent=*/parentBasketOf(currentBasket()), /*pickPropertiesOf=*/currentBasket()); } void BNPView::globalPasteInCurrentBasket() { currentBasket()->setInsertPopupMenu(); pasteInCurrentBasket(); currentBasket()->cancelInsertPopupMenu(); } void BNPView::pasteInCurrentBasket() { currentBasket()->pasteNote(); if (Settings::usePassivePopup()) showPassiveDropped(i18n("Clipboard content pasted to basket %1")); } void BNPView::pasteSelInCurrentBasket() { currentBasket()->pasteNote(QClipboard::Selection); if (Settings::usePassivePopup()) showPassiveDropped(i18n("Selection pasted to basket %1")); } void BNPView::showPassiveDropped(const QString &title) { if (!currentBasket()->isLocked()) { // TODO: Keep basket, so that we show the message only if something was added to a NOT visible basket m_passiveDroppedTitle = title; m_passiveDroppedSelection = currentBasket()->selectedNotes(); QTimer::singleShot(c_delayTooltipTime, this, SLOT(showPassiveDroppedDelayed())); // DELAY IT BELOW: } else showPassiveImpossible(i18n("No note was added.")); } void BNPView::showPassiveDroppedDelayed() { if (isMainWindowActive() || m_passiveDroppedSelection == nullptr) return; QString title = m_passiveDroppedTitle; QImage contentsImage = NoteDrag::feedbackPixmap(m_passiveDroppedSelection).toImage(); QResource::registerResource(contentsImage.bits(), QStringLiteral(":/images/passivepopup_image")); if (Settings::useSystray()) { /*Uncomment after switching to QSystemTrayIcon or port to KStatusNotifierItem See also other occurrences of Global::systemTray below*/ /*KPassivePopup::message(KPassivePopup::Boxed, title.arg(Tools::textToHTMLWithoutP(currentBasket()->basketName())), (contentsImage.isNull() ? QString() : QStringLiteral("")), KIconLoader::global()->loadIcon( currentBasket()->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), 0L, true ), Global::systemTray);*/ } else { KPassivePopup::message(KPassivePopup::Boxed, title.arg(Tools::textToHTMLWithoutP(currentBasket()->basketName())), (contentsImage.isNull() ? QString() : QStringLiteral("")), KIconLoader::global()->loadIcon(currentBasket()->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, true), (QWidget *)this); } } void BNPView::showPassiveImpossible(const QString &message) { if (Settings::useSystray()) { /*KPassivePopup::message(KPassivePopup::Boxed, QString("%1") .arg(i18n("Basket %1 is locked")) .arg(Tools::textToHTMLWithoutP(currentBasket()->basketName())), message, KIconLoader::global()->loadIcon( currentBasket()->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), 0L, true ), Global::systemTray);*/ } else { /*KPassivePopup::message(KPassivePopup::Boxed, QString("%1") .arg(i18n("Basket %1 is locked")) .arg(Tools::textToHTMLWithoutP(currentBasket()->basketName())), message, KIconLoader::global()->loadIcon( currentBasket()->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), 0L, true ), (QWidget*)this);*/ } } void BNPView::showPassiveContentForced() { showPassiveContent(/*forceShow=*/true); } void BNPView::showPassiveContent(bool forceShow /* = false*/) { if (!forceShow && isMainWindowActive()) return; // FIXME: Duplicate code (2 times) QString message; if (Settings::useSystray()) { /*KPassivePopup::message(KPassivePopup::Boxed, "" + Tools::makeStandardCaption( currentBasket()->isLocked() ? QString("%1 %2") .arg(Tools::textToHTMLWithoutP(currentBasket()->basketName()), i18n("(Locked)")) : Tools::textToHTMLWithoutP(currentBasket()->basketName()) ), message, KIconLoader::global()->loadIcon( currentBasket()->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), 0L, true ), Global::systemTray);*/ } else { KPassivePopup::message(KPassivePopup::Boxed, "" + Tools::makeStandardCaption(currentBasket()->isLocked() ? QString("%1 %2").arg(Tools::textToHTMLWithoutP(currentBasket()->basketName()), i18n("(Locked)")) : Tools::textToHTMLWithoutP(currentBasket()->basketName())), message, KIconLoader::global()->loadIcon(currentBasket()->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, true), (QWidget *)this); } } void BNPView::showPassiveLoading(BasketScene *basket) { if (isMainWindowActive()) return; if (Settings::useSystray()) { /*KPassivePopup::message(KPassivePopup::Boxed, Tools::textToHTMLWithoutP(basket->basketName()), i18n("Loading..."), KIconLoader::global()->loadIcon( basket->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), 0L, true ), Global::systemTray);*/ } else { KPassivePopup::message(KPassivePopup::Boxed, Tools::textToHTMLWithoutP(basket->basketName()), i18n("Loading..."), KIconLoader::global()->loadIcon(basket->icon(), KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, true), (QWidget *)this); } } void BNPView::addNoteText() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Text); } void BNPView::addNoteHtml() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Html); } void BNPView::addNoteImage() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Image); } void BNPView::addNoteLink() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Link); } void BNPView::addNoteCrossReference() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::CrossReference); } void BNPView::addNoteColor() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Color); } void BNPView::aboutToHideNewBasketPopup() { QTimer::singleShot(0, this, SLOT(cancelNewBasketPopup())); } void BNPView::cancelNewBasketPopup() { m_newBasketPopup = false; } void BNPView::setNewBasketPopup() { m_newBasketPopup = true; } void BNPView::setWindowTitle(QString s) { Q_EMIT setWindowCaption(s); } void BNPView::updateStatusBarHint() { m_statusbar->updateStatusBarHint(); } void BNPView::setSelectionStatus(QString s) { m_statusbar->setSelectionStatus(s); } void BNPView::setLockStatus(bool isLocked) { m_statusbar->setLockStatus(isLocked); } void BNPView::postStatusbarMessage(const QString &msg) { m_statusbar->postStatusbarMessage(msg); } void BNPView::setStatusBarHint(const QString &hint) { m_statusbar->setStatusBarHint(hint); } void BNPView::setUnsavedStatus(bool isUnsaved) { m_statusbar->setUnsavedStatus(isUnsaved); } void BNPView::setActive(bool active) { KMainWindow *win = Global::activeMainWindow(); if (!win) return; if (active == isMainWindowActive()) return; // qApp->updateUserTimestamp(); // If "activate on mouse hovering systray", or "on drag through systray" Global::systemTray->activate(); } void BNPView::hideOnEscape() { if (Settings::useSystray()) setActive(false); } bool BNPView::isMainWindowActive() { KMainWindow *main = Global::activeMainWindow(); if (main && main->isActiveWindow()) return true; return false; } void BNPView::newBasket() { askNewBasket(); } bool BNPView::createNoteHtml(const QString content, const QString basket) { BasketScene *b = basketForFolderName(basket); if (!b) return false; Note *note = NoteFactory::createNoteHtml(content, b); if (!note) return false; b->insertCreatedNote(note); return true; } bool BNPView::changeNoteHtml(const QString content, const QString basket, const QString noteName) { BasketScene *b = basketForFolderName(basket); if (!b) return false; Note *note = noteForFileName(noteName, *b); if (!note || note->content()->type() != NoteType::Html) return false; HtmlContent *noteContent = (HtmlContent *)note->content(); noteContent->setHtml(content); note->saveAgain(); return true; } bool BNPView::createNoteFromFile(const QString url, const QString basket) { BasketScene *b = basketForFolderName(basket); if (!b) return false; QUrl kurl(url); if (url.isEmpty()) return false; Note *n = NoteFactory::copyFileAndLoad(kurl, b); if (!n) return false; b->insertCreatedNote(n); return true; } QStringList BNPView::listBaskets() { QStringList basketList; QTreeWidgetItemIterator it(m_tree); while (*it) { BasketListViewItem *item = ((BasketListViewItem *)*it); basketList.append(item->basket()->basketName()); basketList.append(item->basket()->folderName()); ++it; } return basketList; } void BNPView::handleCommandLine() { QCommandLineParser *parser = Global::commandLineOpts; /* Custom data folder */ QString customDataFolder = parser->value("data-folder"); if (!customDataFolder.isNull() && !customDataFolder.isEmpty()) { Global::setCustomSavesFolder(customDataFolder); } /* Debug window */ if (parser->isSet("debug")) { new DebugWindow(); Global::debugWindow->show(); } /* Crash Handler to Mail Developers when Crashing: */ #ifndef BASKET_USE_DRKONQI if (!parser->isSet("use-drkonqi")) KCrash::setCrashHandler(Crash::crashHandler); #endif } void BNPView::reloadBasket(const QString &folderName) { basketForFolderName(folderName)->reload(); } /** Scenario of "Hide main window to system tray icon when mouse move out of the window" : * - At enterEvent() we stop m_tryHideTimer * - After that and before next, we are SURE cursor is hovering window * - At leaveEvent() we restart m_tryHideTimer * - Every 'x' ms, timeoutTryHide() seek if cursor hover a widget of the application or not * - If yes, we musn't hide the window * - But if not, we start m_hideTimer to hide main window after a configured elapsed time * - timeoutTryHide() continue to be called and if cursor move again to one widget of the app, m_hideTimer is stopped * - If after the configured time cursor hasn't go back to a widget of the application, timeoutHide() is called * - It then hide the main window to systray icon * - When the user will show it, enterEvent() will be called the first time he enter mouse to it * - ... */ /** Why do as this ? Problems with the use of only enterEvent() and leaveEvent() : * - Resize window or hover titlebar isn't possible : leave/enterEvent * are * > Use the grip or Alt+rightDND to resize window * > Use Alt+DND to move window * - Each menu trigger the leavEvent */ void BNPView::enterEvent(QEvent *) { if (m_tryHideTimer) m_tryHideTimer->stop(); if (m_hideTimer) m_hideTimer->stop(); } void BNPView::leaveEvent(QEvent *) { if (Settings::useSystray() && Settings::hideOnMouseOut() && m_tryHideTimer) m_tryHideTimer->start(50); } void BNPView::timeoutTryHide() { // If a menu is displayed, do nothing for the moment if (qApp->activePopupWidget() != nullptr) return; if (qApp->widgetAt(QCursor::pos()) != nullptr) m_hideTimer->stop(); else if (!m_hideTimer->isActive()) { // Start only one time m_hideTimer->setSingleShot(true); m_hideTimer->start(Settings::timeToHideOnMouseOut() * 100); } // If a subdialog is opened, we mustn't hide the main window: if (qApp->activeWindow() != nullptr && qApp->activeWindow() != Global::activeMainWindow()) m_hideTimer->stop(); } void BNPView::timeoutHide() { // We check that because the setting can have been set to off if (Settings::useSystray() && Settings::hideOnMouseOut()) setActive(false); m_tryHideTimer->stop(); } void BNPView::changedSelectedNotes() { // tabChanged(0); // FIXME: NOT OPTIMIZED } /*void BNPView::areSelectedNotesCheckedChanged(bool checked) { m_actCheckNotes->setChecked(checked && currentBasket()->showCheckBoxes()); }*/ void BNPView::enableActions() { BasketScene *basket = currentBasket(); if (!basket) return; if (m_actLockBasket) m_actLockBasket->setEnabled(!basket->isLocked() && basket->isEncrypted()); if (m_actPassBasket) m_actPassBasket->setEnabled(!basket->isLocked()); m_actPropBasket->setEnabled(!basket->isLocked()); m_actDelBasket->setEnabled(!basket->isLocked()); m_actExportToHtml->setEnabled(!basket->isLocked()); m_actShowFilter->setEnabled(!basket->isLocked()); m_actFilterAllBaskets->setEnabled(!basket->isLocked()); m_actResetFilter->setEnabled(!basket->isLocked()); basket->decoration()->filterBar()->setEnabled(!basket->isLocked()); } void BNPView::showMainWindow() { if (m_HiddenMainWindow) { m_HiddenMainWindow->show(); m_HiddenMainWindow = nullptr; } else { KMainWindow *win = Global::activeMainWindow(); if (win) { win->show(); } } setActive(true); Q_EMIT showPart(); } void BNPView::populateTagsMenu() { QMenu *menu = (QMenu *)(popupMenu("tags")); if (menu == nullptr || currentBasket() == nullptr) // TODO: Display a messagebox. [menu is 0, surely because on first launch, the XMLGUI does not work!] return; menu->clear(); Note *referenceNote; if (currentBasket()->focusedNote() && currentBasket()->focusedNote()->isSelected()) referenceNote = currentBasket()->focusedNote(); else referenceNote = currentBasket()->firstSelected(); populateTagsMenu(*menu, referenceNote); m_lastOpenedTagsMenu = menu; //connect(menu, &QMenu::aboutToHide, this, &BNPView::disconnectTagsMenu); } void BNPView::populateTagsMenu(QMenu &menu, Note *referenceNote) { if (currentBasket() == nullptr) return; currentBasket()->m_tagPopupNote = referenceNote; bool enable = currentBasket()->countSelecteds() > 0; QList::iterator it; Tag *currentTag; State *currentState; int i = 10; for (it = Tag::all.begin(); it != Tag::all.end(); ++it) { // Current tag and first state of it: currentTag = *it; currentState = currentTag->states().first(); QKeySequence sequence; if (!currentTag->shortcut().isEmpty()) sequence = currentTag->shortcut(); StateAction *mi = new StateAction(currentState, QKeySequence(sequence), this, true); // The previously set ID will be set in the actions themselves as data. mi->setData(i); if (referenceNote && referenceNote->hasTag(currentTag)) mi->setChecked(true); menu.addAction(mi); if (!currentTag->shortcut().isEmpty()) m_actionCollection->setDefaultShortcut(mi, sequence); mi->setEnabled(enable); ++i; } menu.addSeparator(); // I don't like how this is implemented; but I can't think of a better way // to do this, so I will have to leave it for now QAction *act = new QAction(i18n("&Assign new Tag..."), &menu); act->setData(1); act->setEnabled(enable); menu.addAction(act); act = new QAction(QIcon::fromTheme("edit-delete"), i18n("&Remove All"), &menu); act->setData(2); if (!currentBasket()->selectedNotesHaveTags()) act->setEnabled(false); menu.addAction(act); act = new QAction(QIcon::fromTheme("configure"), i18n("&Customize..."), &menu); act->setData(3); menu.addAction(act); connect(&menu, &QMenu::triggered, currentBasket(), &BasketScene::toggledTagInMenu); connect(&menu, &QMenu::aboutToHide, currentBasket(), &BasketScene::unlockHovering); connect(&menu, &QMenu::aboutToHide, currentBasket(), &BasketScene::disableNextClick); } void BNPView::connectTagsMenu() { connect(popupMenu("tags"), &QMenu::aboutToShow, this, [this]() { this->populateTagsMenu(); }); connect(popupMenu("tags"), &QMenu::aboutToHide, this, &BNPView::disconnectTagsMenu); } void BNPView::showEvent(QShowEvent *) { if (m_firstShow) { m_firstShow = false; onFirstShow(); } } void BNPView::disconnectTagsMenu() { QTimer::singleShot(0, this, SLOT(disconnectTagsMenuDelayed())); } void BNPView::disconnectTagsMenuDelayed() { disconnect(m_lastOpenedTagsMenu, SIGNAL(triggered(QAction *)), currentBasket(), SLOT(toggledTagInMenu(QAction *))); disconnect(m_lastOpenedTagsMenu, SIGNAL(aboutToHide()), currentBasket(), SLOT(unlockHovering())); disconnect(m_lastOpenedTagsMenu, SIGNAL(aboutToHide()), currentBasket(), SLOT(disableNextClick())); } void BNPView::loadCrossReference(QString link) { // remove "basket://" and any encoding. QString folderName = link.mid(9, link.length() - 9); folderName = QUrl::fromPercentEncoding(folderName.toUtf8()); BasketScene *basket = this->basketForFolderName(folderName); if (!basket) return; this->setCurrentBasketInHistory(basket); } QString BNPView::folderFromBasketNameLink(QStringList pages, QTreeWidgetItem *parent) { QString found; QString page = pages.first(); page = QUrl::fromPercentEncoding(page.toUtf8()); pages.removeFirst(); if (page == "..") { QTreeWidgetItem *p; if (parent) p = parent->parent(); else p = m_tree->currentItem()->parent(); found = this->folderFromBasketNameLink(pages, p); } else if (!parent && page.isEmpty()) { parent = m_tree->invisibleRootItem(); found = this->folderFromBasketNameLink(pages, parent); } else { if (!parent && (page == "." || !page.isEmpty())) { parent = m_tree->currentItem(); } QRegExp re(":\\{([0-9]+)\\}"); re.setMinimal(true); int pos = 0; pos = re.indexIn(page, pos); int basketNum = 1; if (pos != -1) basketNum = re.cap(1).toInt(); page = page.left(page.length() - re.matchedLength()); for (int i = 0; i < parent->childCount(); i++) { QTreeWidgetItem *child = parent->child(i); if (child->text(0).toLower() == page.toLower()) { basketNum--; if (basketNum == 0) { if (pages.count() > 0) { found = this->folderFromBasketNameLink(pages, child); break; } else { found = ((BasketListViewItem *)child)->basket()->folderName(); break; } } } else found = QString(); } } return found; } void BNPView::sortChildrenAsc() { m_tree->currentItem()->sortChildren(0, Qt::AscendingOrder); } void BNPView::sortChildrenDesc() { m_tree->currentItem()->sortChildren(0, Qt::DescendingOrder); } void BNPView::sortSiblingsAsc() { QTreeWidgetItem *parent = m_tree->currentItem()->parent(); if (!parent) m_tree->sortItems(0, Qt::AscendingOrder); else parent->sortChildren(0, Qt::AscendingOrder); } void BNPView::sortSiblingsDesc() { QTreeWidgetItem *parent = m_tree->currentItem()->parent(); if (!parent) m_tree->sortItems(0, Qt::DescendingOrder); else parent->sortChildren(0, Qt::DescendingOrder); } diff --git a/src/common.cpp b/src/common.cpp new file mode 100644 index 0000000..b346ba3 --- /dev/null +++ b/src/common.cpp @@ -0,0 +1,174 @@ +/** + * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "common.h" + +#include +#include +#include +#include +#include + +#include "diskerrordialog.h" + + +bool FileStorage::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 FileStorage::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 FileStorage::saveToFile(const QString &fullPath, const QString &string, bool isEncrypted) +{ + QByteArray array = string.toUtf8(); + return saveToFile(fullPath, array, isEncrypted); +} + +bool FileStorage::saveToFile(const QString &fullPath, const QByteArray &array, bool isEncrypted) +{ + 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. + */ +bool FileStorage::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(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 +} + +bool FileStorage::safelySaveToFile(const QString &fullPath, const QString &string) +{ + QByteArray bytes = string.toUtf8(); + return safelySaveToFile(fullPath, bytes, bytes.length()); +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..ab46392 --- /dev/null +++ b/src/common.h @@ -0,0 +1,24 @@ +/** + * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** Common methods extracted here to simplify the dependency between classes + */ + +#ifndef BASKET_COMMON_H +#define BASKET_COMMON_H + +class QByteArray; +class QString; + +namespace FileStorage { + bool loadFromFile(const QString &fullPath, QString *string); + bool loadFromFile(const QString &fullPath, QByteArray *array); + bool saveToFile(const QString &fullPath, const QString &string, bool isEncrypted = false); + bool saveToFile(const QString &fullPath, const QByteArray &array, bool isEncrypted = false); //[Encrypt and] save binary content + bool safelySaveToFile(const QString &fullPath, const QByteArray &array, unsigned long length); + bool safelySaveToFile(const QString &fullPath, const QString &string); +} + +#endif diff --git a/src/diskerrordialog.cpp b/src/diskerrordialog.cpp index cfe96cf..c9cb08e 100644 --- a/src/diskerrordialog.cpp +++ b/src/diskerrordialog.cpp @@ -1,58 +1,58 @@ /** * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #include "diskerrordialog.h" #include #include #include #include #include #include #include #include #include #include -DiskErrorDialog::DiskErrorDialog(const QString &titleMessage, const QString &message, QWidget *parent) +DiskErrorDialog::DiskErrorDialog(const QString &message, QWidget *parent) : QDialog(parent) { setObjectName("DiskError"); setWindowTitle(i18n("Save Error")); // enableButtonCancel(false); // enableButtonClose(false); // enableButton(Close, false); // okButton->setEnabled(false); setModal(true); // QHBoxLayout *layout = new QHBoxLayout(mainWidget(), /*margin=*/0, spacingHint()); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QHBoxLayout *layout = new QHBoxLayout(mainWidget); QPixmap icon = KIconLoader::global()->loadIcon("drive-harddisk", KIconLoader::NoGroup, 64, KIconLoader::DefaultState, QStringList(), /*path_store=*/nullptr, /*canReturnNull=*/true); QLabel *iconLabel = new QLabel(mainWidget); iconLabel->setPixmap(icon); iconLabel->setFixedSize(iconLabel->sizeHint()); - QLabel *label = new QLabel("

" + titleMessage + "

" + message + "

", mainWidget); + QLabel *label = new QLabel("

" + i18n("Error while saving") + "

" + message + "

", mainWidget); if (!icon.isNull()) layout->addWidget(iconLabel); layout->addWidget(label); } DiskErrorDialog::~DiskErrorDialog() { } void DiskErrorDialog::closeEvent(QCloseEvent *event) { event->ignore(); } void DiskErrorDialog::keyPressEvent(QKeyEvent *) { // Escape should not close the window... } diff --git a/src/diskerrordialog.h b/src/diskerrordialog.h index 8959476..2f53582 100644 --- a/src/diskerrordialog.h +++ b/src/diskerrordialog.h @@ -1,30 +1,30 @@ /** * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût * SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef DISKERRORDIALOG_H #define DISKERRORDIALOG_H #include class QCloseEvent; class QKeyEvent; /** Provide a dialog to avert the user the disk is full. * This dialog is modal and is shown until the user has made space on the disk. * @author Sébastien Laoût */ class DiskErrorDialog : public QDialog { Q_OBJECT public: - DiskErrorDialog(const QString &titleMessage, const QString &message, QWidget *parent = nullptr); + DiskErrorDialog(const QString &message, QWidget *parent = nullptr); ~DiskErrorDialog() override; protected: void closeEvent(QCloseEvent *event) override; void keyPressEvent(QKeyEvent *) override; }; #endif // DISKERRORDIALOG_H diff --git a/src/htmlexporter.cpp b/src/htmlexporter.cpp index b8a4428..7461060 100644 --- a/src/htmlexporter.cpp +++ b/src/htmlexporter.cpp @@ -1,528 +1,529 @@ /** * 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 "common.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 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 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); + bool success = FileStorage::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 d1a586c..887a72c 100644 --- a/src/note.cpp +++ b/src/note.cpp @@ -1,2524 +1,2525 @@ /** * 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 "common.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 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); } 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); + FileStorage::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/notecontent.cpp b/src/notecontent.cpp index 6d6f4a1..a0cd9fc 100644 --- a/src/notecontent.cpp +++ b/src/notecontent.cpp @@ -1,2501 +1,2502 @@ /** * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "notecontent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //For m_simpleRichText->documentLayout() #include //For QPixmap::createHeuristicMask() #include #include #include #include #include #include #include #include #include #include #include //For KIO::file_preview(...) #include #include #include #include #include "basketscene.h" +#include "common.h" #include "config.h" #include "debugwindow.h" #include "file_metadata.h" #include "filter.h" #include "global.h" #include "htmlexporter.h" #include "note.h" #include "notefactory.h" #include "settings.h" #include "tools.h" #include "xmlwork.h" /** * LinkDisplayItem definition * */ QRectF LinkDisplayItem::boundingRect() const { if (m_note) { return QRect(0, 0, m_note->width() - m_note->contentX() - Note::NOTE_MARGIN, m_note->height() - 2 * Note::NOTE_MARGIN); } return QRectF(); } void LinkDisplayItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (!m_note) return; QRectF rect = boundingRect(); m_linkDisplay.paint(painter, 0, 0, rect.width(), rect.height(), m_note->palette(), true, m_note->isSelected(), m_note->hovered(), m_note->hovered() && m_note->hoveredZone() == Note::Custom0); } //** NoteType functions QString NoteType::typeToName(const NoteType::Id noteType) { switch (noteType) { case NoteType::Group: return i18n("Group"); case NoteType::Text: return i18n("Plain Text"); case NoteType::Html: return i18n("Text"); case NoteType::Image: return i18n("Image"); case NoteType::Animation: return i18n("Animation"); case NoteType::Sound: return i18n("Sound"); case NoteType::File: return i18n("File"); case NoteType::Link: return i18n("Link"); case NoteType::CrossReference: return i18n("Cross Reference"); case NoteType::Launcher: return i18n("Launcher"); case NoteType::Color: return i18n("Color"); case NoteType::Unknown: return i18n("Unknown"); } return i18n("Unknown"); } QString NoteType::typeToLowerName(const NoteType::Id noteType) { switch (noteType) { case NoteType::Group: return "group"; case NoteType::Text: return "text"; case NoteType::Html: return "html"; case NoteType::Image: return "image"; case NoteType::Animation: return "animation"; case NoteType::Sound: return "sound"; case NoteType::File: return "file"; case NoteType::Link: return "link"; case NoteType::CrossReference: return "cross_reference"; case NoteType::Launcher: return "launcher"; case NoteType::Color: return "color"; case NoteType::Unknown: return "unknown"; } return "unknown"; } NoteType::Id NoteType::typeFromLowerName(const QString& lowerTypeName) { if (lowerTypeName == "group") { return NoteType::Group; } else if (lowerTypeName == "text") { return NoteType::Text; } else if (lowerTypeName == "html") { return NoteType::Html; } else if (lowerTypeName == "image") { return NoteType::Image; } else if (lowerTypeName == "animation") { return NoteType::Animation; } else if (lowerTypeName == "sound") { return NoteType::Sound; } else if (lowerTypeName == "file") { return NoteType::File; } else if (lowerTypeName == "link") { return NoteType::Link; } else if (lowerTypeName == "cross_reference") { return NoteType::CrossReference; } else if (lowerTypeName == "launcher") { return NoteType::Launcher; } else if (lowerTypeName == "color") { return NoteType::Color; } else if (lowerTypeName == "unknown") { return NoteType::Unknown; } return NoteType::Unknown; } /** class NoteContent: */ const int NoteContent::FEEDBACK_DARKING = 105; NoteContent::NoteContent(Note *parent, const NoteType::Id type, const QString &fileName) : m_type(type) , m_note(parent) { if (parent) { parent->setContent(this); } setFileName(fileName); } void NoteContent::saveToNode(QXmlStreamWriter &stream) { if (useFile()) { stream.writeStartElement("content"); stream.writeCharacters(fileName()); stream.writeEndElement(); } } QRectF NoteContent::zoneRect(int zone, const QPointF & /*pos*/) { if (zone == Note::Content) return QRectF(0, 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else return QRectF(); } QUrl NoteContent::urlToOpen(bool /*with*/) { return (useFile() ? QUrl::fromLocalFile(fullPath()) : QUrl()); } void NoteContent::setFileName(const QString &fileName) { m_fileName = fileName; } bool NoteContent::trySetFileName(const QString &fileName) { if (useFile() && fileName != m_fileName) { QString newFileName = Tools::fileNameForNewFile(fileName, basket()->fullPath()); QDir dir; dir.rename(fullPath(), basket()->fullPathForFileName(newFileName)); return true; } return false; // !useFile() or unsuccessful rename } QString NoteContent::fullPath() { if (note() && useFile()) return note()->fullPath(); else return QString(); } void NoteContent::contentChanged(qreal newMinWidth) { m_minWidth = newMinWidth; if (note()) { // note()->unbufferize(); note()->requestRelayout(); // TODO: It should re-set the width! m_width = 0 ? contentChanged: setWidth, geteight, if size havent changed, only repaint and not relayout } } BasketScene *NoteContent::basket() { if (note()) return note()->basket(); else return nullptr; } void NoteContent::setEdited() { note()->setLastModificationDate(QDateTime::currentDateTime()); basket()->save(); } /** All the Content Classes: */ QString NoteContent::toText(const QString &cuttedFullPath) { return (cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } QString TextContent::toText(const QString & /*cuttedFullPath*/) { return text(); } QString HtmlContent::toText(const QString & /*cuttedFullPath*/) { return Tools::htmlToText(html()); } QString LinkContent::toText(const QString & /*cuttedFullPath*/) { if (autoTitle()) return url().toDisplayString(); else if (title().isEmpty() && url().isEmpty()) return QString(); else if (url().isEmpty()) return title(); else if (title().isEmpty()) return url().toDisplayString(); else return QString("%1 <%2>").arg(title(), url().toDisplayString()); } QString CrossReferenceContent::toText(const QString & /*cuttedFullPath*/) { if (title().isEmpty() && url().isEmpty()) return QString(); else if (url().isEmpty()) return title(); else if (title().isEmpty()) return url().toDisplayString(); else return QString("%1 <%2>").arg(title(), url().toDisplayString()); } QString ColorContent::toText(const QString & /*cuttedFullPath*/) { return color().name(); } QString UnknownContent::toText(const QString & /*cuttedFullPath*/) { return QString(); } // TODO: If imageName.isEmpty() return fullPath() because it's for external use, else return fileName() because it's to display in a tooltip QString TextContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return Tools::textToHTMLWithoutP(text()); } QString HtmlContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { // return Tools::htmlToParagraph(html()); QTextDocument simpleRichText; simpleRichText.setHtml(html()); return Tools::textDocumentToMinimalHTML(&simpleRichText); } QString ImageContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("").arg(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } QString AnimationContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("").arg(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } QString SoundContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("%2").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), fileName()); } // With the icon? QString FileContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("%2").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), fileName()); } // With the icon? QString LinkContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString("%2").arg(url().toDisplayString(), title()); } // With the icon? QString CrossReferenceContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString("%2").arg(url().toDisplayString(), title()); } // With the icon? QString LauncherContent::toHtml(const QString & /*imageName*/, const QString &cuttedFullPath) { return QString("%2").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), name()); } // With the icon? QString ColorContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString("%2").arg(color().name(), color().name()); } QString UnknownContent::toHtml(const QString & /*imageName*/, const QString & /*cuttedFullPath*/) { return QString(); } QPixmap ImageContent::toPixmap() { return pixmap(); } QPixmap AnimationContent::toPixmap() { return m_movie->currentPixmap(); } void NoteContent::toLink(QUrl *url, QString *title, const QString &cuttedFullPath) { if (useFile()) { *url = QUrl::fromUserInput(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); *title = (cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } else { *url = QUrl(); title->clear(); } } void LinkContent::toLink(QUrl *url, QString *title, const QString & /*cuttedFullPath*/) { *url = this->url(); *title = this->title(); } void CrossReferenceContent::toLink(QUrl *url, QString *title, const QString & /*cuttedFullPath*/) { *url = this->url(); *title = this->title(); } void LauncherContent::toLink(QUrl *url, QString *title, const QString &cuttedFullPath) { *url = QUrl::fromUserInput(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); *title = name(); } void UnknownContent::toLink(QUrl *url, QString *title, const QString & /*cuttedFullPath*/) { *url = QUrl(); *title = QString(); } bool TextContent::useFile() const { return true; } bool HtmlContent::useFile() const { return true; } bool ImageContent::useFile() const { return true; } bool AnimationContent::useFile() const { return true; } bool SoundContent::useFile() const { return true; } bool FileContent::useFile() const { return true; } bool LinkContent::useFile() const { return false; } bool CrossReferenceContent::useFile() const { return false; } bool LauncherContent::useFile() const { return true; } bool ColorContent::useFile() const { return false; } bool UnknownContent::useFile() const { return true; } bool TextContent::canBeSavedAs() const { return true; } bool HtmlContent::canBeSavedAs() const { return true; } bool ImageContent::canBeSavedAs() const { return true; } bool AnimationContent::canBeSavedAs() const { return true; } bool SoundContent::canBeSavedAs() const { return true; } bool FileContent::canBeSavedAs() const { return true; } bool LinkContent::canBeSavedAs() const { return true; } bool CrossReferenceContent::canBeSavedAs() const { return true; } bool LauncherContent::canBeSavedAs() const { return true; } bool ColorContent::canBeSavedAs() const { return false; } bool UnknownContent::canBeSavedAs() const { return false; } QString TextContent::saveAsFilters() const { return "text/plain"; } QString HtmlContent::saveAsFilters() const { return "text/html"; } QString ImageContent::saveAsFilters() const { return "image/png"; } // TODO: Offer more types QString AnimationContent::saveAsFilters() const { return "image/gif"; } // TODO: MNG... QString SoundContent::saveAsFilters() const { return "audio/mp3 audio/ogg"; } // TODO: OGG... QString FileContent::saveAsFilters() const { return "*"; } // TODO: Get MIME type of the url target QString LinkContent::saveAsFilters() const { return "*"; } // TODO: idem File + If isDir() const: return QString CrossReferenceContent::saveAsFilters() const { return "*"; } // TODO: idem File + If isDir() const: return QString LauncherContent::saveAsFilters() const { return "application/x-desktop"; } QString ColorContent::saveAsFilters() const { return QString(); } QString UnknownContent::saveAsFilters() const { return QString(); } bool TextContent::match(const FilterData &data) { return text().contains(data.string); } bool HtmlContent::match(const FilterData &data) { return m_textEquivalent /*toText(QString())*/.contains(data.string); } // OPTIM_FILTER bool ImageContent::match(const FilterData & /*data*/) { return false; } bool AnimationContent::match(const FilterData & /*data*/) { return false; } bool SoundContent::match(const FilterData &data) { return fileName().contains(data.string); } bool FileContent::match(const FilterData &data) { return fileName().contains(data.string); } bool LinkContent::match(const FilterData &data) { return title().contains(data.string) || url().toDisplayString().contains(data.string); } bool CrossReferenceContent::match(const FilterData &data) { return title().contains(data.string) || url().toDisplayString().contains(data.string); } bool LauncherContent::match(const FilterData &data) { return exec().contains(data.string) || name().contains(data.string); } bool ColorContent::match(const FilterData &data) { return color().name().contains(data.string); } bool UnknownContent::match(const FilterData &data) { return mimeTypes().contains(data.string); } QString TextContent::editToolTipText() const { return i18n("Edit this plain text"); } QString HtmlContent::editToolTipText() const { return i18n("Edit this text"); } QString ImageContent::editToolTipText() const { return i18n("Edit this image"); } QString AnimationContent::editToolTipText() const { return i18n("Edit this animation"); } QString SoundContent::editToolTipText() const { return i18n("Edit the file name of this sound"); } QString FileContent::editToolTipText() const { return i18n("Edit the name of this file"); } QString LinkContent::editToolTipText() const { return i18n("Edit this link"); } QString CrossReferenceContent::editToolTipText() const { return i18n("Edit this cross reference"); } QString LauncherContent::editToolTipText() const { return i18n("Edit this launcher"); } QString ColorContent::editToolTipText() const { return i18n("Edit this color"); } QString UnknownContent::editToolTipText() const { return i18n("Edit this unknown object"); } QString TextContent::cssClass() const { return QString(); } QString HtmlContent::cssClass() const { return QString(); } QString ImageContent::cssClass() const { return QString(); } QString AnimationContent::cssClass() const { return QString(); } QString SoundContent::cssClass() const { return "sound"; } QString FileContent::cssClass() const { return "file"; } QString LinkContent::cssClass() const { return (LinkLook::lookForURL(m_url) == LinkLook::localLinkLook ? "local" : "network"); } QString CrossReferenceContent::cssClass() const { return "cross_reference"; } QString LauncherContent::cssClass() const { return "launcher"; } QString ColorContent::cssClass() const { return QString(); } QString UnknownContent::cssClass() const { return QString(); } void TextContent::fontChanged() { setText(text()); } void HtmlContent::fontChanged() { QTextDocument *richDoc = m_graphicsTextItem.document(); // This check is important when applying style to a note which is not loaded yet. Example: // Filter all -> open some basket for the first time -> close filter: if a note was tagged as TODO, then it would display no text if (!richDoc->isEmpty()) setHtml(Tools::textDocumentToMinimalHTML(richDoc)); } void ImageContent::fontChanged() { setPixmap(pixmap()); } void AnimationContent::fontChanged() { /*startMovie();*/ } void FileContent::fontChanged() { setFileName(fileName()); } void LinkContent::fontChanged() { setLink(url(), title(), icon(), autoTitle(), autoIcon()); } void CrossReferenceContent::fontChanged() { setCrossReference(url(), title(), icon()); } void LauncherContent::fontChanged() { setLauncher(name(), icon(), exec()); } void ColorContent::fontChanged() { setColor(color()); } void UnknownContent::fontChanged() { loadFromFile(/*lazyLoad=*/false); } // TODO: Optimize: setMimeTypes() // QString TextContent::customOpenCommand() { return (Settings::isTextUseProg() && ! Settings::textProg().isEmpty() ? Settings::textProg() : QString()); } QString HtmlContent::customOpenCommand() { return (Settings::isHtmlUseProg() && !Settings::htmlProg().isEmpty() ? Settings::htmlProg() : QString()); } QString ImageContent::customOpenCommand() { return (Settings::isImageUseProg() && !Settings::imageProg().isEmpty() ? Settings::imageProg() : QString()); } QString AnimationContent::customOpenCommand() { return (Settings::isAnimationUseProg() && !Settings::animationProg().isEmpty() ? Settings::animationProg() : QString()); } QString SoundContent::customOpenCommand() { return (Settings::isSoundUseProg() && !Settings::soundProg().isEmpty() ? Settings::soundProg() : QString()); } void LinkContent::serialize(QDataStream &stream) { stream << url() << title() << icon() << (quint64)autoTitle() << (quint64)autoIcon(); } void CrossReferenceContent::serialize(QDataStream &stream) { stream << url() << title() << icon(); } void ColorContent::serialize(QDataStream &stream) { stream << color(); } QPixmap TextContent::feedbackPixmap(qreal width, qreal height) { QRectF textRect = QFontMetrics(note()->font()).boundingRect(0, 0, width, height, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, text()); QPixmap pixmap(qMin(width, textRect.width()), qMin(height, textRect.height())); pixmap.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&pixmap); painter.setPen(note()->textColor()); painter.setFont(note()->font()); painter.drawText(0, 0, pixmap.width(), pixmap.height(), Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, text()); painter.end(); return pixmap; } QPixmap HtmlContent::feedbackPixmap(qreal width, qreal height) { QTextDocument richText; richText.setHtml(html()); richText.setDefaultFont(note()->font()); richText.setTextWidth(width); QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::Text, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); QPixmap pixmap(qMin(width, richText.idealWidth()), qMin(height, richText.size().height())); pixmap.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&pixmap); painter.setPen(note()->textColor()); painter.translate(0, 0); richText.drawContents(&painter, QRectF(0, 0, pixmap.width(), pixmap.height())); painter.end(); return pixmap; } QPixmap ImageContent::feedbackPixmap(qreal width, qreal height) { if (width >= m_pixmapItem.pixmap().width() && height >= m_pixmapItem.pixmap().height()) { // Full size if (m_pixmapItem.pixmap().hasAlpha()) { QPixmap opaque(m_pixmapItem.pixmap().width(), m_pixmapItem.pixmap().height()); opaque.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&opaque); painter.drawPixmap(0, 0, m_pixmapItem.pixmap()); painter.end(); return opaque; } else { return m_pixmapItem.pixmap(); } } else { // Scaled down QImage imageToScale = m_pixmapItem.pixmap().toImage(); QPixmap pmScaled; pmScaled = QPixmap::fromImage(imageToScale.scaled(width, height, Qt::KeepAspectRatio)); if (pmScaled.hasAlpha()) { QPixmap opaque(pmScaled.width(), pmScaled.height()); opaque.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&opaque); painter.drawPixmap(0, 0, pmScaled); painter.end(); return opaque; } else { return pmScaled; } } } QPixmap AnimationContent::feedbackPixmap(qreal width, qreal height) { QPixmap pixmap = m_movie->currentPixmap(); if (width >= pixmap.width() && height >= pixmap.height()) // Full size return pixmap; else { // Scaled down QImage imageToScale = pixmap.toImage(); QPixmap pmScaled; pmScaled = QPixmap::fromImage(imageToScale.scaled(width, height, Qt::KeepAspectRatio)); return pmScaled; } } QPixmap LinkContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap CrossReferenceContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap ColorContent::feedbackPixmap(qreal width, qreal height) { // TODO: Duplicate code: make a rect() method! QRectF boundingRect = m_colorItem.boundingRect(); QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); QPixmap pixmap(qMin(width, boundingRect.width()), qMin(height, boundingRect.height())); pixmap.fill(note()->backgroundColor().darker(FEEDBACK_DARKING)); QPainter painter(&pixmap); m_colorItem.paint(&painter, nullptr, nullptr); //, pixmap.width(), pixmap.height(), palette, false, false, false); // We don't care of the three last boolean parameters. painter.end(); return pixmap; } QPixmap FileContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap LauncherContent::feedbackPixmap(qreal width, qreal height) { QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); return m_linkDisplayItem.linkDisplay().feedbackPixmap(width, height, palette, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); } QPixmap UnknownContent::feedbackPixmap(qreal width, qreal height) { QRectF boundingRect = m_unknownItem.boundingRect(); QPalette palette; palette = basket()->palette(); palette.setColor(QPalette::WindowText, note()->textColor()); palette.setColor(QPalette::Background, note()->backgroundColor().darker(FEEDBACK_DARKING)); QPixmap pixmap(qMin(width, boundingRect.width()), qMin(height, boundingRect.height())); QPainter painter(&pixmap); m_unknownItem.paint(&painter, nullptr, nullptr); //, pixmap.width() + 1, pixmap.height(), palette, false, false, false); // We don't care of the three last boolean parameters. painter.setPen(note()->backgroundColor().darker(FEEDBACK_DARKING)); painter.drawPoint(0, 0); painter.drawPoint(pixmap.width() - 1, 0); painter.drawPoint(0, pixmap.height() - 1); painter.drawPoint(pixmap.width() - 1, pixmap.height() - 1); painter.end(); return pixmap; } /** class TextContent: */ TextContent::TextContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, NoteType::Text, fileName) , m_graphicsTextItem(parent) { if (parent) { parent->addToGroup(&m_graphicsTextItem); m_graphicsTextItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(lazyLoad); } TextContent::~TextContent() { if (note()) note()->removeFromGroup(&m_graphicsTextItem); } qreal TextContent::setWidthAndGetHeight(qreal /*width*/) { return m_graphicsTextItem.boundingRect().height(); } bool TextContent::loadFromFile(bool lazyLoad) { DEBUG_WIN << "Loading TextContent From " + basket()->folderName() + fileName(); QString content; - bool success = basket()->loadFromFile(fullPath(), &content); + bool success = FileStorage::loadFromFile(fullPath(), &content); if (success) setText(content, lazyLoad); else { qDebug() << "FAILED TO LOAD TextContent: " << fullPath(); setText(QString(), lazyLoad); if (!QFile::exists(fullPath())) saveToFile(); // Reserve the fileName so no new note will have the same name! } return success; } bool TextContent::finishLazyLoad() { m_graphicsTextItem.setFont(note()->font()); contentChanged(m_graphicsTextItem.boundingRect().width() + 1); return true; } bool TextContent::saveToFile() { - return basket()->saveToFile(fullPath(), text()); + return FileStorage::saveToFile(fullPath(), text()); } QString TextContent::linkAt(const QPointF & /*pos*/) { return QString(); /* if (m_simpleRichText) return m_simpleRichText->documentLayout()->anchorAt(pos); else return QString(); // Lazy loaded*/ } QString TextContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening plain text..."); case OpenSeveral: return i18n("Opening plain texts..."); case OpenOneWith: return i18n("Opening plain text with..."); case OpenSeveralWith: return i18n("Opening plain texts with..."); case OpenOneWithDialog: return i18n("Open plain text with:"); case OpenSeveralWithDialog: return i18n("Open plain texts with:"); default: return QString(); } } void TextContent::setText(const QString &text, bool lazyLoad) { m_graphicsTextItem.setText(text); if (!lazyLoad) finishLazyLoad(); else contentChanged(m_graphicsTextItem.boundingRect().width()); } void TextContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString html = "" + Tools::tagCrossReferences(Tools::tagURLs(Tools::textToHTMLWithoutP(text().replace(QChar('\t'), " "))), false, exporter); // Don't collapse multiple spaces! exporter->stream << html.replace(" ", "  ").replace(QChar('\n'), '\n' + spaces.fill(' ', indent + 1)); } /** class HtmlContent: */ HtmlContent::HtmlContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, NoteType::Html, fileName) , m_simpleRichText(nullptr) , m_graphicsTextItem(parent) { if (parent) { parent->addToGroup(&m_graphicsTextItem); m_graphicsTextItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(lazyLoad); } HtmlContent::~HtmlContent() { if (note()) note()->removeFromGroup(&m_graphicsTextItem); delete m_simpleRichText; } qreal HtmlContent::setWidthAndGetHeight(qreal width) { width -= 1; m_graphicsTextItem.setTextWidth(width); return m_graphicsTextItem.boundingRect().height(); } bool HtmlContent::loadFromFile(bool lazyLoad) { DEBUG_WIN << "Loading HtmlContent From " + basket()->folderName() + fileName(); QString content; - bool success = basket()->loadFromFile(fullPath(), &content); + bool success = FileStorage::loadFromFile(fullPath(), &content); if (success) setHtml(content, lazyLoad); else { setHtml(QString(), lazyLoad); if (!QFile::exists(fullPath())) saveToFile(); // Reserve the fileName so no new note will have the same name! } return success; } bool HtmlContent::finishLazyLoad() { qreal width = m_graphicsTextItem.document()->idealWidth(); m_graphicsTextItem.setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsFocusable); m_graphicsTextItem.setTextInteractionFlags(Qt::TextEditorInteraction); /*QString css = ".cross_reference { display: block; width: 100%; text-decoration: none; color: #336600; }" "a:hover.cross_reference { text-decoration: underline; color: #ff8000; }"; m_graphicsTextItem.document()->setDefaultStyleSheet(css);*/ QString convert = Tools::tagURLs(m_html); if (note()->allowCrossReferences()) convert = Tools::tagCrossReferences(convert); m_graphicsTextItem.setHtml(convert); m_graphicsTextItem.setDefaultTextColor(note()->textColor()); m_graphicsTextItem.setFont(note()->font()); m_graphicsTextItem.setTextWidth(1); // We put a width of 1 pixel, so usedWidth() is equal to the minimum width int minWidth = m_graphicsTextItem.document()->idealWidth(); m_graphicsTextItem.setTextWidth(width); contentChanged(minWidth + 1); return true; } bool HtmlContent::saveToFile() { - return basket()->saveToFile(fullPath(), html()); + return FileStorage::saveToFile(fullPath(), html()); } QString HtmlContent::linkAt(const QPointF &pos) { return m_graphicsTextItem.document()->documentLayout()->anchorAt(pos); } QString HtmlContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening text..."); case OpenSeveral: return i18n("Opening texts..."); case OpenOneWith: return i18n("Opening text with..."); case OpenSeveralWith: return i18n("Opening texts with..."); case OpenOneWithDialog: return i18n("Open text with:"); case OpenSeveralWithDialog: return i18n("Open texts with:"); default: return QString(); } } void HtmlContent::setHtml(const QString &html, bool lazyLoad) { m_html = html; /* The code was commented, so now non-Latin text is stored directly in Unicode. * If testing doesn't show any bugs, this block should be deleted QRegExp rx("([^\\x00-\\x7f])"); while (m_html.contains(rx)) { m_html.replace( rx.cap().unicode()[0], QString("&#%1;").arg(rx.cap().unicode()[0].unicode()) ); }*/ m_textEquivalent = toText(QString()); // OPTIM_FILTER if (!lazyLoad) finishLazyLoad(); else contentChanged(10); } void HtmlContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString convert = Tools::tagURLs(html().replace("\t", " ")); if (note()->allowCrossReferences()) convert = Tools::tagCrossReferences(convert, false, exporter); exporter->stream << Tools::htmlToParagraph(convert).replace(" ", "  ").replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class ImageContent: */ ImageContent::ImageContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, NoteType::Image, fileName) , m_pixmapItem(parent) , m_format() { if (parent) { parent->addToGroup(&m_pixmapItem); m_pixmapItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(lazyLoad); } ImageContent::~ImageContent() { if (note()) note()->removeFromGroup(&m_pixmapItem); } qreal ImageContent::setWidthAndGetHeight(qreal width) { width -= 1; // Don't store width: we will get it on paint! if (width >= m_pixmapItem.pixmap().width()) // Full size { m_pixmapItem.setScale(1.0); return m_pixmapItem.boundingRect().height(); } else { // Scaled down qreal scaleFactor = width / m_pixmapItem.pixmap().width(); m_pixmapItem.setScale(scaleFactor); return m_pixmapItem.boundingRect().height() * scaleFactor; } } bool ImageContent::loadFromFile(bool lazyLoad) { if (lazyLoad) return true; else return finishLazyLoad(); } bool ImageContent::finishLazyLoad() { DEBUG_WIN << "Loading ImageContent From " + basket()->folderName() + fileName(); QByteArray content; QPixmap pixmap; - if (basket()->loadFromFile(fullPath(), &content)) { + if (FileStorage::loadFromFile(fullPath(), &content)) { QBuffer buffer(&content); buffer.open(QIODevice::ReadOnly); m_format = QImageReader::imageFormat(&buffer); // See QImageIO to know what formats can be supported. buffer.close(); if (!m_format.isNull()) { pixmap.loadFromData(content); setPixmap(pixmap); return true; } } qDebug() << "FAILED TO LOAD ImageContent: " << fullPath(); m_format = "PNG"; // If the image is set later, it should be saved without destruction, so we use PNG by default. pixmap = QPixmap(1, 1); // Create a 1x1 pixels image instead of an undefined one. pixmap.fill(); pixmap.setMask(pixmap.createHeuristicMask()); setPixmap(pixmap); if (!QFile::exists(fullPath())) saveToFile(); // Reserve the fileName so no new note will have the same name! return false; } bool ImageContent::saveToFile() { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); m_pixmapItem.pixmap().save(&buffer, m_format); - return basket()->saveToFile(fullPath(), ba); + return FileStorage::saveToFile(fullPath(), ba); } void ImageContent::toolTipInfos(QStringList *keys, QStringList *values) { keys->append(i18n("Size")); values->append(i18n("%1 by %2 pixels", QString::number(m_pixmapItem.pixmap().width()), QString::number(m_pixmapItem.pixmap().height()))); } QString ImageContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening image..."); case OpenSeveral: return i18n("Opening images..."); case OpenOneWith: return i18n("Opening image with..."); case OpenSeveralWith: return i18n("Opening images with..."); case OpenOneWithDialog: return i18n("Open image with:"); case OpenSeveralWithDialog: return i18n("Open images with:"); default: return QString(); } } void ImageContent::setPixmap(const QPixmap &pixmap) { m_pixmapItem.setPixmap(pixmap); // Since it's scaled, the height is always greater or equal to the size of the tag emblems (16) contentChanged(16 + 1); // TODO: always good? I don't think... } void ImageContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { qreal width = m_pixmapItem.pixmap().width(); qreal height = m_pixmapItem.pixmap().height(); qreal contentWidth = note()->width() - note()->contentX() - 1 - Note::NOTE_MARGIN; QString imageName = exporter->copyFile(fullPath(), /*createIt=*/true); if (contentWidth <= m_pixmapItem.pixmap().width()) { // Scaled down qreal scale = contentWidth / m_pixmapItem.pixmap().width(); width = m_pixmapItem.pixmap().width() * scale; height = m_pixmapItem.pixmap().height() * scale; exporter->stream << "dataFolderName << imageName << "\" title=\"" << i18n("Click for full size view") << "\">"; } exporter->stream << "dataFolderName << imageName << "\" width=\"" << width << "\" height=\"" << height << "\" alt=\"\">"; if (contentWidth <= m_pixmapItem.pixmap().width()) // Scaled down exporter->stream << ""; } /** class AnimationContent: */ AnimationContent::AnimationContent(Note *parent, const QString &fileName, bool lazyLoad) : NoteContent(parent, NoteType::Animation, fileName) , m_buffer(new QBuffer(this)) , m_movie(new QMovie(this)) , m_currentWidth(0) , m_graphicsPixmap(parent) { if (parent) { parent->addToGroup(&m_graphicsPixmap); m_graphicsPixmap.setPos(parent->contentX(), Note::NOTE_MARGIN); connect(parent->basket(), SIGNAL(activated()), m_movie, SLOT(start())); connect(parent->basket(), SIGNAL(closed()), m_movie, SLOT(stop())); } basket()->addWatchedFile(fullPath()); connect(m_movie, SIGNAL(resized(QSize)), this, SLOT(movieResized())); connect(m_movie, SIGNAL(frameChanged(int)), this, SLOT(movieFrameChanged())); loadFromFile(lazyLoad); } AnimationContent::~AnimationContent() { note()->removeFromGroup(&m_graphicsPixmap); } qreal AnimationContent::setWidthAndGetHeight(qreal width) { m_currentWidth = width; QPixmap pixmap = m_graphicsPixmap.pixmap(); if (pixmap.width() > m_currentWidth) { qreal scaleFactor = m_currentWidth / pixmap.width(); m_graphicsPixmap.setScale(scaleFactor); return pixmap.height() * scaleFactor; } else { m_graphicsPixmap.setScale(1.0); return pixmap.height(); } return 0; } bool AnimationContent::loadFromFile(bool lazyLoad) { if (lazyLoad) return true; else return finishLazyLoad(); } bool AnimationContent::finishLazyLoad() { QByteArray content; - if (basket()->loadFromFile(fullPath(), &content)) { + if (FileStorage::loadFromFile(fullPath(), &content)) { m_buffer->setData(content); startMovie(); contentChanged(16); return true; } m_buffer->setData(nullptr); return false; } bool AnimationContent::saveToFile() { // Impossible! return false; } QString AnimationContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening animation..."); case OpenSeveral: return i18n("Opening animations..."); case OpenOneWith: return i18n("Opening animation with..."); case OpenSeveralWith: return i18n("Opening animations with..."); case OpenOneWithDialog: return i18n("Open animation with:"); case OpenSeveralWithDialog: return i18n("Open animations with:"); default: return QString(); } } bool AnimationContent::startMovie() { if (m_buffer->data().isEmpty()) return false; m_movie->setDevice(m_buffer); m_movie->start(); return true; } void AnimationContent::movieUpdated() { m_graphicsPixmap.setPixmap(m_movie->currentPixmap()); } void AnimationContent::movieResized() { m_graphicsPixmap.setPixmap(m_movie->currentPixmap()); } void AnimationContent::movieFrameChanged() { m_graphicsPixmap.setPixmap(m_movie->currentPixmap()); } void AnimationContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { exporter->stream << QString("\"\"") .arg(exporter->dataFolderName + exporter->copyFile(fullPath(), /*createIt=*/true), QString::number(m_movie->currentPixmap().size().width()), QString::number(m_movie->currentPixmap().size().height())); } /** class FileContent: */ FileContent::FileContent(Note *parent, const QString &fileName) : NoteContent(parent, NoteType::File, fileName) , m_linkDisplayItem(parent) , m_previewJob(nullptr) { basket()->addWatchedFile(fullPath()); setFileName(fileName); // FIXME: TO THAT HERE BECAUSE NoteContent() constructor seems to don't be able to call virtual methods??? if (parent) { parent->addToGroup(&m_linkDisplayItem); m_linkDisplayItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } FileContent::~FileContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); } qreal FileContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } bool FileContent::loadFromFile(bool /*lazyLoad*/) { setFileName(fileName()); // File changed: get new file preview! return true; } void FileContent::toolTipInfos(QStringList *keys, QStringList *values) { // Get the size of the file: uint size = QFileInfo(fullPath()).size(); QString humanFileSize = KIO::convertSize((KIO::filesize_t)size); keys->append(i18n("Size")); values->append(humanFileSize); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(QUrl::fromLocalFile(fullPath())); if (mime.isValid()) { keys->append(i18n("Type")); values->append(mime.comment()); } MetaDataExtractionResult result(fullPath(), mime.name()); KFileMetaData::ExtractorCollection extractorCollection; for (KFileMetaData::Extractor *ex : extractorCollection.fetchExtractors(mime.name())) { ex->extract(&result); auto groups = result.preferredGroups(); DEBUG_WIN << "Metadata Extractor result has " << QString::number(groups.count()) << " groups"; int i = 0; for (auto it = groups.begin(); i < 6 && it != groups.end(); ++it) { if (!it->second.isEmpty()) { keys->append(it->first); values->append(it->second); i++; } } } } int FileContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF FileContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString FileContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this file") : QString()); } Qt::CursorShape FileContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } int FileContent::xEditorIndent() { return m_linkDisplayItem.linkDisplay().iconButtonRect().width() + 2; } QString FileContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening file..."); case OpenSeveral: return i18n("Opening files..."); case OpenOneWith: return i18n("Opening file with..."); case OpenSeveralWith: return i18n("Opening files with..."); case OpenOneWithDialog: return i18n("Open file with:"); case OpenSeveralWithDialog: return i18n("Open files with:"); default: return QString(); } } void FileContent::setFileName(const QString &fileName) { NoteContent::setFileName(fileName); QUrl url = QUrl::fromLocalFile(fullPath()); if (linkLook()->previewEnabled()) m_linkDisplayItem.linkDisplay().setLink(fileName, NoteFactory::iconForURL(url), linkLook(), note()->font()); // FIXME: move iconForURL outside of NoteFactory !!!!! else m_linkDisplayItem.linkDisplay().setLink(fileName, NoteFactory::iconForURL(url), QPixmap(), linkLook(), note()->font()); startFetchingUrlPreview(); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void FileContent::linkLookChanged() { fontChanged(); // setFileName(fileName()); // startFetchingUrlPreview(); } void FileContent::newPreview(const KFileItem &, const QPixmap &preview) { LinkLook *linkLook = this->linkLook(); m_linkDisplayItem.linkDisplay().setLink(fileName(), NoteFactory::iconForURL(QUrl::fromLocalFile(fullPath())), (linkLook->previewEnabled() ? preview : QPixmap()), linkLook, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void FileContent::removePreview(const KFileItem &ki) { newPreview(ki, QPixmap()); } void FileContent::startFetchingUrlPreview() { /* KUrl url(fullPath()); LinkLook *linkLook = this->linkLook(); // delete m_previewJob; if (!url.isEmpty() && linkLook->previewSize() > 0) { QUrl filteredUrl = NoteFactory::filteredURL(url);//KURIFilter::self()->filteredURI(url); KUrl::List urlList; urlList.append(filteredUrl); m_previewJob = KIO::filePreview(urlList, linkLook->previewSize(), linkLook->previewSize(), linkLook->iconSize()); connect(m_previewJob, SIGNAL(gotPreview(const KFileItem&, const QPixmap&)), this, SLOT(newPreview(const KFileItem&, const QPixmap&))); connect(m_previewJob, SIGNAL(failed(const KFileItem&)), this, SLOT(removePreview(const KFileItem&))); } */ } void FileContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString fileName = exporter->copyFile(fullPath(), true); exporter->stream << m_linkDisplayItem.linkDisplay().toHtml(exporter, QUrl::fromLocalFile(exporter->dataFolderName + fileName), QString()).replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class SoundContent: */ SoundContent::SoundContent(Note *parent, const QString &fileName) : FileContent(parent, fileName) { setFileName(fileName); music = new Phonon::MediaObject(this); music->setCurrentSource(Phonon::MediaSource(fullPath())); Phonon::AudioOutput *audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this); Phonon::Path path = Phonon::createPath(music, audioOutput); connect(music, SIGNAL(stateChanged(Phonon::State, Phonon::State)), this, SLOT(stateChanged(Phonon::State, Phonon::State))); } void SoundContent::stateChanged(Phonon::State newState, Phonon::State oldState) { qDebug() << "stateChanged " << oldState << " to " << newState; } QString SoundContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this sound") : QString()); } void SoundContent::setHoveredZone(int oldZone, int newZone) { if (newZone == Note::Custom0 || newZone == Note::Content) { // Start the sound preview: if (oldZone != Note::Custom0 && oldZone != Note::Content) { // Don't restart if it was already in one of those zones if (music->state() == 1) { music->play(); } } } else { // Stop the sound preview, if it was started: if (music->state() != 1) { music->stop(); // delete music;//TODO implement this in slot connected with music alted signal // music = 0; } } } QString SoundContent::messageWhenOpening(OpenMessage where) { switch (where) { case OpenOne: return i18n("Opening sound..."); case OpenSeveral: return i18n("Opening sounds..."); case OpenOneWith: return i18n("Opening sound with..."); case OpenSeveralWith: return i18n("Opening sounds with..."); case OpenOneWithDialog: return i18n("Open sound with:"); case OpenSeveralWithDialog: return i18n("Open sounds with:"); default: return QString(); } } /** class LinkContent: */ LinkContent::LinkContent(Note *parent, const QUrl &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon) : NoteContent(parent, NoteType::Link) , m_linkDisplayItem(parent) , m_access_manager(nullptr) , m_acceptingData(false) , m_previewJob(nullptr) { setLink(url, title, icon, autoTitle, autoIcon); if (parent) { parent->addToGroup(&m_linkDisplayItem); m_linkDisplayItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } LinkContent::~LinkContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); delete m_access_manager; } qreal LinkContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } void LinkContent::saveToNode(QXmlStreamWriter &stream) { stream.writeStartElement("content"); stream.writeAttribute("title", title()); stream.writeAttribute("icon", icon()); stream.writeAttribute("autoIcon", (autoIcon() ? "true" : "false")); stream.writeAttribute("autoTitle", (autoTitle() ? "true" : "false")); stream.writeCharacters(url().toDisplayString()); stream.writeEndElement(); } void LinkContent::toolTipInfos(QStringList *keys, QStringList *values) { keys->append(i18n("Target")); values->append(m_url.toDisplayString()); } int LinkContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF LinkContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString LinkContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this link") : QString()); } Qt::CursorShape LinkContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } QString LinkContent::statusBarMessage(int zone) { if (zone == Note::Custom0 || zone == Note::Content) return m_url.toDisplayString(); else return QString(); } QUrl LinkContent::urlToOpen(bool /*with*/) { return NoteFactory::filteredURL(url()); // KURIFilter::self()->filteredURI(url()); } QString LinkContent::messageWhenOpening(OpenMessage where) { if (url().isEmpty()) return i18n("Link have no URL to open."); switch (where) { case OpenOne: return i18n("Opening link target..."); case OpenSeveral: return i18n("Opening link targets..."); case OpenOneWith: return i18n("Opening link target with..."); case OpenSeveralWith: return i18n("Opening link targets with..."); case OpenOneWithDialog: return i18n("Open link target with:"); case OpenSeveralWithDialog: return i18n("Open link targets with:"); default: return QString(); } } void LinkContent::setLink(const QUrl &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon) { m_autoTitle = autoTitle; m_autoIcon = autoIcon; m_url = NoteFactory::filteredURL(url); // KURIFilter::self()->filteredURI(url); m_title = (autoTitle ? NoteFactory::titleForURL(m_url) : title); m_icon = (autoIcon ? NoteFactory::iconForURL(m_url) : icon); LinkLook *look = LinkLook::lookForURL(m_url); if (look->previewEnabled()) m_linkDisplayItem.linkDisplay().setLink(m_title, m_icon, look, note()->font()); else m_linkDisplayItem.linkDisplay().setLink(m_title, m_icon, QPixmap(), look, note()->font()); startFetchingUrlPreview(); if (autoTitle) startFetchingLinkTitle(); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void LinkContent::linkLookChanged() { fontChanged(); } void LinkContent::newPreview(const KFileItem &, const QPixmap &preview) { LinkLook *linkLook = LinkLook::lookForURL(url()); m_linkDisplayItem.linkDisplay().setLink(title(), icon(), (linkLook->previewEnabled() ? preview : QPixmap()), linkLook, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void LinkContent::removePreview(const KFileItem &ki) { newPreview(ki, QPixmap()); } // QHttp slots for getting link title void LinkContent::httpReadyRead() { if (!m_acceptingData) return; // Check for availability qint64 bytesAvailable = m_reply->bytesAvailable(); if (bytesAvailable <= 0) return; QByteArray buf = m_reply->read(bytesAvailable); m_httpBuff.append(buf); // Stop at 10k bytes if (m_httpBuff.length() > 10000) { m_acceptingData = false; m_reply->abort(); endFetchingLinkTitle(); } } void LinkContent::httpDone(QNetworkReply *reply) { if (m_acceptingData) { m_acceptingData = false; endFetchingLinkTitle(); } // If all done, close and delete the reply. reply->deleteLater(); } void LinkContent::startFetchingLinkTitle() { QUrl newUrl = this->url(); // If this is not an HTTP request, just ignore it. if (newUrl.scheme() == "http") { // If we have no access_manager, create one. if (m_access_manager == nullptr) { m_access_manager = new KIO::Integration::AccessManager(this); connect(m_access_manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(httpDone(QNetworkReply *))); } // If no explicit port, default to port 80. if (newUrl.port() == 0) newUrl.setPort(80); // If no path or query part, default to / if ((newUrl.path() + newUrl.query()).isEmpty()) newUrl = QUrl::fromLocalFile("/"); // Issue request m_reply = m_access_manager->get(QNetworkRequest(newUrl)); m_acceptingData = true; connect(m_reply, SIGNAL(readyRead()), this, SLOT(httpReadyRead())); } } // Code duplicated from FileContent::startFetchingUrlPreview() void LinkContent::startFetchingUrlPreview() { QUrl url = this->url(); LinkLook *linkLook = LinkLook::lookForURL(this->url()); // delete m_previewJob; if (!url.isEmpty() && linkLook->previewSize() > 0) { QUrl filteredUrl = NoteFactory::filteredURL(url); // KURIFilter::self()->filteredURI(url); QList urlList; urlList.append(filteredUrl); m_previewJob = KIO::filePreview(urlList, linkLook->previewSize(), linkLook->previewSize(), linkLook->iconSize()); connect(m_previewJob, SIGNAL(gotPreview(const KFileItem &, const QPixmap &)), this, SLOT(newPreview(const KFileItem &, const QPixmap &))); connect(m_previewJob, SIGNAL(failed(const KFileItem &)), this, SLOT(removePreview(const KFileItem &))); } } void LinkContent::endFetchingLinkTitle() { if (m_httpBuff.length() > 0) { decodeHtmlTitle(); m_httpBuff.clear(); } else DEBUG_WIN << "LinkContent: empty buffer on endFetchingLinkTitle for " + m_url.toString(); } void LinkContent::exportToHTML(HTMLExporter *exporter, int indent) { QString linkTitle = title(); // TODO: // // Append address (useful for print version of the page/basket): // if (exportData.formatForImpression && (!autoTitle() && title() != NoteFactory::titleForURL(url().toDisplayString()))) { // // The address is on a new line, unless title is empty (empty lines was replaced by  ): // if (linkTitle == " "/*" "*/) // linkTitle = url().toDisplayString()/*QString()*/; // else // linkTitle = linkTitle + " <" + url().toDisplayString() + ">"/*+ "
    "*/; // //linkTitle += "" + url().toDisplayString() + ""; // } QUrl linkURL; /* QFileInfo fInfo(url().path()); // DEBUG_WIN << url().path() // << "IsFile:" + QString::number(fInfo.isFile()) // << "IsDir:" + QString::number(fInfo.isDir()); if (exportData.embedLinkedFiles && fInfo.isFile()) { // DEBUG_WIN << "Embed file"; linkURL = exportData.dataFolderName + BasketScene::copyFile(url().path(), exportData.dataFolderPath, true); } else if (exportData.embedLinkedFolders && fInfo.isDir()) { // DEBUG_WIN << "Embed folder"; linkURL = exportData.dataFolderName + BasketScene::copyFile(url().path(), exportData.dataFolderPath, true); } else { // DEBUG_WIN << "Embed LINK"; */ linkURL = url(); /* } */ QString spaces; exporter->stream << m_linkDisplayItem.linkDisplay().toHtml(exporter, linkURL, linkTitle).replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class CrossReferenceContent: */ CrossReferenceContent::CrossReferenceContent(Note *parent, const QUrl &url, const QString &title, const QString &icon) : NoteContent(parent, NoteType::CrossReference) , m_linkDisplayItem(parent) { this->setCrossReference(url, title, icon); if (parent) parent->addToGroup(&m_linkDisplayItem); } CrossReferenceContent::~CrossReferenceContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); } qreal CrossReferenceContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } void CrossReferenceContent::saveToNode(QXmlStreamWriter &stream) { stream.writeStartElement("content"); stream.writeAttribute("title", title()); stream.writeAttribute("icon", icon()); stream.writeCharacters(url().toDisplayString()); stream.writeEndElement(); } void CrossReferenceContent::toolTipInfos(QStringList *keys, QStringList *values) { keys->append(i18n("Target")); values->append(m_url.toDisplayString()); } int CrossReferenceContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF CrossReferenceContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString CrossReferenceContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Open this link") : QString()); } Qt::CursorShape CrossReferenceContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } QString CrossReferenceContent::statusBarMessage(int zone) { if (zone == Note::Custom0 || zone == Note::Content) return i18n("Link to %1", this->title()); else return QString(); } QUrl CrossReferenceContent::urlToOpen(bool /*with*/) { return m_url; } QString CrossReferenceContent::messageWhenOpening(OpenMessage where) { if (url().isEmpty()) return i18n("Link has no basket to open."); switch (where) { case OpenOne: return i18n("Opening basket..."); default: return QString(); } } void CrossReferenceContent::setLink(const QUrl &url, const QString &title, const QString &icon) { this->setCrossReference(url, title, icon); } void CrossReferenceContent::setCrossReference(const QUrl &url, const QString &title, const QString &icon) { m_url = url; m_title = (title.isEmpty() ? url.url() : title); m_icon = icon; LinkLook *look = LinkLook::crossReferenceLook; m_linkDisplayItem.linkDisplay().setLink(m_title, m_icon, look, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void CrossReferenceContent::linkLookChanged() { fontChanged(); } void CrossReferenceContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { QString url = m_url.url(); QString title; if (url.startsWith(QLatin1String("basket://"))) url = url.mid(9, url.length() - 9); if (url.endsWith('/')) url = url.left(url.length() - 1); BasketScene *basket = Global::bnpView->basketForFolderName(url); if (!basket) title = "unknown basket"; else title = basket->basketName(); // if the basket we're trying to link to is the basket that was exported then // we have to use a special way to refer to it for the links. if (basket == exporter->exportedBasket) url = "../../" + exporter->fileName; else { // if we're in the exported basket then the links have to include // the sub directories. if (exporter->currentBasket == exporter->exportedBasket) url.prepend(exporter->basketsFolderName); url.append(".html"); } QString linkIcon = exporter->iconsFolderName + exporter->copyIcon(m_icon, LinkLook::crossReferenceLook->iconSize()); linkIcon = QString("\"\"").arg(linkIcon); exporter->stream << QString("%2 %3").arg(url, linkIcon, title); } /** class LauncherContent: */ LauncherContent::LauncherContent(Note *parent, const QString &fileName) : NoteContent(parent, NoteType::Launcher, fileName) , m_linkDisplayItem(parent) { basket()->addWatchedFile(fullPath()); loadFromFile(/*lazyLoad=*/false); if (parent) { parent->addToGroup(&m_linkDisplayItem); m_linkDisplayItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } LauncherContent::~LauncherContent() { if (note()) note()->removeFromGroup(&m_linkDisplayItem); } qreal LauncherContent::setWidthAndGetHeight(qreal width) { m_linkDisplayItem.linkDisplay().setWidth(width); return m_linkDisplayItem.linkDisplay().height(); } bool LauncherContent::loadFromFile(bool /*lazyLoad*/) // TODO: saveToFile() ?? Is it possible? { DEBUG_WIN << "Loading LauncherContent From " + basket()->folderName() + fileName(); KService service(fullPath()); setLauncher(service.name(), service.icon(), service.exec()); return true; } void LauncherContent::toolTipInfos(QStringList *keys, QStringList *values) { KService service(fullPath()); QString exec = service.exec(); if (service.terminal()) exec = i18n("%1 (run in terminal)", exec); if (!service.comment().isEmpty() && service.comment() != service.name()) { keys->append(i18n("Comment")); values->append(service.comment()); } keys->append(i18n("Command")); values->append(exec); } int LauncherContent::zoneAt(const QPointF &pos) { return (m_linkDisplayItem.linkDisplay().iconButtonAt(pos) ? 0 : Note::Custom0); } QRectF LauncherContent::zoneRect(int zone, const QPointF & /*pos*/) { QRectF linkRect = m_linkDisplayItem.linkDisplay().iconButtonRect(); if (zone == Note::Custom0) return QRectF(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() else if (zone == Note::Content) return linkRect; else return QRectF(); } QString LauncherContent::zoneTip(int zone) { return (zone == Note::Custom0 ? i18n("Launch this application") : QString()); } Qt::CursorShape LauncherContent::cursorFromZone(int zone) const { if (zone == Note::Custom0) return Qt::PointingHandCursor; return Qt::ArrowCursor; } QUrl LauncherContent::urlToOpen(bool with) { if (KService(fullPath()).exec().isEmpty()) return QUrl(); return (with ? QUrl() : QUrl::fromLocalFile(fullPath())); // Can open the application, but not with another application :-) } QString LauncherContent::messageWhenOpening(OpenMessage where) { if (KService(fullPath()).exec().isEmpty()) return i18n("The launcher have no command to run."); switch (where) { case OpenOne: return i18n("Launching application..."); case OpenSeveral: return i18n("Launching applications..."); case OpenOneWith: case OpenSeveralWith: case OpenOneWithDialog: case OpenSeveralWithDialog: // TODO: "Open this application with this file as parameter"? default: return QString(); } } void LauncherContent::setLauncher(const QString &name, const QString &icon, const QString &exec) { m_name = name; m_icon = icon; m_exec = exec; m_linkDisplayItem.linkDisplay().setLink(name, icon, LinkLook::launcherLook, note()->font()); contentChanged(m_linkDisplayItem.linkDisplay().minWidth()); } void LauncherContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; QString fileName = exporter->copyFile(fullPath(), /*createIt=*/true); exporter->stream << m_linkDisplayItem.linkDisplay().toHtml(exporter, QUrl::fromLocalFile(exporter->dataFolderName + fileName), QString()).replace("\n", '\n' + spaces.fill(' ', indent + 1)); } /** class ColorItem: */ const int ColorItem::RECT_MARGIN = 2; ColorItem::ColorItem(Note *parent, const QColor &color) : QGraphicsItem(parent) , m_note(parent) { setColor(color); } void ColorItem::setColor(const QColor &color) { m_color = color; m_textRect = QFontMetrics(m_note->font()).boundingRect(m_color.name()); } QRectF ColorItem::boundingRect() const { qreal rectHeight = (m_textRect.height() + 2) * 3 / 2; qreal rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. return QRectF(0, 0, rectWidth + RECT_MARGIN + m_textRect.width() + RECT_MARGIN, rectHeight); } void ColorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { QRectF boundingRect = this->boundingRect(); qreal rectHeight = (m_textRect.height() + 2) * 3 / 2; qreal rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. // FIXME: Duplicate from CommonColorSelector::drawColorRect: // Fill: painter->fillRect(1, 1, rectWidth - 2, rectHeight - 2, color()); // Stroke: QColor stroke = color().darker(125); painter->setPen(stroke); painter->drawLine(1, 0, rectWidth - 2, 0); painter->drawLine(0, 1, 0, rectHeight - 2); painter->drawLine(1, rectHeight - 1, rectWidth - 2, rectHeight - 1); painter->drawLine(rectWidth - 1, 1, rectWidth - 1, rectHeight - 2); // Round corners: painter->setPen(Tools::mixColor(color(), stroke)); painter->drawPoint(1, 1); painter->drawPoint(1, rectHeight - 2); painter->drawPoint(rectWidth - 2, rectHeight - 2); painter->drawPoint(rectWidth - 2, 1); // Draw the text: painter->setFont(m_note->font()); painter->setPen(m_note->palette().color(QPalette::Active, QPalette::WindowText)); painter->drawText(rectWidth + RECT_MARGIN, 0, m_textRect.width(), boundingRect.height(), Qt::AlignLeft | Qt::AlignVCenter, color().name()); } /** class ColorContent: */ ColorContent::ColorContent(Note *parent, const QColor &color) : NoteContent(parent, NoteType::Color) , m_colorItem(parent, color) { if (parent) { parent->addToGroup(&m_colorItem); m_colorItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } } ColorContent::~ColorContent() { if (note()) note()->removeFromGroup(&m_colorItem); } qreal ColorContent::setWidthAndGetHeight(qreal /*width*/) // We do not need width because we can't word-break, and width is always >= minWidth() { return m_colorItem.boundingRect().height(); } void ColorContent::saveToNode(QXmlStreamWriter &stream) { stream.writeStartElement("content"); stream.writeCharacters(color().name()); stream.writeEndElement(); } void ColorContent::toolTipInfos(QStringList *keys, QStringList *values) { int hue, saturation, value; color().getHsv(&hue, &saturation, &value); keys->append(i18nc("RGB Colorspace: Red/Green/Blue", "RGB")); values->append(i18n("Red: %1, Green: %2, Blue: %3,", QString::number(color().red()), QString::number(color().green()), QString::number(color().blue()))); keys->append(i18nc("HSV Colorspace: Hue/Saturation/Value", "HSV")); values->append(i18n("Hue: %1, Saturation: %2, Value: %3,", QString::number(hue), QString::number(saturation), QString::number(value))); const QString colorName = Tools::cssColorName(color().name()); if (!colorName.isEmpty()) { keys->append(i18n("CSS Color Name")); values->append(colorName); } keys->append(i18n("Is Web Color")); values->append(Tools::isWebColor(color()) ? i18n("Yes") : i18n("No")); } void ColorContent::setColor(const QColor &color) { m_colorItem.setColor(color); contentChanged(m_colorItem.boundingRect().width()); } void ColorContent::addAlternateDragObjects(QMimeData *dragObject) { dragObject->setColorData(color()); } void ColorContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) { // FIXME: Duplicate from setColor(): TODO: rectSize() QRectF textRect = QFontMetrics(note()->font()).boundingRect(color().name()); int rectHeight = (textRect.height() + 2) * 3 / 2; int rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. QString fileName = /*Tools::fileNameForNewFile(*/ QString("color_%1.png").arg(color().name().toLower().mid(1)) /*, exportData.iconsFolderPath)*/; QString fullPath = exporter->iconsFolderPath + fileName; QPixmap colorIcon(rectWidth, rectHeight); QPainter painter(&colorIcon); painter.setBrush(color()); painter.drawRoundedRect(0, 0, rectWidth, rectHeight, 2, 2); colorIcon.save(fullPath, "PNG"); QString iconHtml = QString("\"\"").arg(exporter->iconsFolderName + fileName, QString::number(colorIcon.width()), QString::number(colorIcon.height())); exporter->stream << iconHtml + ' ' + color().name(); } /** class UnknownItem: */ const qreal UnknownItem::DECORATION_MARGIN = 2; UnknownItem::UnknownItem(Note *parent) : QGraphicsItem(parent) , m_note(parent) { } QRectF UnknownItem::boundingRect() const { return QRectF(0, 0, m_textRect.width() + 2 * DECORATION_MARGIN, m_textRect.height() + 2 * DECORATION_MARGIN); } void UnknownItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { QPalette palette = m_note->basket()->palette(); qreal width = boundingRect().width(); qreal height = boundingRect().height(); painter->setPen(palette.color(QPalette::Active, QPalette::WindowText)); // Stroke: QColor stroke = Tools::mixColor(palette.color(QPalette::Active, QPalette::Background), palette.color(QPalette::Active, QPalette::WindowText)); painter->setPen(stroke); painter->drawLine(1, 0, width - 2, 0); painter->drawLine(0, 1, 0, height - 2); painter->drawLine(1, height - 1, width - 2, height - 1); painter->drawLine(width - 1, 1, width - 1, height - 2); // Round corners: painter->setPen(Tools::mixColor(palette.color(QPalette::Active, QPalette::Background), stroke)); painter->drawPoint(1, 1); painter->drawPoint(1, height - 2); painter->drawPoint(width - 2, height - 2); painter->drawPoint(width - 2, 1); painter->setPen(palette.color(QPalette::Active, QPalette::WindowText)); painter->drawText(DECORATION_MARGIN, DECORATION_MARGIN, width - 2 * DECORATION_MARGIN, height - 2 * DECORATION_MARGIN, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_mimeTypes); } void UnknownItem::setMimeTypes(QString mimeTypes) { m_mimeTypes = mimeTypes; m_textRect = QFontMetrics(m_note->font()).boundingRect(0, 0, 1, 500000, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, m_mimeTypes); } void UnknownItem::setWidth(qreal width) { prepareGeometryChange(); m_textRect = QFontMetrics(m_note->font()).boundingRect(0, 0, width, 500000, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, m_mimeTypes); } /** class UnknownContent: */ UnknownContent::UnknownContent(Note *parent, const QString &fileName) : NoteContent(parent, NoteType::Unknown, fileName) , m_unknownItem(parent) { if (parent) { parent->addToGroup(&m_unknownItem); m_unknownItem.setPos(parent->contentX(), Note::NOTE_MARGIN); } basket()->addWatchedFile(fullPath()); loadFromFile(/*lazyLoad=*/false); } UnknownContent::~UnknownContent() { if (note()) note()->removeFromGroup(&m_unknownItem); } qreal UnknownContent::setWidthAndGetHeight(qreal width) { m_unknownItem.setWidth(width); return m_unknownItem.boundingRect().height(); } bool UnknownContent::loadFromFile(bool /*lazyLoad*/) { DEBUG_WIN << "Loading UnknownContent From " + basket()->folderName() + fileName(); QString mimeTypes; QFile file(fullPath()); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&file); QString line; // Get the MIME-types names: do { if (!stream.atEnd()) { line = stream.readLine(); if (!line.isEmpty()) { if (mimeTypes.isEmpty()) mimeTypes += line; else mimeTypes += QString("\n") + line; } } } while (!line.isEmpty() && !stream.atEnd()); file.close(); } m_unknownItem.setMimeTypes(mimeTypes); contentChanged(m_unknownItem.boundingRect().width() + 1); return true; } void UnknownContent::addAlternateDragObjects(QMimeData *dragObject) { QFile file(fullPath()); if (file.open(QIODevice::ReadOnly)) { QDataStream stream(&file); // Get the MIME types names: QStringList mimes; QString line; do { if (!stream.atEnd()) { stream >> line; if (!line.isEmpty()) mimes.append(line); } } while (!line.isEmpty() && !stream.atEnd()); // Add the streams: quint64 size; // TODO: It was quint32 in version 0.5.0 ! QByteArray *array; for (int i = 0; i < mimes.count(); ++i) { // Get the size: stream >> size; // Allocate memory to retrieve size bytes and store them: array = new QByteArray; array->resize(size); stream.readRawData(array->data(), size); // Creata and add the QDragObject: dragObject->setData(mimes.at(i).toLatin1(), *array); delete array; // FIXME: Should we? } file.close(); } } void UnknownContent::exportToHTML(HTMLExporter *exporter, int indent) { QString spaces; exporter->stream << "
    " << mimeTypes().replace("\n", '\n' + spaces.fill(' ', indent + 1 + 1)) << "
    "; } void LinkContent::decodeHtmlTitle() { KEncodingProber prober; prober.feed(m_httpBuff); // Fallback scheme: KEncodingProber - QTextCodec::codecForHtml - UTF-8 QTextCodec *textCodec; if (prober.confidence() > 0.5) textCodec = QTextCodec::codecForName(prober.encoding()); else textCodec = QTextCodec::codecForHtml(m_httpBuff, QTextCodec::codecForName("utf-8")); QString httpBuff = textCodec->toUnicode(m_httpBuff.data(), m_httpBuff.size()); // todo: this should probably strip odd html tags like   etc QRegExp reg("[\\s]*( )?([^<]+)[\\s]*", Qt::CaseInsensitive); reg.setMinimal(true); // qDebug() << *m_httpBuff << " bytes: " << bytes_read; if (reg.indexIn(httpBuff) >= 0) { m_title = reg.cap(2); m_autoTitle = false; setEdited(); // refresh the title setLink(url(), title(), icon(), autoTitle(), autoIcon()); } } diff --git a/src/tag.cpp b/src/tag.cpp index d815148..451bd18 100644 --- a/src/tag.cpp +++ b/src/tag.cpp @@ -1,757 +1,758 @@ /** * SPDX-FileCopyrightText: (C) 2005 Sébastien Laoût * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "tag.h" #include #include #include #include #include #include #include #include #include #include "basketscene.h" #include "bnpview.h" +#include "common.h" #include "debugwindow.h" #include "gitwrapper.h" #include "global.h" #include "tools.h" #include "xmlwork.h" /** class State: */ State::State(const QString &id, Tag *tag) : m_id(id) , m_name() , m_emblem() , m_bold(false) , m_italic(false) , m_underline(false) , m_strikeOut(false) , m_textColor() , m_fontName() , m_fontSize(-1) , m_backgroundColor() , m_textEquivalent() , m_onAllTextLines(false) , m_allowCrossReferences(true) , m_parentTag(tag) { } State::~State() { } State *State::nextState(bool cycle /*= true*/) { if (!parentTag()) return nullptr; List states = parentTag()->states(); // The tag contains only one state: if (states.count() == 1) return nullptr; // Find the next state: for (List::iterator it = states.begin(); it != states.end(); ++it) // Found the current state in the list: if (*it == this) { // Find the next state: State *next = *(++it); if (it == states.end()) return (cycle ? states.first() : 0); return next; } // Should not happens: Q_ASSERT(false); return nullptr; } QString State::fullName() { if (!parentTag() || parentTag()->states().count() == 1) return (name().isEmpty() && parentTag() ? parentTag()->name() : name()); return QString(i18n("%1: %2", parentTag()->name(), name())); } QFont State::font(QFont base) { if (bold()) base.setBold(true); if (italic()) base.setItalic(true); if (underline()) base.setUnderline(true); if (strikeOut()) base.setStrikeOut(true); if (!fontName().isEmpty()) base.setFamily(fontName()); if (fontSize() > 0) base.setPointSize(fontSize()); return base; } QString State::toCSS(const QString &gradientFolderPath, const QString &gradientFolderName, const QFont &baseFont) { QString css; if (bold()) css += " font-weight: bold;"; if (italic()) css += " font-style: italic;"; if (underline() && strikeOut()) css += " text-decoration: underline line-through;"; else if (underline()) css += " text-decoration: underline;"; else if (strikeOut()) css += " text-decoration: line-through;"; if (textColor().isValid()) css += " color: " + textColor().name() + ';'; if (!fontName().isEmpty()) { QString fontFamily = Tools::cssFontDefinition(fontName(), /*onlyFontFamily=*/true); css += " font-family: " + fontFamily + ';'; } if (fontSize() > 0) css += " font-size: " + QString::number(fontSize()) + "px;"; if (backgroundColor().isValid()) { css += " background-color: " + backgroundColor().name() + ";"; } if (css.isEmpty()) return QString(); else return " .tag_" + id() + " {" + css + " }\n"; } void State::merge(const List &states, State *result, int *emblemsCount, bool *haveInvisibleTags, const QColor &backgroundColor) { *result = State(); // Reset to default values. *emblemsCount = 0; *haveInvisibleTags = false; for (List::const_iterator it = states.begin(); it != states.end(); ++it) { State *state = *it; bool isVisible = false; // For each property, if that properties have a value (is not default) is the current state of the list, // and if it haven't been set to the result state by a previous state, then it's visible and we assign the property to the result state. if (!state->emblem().isEmpty()) { ++*emblemsCount; isVisible = true; } if (state->bold() && !result->bold()) { result->setBold(true); isVisible = true; } if (state->italic() && !result->italic()) { result->setItalic(true); isVisible = true; } if (state->underline() && !result->underline()) { result->setUnderline(true); isVisible = true; } if (state->strikeOut() && !result->strikeOut()) { result->setStrikeOut(true); isVisible = true; } if (state->textColor().isValid() && !result->textColor().isValid()) { result->setTextColor(state->textColor()); isVisible = true; } if (!state->fontName().isEmpty() && result->fontName().isEmpty()) { result->setFontName(state->fontName()); isVisible = true; } if (state->fontSize() > 0 && result->fontSize() <= 0) { result->setFontSize(state->fontSize()); isVisible = true; } if (state->backgroundColor().isValid() && !result->backgroundColor().isValid() && state->backgroundColor() != backgroundColor) { // vv result->setBackgroundColor(state->backgroundColor()); // This is particular: if the note background color is the same as the basket one, don't use that. isVisible = true; } // If it's not visible, well, at least one tag is not visible: the note will display "..." at the tags arrow place to show that: if (!isVisible) *haveInvisibleTags = true; } } void State::copyTo(State *other) { other->m_id = m_id; other->m_name = m_name; other->m_emblem = m_emblem; other->m_bold = m_bold; other->m_italic = m_italic; other->m_underline = m_underline; other->m_strikeOut = m_strikeOut; other->m_textColor = m_textColor; other->m_fontName = m_fontName; other->m_fontSize = m_fontSize; other->m_backgroundColor = m_backgroundColor; other->m_textEquivalent = m_textEquivalent; other->m_onAllTextLines = m_onAllTextLines; // TODO other->m_allowCrossReferences = m_allowCrossReferences; // TODO: other->m_parentTag; } /** class Tag: */ Tag::List Tag::all = Tag::List(); long Tag::nextStateUid = 1; long Tag::getNextStateUid() { return nextStateUid++; // Return the next Uid and THEN increment the Uid } Tag::Tag() { static int tagNumber = 0; ++tagNumber; QString sAction = "tag_shortcut_number_" + QString::number(tagNumber); KActionCollection *ac = Global::bnpView->actionCollection(); m_action = ac->addAction(sAction, Global::bnpView, SLOT(activatedTagShortcut())); m_action->setText("FAKE TEXT"); m_action->setIcon(QIcon::fromTheme("FAKE ICON")); ac->setShortcutsConfigurable(m_action, false); // We do it in the tag properties dialog m_inheritedBySiblings = false; } Tag::~Tag() { delete m_action; } void Tag::setName(const QString &name) { m_name = name; m_action->setText("TAG SHORTCUT: " + name); // TODO: i18n (for debug purpose only by now). } State *Tag::stateForId(const QString &id) { for (List::iterator it = all.begin(); it != all.end(); ++it) for (State::List::iterator it2 = (*it)->states().begin(); it2 != (*it)->states().end(); ++it2) if ((*it2)->id() == id) return *it2; return nullptr; } Tag *Tag::tagForKAction(QAction *action) { for (List::iterator it = all.begin(); it != all.end(); ++it) if ((*it)->m_action == action) return *it; return nullptr; } QMap Tag::loadTags(const QString &path /* = QString()*/ /*, bool merge = false*/) { QMap mergedStates; bool merge = !path.isEmpty(); QString fullPath = (merge ? path : Global::savesFolder() + "tags.xml"); QString doctype = "basketTags"; QDir dir; if (!dir.exists(fullPath)) { if (merge) return mergedStates; DEBUG_WIN << "Tags file does not exist: Creating it..."; createDefaultTagsSet(fullPath); } QScopedPointer document(XMLWork::openFile(doctype, fullPath)); if (!document) { DEBUG_WIN << "FAILED to read the tags file"; return mergedStates; } QDomElement docElem = document->documentElement(); if (!merge) nextStateUid = docElem.attribute("nextStateUid", QString::number(nextStateUid)).toLong(); QDomNode node = docElem.firstChild(); while (!node.isNull()) { QDomElement element = node.toElement(); if ((!element.isNull()) && element.tagName() == "tag") { Tag *tag = new Tag(); // Load properties: QString name = XMLWork::getElementText(element, "name"); QString shortcut = XMLWork::getElementText(element, "shortcut"); QString inherited = XMLWork::getElementText(element, "inherited", "false"); tag->setName(name); tag->setShortcut(QKeySequence(shortcut)); tag->setInheritedBySiblings(XMLWork::trueOrFalse(inherited)); // Load states: QDomNode subNode = element.firstChild(); while (!subNode.isNull()) { QDomElement subElement = subNode.toElement(); if ((!subElement.isNull()) && subElement.tagName() == "state") { State *state = new State(subElement.attribute("id"), tag); state->setName(XMLWork::getElementText(subElement, "name")); state->setEmblem(XMLWork::getElementText(subElement, "emblem")); QDomElement textElement = XMLWork::getElement(subElement, "text"); state->setBold(XMLWork::trueOrFalse(textElement.attribute("bold", "false"))); state->setItalic(XMLWork::trueOrFalse(textElement.attribute("italic", "false"))); state->setUnderline(XMLWork::trueOrFalse(textElement.attribute("underline", "false"))); state->setStrikeOut(XMLWork::trueOrFalse(textElement.attribute("strikeOut", "false"))); QString textColor = textElement.attribute("color", QString()); state->setTextColor(textColor.isEmpty() ? QColor() : QColor(textColor)); QDomElement fontElement = XMLWork::getElement(subElement, "font"); state->setFontName(fontElement.attribute("name", QString())); QString fontSize = fontElement.attribute("size", QString()); state->setFontSize(fontSize.isEmpty() ? -1 : fontSize.toInt()); QString backgroundColor = XMLWork::getElementText(subElement, "backgroundColor", QString()); state->setBackgroundColor(backgroundColor.isEmpty() ? QColor() : QColor(backgroundColor)); QDomElement textEquivalentElement = XMLWork::getElement(subElement, "textEquivalent"); state->setTextEquivalent(textEquivalentElement.attribute("string", QString())); state->setOnAllTextLines(XMLWork::trueOrFalse(textEquivalentElement.attribute("onAllTextLines", "false"))); QString allowXRef = XMLWork::getElementText(subElement, "allowCrossReferences", "true"); state->setAllowCrossReferences(XMLWork::trueOrFalse(allowXRef)); tag->appendState(state); } subNode = subNode.nextSibling(); } // If the Tag is Valid: if (tag->countStates() > 0) { // Rename Things if Needed: State *firstState = tag->states().first(); if (tag->countStates() == 1 && firstState->name().isEmpty()) firstState->setName(tag->name()); if (tag->name().isEmpty()) tag->setName(firstState->name()); // Add or Merge the Tag: if (!merge) { all.append(tag); } else { Tag *similarTag = tagSimilarTo(tag); // Tag does not exists, add it: if (similarTag == nullptr) { // We are merging the new states, so we should choose new and unique (on that computer) ids for those states: for (State::List::iterator it = tag->states().begin(); it != tag->states().end(); ++it) { State *state = *it; QString uid = state->id(); QString newUid = "tag_state_" + QString::number(getNextStateUid()); state->setId(newUid); mergedStates[uid] = newUid; } // TODO: if shortcut is already assigned to a previous note, do not import it, keep the user settings! all.append(tag); // Tag already exists, rename to their ids: } else { State::List::iterator it2 = similarTag->states().begin(); for (State::List::iterator it = tag->states().begin(); it != tag->states().end(); ++it, ++it2) { State *state = *it; State *similarState = *it2; QString uid = state->id(); QString newUid = similarState->id(); if (uid != newUid) mergedStates[uid] = newUid; } delete tag; // Already exists, not to be merged. Delete the shortcut and all. } } } } node = node.nextSibling(); } return mergedStates; } Tag *Tag::tagSimilarTo(Tag *tagToTest) { // Tags are considered similar if they have the same name, the same number of states, in the same order, and the same look. // Keyboard shortcut, text equivalent and onEveryLines are user settings, and thus not considered during the comparison. // Default tags (To Do, Important, Idea...) do not take into account the name of the tag and states during the comparison. // Default tags are equal only if they have the same number of states, in the same order, and the same look. // This is because default tag names are translated differently in every countries, but they are essentially the same! // User tags begins with "tag_state_" followed by a number. Default tags are the other ones. // Browse all tags: for (List::iterator it = all.begin(); it != all.end(); ++it) { Tag *tag = *it; bool same = true; bool sameName; bool defaultTag = true; // We test only name and look. Shortcut and whenever it is inherited by sibling new notes are user settings only! sameName = tag->name() == tagToTest->name(); if (tag->countStates() != tagToTest->countStates()) continue; // Tag is different! // We found a tag with same name, check if every states/look are same too: State::List::iterator itTest = tagToTest->states().begin(); for (State::List::iterator it2 = (*it)->states().begin(); it2 != (*it)->states().end(); ++it2, ++itTest) { State *state = *it2; State *stateToTest = *itTest; if (state->id().startsWith(QLatin1String("tag_state_")) || stateToTest->id().startsWith(QLatin1String("tag_state_"))) { defaultTag = false; } if (state->name() != stateToTest->name()) { sameName = false; } if (state->emblem() != stateToTest->emblem()) { same = false; break; } if (state->bold() != stateToTest->bold()) { same = false; break; } if (state->italic() != stateToTest->italic()) { same = false; break; } if (state->underline() != stateToTest->underline()) { same = false; break; } if (state->strikeOut() != stateToTest->strikeOut()) { same = false; break; } if (state->textColor() != stateToTest->textColor()) { same = false; break; } if (state->fontName() != stateToTest->fontName()) { same = false; break; } if (state->fontSize() != stateToTest->fontSize()) { same = false; break; } if (state->backgroundColor() != stateToTest->backgroundColor()) { same = false; break; } // Text equivalent (as well as onAllTextLines) is also a user setting! } // We found an existing tag that is "exactly" the same: if (same && (sameName || defaultTag)) return tag; } // Not found: return nullptr; } void Tag::saveTags() { DEBUG_WIN << "Saving tags..."; saveTagsTo(all, Global::savesFolder() + "tags.xml"); GitWrapper::commitTagsXml(); } void Tag::saveTagsTo(QList &list, const QString &fullPath) { // Create Document: QDomDocument document(/*doctype=*/"basketTags"); QDomElement root = document.createElement("basketTags"); root.setAttribute("nextStateUid", static_cast(nextStateUid)); document.appendChild(root); // Save all tags: for (List::iterator it = list.begin(); it != list.end(); ++it) { Tag *tag = *it; // Create tag node: QDomElement tagNode = document.createElement("tag"); root.appendChild(tagNode); // Save tag properties: XMLWork::addElement(document, tagNode, "name", tag->name()); XMLWork::addElement(document, tagNode, "shortcut", tag->shortcut().toString()); XMLWork::addElement(document, tagNode, "inherited", XMLWork::trueOrFalse(tag->inheritedBySiblings())); // Save all states: for (State::List::iterator it2 = (*it)->states().begin(); it2 != (*it)->states().end(); ++it2) { State *state = *it2; // Create state node: QDomElement stateNode = document.createElement("state"); tagNode.appendChild(stateNode); // Save state properties: stateNode.setAttribute("id", state->id()); XMLWork::addElement(document, stateNode, "name", state->name()); XMLWork::addElement(document, stateNode, "emblem", state->emblem()); QDomElement textNode = document.createElement("text"); stateNode.appendChild(textNode); QString textColor = (state->textColor().isValid() ? state->textColor().name() : QString()); textNode.setAttribute("bold", XMLWork::trueOrFalse(state->bold())); textNode.setAttribute("italic", XMLWork::trueOrFalse(state->italic())); textNode.setAttribute("underline", XMLWork::trueOrFalse(state->underline())); textNode.setAttribute("strikeOut", XMLWork::trueOrFalse(state->strikeOut())); textNode.setAttribute("color", textColor); QDomElement fontNode = document.createElement("font"); stateNode.appendChild(fontNode); fontNode.setAttribute("name", state->fontName()); fontNode.setAttribute("size", state->fontSize()); QString backgroundColor = (state->backgroundColor().isValid() ? state->backgroundColor().name() : QString()); XMLWork::addElement(document, stateNode, "backgroundColor", backgroundColor); QDomElement textEquivalentNode = document.createElement("textEquivalent"); stateNode.appendChild(textEquivalentNode); textEquivalentNode.setAttribute("string", state->textEquivalent()); textEquivalentNode.setAttribute("onAllTextLines", XMLWork::trueOrFalse(state->onAllTextLines())); XMLWork::addElement(document, stateNode, "allowCrossReferences", XMLWork::trueOrFalse(state->allowCrossReferences())); } } // Write to Disk: - if (!BasketScene::safelySaveToFile(fullPath, "\n" + document.toString())) + if (!FileStorage::safelySaveToFile(fullPath, "\n" + document.toString())) DEBUG_WIN << "FAILED to save tags!"; } void Tag::copyTo(Tag *other) { other->m_name = m_name; other->m_action->setShortcut(m_action->shortcut()); other->m_inheritedBySiblings = m_inheritedBySiblings; } void Tag::createDefaultTagsSet(const QString &fullPath) { QString xml = QString( "\n" "\n" " \n" " %1\n" // "To Do" " Ctrl+1\n" " true\n" " \n" " %2\n" // "Unchecked" " tag_checkbox\n" " \n" " \n" " \n" " \n" " \n" " \n" " %3\n" // "Done" " tag_checkbox_checked\n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" " \n" " %4\n" // "Progress" " Ctrl+2\n" " true\n" " \n" " %5\n" // "0 %" " tag_progress_000\n" " \n" " \n" " \n" " %6\n" // "25 %" " tag_progress_025\n" " \n" " \n" " \n" " %7\n" // "50 %" " tag_progress_050\n" " \n" " \n" " \n" " %8\n" // "75 %" " tag_progress_075\n" " \n" " \n" " \n" " %9\n" // "100 %" " tag_progress_100\n" " \n" " \n" " \n" "\n") .arg(i18n("To Do"), i18n("Unchecked"), i18n("Done")) // %1 %2 %3 .arg(i18n("Progress"), i18n("0 %"), i18n("25 %")) // %4 %5 %6 .arg(i18n("50 %"), i18n("75 %"), i18n("100 %")) // %7 %8 %9 + QString( " \n" " %1\n" // "Priority" " Ctrl+3\n" " true\n" " \n" " %2\n" // "Low" " tag_priority_low\n" " \n" " \n" " \n" " %3\n" // "Medium " tag_priority_medium\n" " \n" " \n" " \n" " %4\n" // "High" " tag_priority_high\n" " \n" " \n" " \n" "\n" " \n" " %5\n" // "Preference" " Ctrl+4\n" " true\n" " \n" " %6\n" // "Bad" " tag_preference_bad\n" " \n" " \n" " \n" " %7\n" // "Good" " tag_preference_good\n" " \n" " \n" " \n" " %8\n" // "Excellent" " tag_preference_excellent\n" " \n" " \n" " \n" "\n" " \n" " %9\n" // "Highlight" " Ctrl+5\n" " \n" " #ffffcc\n" " \" />\n" " \n" " \n" "\n") .arg(i18n("Priority"), i18n("Low"), i18n("Medium")) // %1 %2 %3 .arg(i18n("High"), i18n("Preference"), i18n("Bad")) // %4 %5 %6 .arg(i18n("Good"), i18n("Excellent"), i18n("Highlight")) // %7 %8 %9 + QString( " \n" " %1\n" // "Important" " Ctrl+6\n" " \n" " tag_important\n" " #ffcccc\n" " \n" " \n" " \n" "\n" " \n" " %2\n" // "Very Important" " Ctrl+7\n" " \n" " tag_important\n" " \n" " #ff0000\n" " \n" " \n" " \n" "\n" " \n" " %3\n" // "Information" " Ctrl+8\n" " \n" " dialog-information\n" " \n" " \n" " \n" "\n" " \n" " %4\n" // "Idea" " Ctrl+9\n" " \n" " ktip\n" " \n" // I. " \n" " " "\n" "\n" " \n" " %6\n" // "Title" " Ctrl+0\n" " \n" " \n" " \n" " \n" " \n" "\n" " \n" " %7\n" // "Code" " \n" " \n" " \n" " false\n" " \n" " \n" "\n" " \n" " \n" " %8\n" // "Work" " \n" " \n" // W. " \n" " " "\n" "\n") .arg(i18n("Important"), i18n("Very Important"), i18n("Information")) // %1 %2 %3 .arg(i18n("Idea"), i18nc("The initial of 'Idea'", "I."), i18n("Title")) // %4 %5 %6 .arg(i18n("Code"), i18n("Work"), i18nc("The initial of 'Work'", "W.")) // %7 %8 %9 + QString( " \n" " \n" " %1\n" // "Personal" " \n" " \n" // P. " \n" " \n" "\n" " \n" " \n" " %3\n" // "Funny" " tag_fun\n" " \n" " \n" "\n" "") .arg(i18n("Personal"), i18nc("The initial of 'Personal'", "P."), i18n("Funny")); // %1 %2 %3 // Write to Disk: QFile file(fullPath); if (file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); stream.setCodec("UTF-8"); stream << "\n"; stream << xml; file.close(); } else DEBUG_WIN << "FAILED to create the tags file!"; } // StateAction StateAction::StateAction(State *state, const QKeySequence &shortcut, QWidget *parent, bool withTagName) : KToggleAction(parent) , m_state(state) { setText(m_state->name()); if (withTagName && m_state->parentTag()) setText(m_state->parentTag()->name()); setIcon(QIcon(m_state->emblem())); setShortcut(shortcut); } StateAction::~StateAction() { // pass }