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 <pestophag...@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

_______________________________________________
subsurface mailing list
subsurface@subsurface-divelog.org
http://lists.subsurface-divelog.org/cgi-bin/mailman/listinfo/subsurface

Reply via email to