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)
}
}
}
}