mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
Parse html links in the Notes section.
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@gmail.com> Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
This commit is contained in:
parent
99ed01e571
commit
0a2f5d25c0
3 changed files with 178 additions and 0 deletions
|
@ -195,6 +195,9 @@ MainTab::MainTab(QWidget *parent) : QTabWidget(parent),
|
||||||
connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished,
|
connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished,
|
||||||
ui.location, &DiveLocationLineEdit::fixPopupPosition);
|
ui.location, &DiveLocationLineEdit::fixPopupPosition);
|
||||||
|
|
||||||
|
// enable URL clickability in notes:
|
||||||
|
new TextHyperlinkEventFilter(ui.notes);//destroyed when ui.notes is destroyed
|
||||||
|
|
||||||
acceptingEdit = false;
|
acceptingEdit = false;
|
||||||
|
|
||||||
ui.diveTripLocation->hide();
|
ui.diveTripLocation->hide();
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
#include <QCalendarWidget>
|
#include <QCalendarWidget>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QToolTip>
|
||||||
|
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
|
@ -734,3 +736,156 @@ void MultiFilter::closeFilter()
|
||||||
MultiFilterSortModel::instance()->clearFilter();
|
MultiFilterSortModel::instance()->clearFilter();
|
||||||
hide();
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -231,6 +231,26 @@ private:
|
||||||
Ui::FilterWidget ui;
|
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();
|
bool isGnome3Session();
|
||||||
QImage grayImage(const QImage &coloredImg);
|
QImage grayImage(const QImage &coloredImg);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue