[PATCH] Parse html links in the Notes section.

K. "pestophagous" Heller pestophagous at gmail.com
Mon Oct 12 22:08:35 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>
---
 qt-ui/maintab.cpp       |   5 ++
 qt-ui/simplewidgets.cpp | 136 ++++++++++++++++++++++++++++++++++++++++++++++++
 qt-ui/simplewidgets.h   |  20 +++++++
 3 files changed, 161 insertions(+)

diff --git a/qt-ui/maintab.cpp b/qt-ui/maintab.cpp
index e97812c..35cda6e 100644
--- a/qt-ui/maintab.cpp
+++ b/qt-ui/maintab.cpp
@@ -202,6 +202,11 @@ MainTab::MainTab(QWidget *parent) : QTabWidget(parent),
 	connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished,
 					ui.location, &DiveLocationLineEdit::fixPopupPosition);
 
+	//if (true).  // to make URL clickability optional, simply branch here based on a preference.
+	{
+		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 57fc56b..e16d05f 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"
@@ -735,3 +737,137 @@ void MultiFilter::closeFilter()
 	MultiFilterSortModel::instance()->clearFilter();
 	hide();
 }
+
+TextHyperlinkEventFilter::TextHyperlinkEventFilter(QTextEdit *txtEdit) : QObject(txtEdit),
+									 textEdit(txtEdit),
+									 scrollView(textEdit->viewport())
+{
+	// lesson learned. install filter on viewport:
+	// http://stackoverflow.com/questions/31581453/qplaintextedit-double-click-event
+	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;
+
+	// --------------------
+
+	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 false; // (slight lie). indicate that we didn't do anything with the event.
+}
+
+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 {
+		QToolTip::showText(pos, tr("Ctrl+click to visit %1").arg(urlStr));
+	}
+}
+
+bool TextHyperlinkEventFilter::stringMeetsOurUrlRequirements(const QString &maybeUrlStr)
+{
+	QUrl url(maybeUrlStr, QUrl::StrictMode);
+	return url.isValid() && (!url.scheme().isEmpty());
+}
+
+QString TextHyperlinkEventFilter::fromCursorTilWhitespace(QTextCursor *cursor, const bool searchBackwards)
+{
+	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;
+}
+
+QString TextHyperlinkEventFilter::tryToFormulateUrl(QTextCursor *cursor)
+{
+	// If instead of (or in addition to) QTextCursor::WordUnderCursor we could
+	// also have some Qt feature like 'unbroken-string-of-nonwhitespace-under-cursor',
+	// then the following logic would not be necessary to do.
+	// http://stackoverflow.com/questions/19262064/pyqt-qtextcursor-wordundercursor-not-working-as-expected
+
+	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;
+}
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