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 <[email protected]>
---

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?) is that too glib?

 desktop-widgets/maintab.cpp       |   3 +
 desktop-widgets/simplewidgets.cpp | 155 ++++++++++++++++++++++++++++++++++++++
 desktop-widgets/simplewidgets.h   |  20 +++++
 3 files changed, 178 insertions(+)

diff --git a/desktop-widgets/maintab.cpp b/desktop-widgets/maintab.cpp
index 965d44b..610d760 100644
--- a/desktop-widgets/maintab.cpp
+++ b/desktop-widgets/maintab.cpp
@@ -195,6 +195,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/desktop-widgets/simplewidgets.cpp 
b/desktop-widgets/simplewidgets.cpp
index 43ad1dd..c3c7072 100644
--- a/desktop-widgets/simplewidgets.cpp
+++ b/desktop-widgets/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/desktop-widgets/simplewidgets.h b/desktop-widgets/simplewidgets.h
index 595c4cd..8a7a5df 100644
--- a/desktop-widgets/simplewidgets.h
+++ b/desktop-widgets/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

_______________________________________________
subsurface mailing list
[email protected]
http://lists.subsurface-divelog.org/cgi-bin/mailman/listinfo/subsurface

Reply via email to