diff --git a/framework/src/domain/mime/htmlutils.cpp b/framework/src/domain/mime/htmlutils.cpp index 3d8d9ad8..f38afcec 100644 --- a/framework/src/domain/mime/htmlutils.cpp +++ b/framework/src/domain/mime/htmlutils.cpp @@ -1,286 +1,287 @@ /* Copyright (c) 2017 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "htmlutils.h" -#include +#include +#include static QString resolveEntities(const QString &in) { QString out; for(int i = 0; i < (int)in.length(); ++i) { if(in[i] == '&') { // find a semicolon ++i; int n = in.indexOf(';', i); if(n == -1) break; QString type = in.mid(i, (n-i)); i = n; // should be n+1, but we'll let the loop increment do it if(type == "amp") out += '&'; else if(type == "lt") out += '<'; else if(type == "gt") out += '>'; else if(type == "quot") out += '\"'; else if(type == "apos") out += '\''; else if(type == "nbsp") out += 0xa0; } else { out += in[i]; } } return out; } static bool linkify_pmatch(const QString &str1, int at, const QString &str2) { if(str2.length() > (str1.length()-at)) return false; for(int n = 0; n < (int)str2.length(); ++n) { if(str1.at(n+at).toLower() != str2.at(n).toLower()) return false; } return true; } static bool linkify_isOneOf(const QChar &c, const QString &charlist) { for(int i = 0; i < (int)charlist.length(); ++i) { if(c == charlist.at(i)) return true; } return false; } // encodes a few dangerous html characters static QString linkify_htmlsafe(const QString &in) { QString out; for(int n = 0; n < in.length(); ++n) { if(linkify_isOneOf(in.at(n), "\"\'`<>")) { // hex encode QString hex; hex.sprintf("%%%02X", in.at(n).toLatin1()); out.append(hex); } else { out.append(in.at(n)); } } return out; } static bool linkify_okUrl(const QString &url) { if(url.at(url.length()-1) == '.') return false; return true; } static bool linkify_okEmail(const QString &addy) { // this makes sure that there is an '@' and a '.' after it, and that there is // at least one char for each of the three sections int n = addy.indexOf('@'); if(n == -1 || n == 0) return false; int d = addy.indexOf('.', n+1); if(d == -1 || d == 0) return false; if((addy.length()-1) - d <= 0) return false; if(addy.indexOf("..") != -1) return false; return true; } /** * takes a richtext string and heuristically adds links for uris of common protocols * @return a richtext string with link markup added */ QString HtmlUtils::linkify(const QString &in) { QString out = in; int x1, x2; bool isUrl, isAtStyle; QString linked, link, href; for(int n = 0; n < (int)out.length(); ++n) { isUrl = false; isAtStyle = false; x1 = n; if(linkify_pmatch(out, n, "xmpp:")) { n += 5; isUrl = true; href = ""; } else if(linkify_pmatch(out, n, "mailto:")) { n += 7; isUrl = true; href = ""; } else if(linkify_pmatch(out, n, "http://")) { n += 7; isUrl = true; href = ""; } else if(linkify_pmatch(out, n, "https://")) { n += 8; isUrl = true; href = ""; } else if(linkify_pmatch(out, n, "ftp://")) { n += 6; isUrl = true; href = ""; } else if(linkify_pmatch(out, n, "news://")) { n += 7; isUrl = true; href = ""; } else if (linkify_pmatch(out, n, "ed2k://")) { n += 7; isUrl = true; href = ""; } else if (linkify_pmatch(out, n, "magnet:")) { n += 7; isUrl = true; href = ""; } else if(linkify_pmatch(out, n, "www.")) { isUrl = true; href = "http://"; } else if(linkify_pmatch(out, n, "ftp.")) { isUrl = true; href = "ftp://"; } else if(linkify_pmatch(out, n, "@")) { isAtStyle = true; href = "x-psi-atstyle:"; } if(isUrl) { // make sure the previous char is not alphanumeric if(x1 > 0 && out.at(x1-1).isLetterOrNumber()) continue; // find whitespace (or end) QMap brackets; brackets['('] = brackets[')'] = brackets['['] = brackets[']'] = brackets['{'] = brackets['}'] = 0; QMap openingBracket; openingBracket[')'] = '('; openingBracket[']'] = '['; openingBracket['}'] = '{'; for(x2 = n; x2 < (int)out.length(); ++x2) { if(out.at(x2).isSpace() || linkify_isOneOf(out.at(x2), "\"\'`<>") || linkify_pmatch(out, x2, """) || linkify_pmatch(out, x2, "'") || linkify_pmatch(out, x2, ">") || linkify_pmatch(out, x2, "<") ) { break; } if(brackets.keys().contains(out.at(x2))) { ++brackets[out.at(x2)]; } } int len = x2-x1; QString pre = resolveEntities(out.mid(x1, x2-x1)); // go backward hacking off unwanted punctuation int cutoff; for(cutoff = pre.length()-1; cutoff >= 0; --cutoff) { if(!linkify_isOneOf(pre.at(cutoff), "!?,.()[]{}<>\"")) break; if(linkify_isOneOf(pre.at(cutoff), ")]}") && brackets[pre.at(cutoff)] - brackets[openingBracket[pre.at(cutoff)]] <= 0 ) { break; // in theory, there could be == above, but these are urls, not math ;) } if(brackets.keys().contains(pre.at(cutoff))) { --brackets[pre.at(cutoff)]; } } ++cutoff; //++x2; link = pre.mid(0, cutoff); if(!linkify_okUrl(link)) { n = x1 + link.length(); continue; } href += link; // attributes need to be encoded too. href = href.toHtmlEscaped(); href = linkify_htmlsafe(href); //printf("link: [%s], href=[%s]\n", link.latin1(), href.latin1()); linked = QString("").arg(href) + QUrl{link}.toDisplayString(QUrl::RemoveQuery) + "" + pre.mid(cutoff).toHtmlEscaped(); out.replace(x1, len, linked); n = x1 + linked.length() - 1; } else if(isAtStyle) { // go backward till we find the beginning if(x1 == 0) continue; --x1; for(; x1 >= 0; --x1) { if(!linkify_isOneOf(out.at(x1), "_.-+") && !out.at(x1).isLetterOrNumber()) break; } ++x1; // go forward till we find the end x2 = n + 1; for(; x2 < (int)out.length(); ++x2) { if(!linkify_isOneOf(out.at(x2), "_.-+") && !out.at(x2).isLetterOrNumber()) break; } int len = x2-x1; link = out.mid(x1, len); //link = resolveEntities(link); if(!linkify_okEmail(link)) { n = x1 + link.length(); continue; } href += link; //printf("link: [%s], href=[%s]\n", link.latin1(), href.latin1()); linked = QString("").arg(href) + link + ""; out.replace(x1, len, linked); n = x1 + linked.length() - 1; } } return out; } diff --git a/framework/src/domain/mime/htmlutils.h b/framework/src/domain/mime/htmlutils.h index b59da1dc..de1742a6 100644 --- a/framework/src/domain/mime/htmlutils.h +++ b/framework/src/domain/mime/htmlutils.h @@ -1,25 +1,43 @@ /* Copyright (c) 2017 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #pragma once +#include #include +#include namespace HtmlUtils { QString linkify(const QString &in); + + class HtmlUtils : public QObject { + Q_OBJECT + public: + Q_INVOKABLE QString linkify(const QString &s) { + return ::HtmlUtils::linkify(s); + }; + + Q_INVOKABLE QString toHtml(const QString &s) { + if (Qt::mightBeRichText(s)) { + return s; + } else { + return ::HtmlUtils::linkify(Qt::convertFromPlainText(s)); + } + } + }; } diff --git a/framework/src/frameworkplugin.cpp b/framework/src/frameworkplugin.cpp index 390f9a97..baf46559 100644 --- a/framework/src/frameworkplugin.cpp +++ b/framework/src/frameworkplugin.cpp @@ -1,226 +1,235 @@ /* Copyright (c) 2016 Michael Bohlender Copyright (c) 2016 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "frameworkplugin.h" #include "domain/maillistmodel.h" #include "domain/folderlistmodel.h" +#include "domain/mime/htmlutils.h" #include "domain/perioddayeventmodel.h" #include "domain/multidayeventmodel.h" #include "domain/eventoccurrencemodel.h" #include "domain/todomodel.h" #include "domain/composercontroller.h" #include "domain/mime/messageparser.h" #include "domain/retriever.h" #include "domain/outboxmodel.h" #include "domain/mouseproxy.h" #include "domain/contactcontroller.h" #include "domain/eventcontroller.h" #include "domain/invitationcontroller.h" #include "domain/todocontroller.h" #include "domain/peoplemodel.h" #include "domain/textdocumenthandler.h" #include "domain/settings/accountsettings.h" #include "accounts/accountsmodel.h" #include "accounts/accountfactory.h" #include "settings/settings.h" #include "fabric.h" #include "kubeimage.h" #include "clipboardproxy.h" #include "startupcheck.h" #include "keyring.h" #include "controller.h" #include "domainobjectcontroller.h" #include "extensionmodel.h" #include "viewhighlighter.h" #include "file.h" #include "logmodel.h" #include "entitymodel.h" #include "entitycontroller.h" #include "qquicktreemodeladaptor.h" #include #include #include #include class KubeImageProvider : public QQuickImageProvider { public: KubeImageProvider() : QQuickImageProvider(QQuickImageProvider::Pixmap) { } static QSize selectSize(const QSize &requestedSize, const QList &availableSizes) { auto expectedSize = requestedSize; //Get the largest size that is still smaller or equal than requested //Except if we only have larger sizes, then just pick the closest one bool first = true; for (const auto &s : availableSizes) { if (first && s.width() > requestedSize.width()) { return s; } first = false; if (s.width() <= requestedSize.width()) { expectedSize = s; } } return expectedSize; } QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) Q_DECL_OVERRIDE { //The platform theme plugin can overwrite our setting again once it gets loaded, //so we check on every icon load request... if (QIcon::themeName() != "kube") { QIcon::setThemeName("kube"); } const auto icon = QIcon::fromTheme(id); static auto devicePixelRatio = static_cast(QApplication::instance())->devicePixelRatio(); //availableSizes() does not take the devicePixelRatio into account, so if we divide the request by it first, //we will end up with the correct size after multiplying it later. const auto expectedSize = selectSize(requestedSize / devicePixelRatio, icon.availableSizes()); auto pixmap = icon.pixmap(expectedSize * devicePixelRatio); pixmap.setDevicePixelRatio(devicePixelRatio); if (size) { *size = pixmap.size(); } return pixmap; } }; static QObject *fabric_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return new Kube::Fabric::Fabric; } +static QObject *htmlutils_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + return new HtmlUtils::HtmlUtils; +} + static QObject *keyring_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) auto instance = Kube::Keyring::instance(); QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership); return instance; } static QString findFile(const QString file, const QStringList importPathList) { for (const auto &path : importPathList) { const QString f = path + file; if (QFileInfo::exists(f)) { return f; } } return {}; } void FrameworkPlugin::initializeEngine(QQmlEngine *engine, const char *uri) { Q_UNUSED(uri); engine->addImageProvider(QLatin1String("kube"), new KubeImageProvider); QString kubeIcons = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kube-icons.rcc")); //For windows if (kubeIcons.isEmpty()) { const auto locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation) + QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); kubeIcons = findFile(QStringLiteral("/kube/kube-icons.rcc"), locations); } //For osx if (kubeIcons.isEmpty()) { //On Mac OS we want to include Contents/Resources/ in the bundle, and that path is in AppDataLocations. QStringList iconSearchPaths; for (const auto &p : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)) { auto iconPath = p; //I'm getting broken paths reported from standardLocations if (iconPath.contains("kube.appContents")) { iconPath.replace("kube.appContents", "kube.app/Contents"); } if (iconPath.contains("kube-kolabnow.appContents")) { iconPath.replace("kube-kolabnow.appContents", "kube-kolabnow.app/Contents"); } iconSearchPaths << iconPath; } kubeIcons = findFile(QStringLiteral("/kube/kube-icons.rcc"), iconSearchPaths); } if (!QResource::registerResource(kubeIcons, "/icons/kube")) { qWarning() << "Failed to register icon resource!" << kubeIcons; qWarning() << "Searched paths: " << QStandardPaths::standardLocations(QStandardPaths::AppDataLocation) + QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); Q_ASSERT(false); } else { QIcon::setThemeSearchPaths(QStringList() << QStringLiteral(":/icons")); QIcon::setThemeName(QStringLiteral("kube")); } } void FrameworkPlugin::registerTypes (const char *uri) { qmlRegisterType(uri, 1, 0, "FolderListModel"); qmlRegisterType(uri, 1, 0, "MailListModel"); qmlRegisterType(uri, 1, 0, "PeriodDayEventModel"); qmlRegisterType(uri, 1, 0, "MultiDayEventModel"); qmlRegisterType(uri, 1, 0, "EventOccurrenceModel"); qmlRegisterType(uri, 1, 0, "EventController"); qmlRegisterType(uri, 1, 0, "InvitationController"); qmlRegisterType(uri, 1, 0, "TodoModel"); qmlRegisterType(uri, 1, 0, "TodoController"); qmlRegisterType(uri, 1, 0, "ComposerController"); qmlRegisterUncreatableType(uri, 1, 0, "ListPropertyController", "abstract"); qmlRegisterUncreatableType(uri, 1, 0, "Selector", "abstract"); qmlRegisterUncreatableType(uri, 1, 0, "Completer", "abstract"); qmlRegisterType(uri, 1, 0, "ControllerAction"); qmlRegisterType(uri, 1, 0, "MessageParser"); qmlRegisterType(uri, 1, 0, "Retriever"); qmlRegisterType(uri, 1, 0, "OutboxModel"); qmlRegisterType(uri, 1, 0, "MouseProxy"); qmlRegisterType(uri, 1, 0,"ContactController"); qmlRegisterType(uri, 1, 0,"PeopleModel"); qmlRegisterType(uri, 1, 0, "TextDocumentHandler"); qmlRegisterType(uri, 1, 0, "LogModel"); qmlRegisterType(uri, 1, 0, "EntityModel"); qmlRegisterType(uri, 1, 0, "EntityLoader"); qmlRegisterType(uri, 1, 0, "EntityController"); qmlRegisterType(uri, 1, 0, "CheckedEntities"); qmlRegisterType(uri, 1, 0, "CheckableEntityModel"); qmlRegisterType(uri, 1, 0, "TreeModelAdaptor"); + qmlRegisterSingletonType(uri, 1, 0, "HtmlUtils", htmlutils_singletontype_provider); qmlRegisterType(uri, 1, 0, "AccountFactory"); qmlRegisterType(uri, 1, 0, "AccountsModel"); qmlRegisterType(uri, 1, 0, "AccountSettings"); qmlRegisterType(uri, 1, 0, "ExtensionModel"); qmlRegisterType(uri, 1, 0, "File"); qmlRegisterType(uri, 1, 0, "Settings"); qmlRegisterType(uri, 1, 0, "Listener"); qmlRegisterType(uri, 1, 0, "DomainObjectController"); qmlRegisterSingletonType(uri, 1, 0, "Fabric", fabric_singletontype_provider); qmlRegisterType(uri, 1, 0, "KubeImage"); qmlRegisterType(uri, 1, 0, "Clipboard"); qmlRegisterType(uri, 1, 0, "StartupCheck"); qmlRegisterType(uri, 1, 0, "ViewHighlighter"); qmlRegisterSingletonType(uri, 1, 0, "Keyring", keyring_singletontype_provider); } diff --git a/views/todo/qml/TodoView.qml b/views/todo/qml/TodoView.qml index 1e0970a3..88c58489 100644 --- a/views/todo/qml/TodoView.qml +++ b/views/todo/qml/TodoView.qml @@ -1,131 +1,132 @@ /* * Copyright (C) 2018 Michael Bohlender, * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import org.kube.framework 1.0 as Kube FocusScope { id: root property var controller: null signal done() onControllerChanged: { //Wait for a controller to be set before we add a todo-view if (controller) { stackView.push(eventDetails, StackView.Immediate) } } function edit() { var item = stackView.push(editor, StackView.Immediate) item.forceActiveFocus() } StackView { id: stackView anchors.fill: parent clip: true visible: controller } Component { id: eventDetails Rectangle { color: Kube.Colors.paperWhite ColumnLayout { id: contentLayout anchors { fill: parent margins: Kube.Units.largeSpacing } spacing: Kube.Units.smallSpacing Kube.Heading { Layout.fillWidth: true text: controller.summary } Kube.SelectableLabel { visible: !isNaN(controller.due) text: qsTr("Due on ") + controller.due.toLocaleString(Qt.locale(), "dd. MMMM") opacity: 0.75 } Kube.SelectableLabel { visible: !isNaN(controller.start) text: qsTr("Start on ") + controller.start.toLocaleString(Qt.locale(), "dd. MMMM") opacity: 0.75 } Rectangle { Layout.fillWidth: true height: 1 color: Kube.Colors.textColor opacity: 0.5 } Kube.TextArea { Layout.fillWidth: true - text: controller.description + text: Kube.HtmlUtils.toHtml(controller.description) + textFormat: Kube.TextArea.RichText } Item { Layout.fillHeight: true width: 1 } RowLayout { width: parent.width Kube.Button { text: qsTr("Remove") onClicked: { root.controller.remove() } } Item { Layout.fillWidth: true } Kube.Button { text: qsTr("Edit") onClicked: root.edit() } } } } } Component { id: editor TodoEditor { controller: root.controller editMode: true onDone: { //Reload root.controller.todo = root.controller.todo stackView.pop(StackView.Immediate) } } } }