diff --git a/data/gui/themes/default/widget/multiline_text_default.cfg b/data/gui/themes/default/widget/multiline_text_default.cfg index 99723dd4d3a..d651d526533 100644 --- a/data/gui/themes/default/widget/multiline_text_default.cfg +++ b/data/gui/themes/default/widget/multiline_text_default.cfg @@ -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] diff --git a/src/gui/widgets/multiline_text.cpp b/src/gui/widgets/multiline_text.cpp index 1abb349bed6..595c8e82e74 100644 --- a/src/gui/widgets/multiline_text.cpp +++ b/src/gui/widgets/multiline_text.cpp @@ -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(text_x_offset_) - || mouse.y < static_cast(text_y_offset_) - || mouse.y >= static_cast(text_y_offset_ + get_lines_count() * font::get_line_spacing_factor() * text_height_)) { + if(mouse < text_offset + || mouse.y >= static_cast(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)) { } diff --git a/src/gui/widgets/multiline_text.hpp b/src/gui/widgets/multiline_text.hpp index 7e4d14696d5..1e29a9a4dc3 100644 --- a/src/gui/widgets/multiline_text.hpp +++ b/src/gui/widgets/multiline_text.hpp @@ -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 diff --git a/src/gui/widgets/scroll_text.cpp b/src/gui/widgets/scroll_text.cpp index d7945585e1e..4af7d25453c 100644 --- a/src/gui/widgets/scroll_text.cpp +++ b/src/gui/widgets/scroll_text.cpp @@ -1,6 +1,6 @@ /* Copyright (C) 2023 - 2024 - by babaissarkar(Subhraman Sarkar) + by Subhraman Sarkar (babaissarkar) 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( 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 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(); assert(conf); diff --git a/src/gui/widgets/scroll_text.hpp b/src/gui/widgets/scroll_text.hpp index 2021111fe63..fdc1ed8f0e7 100644 --- a/src/gui/widgets/scroll_text.hpp +++ b/src/gui/widgets/scroll_text.hpp @@ -1,6 +1,6 @@ /* Copyright (C) 2023 - 2024 - by babaissarkar(Subhraman Sarkar) + by Subhraman Sarkar (babaissarkar) 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 diff --git a/src/gui/widgets/styled_widget.cpp b/src/gui/widgets/styled_widget.cpp index 625751c42b4..aaab4cf727f 100644 --- a/src/gui/widgets/styled_widget.cpp +++ b/src/gui/widgets/styled_widget.cpp @@ -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" diff --git a/src/gui/widgets/text_box_base.cpp b/src/gui/widgets/text_box_base.cpp index 69b5da8a02b..8daab644998 100644 --- a/src/gui/widgets/text_box_base.cpp +++ b/src/gui/widgets/text_box_base.cpp @@ -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(selection_start_ - offset); - } - + selection_length_ = (selection_start_ == offset) ? 0 : -static_cast(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(); diff --git a/src/gui/widgets/text_box_base.hpp b/src/gui/widgets/text_box_base.hpp index 9aa5f056670..655e33c50b8 100644 --- a/src/gui/widgets/text_box_base.hpp +++ b/src/gui/widgets/text_box_base.hpp @@ -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 cb)