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 <pulkomandy@pulkomandy.tk>
Tested-by: Commit checker robot <no-reply+buildbot@haiku-os.org>
This commit is contained in:
PulkoMandy 2023-11-23 20:49:45 +01:00 committed by Adrien Destugues
parent 9cd2a23203
commit 6af1381397
6 changed files with 114 additions and 9 deletions

View File

@ -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
{

View File

@ -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;

View File

@ -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);

View File

@ -5,6 +5,7 @@
#ifndef TEXT_DOCUMENT_VIEW_H
#define TEXT_DOCUMENT_VIEW_H
#include <Invoker.h>
#include <String.h>
#include <View.h>
@ -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();

View File

@ -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)
{

View File

@ -6,6 +6,7 @@
#define TEXT_SPAN_H
#include <Cursor.h>
#include <String.h>
#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;
};