File indexing completed on 2024-04-21 16:32:00

0001 /**
0002  * SPDX-FileCopyrightText: (C) 2003 Sébastien Laoût <slaout@linux62.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "notefactory.h"
0008 
0009 #include <QDomElement>
0010 #include <QFileDialog>
0011 #include <QGraphicsView>
0012 #include <QGuiApplication>
0013 #include <QLocale>
0014 #include <QMenu>
0015 #include <QMimeDatabase>
0016 #include <QMimeType>
0017 #include <QUrl>
0018 #include <QtCore/QDir>
0019 #include <QtCore/QFile>
0020 #include <QtCore/QFileInfo>
0021 #include <QtCore/QMimeData>
0022 #include <QtCore/QRegExp>
0023 #include <QtCore/QString>
0024 #include <QtCore/QTextStream>
0025 #include <QtCore/QVector>
0026 #include <QtCore/qnamespace.h>
0027 #include <QtGui/QBitmap> //For createHeuristicMask
0028 #include <QtGui/QColor>
0029 #include <QtGui/QImage>
0030 #include <QtGui/QImageReader>
0031 #include <QtGui/QMovie>
0032 #include <QtGui/QPixmap>
0033 #include <QtGui/QTextDocument> //For Qt::mightBeRichText(...)
0034 
0035 #include <KAboutData> //For KGlobal::mainComponent().aboutData(...)
0036 #include <KIconDialog>
0037 #include <KIconLoader>
0038 #include <KLocalizedString>
0039 #include <KMessageBox>
0040 #include <KModifierKeyInfo>
0041 #include <KOpenWithDialog>
0042 #include <KUriFilter>
0043 
0044 #include <KIO/CopyJob>
0045 
0046 #include "basketlistview.h"
0047 #include "basketscene.h"
0048 #include "file_mimetypes.h"
0049 #include "global.h"
0050 #include "note.h"
0051 #include "notedrag.h"
0052 #include "settings.h"
0053 #include "tools.h"
0054 #include "variouswidgets.h" //For IconSizeDialog
0055 #include "xmlwork.h"
0056 
0057 #include "debugwindow.h"
0058 
0059 /** Create notes from scratch (just a content) */
0060 
0061 Note *NoteFactory::createNoteText(const QString &text, BasketScene *parent, bool reallyPlainText /* = false*/)
0062 {
0063     QList<State*> tags; int tagsLength = 0; Note* note;
0064 
0065     if (Settings::detectTextTags())
0066     {
0067         tags = Tools::detectTags(text, tagsLength);
0068     }
0069     QString textConverted = text.mid(tagsLength);
0070 
0071     if (reallyPlainText)
0072     {
0073         note = new Note(parent);
0074         TextContent *content = new TextContent(note, createFileForNewNote(parent, "txt"));
0075         content->setText(text);
0076         content->saveToFile();
0077     }
0078         else
0079     {
0080         textConverted = QString("<html><head><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"><meta name=\"qrichtext\" content=\"1\" /></head><body>%1</body></html>")
0081             .arg(Tools::textToHTMLWithoutP(textConverted));
0082 
0083         note = createNoteHtml(textConverted, parent);
0084     }
0085 
0086     if (note)
0087     for (State* state: tags)
0088     {
0089         note->addState(state, true);
0090     }
0091 
0092     return note;
0093 }
0094 
0095 Note *NoteFactory::createNoteHtml(const QString &html, BasketScene *parent)
0096 {
0097     Note *note = new Note(parent);
0098     HtmlContent *content = new HtmlContent(note, createFileForNewNote(parent, "html"));
0099     content->setHtml(html);
0100     content->saveToFile();
0101     return note;
0102 }
0103 
0104 Note *NoteFactory::createNoteLink(const QUrl &url, BasketScene *parent)
0105 {
0106     Note *note = new Note(parent);
0107     new LinkContent(note, url, titleForURL(url), iconForURL(url), /*autoTitle=*/true, /*autoIcon=*/true);
0108     return note;
0109 }
0110 
0111 Note *NoteFactory::createNoteLink(const QUrl &url, const QString &title, BasketScene *parent)
0112 {
0113     Note *note = new Note(parent);
0114     new LinkContent(note, url, title, iconForURL(url), /*autoTitle=*/false, /*autoIcon=*/true);
0115     return note;
0116 }
0117 
0118 Note *NoteFactory::createNoteCrossReference(const QUrl &url, BasketScene *parent)
0119 {
0120     Note *note = new Note(parent);
0121     new CrossReferenceContent(note, url, titleForURL(url), iconForURL(url));
0122     return note;
0123 }
0124 
0125 Note *NoteFactory::createNoteCrossReference(const QUrl &url, const QString &title, BasketScene *parent)
0126 {
0127     Note *note = new Note(parent);
0128     new CrossReferenceContent(note, url, title, iconForURL(url));
0129     return note;
0130 }
0131 
0132 Note *NoteFactory::createNoteCrossReference(const QUrl &url, const QString &title, const QString &icon, BasketScene *parent)
0133 {
0134     Note *note = new Note(parent);
0135     new CrossReferenceContent(note, url, title, icon);
0136     return note;
0137 }
0138 
0139 Note *NoteFactory::createNoteImage(const QPixmap &image, BasketScene *parent)
0140 {
0141     Note *note = new Note(parent);
0142     ImageContent *content = new ImageContent(note, createFileForNewNote(parent, "png"));
0143     content->setPixmap(image);
0144     content->saveToFile();
0145     return note;
0146 }
0147 
0148 Note *NoteFactory::createNoteColor(const QColor &color, BasketScene *parent)
0149 {
0150     Note *note = new Note(parent);
0151     new ColorContent(note, color);
0152     return note;
0153 }
0154 
0155 /** Return a string list containing {url1, title1, url2, title2, url3, title3...}
0156  */
0157 QStringList NoteFactory::textToURLList(const QString &text)
0158 {
0159     // List to return:
0160     QStringList list;
0161 
0162     // Split lines:
0163     QStringList texts = text.split('\n');
0164 
0165     // For each lines:
0166     QStringList::iterator it;
0167     for (it = texts.begin(); it != texts.end(); ++it) {
0168         // Strip white spaces:
0169         (*it) = (*it).trimmed();
0170 
0171         // Don't care of empty entries:
0172         if ((*it).isEmpty())
0173             continue;
0174 
0175         // Compute lower case equivalent:
0176         QString ltext = (*it).toLower();
0177 
0178         /* Search for mail address ("*@*.*" ; "*" can contain '_', '-', or '.') and add protocol to it */
0179         QString mailExpString = "[\\w-\\.]+@[\\w-\\.]+\\.[\\w]+";
0180         QRegExp mailExp("^" + mailExpString + '$');
0181         if (mailExp.exactMatch(ltext)) {
0182             ltext.insert(0, "mailto:");
0183             (*it).insert(0, "mailto:");
0184         }
0185 
0186         // TODO: Recognize "<link>" (link between '<' and '>')
0187         // TODO: Replace " at " by "@" and " dot " by "." to look for e-mail addresses
0188 
0189         /* Search for mail address like "Name <address@provider.net>" */
0190         QRegExp namedMailExp("^([\\w\\s]+)\\s<(" + mailExpString + ")>$");
0191         // namedMailExp.setCaseSensitive(true); // For the name to be keeped with uppercases // DOESN'T WORK !
0192         if (namedMailExp.exactMatch(ltext)) {
0193             QString name = namedMailExp.cap(1);
0194             QString address = "mailto:" + namedMailExp.cap(2);
0195             // Threat it NOW, as it's an exception (it have a title):
0196             list.append(address);
0197             list.append(name);
0198             continue;
0199         }
0200 
0201         /* Search for an url and create an URL note */
0202         if ((ltext.startsWith('/') && ltext[1] != '/' && ltext[1] != '*') || // Take files but not C/C++/... comments !
0203             ltext.startsWith(QLatin1String("file:")) || ltext.startsWith(QLatin1String("http://")) || ltext.startsWith(QLatin1String("https://")) || ltext.startsWith(QLatin1String("www.")) || ltext.startsWith(QLatin1String("ftp.")) ||
0204             ltext.startsWith(QLatin1String("ftp://")) || ltext.startsWith(QLatin1String("mailto:"))) {
0205             // First, correct the text to use the good format for the url
0206             if (ltext.startsWith('/'))
0207                 (*it).insert(0, "file:");
0208             if (ltext.startsWith(QLatin1String("www.")))
0209                 (*it).insert(0, "http://");
0210             if (ltext.startsWith(QLatin1String("ftp.")))
0211                 (*it).insert(0, "ftp://");
0212 
0213             // And create the Url note (or launcher if URL point a .desktop file)
0214             list.append(*it);
0215             list.append(QString()); // We don't have any title
0216         } else
0217             return QStringList(); // FAILED: treat the text as a text, and not as a URL list!
0218     }
0219     return list;
0220 }
0221 
0222 Note *NoteFactory::createNoteFromText(const QString &text, BasketScene *parent)
0223 {
0224     /* Search for a color (#RGB , #RRGGBB , #RRRGGGBBB , #RRRRGGGGBBBB) and create a color note */
0225     QRegExp exp("^#(?:[a-fA-F\\d]{3}){1,4}$");
0226     if (exp.exactMatch(text))
0227         return createNoteColor(QColor(text), parent);
0228 
0229     /* Try to convert the text as a URL or a list of URLs */
0230     QStringList uriList = textToURLList(text);
0231     if (!uriList.isEmpty()) {
0232         // TODO: This code is almost duplicated from fropURLs()!
0233         Note *note;
0234         Note *firstNote = nullptr;
0235         Note *lastInserted = nullptr;
0236         QStringList::iterator it;
0237         for (it = uriList.begin(); it != uriList.end(); ++it) {
0238             QString url = (*it);
0239             ++it;
0240             QString title = (*it);
0241             if (title.isEmpty())
0242                 note = createNoteLinkOrLauncher(QUrl::fromUserInput(url), parent);
0243             else
0244                 note = createNoteLink(QUrl::fromUserInput(url), title, parent);
0245 
0246             // If we got a new note, insert it in a linked list (we will return the first note of that list):
0247             if (note) {
0248                 //              qDebug() << "Drop URL: " << (*it).toDisplayString();
0249                 if (!firstNote)
0250                     firstNote = note;
0251                 else {
0252                     lastInserted->setNext(note);
0253                     note->setPrev(lastInserted);
0254                 }
0255                 lastInserted = note;
0256             }
0257         }
0258         return firstNote; // It don't return ALL inserted notes !
0259     }
0260 
0261     // QString newText = text.trimmed(); // The text for a new note, without useless spaces
0262     /* Else, it's a text or an HTML note, so, create it */
0263     if (Qt::mightBeRichText(/*newT*/ text))
0264         return createNoteHtml(/*newT*/ text, parent);
0265     else
0266         return createNoteText(/*newT*/ text, parent);
0267 }
0268 
0269 Note *NoteFactory::createNoteLauncher(const QUrl &url, BasketScene *parent)
0270 {
0271     if (url.isEmpty())
0272         return createNoteLauncher(QString(), QString(), QString(), parent);
0273     else
0274         return copyFileAndLoad(url, parent);
0275 }
0276 
0277 Note *NoteFactory::createNoteLauncher(const QString &command, const QString &name, const QString &icon, BasketScene *parent)
0278 {
0279     QString fileName = createNoteLauncherFile(command, name, icon, parent);
0280     if (fileName.isEmpty())
0281         return nullptr;
0282     else
0283         return loadFile(fileName, parent);
0284 }
0285 
0286 QString NoteFactory::createNoteLauncherFile(const QString &command, const QString &name, const QString &icon, BasketScene *parent)
0287 {
0288     QString content = QString(
0289                           "[Desktop Entry]\n"
0290                           "Exec=%1\n"
0291                           "Name=%2\n"
0292                           "Icon=%3\n"
0293                           "Encoding=UTF-8\n"
0294                           "Type=Application\n")
0295                           .arg(command, name, icon.isEmpty() ? QString("exec") : icon);
0296     QString fileName = fileNameForNewNote(parent, "launcher.desktop");
0297     QString fullPath = parent->fullPathForFileName(fileName);
0298     //  parent->dontCareOfCreation(fullPath);
0299     QFile file(fullPath);
0300     if (file.open(QIODevice::WriteOnly)) {
0301         QTextStream stream(&file);
0302         stream.setCodec("UTF-8");
0303         stream << content;
0304         file.close();
0305         return fileName;
0306     } else
0307         return QString();
0308 }
0309 
0310 Note *NoteFactory::createNoteLinkOrLauncher(const QUrl &url, BasketScene *parent)
0311 {
0312     // IMPORTANT: we create the service ONLY if the extension is ".desktop".
0313     //            Otherwise, KService take a long time to analyze all the file
0314     //            and output such things to stdout:
0315     //            "Invalid entry (missing '=') at /my/file.ogg:11984"
0316     //            "Invalid entry (missing ']') at /my/file.ogg:11984"...
0317     KService::Ptr service;
0318     if (url.fileName().endsWith(QLatin1String(".desktop")))
0319         service = new KService(url.path());
0320 
0321     // If link point to a .desktop file then add a launcher, otherwise it's a link
0322     if (service && service->isValid())
0323         return createNoteLauncher(url, parent);
0324     else
0325         return createNoteLink(url, parent);
0326 }
0327 
0328 bool NoteFactory::movingNotesInTheSameBasket(const QMimeData *source, BasketScene *parent, Qt::DropAction action)
0329 {
0330     if (NoteDrag::canDecode(source))
0331         return action == Qt::MoveAction && NoteDrag::basketOf(source) == parent;
0332     else
0333         return false;
0334 }
0335 
0336 Note *NoteFactory::dropNote(const QMimeData *source, BasketScene *parent, bool fromDrop, Qt::DropAction action, Note * /*noteSource*/)
0337 {
0338     if (source == nullptr) {
0339         return nullptr;
0340     }
0341 
0342     Note *note = nullptr;
0343 
0344     QStringList formats = source->formats();
0345     /* No data */
0346     if (formats.size() == 0) {
0347         // TODO: add a parameter to say if it's from a clipboard paste, a selection paste, or a drop
0348         //       To be able to say "The clipboard/selection/drop is empty".
0349         //      KMessageBox::error(parent, i18n("There is no data to insert."), i18n("No Data"));
0350         return nullptr;
0351     }
0352 
0353     /* Debug */
0354     if (Global::debugWindow) {
0355         *Global::debugWindow << "<b>Drop :</b>";
0356         for (int i = 0; i < formats.size(); ++i)
0357             *Global::debugWindow << "\t[" + QString::number(i) + "] " + formats[i];
0358         switch (action) { // The source want that we:
0359         case Qt::CopyAction:
0360             *Global::debugWindow << ">> Drop action: Copy";
0361             break;
0362         case Qt::MoveAction:
0363             *Global::debugWindow << ">> Drop action: Move";
0364             break;
0365         case Qt::LinkAction:
0366             *Global::debugWindow << ">> Drop action: Link";
0367             break;
0368         default:
0369             *Global::debugWindow << ">> Drop action: Unknown"; //  supported by Qt!
0370         }
0371     }
0372 
0373     /* Copy or move a Note */
0374     if (NoteDrag::canDecode(source)) {
0375         bool moveFiles = fromDrop && action == Qt::MoveAction;
0376         bool moveNotes = moveFiles;
0377         return NoteDrag::decode(source, parent, moveFiles, moveNotes); // Filename will be kept
0378     }
0379 
0380     /* Else : Drop object to note */
0381 
0382     QImage image = qvariant_cast<QImage>(source->imageData());
0383     if (!image.isNull())
0384         return createNoteImage(QPixmap::fromImage(image), parent);
0385 
0386     if (source->hasColor()) {
0387         return createNoteColor(qvariant_cast<QColor>(source->colorData()), parent);
0388     }
0389 
0390     // And then the hack (if provide color MIME type or a text that contains color), using createNote Color RegExp:
0391     QString hack;
0392     QRegExp exp("^#(?:[a-fA-F\\d]{3}){1,4}$");
0393     hack = source->text();
0394     if (source->hasFormat("application/x-color") || (!hack.isNull() && exp.exactMatch(hack))) {
0395         QColor color = qvariant_cast<QColor>(source->colorData());
0396         if (color.isValid())
0397             return createNoteColor(color, parent);
0398         //          if ( (note = createNoteColor(color, parent)) )
0399         //              return note;
0400         //          // Theoretically it should be returned. If not, continue by dropping other things
0401     }
0402 
0403     QList<QUrl> urls = source->urls();
0404     if (!urls.isEmpty()) {
0405         // If it's a Paste, we should know if files should be copied (copy&paste) or moved (cut&paste):
0406         if (!fromDrop && Tools::isAFileCut(source))
0407             action = Qt::MoveAction;
0408         return dropURLs(urls, parent, action, fromDrop);
0409     }
0410 
0411     // FIXME: use dropURLs() also from Mozilla?
0412 
0413     /*
0414      * Mozilla's stuff sometimes uses utf-16-le - little-endian UTF-16.
0415      *
0416      * This has the property that for the ASCII subset case (And indeed, the
0417      * ISO-8859-1 subset, I think), if you treat it as a C-style string,
0418      * it'll come out to one character long in most cases, since it looks
0419      * like:
0420      *
0421      * "<\0H\0T\0M\0L\0>\0"
0422      *
0423      * A strlen() call on that will give you 1, which simply isn't correct.
0424      * That might, I suppose, be the answer, or something close.
0425      *
0426      * Also, Mozilla's drag/drop code predates the use of MIME types in XDnD
0427      * - hence it'll throw about STRING and UTF8_STRING quite happily, hence
0428      * the odd named types.
0429      *
0430      * Thanks to Dave Cridland for having said me that.
0431      */
0432     if (source->hasFormat("text/x-moz-url")) { // FOR MOZILLA
0433         // Get the array and create a QChar array of 1/2 of the size
0434         QByteArray mozilla = source->data("text/x-moz-url");
0435         QVector<QChar> chars(mozilla.count() / 2);
0436         // A small debug work to know the value of each bytes
0437         if (Global::debugWindow)
0438             for (int i = 0; i < mozilla.count(); i++)
0439                 *Global::debugWindow << QString("'") + QChar(mozilla[i]) + "' " + QString::number(int(mozilla[i]));
0440         // text/x-moz-url give the URL followed by the link title and separated by OxOA (10 decimal: new line?)
0441         uint size = 0;
0442         QChar *name = nullptr;
0443         // For each little endian mozilla chars, copy it to the array of QChars
0444         for (int i = 0; i < mozilla.count(); i += 2) {
0445             chars[i / 2] = QChar(mozilla[i], mozilla[i + 1]);
0446             if (mozilla.at(i) == 0x0A) {
0447                 size = i / 2;
0448                 name = &(chars[i / 2 + 1]);
0449             }
0450         }
0451         // Create a QString that take the address of the first QChar and a length
0452         if (name == nullptr) { // We haven't found name (FIXME: Is it possible ?)
0453             QString normalHtml(&(chars[0]), chars.size());
0454             return createNoteLink(normalHtml, parent);
0455         } else {
0456             QString normalHtml(&(chars[0]), size);
0457             QString normalTitle(name, chars.size() - size - 1);
0458             return createNoteLink(normalHtml, normalTitle, parent);
0459         }
0460     }
0461 
0462     if (source->hasFormat("text/html")) {
0463         QString html;
0464         QString subtype("html");
0465         // If the text/html comes from Mozilla or GNOME it can be UTF-16 encoded: we need ExtendedTextDrag to check that
0466         ExtendedTextDrag::decode(source, html, subtype);
0467         return createNoteHtml(html, parent);
0468     }
0469 
0470     QString text;
0471     // If the text/plain comes from GEdit or GNOME it can be empty: we need ExtendedTextDrag to check other MIME types
0472     if (ExtendedTextDrag::decode(source, text))
0473         return createNoteFromText(text, parent);
0474 
0475     /* Create a cross reference note */
0476 
0477     if (source->hasFormat(BasketTreeListView::TREE_ITEM_MIME_STRING)) {
0478         QByteArray data = source->data(BasketTreeListView::TREE_ITEM_MIME_STRING);
0479         QDataStream stream(&data, QIODevice::ReadOnly);
0480         QString basketName, folderName, icon;
0481 
0482         while (!stream.atEnd())
0483             stream >> basketName >> folderName >> icon;
0484 
0485         return createNoteCrossReference(QUrl("basket://" + folderName), basketName, icon, parent);
0486     }
0487 
0488     /* Unsuccessful drop */
0489     note = createNoteUnknown(source, parent);
0490     QString message = i18n(
0491         "<p>%1 doesn't support the data you've dropped.<br>"
0492         "It however created a generic note, allowing you to drag or copy it to an application that understand it.</p>"
0493         "<p>If you want the support of these data, please contact developer.</p>",
0494         QGuiApplication::applicationDisplayName());
0495     KMessageBox::information(parent->graphicsView()->viewport(), message, i18n("Unsupported MIME Type(s)"), "unsupportedDropInfo", KMessageBox::AllowLink);
0496     return note;
0497 }
0498 
0499 Note *NoteFactory::createNoteUnknown(const QMimeData *source, BasketScene *parent /*, const QString &annotations*/)
0500 {
0501     // Save the MimeSource in a file: create and open the file:
0502     QString fileName = createFileForNewNote(parent, "unknown");
0503     QFile file(parent->fullPath() + fileName);
0504     if (!file.open(QIODevice::WriteOnly))
0505         return nullptr;
0506     QDataStream stream(&file);
0507 
0508     // Echo MIME types:
0509     QStringList formats = source->formats();
0510     for (int i = 0; i < formats.size(); ++i)
0511         stream << QString(formats[i]); // Output the '\0'-terminated format name string
0512 
0513     // Echo end of MIME types list delimiter:
0514     stream << QString();
0515 
0516     // Echo the length (in bytes) and then the data, and then same for next MIME type:
0517     for (int i = 0; i < formats.size(); ++i) {
0518         QByteArray data = source->data(formats[i]);
0519         stream << (quint32)data.count();
0520         stream.writeRawData(data.data(), data.count());
0521     }
0522     file.close();
0523 
0524     Note *note = new Note(parent);
0525     new UnknownContent(note, fileName);
0526     return note;
0527 }
0528 
0529 Note *NoteFactory::dropURLs(QList<QUrl> urls, BasketScene *parent, Qt::DropAction action, bool fromDrop)
0530 {
0531     KModifierKeyInfo keyinfo;
0532     int shouldAsk = 0; // shouldAsk==0: don't ask ; shouldAsk==1: ask for "file" ; shouldAsk>=2: ask for "files"
0533     bool shiftPressed = keyinfo.isKeyPressed(Qt::Key_Shift);
0534     bool ctrlPressed = keyinfo.isKeyPressed(Qt::Key_Control);
0535     bool modified = fromDrop && (shiftPressed || ctrlPressed);
0536 
0537     if (modified)        // Then no menu + modified action
0538         ;                // action is already set: no work to do
0539     else if (fromDrop) { // Compute if user should be asked or not
0540         for (QList<QUrl>::iterator it = urls.begin(); it != urls.end(); ++it)
0541             if ((*it).scheme() != "mailto") { // Do not ask when dropping mail address :-)
0542                 shouldAsk++;
0543                 if (shouldAsk == 1 /*2*/) // Sufficient
0544                     break;
0545             }
0546         if (shouldAsk) {
0547             QMenu menu(parent->graphicsView());
0548             QList<QAction *> actList;
0549             actList << new QAction(QIcon::fromTheme("go-jump"), i18n("&Move Here\tShift"), &menu) << new QAction(QIcon::fromTheme("edit-copy"), i18n("&Copy Here\tCtrl"), &menu)
0550                     << new QAction(QIcon::fromTheme("insert-link"), i18n("&Link Here\tCtrl+Shift"), &menu);
0551 
0552             Q_FOREACH (QAction *a, actList)
0553                 menu.addAction(a);
0554 
0555             menu.addSeparator();
0556             menu.addAction(QIcon::fromTheme("dialog-cancel"), i18n("C&ancel\tEscape"));
0557             int id = actList.indexOf(menu.exec(QCursor::pos()));
0558             switch (id) {
0559             case 0:
0560                 action = Qt::MoveAction;
0561                 break;
0562             case 1:
0563                 action = Qt::CopyAction;
0564                 break;
0565             case 2:
0566                 action = Qt::LinkAction;
0567                 break;
0568             default:
0569                 return nullptr;
0570             }
0571             modified = true;
0572         }
0573     } else { // fromPaste
0574         ;
0575     }
0576 
0577     /* Policy of drops of URL:
0578     *   Email: [Modifier keys: Useless]
0579     +    - Link mail address
0580     *   Remote URL: [Modifier keys: {Copy,Link}]
0581     +    - Download as Image, Animation and Launcher
0582     +    - Link other URLs
0583     *   Local URL: [Modifier keys: {Copy,Move,Link}]
0584     *    - Copy as Image, Animation and Launcher [Modifier keys: {Copy,Move,Link}]
0585     *    - Link folder [Modifier keys: Useless]
0586     *    - Make Launcher of executable [Modifier keys: {Copy_exec,Move_exec,Link_Launcher}]
0587     *    - Ask for file (if use want to copy and it is a sound: make Sound)
0588     * Policy of pastes of URL: [NO modifier keys]
0589     *   - Same as drops
0590     *   - But copy when ask should be done
0591     *   - Unless cut-selection is true: move files instead
0592     * Policy of file created in the basket dir: [NO modifier keys]
0593     *   - View as Image, Animation, Sound, Launcher
0594     *   - View as File
0595     */
0596     Note *note;
0597     Note *firstNote = nullptr;
0598     Note *lastInserted = nullptr;
0599     for (QList<QUrl>::iterator it = urls.begin(); it != urls.end(); ++it) {
0600         if (((*it).scheme() == "mailto") || (action == Qt::LinkAction))
0601             note = createNoteLinkOrLauncher(*it, parent);
0602         //         else if (!(*it).isLocalFile()) {
0603         //             if (action != Qt::LinkAction && (maybeImageOrAnimation(*it)/* || maybeSound(*it)*/))
0604         //                 note = copyFileAndLoad(*it, parent);
0605         //             else
0606         //                 note = createNoteLinkOrLauncher(*it, parent);
0607         //         }
0608         else {
0609             if (action == Qt::CopyAction)
0610                 note = copyFileAndLoad(*it, parent);
0611             else if (action == Qt::MoveAction)
0612                 note = moveFileAndLoad(*it, parent);
0613             else
0614                 note = createNoteLinkOrLauncher(*it, parent);
0615         }
0616 
0617         // If we got a new note, insert it in a linked list (we will return the first note of that list):
0618         if (note) {
0619             DEBUG_WIN << "Drop URL: " + (*it).toDisplayString();
0620             if (!firstNote)
0621                 firstNote = note;
0622             else {
0623                 lastInserted->setNext(note);
0624                 note->setPrev(lastInserted);
0625             }
0626             lastInserted = note;
0627         }
0628     }
0629     return firstNote;
0630 }
0631 
0632 void NoteFactory::consumeContent(QDataStream &stream, NoteType::Id type)
0633 {
0634     if (type == NoteType::Link) {
0635         QUrl url;
0636         QString title, icon;
0637         quint64 autoTitle64, autoIcon64;
0638         stream >> url >> title >> icon >> autoTitle64 >> autoIcon64;
0639     } else if (type == NoteType::CrossReference) {
0640         QUrl url;
0641         QString title, icon;
0642         stream >> url >> title >> icon;
0643     } else if (type == NoteType::Color) {
0644         QColor color;
0645         stream >> color;
0646     }
0647 }
0648 
0649 Note *NoteFactory::decodeContent(QDataStream &stream, NoteType::Id type, BasketScene *parent)
0650 {
0651     /*  if (type == NoteType::Text) {
0652         QString text;
0653         stream >> text;
0654         return NoteFactory::createNoteText(text, parent);
0655     } else if (type == NoteType::Html) {
0656         QString html;
0657         stream >> html;
0658         return NoteFactory::createNoteHtml(html, parent);
0659     } else if (type == NoteType::Image) {
0660         QPixmap pixmap;
0661         stream >> pixmap;
0662         return NoteFactory::createNoteImage(pixmap, parent);
0663     } else */
0664     if (type == NoteType::Link) {
0665         QUrl url;
0666         QString title, icon;
0667         quint64 autoTitle64, autoIcon64;
0668         bool autoTitle, autoIcon;
0669         stream >> url >> title >> icon >> autoTitle64 >> autoIcon64;
0670         autoTitle = (bool)autoTitle64;
0671         autoIcon = (bool)autoIcon64;
0672         Note *note = NoteFactory::createNoteLink(url, parent);
0673         ((LinkContent *)(note->content()))->setLink(url, title, icon, autoTitle, autoIcon);
0674         return note;
0675     } else if (type == NoteType::CrossReference) {
0676         QUrl url;
0677         QString title, icon;
0678         stream >> url >> title >> icon;
0679         Note *note = NoteFactory::createNoteCrossReference(url, parent);
0680         ((CrossReferenceContent *)(note->content()))->setCrossReference(url, title, icon);
0681         return note;
0682     } else if (type == NoteType::Color) {
0683         QColor color;
0684         stream >> color;
0685         return NoteFactory::createNoteColor(color, parent);
0686     } else
0687         return nullptr; // NoteFactory::loadFile() is sufficient
0688 }
0689 
0690 bool NoteFactory::maybeText(const QMimeType &mimeType)
0691 {
0692     return mimeType.inherits(MimeTypes::TEXT);
0693 }
0694 
0695 bool NoteFactory::maybeHtml(const QMimeType &mimeType)
0696 {
0697     return mimeType.inherits(MimeTypes::HTML);
0698 }
0699 
0700 bool NoteFactory::maybeImage(const QMimeType &mimeType)
0701 {
0702     return mimeType.name().startsWith(MimeTypes::IMAGE);
0703 }
0704 
0705 bool NoteFactory::maybeAnimation(const QMimeType &mimeType)
0706 {
0707     return mimeType.inherits(MimeTypes::ANIMATION) || mimeType.name() == MimeTypes::ANIMATION_MNG;
0708 }
0709 
0710 bool NoteFactory::maybeSound(const QMimeType &mimeType)
0711 {
0712     return mimeType.name().startsWith(MimeTypes::AUDIO);
0713 }
0714 
0715 bool NoteFactory::maybeLauncher(const QMimeType &mimeType)
0716 {
0717     return mimeType.inherits(MimeTypes::LAUNCHER);
0718 }
0719 
0720 ////////////// NEW:
0721 
0722 Note *NoteFactory::copyFileAndLoad(const QUrl &url, BasketScene *parent)
0723 {
0724     QString fileName = fileNameForNewNote(parent, url.fileName());
0725     QString fullPath = parent->fullPathForFileName(fileName);
0726 
0727     if (Global::debugWindow)
0728         *Global::debugWindow << "copyFileAndLoad: " + url.toDisplayString() + " to " + fullPath;
0729 
0730     //  QString annotations = i18n("Original file: %1", url.toDisplayString());
0731     //  parent->dontCareOfCreation(fullPath);
0732 
0733     KIO::CopyJob *copyJob = KIO::copy(url, QUrl::fromLocalFile(fullPath), KIO::Overwrite | KIO::Resume);
0734     parent->connect(copyJob, &KIO::CopyJob::copyingDone, parent, &BasketScene::slotCopyingDone2);
0735 
0736     NoteType::Id type = typeForURL(url, parent); // Use the type of the original file because the target doesn't exist yet
0737     return loadFile(fileName, type, parent);
0738 }
0739 
0740 Note *NoteFactory::moveFileAndLoad(const QUrl &url, BasketScene *parent)
0741 {
0742     // Globally the same as copyFileAndLoad() but move instead of copy (KIO::move())
0743     QString fileName = fileNameForNewNote(parent, url.fileName());
0744     QString fullPath = parent->fullPathForFileName(fileName);
0745 
0746     if (Global::debugWindow)
0747         *Global::debugWindow << "moveFileAndLoad: " + url.toDisplayString() + " to " + fullPath;
0748 
0749     //  QString annotations = i18n("Original file: %1", url.toDisplayString());
0750     //  parent->dontCareOfCreation(fullPath);
0751 
0752     KIO::CopyJob *copyJob = KIO::move(url, QUrl::fromLocalFile(fullPath), KIO::Overwrite | KIO::Resume);
0753 
0754     parent->connect(copyJob, &KIO::CopyJob::copyingDone, parent, &BasketScene::slotCopyingDone2);
0755 
0756     NoteType::Id type = typeForURL(url, parent); // Use the type of the original file because the target doesn't exist yet
0757     return loadFile(fileName, type, parent);
0758 }
0759 
0760 Note *NoteFactory::loadFile(const QString &fileName, BasketScene *parent)
0761 {
0762     // The file MUST exists
0763     QFileInfo file(QUrl::fromLocalFile(parent->fullPathForFileName(fileName)).path());
0764     if (!file.exists())
0765         return nullptr;
0766 
0767     NoteType::Id type = typeForURL(parent->fullPathForFileName(fileName), parent);
0768     Note *note = loadFile(fileName, type, parent);
0769     return note;
0770 }
0771 
0772 Note *NoteFactory::loadFile(const QString &fileName, NoteType::Id type, BasketScene *parent)
0773 {
0774     Note *note = new Note(parent);
0775     switch (type) {
0776     case NoteType::Text:
0777         new TextContent(note, fileName);
0778         break;
0779     case NoteType::Html:
0780         new HtmlContent(note, fileName);
0781         break;
0782     case NoteType::Image:
0783         new ImageContent(note, fileName);
0784         break;
0785     case NoteType::Animation:
0786         new AnimationContent(note, fileName);
0787         break;
0788     case NoteType::Sound:
0789         new SoundContent(note, fileName);
0790         break;
0791     case NoteType::File:
0792         new FileContent(note, fileName);
0793         break;
0794     case NoteType::Launcher:
0795         new LauncherContent(note, fileName);
0796         break;
0797     case NoteType::Unknown:
0798         new UnknownContent(note, fileName);
0799         break;
0800 
0801     default:
0802     case NoteType::Link:
0803     case NoteType::CrossReference:
0804     case NoteType::Color:
0805         return nullptr;
0806     }
0807 
0808     return note;
0809 }
0810 
0811 NoteType::Id NoteFactory::typeForURL(const QUrl &url, BasketScene * /*parent*/)
0812 {
0813     bool viewText = Settings::viewTextFileContent();
0814     bool viewHTML = Settings::viewHtmlFileContent();
0815     bool viewImage = Settings::viewImageFileContent();
0816     bool viewSound = Settings::viewSoundFileContent();
0817 
0818     QMimeDatabase db;
0819     QMimeType mimeType = db.mimeTypeForUrl(url);
0820 
0821     if (Global::debugWindow) {
0822         if (mimeType.isValid())
0823             *Global::debugWindow << "typeForURL: " + url.toDisplayString() + " ; MIME type = " + mimeType.name();
0824         else
0825             *Global::debugWindow << "typeForURL: mimeType is empty for " + url.toDisplayString();
0826     }
0827 
0828     // Go from specific to more generic
0829     if (maybeLauncher(mimeType))
0830         return NoteType::Launcher;
0831     else if (viewHTML && (maybeHtml(mimeType)))
0832         return NoteType::Html;
0833     else if (viewText && maybeText(mimeType))
0834         return NoteType::Text;
0835     else if (viewImage && maybeAnimation(mimeType))
0836         return NoteType::Animation; // See Note::movieStatus(int)
0837     else if (viewImage && maybeImage(mimeType))
0838         return NoteType::Image; //  for more explanations
0839     else if (viewSound && maybeSound(mimeType))
0840         return NoteType::Sound;
0841     else
0842         return NoteType::File;
0843 }
0844 
0845 QString NoteFactory::fileNameForNewNote(BasketScene *parent, const QString &wantedName)
0846 {
0847     return Tools::fileNameForNewFile(wantedName, parent->fullPath());
0848 }
0849 
0850 // Create a file to store a new note in Basket parent and with extension extension.
0851 // If wantedName is provided, the function will first try to use this file name, or derive it if it's impossible
0852 //  (extension willn't be used for that case)
0853 QString NoteFactory::createFileForNewNote(BasketScene *parent, const QString &extension, const QString &wantedName)
0854 {
0855     QString fileName;
0856     QString fullName;
0857 
0858     if (wantedName.isEmpty()) { // TODO: fileNameForNewNote(parent, "note1."+extension);
0859         QDir dir;
0860         int nb = parent->count() + 1;
0861         QString time = QTime::currentTime().toString("hhmmss");
0862 
0863         for (;; ++nb) {
0864             fileName = QString("note%1-%2.%3").arg(nb).arg(time).arg(extension);
0865             fullName = parent->fullPath() + fileName;
0866             dir = QDir(fullName);
0867             if (!dir.exists(fullName))
0868                 break;
0869         }
0870     } else {
0871         fileName = fileNameForNewNote(parent, wantedName);
0872         fullName = parent->fullPath() + fileName;
0873     }
0874 
0875     // Create the file
0876     //  parent->dontCareOfCreation(fullName);
0877     QFile file(fullName);
0878     file.open(QIODevice::WriteOnly);
0879     file.close();
0880 
0881     return fileName;
0882 }
0883 
0884 QUrl NoteFactory::filteredURL(const QUrl &url)
0885 {
0886     // KURIFilter::filteredURI() is slow if the URL contains only letters, digits and '-' or '+'.
0887     // So, we don't use that function is that case:
0888     bool isSlow = true;
0889     for (int i = 0; i < url.url().length(); ++i) {
0890         QChar c = url.url()[i];
0891         if (!c.isLetterOrNumber() && c != '-' && c != '+') {
0892             isSlow = false;
0893             break;
0894         }
0895     }
0896     if (isSlow)
0897         return url;
0898     else {
0899         QStringList list;
0900         list << QLatin1String("kshorturifilter") << QLatin1String("kuriikwsfilter") << QLatin1String("kurisearchfilter") << QLatin1String("localdomainfilter") << QLatin1String("fixuphosturifilter");
0901         return KUriFilter::self()->filteredUri(url, list);
0902     }
0903 }
0904 
0905 QString NoteFactory::titleForURL(const QUrl &url)
0906 {
0907     QString title = url.toDisplayString();
0908     QString home = "file:" + QDir::homePath() + '/';
0909 
0910     if (title.startsWith(QLatin1String("mailto:")))
0911         return title.remove(0, 7);
0912 
0913     if (title.startsWith(home))
0914         title = "~/" + title.remove(0, home.length());
0915 
0916     if (title.startsWith(QLatin1String("file://")))
0917         title = title.remove(0, 7); // 7 == QString("file://").length() - 1
0918     else if (title.startsWith(QLatin1String("file:")))
0919         title = title.remove(0, 5); // 5 == QString("file:").length() - 1
0920     else if (title.startsWith(QLatin1String("http://www.")))
0921         title = title.remove(0, 11); // 11 == QString("http://www.").length() - 1
0922     else if (title.startsWith(QLatin1String("https://www.")))
0923         title = title.remove(0, 12); // 12 == QString("https://www.").length() - 1
0924     else if (title.startsWith(QLatin1String("http://")))
0925         title = title.remove(0, 7); // 7 == QString("http://").length() - 1
0926     else if (title.startsWith(QLatin1String("https://")))
0927         title = title.remove(0, 8); // 8 == QString("https://").length() - 1
0928 
0929     if (!url.isLocalFile()) {
0930         if (title.endsWith(QLatin1String("/index.html")) && title.length() > 11)
0931             title.truncate(title.length() - 11); // 11 == QString("/index.html").length()
0932         else if (title.endsWith(QLatin1String("/index.htm")) && title.length() > 10)
0933             title.truncate(title.length() - 10); // 10 == QString("/index.htm").length()
0934         else if (title.endsWith(QLatin1String("/index.xhtml")) && title.length() > 12)
0935             title.truncate(title.length() - 12); // 12 == QString("/index.xhtml").length()
0936         else if (title.endsWith(QLatin1String("/index.php")) && title.length() > 10)
0937             title.truncate(title.length() - 10); // 10 == QString("/index.php").length()
0938         else if (title.endsWith(QLatin1String("/index.asp")) && title.length() > 10)
0939             title.truncate(title.length() - 10); // 10 == QString("/index.asp").length()
0940         else if (title.endsWith(QLatin1String("/index.php3")) && title.length() > 11)
0941             title.truncate(title.length() - 11); // 11 == QString("/index.php3").length()
0942         else if (title.endsWith(QLatin1String("/index.php4")) && title.length() > 11)
0943             title.truncate(title.length() - 11); // 11 == QString("/index.php4").length()
0944         else if (title.endsWith(QLatin1String("/index.php5")) && title.length() > 11)
0945             title.truncate(title.length() - 11); // 11 == QString("/index.php5").length()
0946     }
0947 
0948     if (title.length() > 2 && title.endsWith('/')) // length > 2 because "/" and "~/" shouldn't be transformed to QString() and "~"
0949         title.truncate(title.length() - 1);        // eg. transform "www.kde.org/" to "www.kde.org"
0950 
0951     return title;
0952 }
0953 
0954 QString NoteFactory::iconForURL(const QUrl &url)
0955 {
0956 
0957     if (url.scheme() == "mailto") {
0958         return QStringLiteral("message");
0959     } else if (url.scheme() == "http" || url.scheme() == "https") {
0960         return QStringLiteral("text-html");
0961     } else {
0962         return QStringLiteral("unknown");
0963     }
0964 }
0965 
0966 // TODO: Can I add "autoTitle" and "autoIcon" entries to .desktop files? or just store them in basket, as now...
0967 
0968 /* Try our better to find an icon suited to the command line
0969  * eg. "/usr/bin/kwrite-3.2 ~/myfile.txt /home/other/file.xml"
0970  * will give the "kwrite" icon!
0971  */
0972 QString NoteFactory::iconForCommand(const QString &command)
0973 {
0974     QString icon;
0975 
0976     // 1. Use first word as icon (typically the program without argument)
0977     icon = command.split(' ').first();
0978     // 2. If the command is a full path, take only the program file name
0979     icon = icon.mid(icon.lastIndexOf('/') + 1); // strip path if given [But it doesn't care of such
0980     // "myprogram /my/path/argument" -> return "argument". Would
0981     // must first strip first word and then strip path... Useful ??
0982     // 3. Use characters before any '-' (e.g. use "gimp" icon if run command is "gimp-1.3")
0983     if (!isIconExist(icon))
0984         icon = icon.split('-').first();
0985     // 4. If the icon still not findable, use a generic icon
0986     if (!isIconExist(icon))
0987         icon = "exec";
0988 
0989     return icon;
0990 }
0991 
0992 bool NoteFactory::isIconExist(const QString &icon)
0993 {
0994     return !KIconLoader::global()->loadIcon(icon, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, true).isNull();
0995 }
0996 
0997 Note *NoteFactory::createEmptyNote(NoteType::Id type, BasketScene *parent)
0998 {
0999     QPixmap *pixmap;
1000     switch (type) {
1001     case NoteType::Text:
1002         return NoteFactory::createNoteText(QString(), parent, /*reallyPlainText=*/true);
1003     case NoteType::Html:
1004         return NoteFactory::createNoteHtml(QString(), parent);
1005     case NoteType::Image:
1006         pixmap = new QPixmap(QSize(Settings::defImageX(), Settings::defImageY()));
1007         pixmap->fill();
1008         pixmap->setMask(pixmap->createHeuristicMask());
1009         return NoteFactory::createNoteImage(*pixmap, parent);
1010     case NoteType::Link:
1011         return NoteFactory::createNoteLink(QUrl(), parent);
1012     case NoteType::CrossReference:
1013         return NoteFactory::createNoteCrossReference(QUrl(), parent);
1014     case NoteType::Launcher:
1015         return NoteFactory::createNoteLauncher(QUrl(), parent);
1016     case NoteType::Color:
1017         return NoteFactory::createNoteColor(Qt::black, parent);
1018     default:
1019     case NoteType::Animation:
1020     case NoteType::Sound:
1021     case NoteType::File:
1022     case NoteType::Unknown:
1023         return nullptr; // Not possible!
1024     }
1025 }
1026 
1027 Note *NoteFactory::importKMenuLauncher(BasketScene *parent)
1028 {
1029     QPointer<KOpenWithDialog> dialog = new KOpenWithDialog(parent->graphicsView()->viewport());
1030     dialog->setSaveNewApplications(true); // To create temp file, needed by createNoteLauncher()
1031     dialog->exec();
1032     if (dialog->service()) {
1033         // * locateLocal() return a local file even if it is a system wide one (local one doesn't exists)
1034         // * desktopEntryPath() returns the full path for system wide resources, but relative path if in home
1035         QString serviceFilePath = dialog->service()->entryPath();
1036         if (!serviceFilePath.startsWith('/'))
1037             serviceFilePath = dialog->service()->locateLocal();
1038         return createNoteLauncher(QUrl::fromUserInput(serviceFilePath), parent);
1039     }
1040     return nullptr;
1041 }
1042 
1043 Note *NoteFactory::importIcon(BasketScene *parent)
1044 {
1045     QString iconName = KIconDialog::getIcon(KIconLoader::Desktop, KIconLoader::Application, false, Settings::defIconSize());
1046     if (!iconName.isEmpty()) {
1047         QPointer<IconSizeDialog> dialog = new IconSizeDialog(i18n("Import Icon as Image"), i18n("Choose the size of the icon to import as an image:"), iconName, Settings::defIconSize(), nullptr);
1048         dialog->exec();
1049         if (dialog->iconSize() > 0) {
1050             Settings::setDefIconSize(dialog->iconSize());
1051             Settings::saveConfig();
1052             return createNoteImage(QIcon::fromTheme(iconName).pixmap(dialog->iconSize()), parent); // TODO: wantedName = iconName !
1053         }
1054     }
1055     return nullptr;
1056 }
1057 
1058 Note *NoteFactory::importFileContent(BasketScene *parent)
1059 {
1060     QUrl url = QFileDialog::getOpenFileUrl(parent->graphicsView(), i18n("Load File Content into a Note"), QUrl(), QString());
1061     if (!url.isEmpty())
1062         return copyFileAndLoad(url, parent);
1063     return nullptr;
1064 }
1065 
1066 void NoteFactory::loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad)
1067 {
1068     if (lowerTypeName == "text") {
1069         new TextContent(parent, content.text(), lazyLoad);
1070     } else if (lowerTypeName == "html") {
1071         new HtmlContent(parent, content.text(), lazyLoad);
1072     } else if (lowerTypeName == "image") {
1073         new ImageContent(parent, content.text(), lazyLoad);
1074     } else if (lowerTypeName == "animation") {
1075         new AnimationContent(parent, content.text(), lazyLoad);
1076     } else if (lowerTypeName == "sound") {
1077         new SoundContent(parent, content.text());
1078     } else if (lowerTypeName == "file")  {
1079         new FileContent(parent, content.text());
1080     } else if (lowerTypeName == "link") {
1081         bool autoTitle = content.attribute("title") == content.text();
1082         bool autoIcon = content.attribute("icon") == NoteFactory::iconForURL(QUrl::fromUserInput(content.text()));
1083         autoTitle = XMLWork::trueOrFalse(content.attribute("autoTitle"), autoTitle);
1084         autoIcon = XMLWork::trueOrFalse(content.attribute("autoIcon"), autoIcon);
1085         new LinkContent(parent, QUrl::fromUserInput(content.text()), content.attribute("title"), content.attribute("icon"), autoTitle, autoIcon);
1086     } else if (lowerTypeName == "cross_reference") {
1087         new CrossReferenceContent(parent, QUrl::fromUserInput(content.text()), content.attribute("title"), content.attribute("icon"));
1088     } else if (lowerTypeName == "launcher") {
1089         new LauncherContent(parent, content.text());
1090     } else if (lowerTypeName == "color") {
1091         new ColorContent(parent, QColor(content.text()));
1092     } else if (lowerTypeName == "unknown") {
1093         new UnknownContent(parent, content.text());
1094     }
1095 }