scroll_text: markup and link awareness support

support for copying and partial editing only.
this does not make multiline_text a rich text editor.
This commit is contained in:
Subhraman Sarkar 2024-09-17 07:46:22 +05:30 committed by Charles Dang
parent 6023eb338c
commit 54535fca3b
8 changed files with 140 additions and 85 deletions

View File

@ -57,6 +57,8 @@
inactive_color = ["{GUI__FONT_COLOR_DISABLED__DEFAULT}"]
)"
text = "(text)"
text_link_aware = "(text_link_aware)"
text_markup = "(text_markup)"
highlight_color = "21, 53, 80"
highlight_start = "(highlight_start)"
highlight_end = "(highlight_end)"
@ -96,6 +98,8 @@
reg_color = [{COLOR}]
)"
text_link_aware = "(text_link_aware)"
text_markup = "(text_markup)"
text = "(
if(text = '' and hint_text != '', hint_text, text))"
[/text]
@ -149,8 +153,6 @@
text_y_offset = 2
text_extra_width = {EXTRA_WIDTH}
#functions = "(def show_hint_text() (text = '' and hint_text != '');)"
[state_enabled]
[draw]

View File

@ -17,8 +17,12 @@
#include "gui/widgets/multiline_text.hpp"
#include "cursor.hpp"
#include "desktop/clipboard.hpp"
#include "desktop/open.hpp"
#include "gui/core/log.hpp"
#include "gui/core/register_widget.hpp"
#include "gui/dialogs/message.hpp"
#include "gui/widgets/window.hpp"
#include "serialization/unicode.hpp"
#include "font/text.hpp"
@ -37,7 +41,7 @@ namespace gui2
REGISTER_WIDGET(multiline_text)
multiline_text::multiline_text(const implementation::builder_styled_widget& builder)
multiline_text::multiline_text(const implementation::builder_multiline_text& builder)
: text_box_base(builder, type())
, history_()
, max_input_length_(0)
@ -45,6 +49,7 @@ multiline_text::multiline_text(const implementation::builder_styled_widget& buil
, text_y_offset_(0)
, text_height_(0)
, dragging_(false)
, link_aware_(builder.link_aware)
{
set_wants_mouse_left_double_click();
@ -66,6 +71,15 @@ multiline_text::multiline_text(const implementation::builder_styled_widget& buil
update_offsets();
}
void multiline_text::set_link_aware(bool link_aware)
{
if(link_aware != link_aware_) {
link_aware_ = link_aware;
update_canvas();
queue_redraw();
}
}
void multiline_text::place(const point& origin, const point& size)
{
// Inherited.
@ -124,16 +138,22 @@ void multiline_text::update_canvas()
const int max_width = get_text_maximum_width();
const int max_height = get_text_maximum_height();
const point cpos = get_cursor_pos_from_index(start + length);
unsigned byte_pos = start + length;
if (get_use_markup() && (start + length > utf8::size(plain_text()) + 1)) {
byte_pos = utf8::size(plain_text());
}
const point cpos = get_cursor_pos_from_index(byte_pos);
for(auto & tmp : get_canvases())
{
tmp.set_variable("text", wfl::variant(get_value()));
tmp.set_variable("text_markup", wfl::variant(get_use_markup()));
tmp.set_variable("text_x_offset", wfl::variant(text_x_offset_));
tmp.set_variable("text_y_offset", wfl::variant(text_y_offset_));
tmp.set_variable("text_maximum_width", wfl::variant(max_width));
tmp.set_variable("text_maximum_height", wfl::variant(max_height));
tmp.set_variable("text_link_aware", wfl::variant(get_link_aware()));
tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
tmp.set_variable("editable", wfl::variant(is_editable()));
@ -191,26 +211,22 @@ void multiline_text::delete_selection()
void multiline_text::handle_mouse_selection(point mouse, const bool start_selection)
{
mouse.x -= get_x();
mouse.y -= get_y();
mouse -= get_origin();
point text_offset(text_x_offset_, text_y_offset_);
// FIXME we don't test for overflow in width
if(mouse.x < static_cast<int>(text_x_offset_)
|| mouse.y < static_cast<int>(text_y_offset_)
|| mouse.y >= static_cast<int>(text_y_offset_ + get_lines_count() * font::get_line_spacing_factor() * text_height_)) {
if(mouse < text_offset
|| mouse.y >= static_cast<int>(text_y_offset_ + get_lines_count() * font::get_line_spacing_factor() * text_height_))
{
return;
}
point cursor_pos = get_column_line(point(mouse.x - text_x_offset_, mouse.y - text_y_offset_));
int offset = cursor_pos.x;
int line = cursor_pos.y;
const auto& [offset, line] = get_column_line(mouse - text_offset);
if(offset < 0) {
return;
}
offset += get_line_start_offset(line);
set_cursor(offset, !start_selection);
set_cursor(offset + get_line_start_offset(line), !start_selection);
update_canvas();
queue_redraw();
@ -319,10 +335,10 @@ void multiline_text::handle_key_down_arrow(SDL_Keymod modifier, bool& handled)
handled = true;
size_t offset = get_selection_start();
unsigned offset = get_selection_start();
const unsigned line_num = get_line_number(offset);
if (line_num == get_lines_count()) {
if (line_num == get_lines_count()-1) {
return;
}
@ -330,15 +346,7 @@ void multiline_text::handle_key_down_arrow(SDL_Keymod modifier, bool& handled)
const unsigned next_line_start = get_line_start_offset(line_num+1);
const unsigned next_line_end = get_line_end_offset(line_num+1);
if (line_num < get_lines_count()-1) {
offset = offset - line_start + next_line_start;
if (offset > next_line_end) {
offset = next_line_end;
}
}
offset += get_selection_length();
offset = std::min(offset - line_start + next_line_start, next_line_end) + get_selection_length();
if (offset <= get_length()) {
set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
@ -354,7 +362,7 @@ void multiline_text::handle_key_up_arrow(SDL_Keymod modifier, bool& handled)
handled = true;
size_t offset = get_selection_start();
unsigned offset = get_selection_start();
const unsigned line_num = get_line_number(offset);
if (line_num == 0) {
@ -365,15 +373,7 @@ void multiline_text::handle_key_up_arrow(SDL_Keymod modifier, bool& handled)
const unsigned prev_line_start = get_line_start_offset(line_num-1);
const unsigned prev_line_end = get_line_end_offset(line_num-1);
if (line_num > 0) {
offset = offset - line_start + prev_line_start;
if (offset > prev_line_end) {
offset = prev_line_end;
}
}
offset += get_selection_length();
offset = std::min(offset - line_start + prev_line_start, prev_line_end) + get_selection_length();
/* offset is unsigned int */
if (offset <= get_length()) {
@ -392,6 +392,17 @@ void multiline_text::signal_handler_mouse_motion(const event::ui_event event,
if(dragging_) {
handle_mouse_selection(coordinate, false);
} else {
if(!get_link_aware()) {
return; // without marking event as "handled"
}
point mouse = coordinate - get_origin();
if (!get_label_link(mouse).empty()) {
cursor::set(cursor::HYPERLINK);
} else {
cursor::set(cursor::IBEAM);
}
}
handled = true;
@ -405,7 +416,27 @@ void multiline_text::signal_handler_left_button_down(const event::ui_event event
get_window()->keyboard_capture(this);
get_window()->mouse_capture();
handle_mouse_selection(get_mouse_position(), true);
point mouse_pos = get_mouse_position();
if (get_link_aware()) {
std::string link = get_label_link(mouse_pos - get_origin());
DBG_GUI_E << "Clicked Link:\"" << link << "\"";
if (!link.empty()) {
if (desktop::open_object_is_supported()) {
if(show_message(_("Open link?"), link, dialogs::message::yes_no_buttons) == gui2::retval::OK) {
desktop::open_object(link);
}
} else {
desktop::clipboard::copy_to_clipboard(link, true);
show_message("", _("Opening links is not supported, contact your packager. Link URL has been copied to the clipboard."), dialogs::message::auto_close);
}
} else {
handle_mouse_selection(mouse_pos, true);
}
} else {
handle_mouse_selection(mouse_pos, true);
}
handled = true;
}
@ -464,6 +495,7 @@ builder_multiline_text::builder_multiline_text(const config& cfg)
, hint_image(cfg["hint_image"])
, editable(cfg["editable"].to_bool(true))
, wrap(cfg["wrap"].to_bool(true))
, link_aware(cfg["link_aware"].to_bool(false))
{
}

View File

@ -33,7 +33,7 @@ class multiline_text : public text_box_base
friend struct implementation::builder_multiline_text;
public:
explicit multiline_text(const implementation::builder_styled_widget& builder);
explicit multiline_text(const implementation::builder_multiline_text& builder);
/** See @ref widget::can_wrap. */
bool can_wrap() const override
@ -143,6 +143,14 @@ public:
update_layout();
}
/** See @ref styled_widget::get_link_aware. */
virtual bool get_link_aware() const override
{
return link_aware_;
}
void set_link_aware(bool l);
private:
/** Inherited from text_box_base. */
void paste_selection(const bool mouse) override
@ -194,6 +202,12 @@ private:
/** Is the mouse in dragging mode, this affects selection in mouse move */
bool dragging_;
/**
* Whether the text area is link aware, rendering links with special formatting
* and handling click events.
*/
bool link_aware_;
/** Helper text to display (such as "Search") if the text box is empty. */
std::string hint_text_;
@ -351,6 +365,7 @@ public:
bool editable;
bool wrap;
bool link_aware;
};
} // namespace implementation

View File

@ -1,6 +1,6 @@
/*
Copyright (C) 2023 - 2024
by babaissarkar(Subhraman Sarkar) <suvrax@gmail.com>
by Subhraman Sarkar (babaissarkar) <suvrax@gmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
@ -41,8 +41,9 @@ scroll_text::scroll_text(const implementation::builder_scroll_text& builder)
, state_(ENABLED)
, wrap_on_(false)
, text_alignment_(builder.text_alignment)
, editable_(true)
, editable_(builder.editable)
, max_size_(point(0,0))
, link_aware_(builder.link_aware)
{
connect_signal<event::LEFT_BUTTON_DOWN>(
std::bind(&scroll_text::signal_handler_left_button_down, this, std::placeholders::_2),
@ -81,6 +82,15 @@ std::string scroll_text::get_value()
}
}
void scroll_text::set_link_aware(bool l)
{
link_aware_ = l;
if(multiline_text* widget = get_internal_text_box()) {
widget->set_link_aware(l);
}
}
void scroll_text::set_text_alignment(const PangoAlignment text_alignment)
{
// Inherit.
@ -93,16 +103,6 @@ void scroll_text::set_text_alignment(const PangoAlignment text_alignment)
}
}
void scroll_text::set_use_markup(bool use_markup)
{
// Inherit.
styled_widget::set_use_markup(use_markup);
if(multiline_text* widget = get_internal_text_box()) {
widget->set_use_markup(use_markup);
}
}
void scroll_text::set_self_active(const bool active)
{
state_ = active ? ENABLED : DISABLED;
@ -126,6 +126,7 @@ void scroll_text::finalize_subclass()
text->set_editable(is_editable());
text->set_label(get_label());
text->set_text_alignment(text_alignment_);
text->set_link_aware(link_aware_);
text->set_use_markup(get_use_markup());
}
@ -244,6 +245,7 @@ builder_scroll_text::builder_scroll_text(const config& cfg)
, horizontal_scrollbar_mode(get_scrollbar_mode(cfg["horizontal_scrollbar_mode"]))
, text_alignment(decode_text_alignment(cfg["text_alignment"]))
, editable(cfg["editable"].to_bool(true))
, link_aware(cfg["link_aware"].to_bool(false))
{
// Scrollbar default to auto. AUTO_VISIBLE_FIRST_RUN doesn't work.
if (horizontal_scrollbar_mode == scrollbar_container::AUTO_VISIBLE_FIRST_RUN) {
@ -261,8 +263,6 @@ std::unique_ptr<widget> builder_scroll_text::build() const
widget->set_vertical_scrollbar_mode(vertical_scrollbar_mode);
widget->set_horizontal_scrollbar_mode(horizontal_scrollbar_mode);
widget->set_editable(editable);
widget->set_text_alignment(text_alignment);
const auto conf = widget->cast_config_to<scroll_text_definition>();
assert(conf);

View File

@ -1,6 +1,6 @@
/*
Copyright (C) 2023 - 2024
by babaissarkar(Subhraman Sarkar) <suvrax@gmail.com>
by Subhraman Sarkar (babaissarkar) <suvrax@gmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
@ -53,9 +53,6 @@ public:
/** See @ref styled_widget::set_text_alignment. */
virtual void set_text_alignment(const PangoAlignment text_alignment) override;
/** See @ref styled_widget::set_use_markup. */
virtual void set_use_markup(bool use_markup) override;
/** See @ref container_base::set_self_active. */
virtual void set_self_active(const bool active) override;
@ -70,6 +67,14 @@ public:
bool can_wrap() const override;
void set_can_wrap(bool can_wrap);
void set_link_aware(bool l);
/** See @ref styled_widget::get_link_aware. */
virtual bool get_link_aware() const override
{
return link_aware_;
}
void set_editable(bool editable)
{
editable_ = editable;
@ -110,6 +115,8 @@ private:
point max_size_;
bool link_aware_;
void finalize_subclass() override;
/** Used for moving scrollbars.
@ -183,6 +190,7 @@ struct builder_scroll_text : public builder_styled_widget
scrollbar_container::scrollbar_mode horizontal_scrollbar_mode;
const PangoAlignment text_alignment;
bool editable;
bool link_aware;
};
} // namespace implementation

View File

@ -487,7 +487,7 @@ point styled_widget::get_best_text_size(point minimum_size, point maximum_size)
<< "Status:\n"
<< "minimum_size: " << minimum_size << "\n"
<< "maximum_size: " << maximum_size << "\n"
<< "text_maximum_width_: " << text_maximum_width_ << "\n"
<< "maximum width of text: " << text_maximum_width_ << "\n"
<< "can_wrap: " << can_wrap() << "\n"
<< "characters_per_line: " << get_characters_per_line() << "\n"
<< "truncated: " << renderer_.is_truncated() << "\n"

View File

@ -123,7 +123,7 @@ void text_box_base::set_maximum_length(const std::size_t maximum_length)
void text_box_base::set_value(const std::string& text)
{
if(text != text_.text()) {
text_.set_text(text, false);
text_.set_text(text, get_use_markup());
// default to put the cursor at the end of the buffer.
selection_start_ = text_.get_length();
@ -138,31 +138,18 @@ void text_box_base::set_cursor(const std::size_t offset, const bool select)
reset_cursor_state();
if(select) {
if(selection_start_ == offset) {
selection_length_ = 0;
} else {
selection_length_ = -static_cast<int>(selection_start_ - offset);
}
selection_length_ = (selection_start_ == offset) ? 0 : -static_cast<int>(selection_start_ - offset);
#ifdef __unix__
// selecting copies on UNIX systems.
copy_selection(true);
#endif
update_canvas();
queue_redraw();
} else {
if (offset <= text_.get_length()) {
selection_start_ = offset;
} else {
selection_start_ = 0;
}
selection_start_ = (offset <= text_.get_length()) ? offset : 0;
selection_length_ = 0;
update_canvas();
queue_redraw();
}
update_canvas();
queue_redraw();
}
void text_box_base::insert_char(const std::string& unicode)
@ -174,10 +161,14 @@ void text_box_base::insert_char(const std::string& unicode)
delete_selection();
if(text_.insert_text(selection_start_, unicode)) {
if(text_.insert_text(selection_start_, unicode, get_use_markup())) {
// Update status
set_cursor(selection_start_ + utf8::size(unicode), false);
size_t plain_text_len = utf8::size(plain_text());
size_t cursor_pos = selection_start_ + utf8::size(unicode);
if (get_use_markup() && (selection_start_ + utf8::size(unicode) > plain_text_len + 1)) {
cursor_pos = plain_text_len;
}
set_cursor(cursor_pos, false);
update_canvas();
queue_redraw();
}
@ -213,7 +204,7 @@ void text_box_base::copy_selection(const bool mouse)
}
unsigned end, start = selection_start_;
const std::string txt = text_.text();
const std::string txt = get_use_markup() ? plain_text() : text_.text();
if(selection_length_ > 0) {
end = utf8::index(txt, start + selection_length_);
@ -240,7 +231,7 @@ void text_box_base::paste_selection(const bool mouse)
delete_selection();
selection_start_ += text_.insert_text(selection_start_, text);
selection_start_ += text_.insert_text(selection_start_, text, get_use_markup());
update_canvas();
queue_redraw();
@ -383,7 +374,7 @@ void text_box_base::handle_key_right_arrow(SDL_Keymod modifier, bool& handled)
handled = true;
const std::size_t offset = selection_start_ + 1 + selection_length_;
if(offset <= text_.get_length()) {
if(offset <= (get_use_markup() ? utf8::size(plain_text()) : text_.get_length())) {
set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
}
}
@ -498,13 +489,13 @@ void text_box_base::handle_editing(bool& handled, const std::string& unicode, in
// SDL_TextEditingEvent.
// start is start position of the separated event in entire composition text
if(start == 0) {
text_.set_text(text_cached_, false);
text_.set_text(text_cached_, get_use_markup());
}
text_.insert_text(ime_start_point_ + start, unicode);
text_.insert_text(ime_start_point_ + start, unicode, get_use_markup());
#else
std::string new_text(text_cached_);
utf8::insert(new_text, ime_start_point_, unicode);
text_.set_text(new_text, false);
text_.set_text(new_text, get_use_markup());
#endif
int maximum_length = text_.get_length();

View File

@ -147,6 +147,13 @@ public:
return text_.text();
}
std::string plain_text()
{
char* plain_text = nullptr;
pango_parse_markup(text().c_str(), text().size(), 0, nullptr, &plain_text, nullptr, nullptr);
return plain_text ? std::string(plain_text) : std::string();
}
/** Set the text_changed callback. */
void set_text_changed_callback(
std::function<void(text_box_base* textbox, const std::string text)> cb)