[PATCH v2] Parse html links in the Notes section.
K. "pestophagous" Heller
pestophagous at gmail.com
Sun Oct 25 14:46:39 PDT 2015
In the spirit of "Do the simplest thing that could
possibly work": capture Ctrl+leftclick mouse events
in the Notes area. If the string under the clicked
position is a valid url, then launch it.
Many common URI schemes will work. Typing a url that
starts with https:// will work. So will mailto: and
file://
See #733
Signed-off-by: K. Heller <pestophagous at gmail.com>
---
This is a re-submission of an earlier patch.
I am addressing the items from the checklist that was originally posted here:
http://lists.subsurface-divelog.org/pipermail/subsurface/2015-October/022628.html
Compared to the first submitted version, these are now complete:
[x] remove the "if (true)" comment about making things preference-based.
[x] fix indentation in ctor, and also whitespace damage in comment block.
[x] replace "Ctrl" in the tooltip text with something that would say "Cmd" on mac.
[x] find some better prose to explain the cursor comings-and-goings
There was one other item:
[_] ... look into security implications??
I found that QDesktopServices::openUrl relies on xdg-open. I found a (minor?) CVE for xdg-open.
http://seclists.org/fulldisclosure/2014/Nov/36
http://www.openwall.com/lists/oss-security/2015/01/01/3
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9622
I suppose that as is 'customary' by now: let he/she who clicks a link beware. In the case of this feature, it should be obvious to the user what the clicked URL string consists of. (famous last words?)
qt-ui/maintab.cpp | 3 +
qt-ui/simplewidgets.cpp | 155 ++++++++++++++++++++++++++++++++++++++++++++++++
qt-ui/simplewidgets.h | 20 +++++++
3 files changed, 178 insertions(+)
diff --git a/qt-ui/maintab.cpp b/qt-ui/maintab.cpp
index 0afb7b4..597e7ae 100644
--- a/qt-ui/maintab.cpp
+++ b/qt-ui/maintab.cpp
@@ -202,6 +202,9 @@ MainTab::MainTab(QWidget *parent) : QTabWidget(parent),
connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished,
ui.location, &DiveLocationLineEdit::fixPopupPosition);
+ // enable URL clickability in notes:
+ new TextHyperlinkEventFilter(ui.notes);//destroyed when ui.notes is destroyed
+
acceptingEdit = false;
ui.diveTripLocation->hide();
diff --git a/qt-ui/simplewidgets.cpp b/qt-ui/simplewidgets.cpp
index 62a9cc6..e213c64 100644
--- a/qt-ui/simplewidgets.cpp
+++ b/qt-ui/simplewidgets.cpp
@@ -7,6 +7,8 @@
#include <QCalendarWidget>
#include <QKeyEvent>
#include <QAction>
+#include <QDesktopServices>
+#include <QToolTip>
#include "file.h"
#include "mainwindow.h"
@@ -734,3 +736,156 @@ void MultiFilter::closeFilter()
MultiFilterSortModel::instance()->clearFilter();
hide();
}
+
+TextHyperlinkEventFilter::TextHyperlinkEventFilter(QTextEdit *txtEdit) : QObject(txtEdit),
+ textEdit(txtEdit),
+ scrollView(textEdit->viewport())
+{
+ // If you install the filter on textEdit, you fail to capture any clicks.
+ // The clicks go to the viewport. http://stackoverflow.com/a/31582977/10278
+ textEdit->viewport()->installEventFilter(this);
+}
+
+bool TextHyperlinkEventFilter::eventFilter(QObject *target, QEvent *evt)
+{
+ if (target != scrollView)
+ return false;
+
+ if (evt->type() != QEvent::MouseButtonPress &&
+ evt->type() != QEvent::ToolTip)
+ return false;
+
+ // --------------------
+
+ // Note: Qt knows that on Mac OSX, ctrl (and Control) are the command key.
+ const bool isCtrlClick = evt->type() == QEvent::MouseButtonPress &&
+ static_cast<QMouseEvent *>(evt)->modifiers() & Qt::ControlModifier &&
+ static_cast<QMouseEvent *>(evt)->button() == Qt::LeftButton;
+
+ const bool isTooltip = evt->type() == QEvent::ToolTip;
+
+ QString urlUnderCursor;
+
+ if (isCtrlClick || isTooltip) {
+ QTextCursor cursor = isCtrlClick ?
+ textEdit->cursorForPosition(static_cast<QMouseEvent *>(evt)->pos()) :
+ textEdit->cursorForPosition(static_cast<QHelpEvent *>(evt)->pos());
+
+ urlUnderCursor = tryToFormulateUrl(&cursor);
+ }
+
+ if (isCtrlClick) {
+ handleUrlClick(urlUnderCursor);
+ }
+
+ if (isTooltip) {
+ handleUrlTooltip(urlUnderCursor, static_cast<QHelpEvent *>(evt)->globalPos());
+ }
+
+ // 'return true' would mean that all event handling stops for this event.
+ // 'return false' lets Qt continue propagating the event to the target.
+ // Since our URL behavior is meant as 'additive' and not necessarily mutually
+ // exclusive with any default behaviors, it seems ok to return false to
+ // avoid unintentially hijacking any 'normal' event handling.
+ return false;
+}
+
+void TextHyperlinkEventFilter::handleUrlClick(const QString &urlStr)
+{
+ if (!urlStr.isEmpty()) {
+ QUrl url(urlStr, QUrl::StrictMode);
+ QDesktopServices::openUrl(url);
+ }
+}
+
+void TextHyperlinkEventFilter::handleUrlTooltip(const QString &urlStr, const QPoint &pos)
+{
+ if (urlStr.isEmpty()) {
+ QToolTip::hideText();
+ } else {
+ // per Qt docs, QKeySequence::toString does localization "tr()" on strings like Ctrl.
+ // Note: Qt knows that on Mac OSX, ctrl (and Control) are the command key.
+ const QString ctrlKeyName = QKeySequence(Qt::CTRL).toString();
+ // ctrlKeyName comes with a trailing '+', as in: 'Ctrl+'
+ QToolTip::showText(pos, tr("%1click to visit %2").arg(ctrlKeyName).arg(urlStr));
+ }
+}
+
+bool TextHyperlinkEventFilter::stringMeetsOurUrlRequirements(const QString &maybeUrlStr)
+{
+ QUrl url(maybeUrlStr, QUrl::StrictMode);
+ return url.isValid() && (!url.scheme().isEmpty());
+}
+
+QString TextHyperlinkEventFilter::tryToFormulateUrl(QTextCursor *cursor)
+{
+ // tryToFormulateUrl exists because WordUnderCursor will not
+ // treat "http://m.abc.def" as a word.
+
+ // tryToFormulateUrl invokes fromCursorTilWhitespace two times (once
+ // with a forward moving cursor and once in the backwards direction) in
+ // order to expand the selection to try to capture a complete string
+ // like "http://m.abc.def"
+
+ // loosely inspired by advice here: http://stackoverflow.com/q/19262064/10278
+
+ cursor->select(QTextCursor::WordUnderCursor);
+ QString maybeUrlStr = cursor->selectedText();
+
+ const bool soFarSoGood = !maybeUrlStr.simplified().replace(" ", "").isEmpty();
+
+ if (soFarSoGood && !stringMeetsOurUrlRequirements(maybeUrlStr)) {
+ // If we don't yet have a full url, try to expand til we get one. Note:
+ // after requesting WordUnderCursor, empirically (all platforms, in
+ // Qt5), the 'anchor' is just past the end of the word.
+
+ QTextCursor cursor2(*cursor);
+ QString left = fromCursorTilWhitespace(cursor, true /*searchBackwards*/);
+ QString right = fromCursorTilWhitespace(&cursor2, false);
+ maybeUrlStr = left + right;
+ }
+
+ return stringMeetsOurUrlRequirements(maybeUrlStr) ? maybeUrlStr : QString::null;
+}
+
+QString TextHyperlinkEventFilter::fromCursorTilWhitespace(QTextCursor *cursor, const bool searchBackwards)
+{
+ // fromCursorTilWhitespace calls cursor->movePosition repeatedly, while
+ // preserving the original 'anchor' (qt terminology) of the cursor.
+ // We widen the selection with 'movePosition' until hitting any whitespace.
+
+ QString result;
+ QString grownText;
+ QString noSpaces;
+ bool movedOk = false;
+
+ do {
+ result = grownText; // this is a no-op on the first visit.
+
+ if (searchBackwards) {
+ movedOk = cursor->movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
+ } else {
+ movedOk = cursor->movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
+ }
+
+ grownText = cursor->selectedText();
+ noSpaces = grownText.simplified().replace(" ", "");
+ } while (grownText == noSpaces && movedOk);
+
+ // while growing the selection forwards, we have an extra step to do:
+ if (!searchBackwards) {
+ /*
+ The cursor keeps jumping to the start of the next word.
+ (for example) in the string "mn.abcd.edu is the spot" you land at
+ m,a,e,i (the 'i' in 'is). if we stop at e, then we only capture
+ "mn.abcd." for the url (wrong). So we have to go to 'i', to
+ capture "mn.abcd.edu " (with trailing space), and then clean it up.
+ */
+ QStringList list = grownText.split(QRegExp("\\s"), QString::SkipEmptyParts);
+ if (!list.isEmpty()) {
+ result = list[0];
+ }
+ }
+
+ return result;
+}
diff --git a/qt-ui/simplewidgets.h b/qt-ui/simplewidgets.h
index 595c4cd..8a7a5df 100644
--- a/qt-ui/simplewidgets.h
+++ b/qt-ui/simplewidgets.h
@@ -231,6 +231,26 @@ private:
Ui::FilterWidget ui;
};
+class TextHyperlinkEventFilter : public QObject {
+ Q_OBJECT
+public:
+ explicit TextHyperlinkEventFilter(QTextEdit *txtEdit);
+
+ virtual bool eventFilter(QObject *target, QEvent *evt);
+
+private:
+ void handleUrlClick(const QString &urlStr);
+ void handleUrlTooltip(const QString &urlStr, const QPoint &pos);
+ bool stringMeetsOurUrlRequirements(const QString &maybeUrlStr);
+ QString fromCursorTilWhitespace(QTextCursor *cursor, const bool searchBackwards);
+ QString tryToFormulateUrl(QTextCursor *cursor);
+
+ QTextEdit const *const textEdit;
+ QWidget const *const scrollView;
+
+ Q_DISABLE_COPY(TextHyperlinkEventFilter)
+};
+
bool isGnome3Session();
QImage grayImage(const QImage &coloredImg);
--
2.5.0
More information about the subsurface
mailing list