From 6af13813975b3f518bd89fc7a1c264f12e252879 Mon Sep 17 00:00:00 2001 From: PulkoMandy Date: Thu, 23 Nov 2023 20:49:45 +0100 Subject: [PATCH] HaikuDepot TextView: add support for hyperlinks/clikable areas Specific text spans can be assigned a cursor and BMessage to send when they are clicked. This allows for implementing hyperlinks, specific popup menus, and clickable text of any type. With some extra work it can also be used to implement spell checking suggestions, buttons, and so on. Change-Id: I390e0c44656da76a950c432bdd934bd51af49baf Reviewed-on: https://review.haiku-os.org/c/haiku/+/7130 Reviewed-by: Adrien Destugues Tested-by: Commit checker robot --- src/apps/haikudepot/textview/TextDocument.cpp | 42 +++++++++++++++++++ src/apps/haikudepot/textview/TextDocument.h | 2 + .../haikudepot/textview/TextDocumentView.cpp | 33 +++++++++++++-- .../haikudepot/textview/TextDocumentView.h | 3 +- src/apps/haikudepot/textview/TextSpan.cpp | 33 +++++++++++++-- src/apps/haikudepot/textview/TextSpan.h | 10 +++++ 6 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/apps/haikudepot/textview/TextDocument.cpp b/src/apps/haikudepot/textview/TextDocument.cpp index ba4faa0aba..73114fd9f3 100644 --- a/src/apps/haikudepot/textview/TextDocument.cpp +++ b/src/apps/haikudepot/textview/TextDocument.cpp @@ -180,6 +180,48 @@ TextDocument::CharacterStyleAt(int32 textOffset) const } +const BMessage* +TextDocument::ClickMessageAt(int32 textOffset) const +{ + int32 paragraphOffset; + const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); + + textOffset -= paragraphOffset; + int32 index; + int32 count = paragraph.CountTextSpans(); + + for (index = 0; index < count; index++) { + const TextSpan& span = paragraph.TextSpanAtIndex(index); + if (textOffset - span.CountChars() < 0) + return span.ClickMessage(); + textOffset -= span.CountChars(); + } + + return NULL; +} + + +BCursor +TextDocument::CursorAt(int32 textOffset) const +{ + int32 paragraphOffset; + const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); + + textOffset -= paragraphOffset; + int32 index; + int32 count = paragraph.CountTextSpans(); + + for (index = 0; index < count; index++) { + const TextSpan& span = paragraph.TextSpanAtIndex(index); + if (textOffset - span.CountChars() < 0) + return span.Cursor(); + textOffset -= span.CountChars(); + } + + return BCursor((BMessage*)NULL); +} + + const ParagraphStyle& TextDocument::ParagraphStyleAt(int32 textOffset) const { diff --git a/src/apps/haikudepot/textview/TextDocument.h b/src/apps/haikudepot/textview/TextDocument.h index 5cb838487e..a36dc95467 100644 --- a/src/apps/haikudepot/textview/TextDocument.h +++ b/src/apps/haikudepot/textview/TextDocument.h @@ -55,6 +55,8 @@ public: // Style access const CharacterStyle& CharacterStyleAt(int32 textOffset) const; const ParagraphStyle& ParagraphStyleAt(int32 textOffset) const; + BCursor CursorAt(int32 textOffset) const; + const BMessage* ClickMessageAt(int32 textOffset) const; int32 CountParagraphs() const; const Paragraph& ParagraphAtIndex(int32 index) const; diff --git a/src/apps/haikudepot/textview/TextDocumentView.cpp b/src/apps/haikudepot/textview/TextDocumentView.cpp index e474646766..379d0b872f 100644 --- a/src/apps/haikudepot/textview/TextDocumentView.cpp +++ b/src/apps/haikudepot/textview/TextDocumentView.cpp @@ -137,14 +137,28 @@ TextDocumentView::MakeFocus(bool focus) void TextDocumentView::MouseDown(BPoint where) { + BMessage* currentMessage = NULL; + if (Window() != NULL) + currentMessage = Window()->CurrentMessage(); + + // First of all, check for links and other clickable things + bool unused; + int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused); + const BMessage* message = fTextDocument->ClickMessageAt(offset); + if (message != NULL) { + BMessage clickMessage(*message); + clickMessage.Append(*currentMessage); + Invoke(&clickMessage); + } + if (!fSelectionEnabled) return; MakeFocus(); int32 modifiers = 0; - if (Window() != NULL && Window()->CurrentMessage() != NULL) - Window()->CurrentMessage()->FindInt32("modifiers", &modifiers); + if (currentMessage != NULL) + currentMessage->FindInt32("modifiers", &modifiers); fMouseDown = true; SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); @@ -165,11 +179,22 @@ void TextDocumentView::MouseMoved(BPoint where, uint32 transit, const BMessage* dragMessage) { + BCursor cursor(B_CURSOR_ID_I_BEAM); + + if (transit != B_EXITED_VIEW) { + bool unused; + int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused); + const BCursor& newCursor = fTextDocument->CursorAt(offset); + if (newCursor.InitCheck() == B_OK) { + cursor = newCursor; + SetViewCursor(&cursor); + } + } + if (!fSelectionEnabled) return; - BCursor iBeamCursor(B_CURSOR_ID_I_BEAM); - SetViewCursor(&iBeamCursor); + SetViewCursor(&cursor); if (fMouseDown) SetCaret(where, true); diff --git a/src/apps/haikudepot/textview/TextDocumentView.h b/src/apps/haikudepot/textview/TextDocumentView.h index c5b8c705a4..ffd33e340a 100644 --- a/src/apps/haikudepot/textview/TextDocumentView.h +++ b/src/apps/haikudepot/textview/TextDocumentView.h @@ -5,6 +5,7 @@ #ifndef TEXT_DOCUMENT_VIEW_H #define TEXT_DOCUMENT_VIEW_H +#include #include #include @@ -17,7 +18,7 @@ class BClipboard; class BMessageRunner; -class TextDocumentView : public BView { +class TextDocumentView : public BView, public BInvoker { public: TextDocumentView(const char* name = NULL); virtual ~TextDocumentView(); diff --git a/src/apps/haikudepot/textview/TextSpan.cpp b/src/apps/haikudepot/textview/TextSpan.cpp index f9fbd588e1..504b0426ed 100644 --- a/src/apps/haikudepot/textview/TextSpan.cpp +++ b/src/apps/haikudepot/textview/TextSpan.cpp @@ -10,7 +10,9 @@ TextSpan::TextSpan() : fText(), fCharCount(0), - fStyle() + fStyle(), + fCursor((BMessage*)NULL), + fClickMessage() { } @@ -19,7 +21,9 @@ TextSpan::TextSpan(const BString& text, const CharacterStyle& style) : fText(text), fCharCount(text.CountChars()), - fStyle(style) + fStyle(style), + fCursor((BMessage*)NULL), + fClickMessage() { } @@ -28,7 +32,9 @@ TextSpan::TextSpan(const TextSpan& other) : fText(other.fText), fCharCount(other.fCharCount), - fStyle(other.fStyle) + fStyle(other.fStyle), + fCursor(other.fCursor), + fClickMessage(other.fClickMessage) { } @@ -39,6 +45,8 @@ TextSpan::operator=(const TextSpan& other) fText = other.fText; fCharCount = other.fCharCount; fStyle = other.fStyle; + fCursor = other.fCursor; + fClickMessage = other.fClickMessage; return *this; } @@ -49,7 +57,10 @@ TextSpan::operator==(const TextSpan& other) const { return fCharCount == other.fCharCount && fStyle == other.fStyle - && fText == other.fText; + && fText == other.fText + && fCursor == other.fCursor + && fClickMessage.what == other.fClickMessage.what + && fClickMessage.HasSameData(other.fClickMessage); } @@ -75,6 +86,20 @@ TextSpan::SetStyle(const CharacterStyle& style) } +void +TextSpan::SetCursor(const BCursor& cursor) +{ + fCursor = cursor; +} + + +void +TextSpan::SetClickMessage(BMessage* message) +{ + fClickMessage = *message; +} + + bool TextSpan::Append(const BString& text) { diff --git a/src/apps/haikudepot/textview/TextSpan.h b/src/apps/haikudepot/textview/TextSpan.h index efa5d9d460..6a20bcaa7e 100644 --- a/src/apps/haikudepot/textview/TextSpan.h +++ b/src/apps/haikudepot/textview/TextSpan.h @@ -6,6 +6,7 @@ #define TEXT_SPAN_H +#include #include #include "CharacterStyle.h" @@ -39,6 +40,12 @@ public: TextSpan SubSpan(int32 start, int32 count) const; + void SetCursor(const BCursor& cursor); + inline const BCursor& Cursor() const + { return fCursor; } + void SetClickMessage(BMessage* message); + inline const BMessage* ClickMessage() const + { return fClickMessage.IsEmpty() ? NULL : &fClickMessage; } private: void _TruncateInsert(int32& start) const; void _TruncateRemove(int32& start, @@ -48,6 +55,9 @@ private: BString fText; int32 fCharCount; CharacterStyle fStyle; + + BCursor fCursor; + BMessage fClickMessage; };