diff --git a/data/gui/default/widget/label_default.cfg b/data/gui/default/widget/label_default.cfg index fc883c2164ed..a4017604132c 100644 --- a/data/gui/default/widget/label_default.cfg +++ b/data/gui/default/widget/label_default.cfg @@ -23,6 +23,9 @@ text_font_size = {FONT_SIZE} text_font_style = {FONT_STYLE} + link_aware = true + link_color = #ffff00 + [state_enabled] [draw] @@ -38,6 +41,8 @@ color = {FONT_COLOR_ENABLED} text = "(text)" text_markup = "(text_markup)" + text_link_aware = "(text_link_aware)" + text_link_color = "(text_link_color)" [/text] [/draw] @@ -59,6 +64,8 @@ color = {FONT_COLOR_DISABLED} text = "(text)" text_markup = "(text_markup)" + text_link_aware = "(text_link_aware)" + text_link_color = "(text_link_color)" [/text] [/draw] diff --git a/data/gui/schema.cfg b/data/gui/schema.cfg index 312b18fa6a01..da941a9e4c52 100644 --- a/data/gui/schema.cfg +++ b/data/gui/schema.cfg @@ -366,6 +366,16 @@ type="f_bool" default=false [/key] + [key] + name="text_link_aware" + type="f_bool" + default=false + [/key] + [key] + name="text_link_color" + type="string" + default="#ffff00" + [/key] [key] name="w" type="f_unsigned" @@ -680,6 +690,16 @@ max="1" super="generic/state" [/tag] + [key] + name="link_aware" + type="bool" + default="false" + [/key] + [key] + name="link_color" + type="string" + default="#ffff00" + [/key] [/tag] [/tag] [tag] diff --git a/src/gui/auxiliary/canvas.cpp b/src/gui/auxiliary/canvas.cpp index 149380ab7163..22f0ce1950a9 100644 --- a/src/gui/auxiliary/canvas.cpp +++ b/src/gui/auxiliary/canvas.cpp @@ -1270,6 +1270,12 @@ class ttext : public tcanvas::tshape /** The text markup switch of the text. */ tformula text_markup_; + /** The link aware switch of the text. */ + tformula link_aware_; + + /** The link color of the text. */ + tformula link_color_; + /** The maximum width for the text. */ tformula maximum_width_; @@ -1303,6 +1309,10 @@ class ttext : public tcanvas::tshape * color & color & "" & The color of the text. $ * text & f_tstring & "" & The text to draw (translatable). $ * text_markup & f_bool & false & Can the text have mark-up? $ + * text_link_aware & f_bool & false & + * Is the text link aware? $ + * text_link_color & f_string & "#ffff00" & + * The color of links in the text $ * maximum_width & f_int & -1 & The maximum width the text is allowed to * be. $ * maximum_height & f_int & -1 & The maximum height the text is allowed @@ -1336,6 +1346,8 @@ ttext::ttext(const config& cfg) , color_(decode_color(cfg["color"])) , text_(cfg["text"]) , text_markup_(cfg["text_markup"], false) + , link_aware_(cfg["text_link_aware"], false) + , link_color_(cfg["text_link_color"], "#ffff00") , maximum_width_(cfg["maximum_width"], -1) , characters_per_line_(cfg["text_characters_per_line"]) , maximum_height_(cfg["maximum_height"], -1) @@ -1364,6 +1376,9 @@ void ttext::draw(surface& canvas, } static font::ttext text_renderer; + + text_renderer.set_link_aware(link_aware_(variables)) + .set_link_color(link_color_(variables)); text_renderer.set_text(text, text_markup_(variables)); text_renderer.set_font_size(font_size_) diff --git a/src/gui/auxiliary/widget_definition/label.cpp b/src/gui/auxiliary/widget_definition/label.cpp index 0d755495b62a..2992f27000a0 100644 --- a/src/gui/auxiliary/widget_definition/label.cpp +++ b/src/gui/auxiliary/widget_definition/label.cpp @@ -41,12 +41,24 @@ tlabel_definition::tlabel_definition(const config& cfg) * The reason is that labels are often used as visual indication of the state * of the widget it labels. * + * Note: The above is outdated, if "link_aware" is enabled then there is interaction. + * + * * The following states exist: * * state_enabled, the label is enabled. * * state_disabled, the label is disabled. * @begin{parent}{name="gui/"} * @begin{tag}{name="label_definition"}{min=0}{max=-1}{super="generic/widget_definition"} * @begin{tag}{name="resolution"}{min=0}{max=-1}{super="generic/widget_definition/resolution"} + * @begin{table}{config} + * link_aware & f_bool & false & Whether the label is link aware. This means + * it is rendered with links highlighted, + * and responds to click events on those + * links. $ + * link_color & string & #ffff00 & The color to render links with. This + * string will be used verbatim in pango + * markup for each link. $ + * @end{table} * @begin{tag}{name="state_enabled"}{min=0}{max=1}{super="generic/state"} * @end{tag}{name="state_enabled"} * @begin{tag}{name="state_disabled"}{min=0}{max=1}{super="generic/state"} @@ -57,6 +69,8 @@ tlabel_definition::tlabel_definition(const config& cfg) */ tlabel_definition::tresolution::tresolution(const config& cfg) : tresolution_definition_(cfg) + , link_aware(cfg["link_aware"].to_bool(false)) + , link_color(cfg["link_color"].str().size() > 0 ? cfg["link_color"].str() : "#ffff00") { // Note the order should be the same as the enum tstate is label.hpp. state.push_back(tstate_definition(cfg.child("state_enabled"))); diff --git a/src/gui/auxiliary/widget_definition/label.hpp b/src/gui/auxiliary/widget_definition/label.hpp index a814626e98df..786a58cdd691 100644 --- a/src/gui/auxiliary/widget_definition/label.hpp +++ b/src/gui/auxiliary/widget_definition/label.hpp @@ -28,6 +28,9 @@ struct tlabel_definition : public tcontrol_definition struct tresolution : public tresolution_definition_ { explicit tresolution(const config& cfg); + + bool link_aware; + std::string link_color; }; }; diff --git a/src/gui/widgets/control.cpp b/src/gui/widgets/control.cpp index f06a5e59ceed..6d02740140ba 100644 --- a/src/gui/widgets/control.cpp +++ b/src/gui/widgets/control.cpp @@ -173,6 +173,16 @@ unsigned tcontrol::get_characters_per_line() const return 0; } +bool tcontrol::get_link_aware() const +{ + return false; +} + +std::string tcontrol::get_link_color() const +{ + return "#ffff00"; +} + void tcontrol::layout_initialise(const bool full_initialisation) { // Inherited. @@ -353,6 +363,8 @@ void tcontrol::update_canvas() { canvas.set_variable("text", variant(label_)); canvas.set_variable("text_markup", variant(use_markup_)); + canvas.set_variable("text_link_aware", variant(get_link_aware())); + canvas.set_variable("text_link_color", variant(get_link_color())); canvas.set_variable("text_alignment", variant(encode_text_alignment(text_alignment_))); canvas.set_variable("text_maximum_width", variant(max_width)); @@ -437,6 +449,9 @@ tpoint tcontrol::get_best_text_size(const tpoint& minimum_size, const tpoint border(config_->text_extra_width, config_->text_extra_height); tpoint size = minimum_size - border; + renderer_.set_link_aware(get_link_aware()) + .set_link_color(get_link_color()); + renderer_.set_text(label_, use_markup_); renderer_.set_font_size(config_->text_font_size); @@ -543,4 +558,14 @@ void tcontrol::signal_handler_notify_remove_tooltip(const event::tevent event, handled = true; } +std::string tcontrol::get_label_token(const gui2::tpoint & position, const char * delim) const +{ + return renderer_.get_token(position, delim); +} + +std::string tcontrol::get_label_link(const gui2::tpoint & position) const +{ + return renderer_.get_link(position); +} + } // namespace gui2 diff --git a/src/gui/widgets/control.hpp b/src/gui/widgets/control.hpp index c6d130d13d9e..764cfc6a4061 100644 --- a/src/gui/widgets/control.hpp +++ b/src/gui/widgets/control.hpp @@ -141,6 +141,29 @@ class tcontrol : public twidget */ virtual unsigned get_characters_per_line() const; + /** + * Returns whether the label should be link_aware, in + * in rendering and in searching for links with get_link. + * + * This value is used to call @ref ttext::set_link_aware + * (indirectly). + * + * @returns The link aware status. This impl always + * always returns false. + */ + virtual bool get_link_aware() const; + + /** + * Returns the color string to be used with links. + * + * This value is used to call @ref ttext::set_link_color + * (indirectly). + * + * @returns The link color string. This impl returns "#ffff00". + * + */ + virtual std::string get_link_color() const; + /** * See @ref twidget::layout_initialise. * @@ -421,6 +444,11 @@ class tcontrol : public twidget int x_offset, int y_offset) OVERRIDE; + /** Exposes font::ttext::get_token, for the text label of this control */ + std::string get_label_token(const gui2::tpoint & position, const char * delimiters = " \n\r\t") const; + + std::string get_label_link(const gui2::tpoint & position) const; + private: #ifdef GUI2_EXPERIMENTAL_LISTBOX /** diff --git a/src/gui/widgets/label.cpp b/src/gui/widgets/label.cpp index e68db6149c19..c2368d8da28a 100644 --- a/src/gui/widgets/label.cpp +++ b/src/gui/widgets/label.cpp @@ -16,18 +16,39 @@ #include "gui/widgets/label.hpp" +#include "gui/auxiliary/log.hpp" #include "gui/auxiliary/widget_definition/label.hpp" #include "gui/auxiliary/window_builder/label.hpp" +#include "gui/dialogs/message.hpp" #include "gui/widgets/detail/register.tpp" #include "gui/widgets/settings.hpp" +#include "gui/widgets/window.hpp" + +#include "desktop/clipboard.hpp" +#include "desktop/open.hpp" +#include "gettext.hpp" #include +#include +#include namespace gui2 { REGISTER_WIDGET(label) +tlabel::tlabel() + : tcontrol(COUNT) + , state_(ENABLED) + , can_wrap_(false) + , characters_per_line_(0) + , link_aware_(false) + , link_color_("#ffff00") +{ + connect_signal(boost::bind(&tlabel::signal_handler_left_button_click, this, _2, _3)); + connect_signal(boost::bind(&tlabel::signal_handler_right_button_click, this, _2, _3)); +} + bool tlabel::can_wrap() const { return can_wrap_ || characters_per_line_ != 0; @@ -38,6 +59,16 @@ unsigned tlabel::get_characters_per_line() const return characters_per_line_; } +bool tlabel::get_link_aware() const +{ + return link_aware_; +} + +std::string tlabel::get_link_color() const +{ + return link_color_; +} + void tlabel::set_active(const bool active) { if(get_active() != active) { @@ -65,6 +96,27 @@ void tlabel::set_characters_per_line(const unsigned characters_per_line) characters_per_line_ = characters_per_line; } +void tlabel::set_link_aware(bool link_aware) +{ + if(link_aware == link_aware_) { + return; + } + + link_aware_ = link_aware; + update_canvas(); + set_is_dirty(true); +} + +void tlabel::set_link_color(const std::string & color) +{ + if(color == link_color_) { + return; + } + link_color_ = color; + update_canvas(); + set_is_dirty(true); +} + void tlabel::set_state(const tstate state) { if(state != state_) { @@ -79,4 +131,86 @@ const std::string& tlabel::get_control_type() const return type; } +void tlabel::load_config_extra() +{ + assert(config()); + + boost::intrusive_ptr + conf = boost::dynamic_pointer_cast( + config()); + + assert(conf); + + set_link_aware(conf->link_aware); + set_link_color(conf->link_color); +} + + +void tlabel::signal_handler_left_button_click(const event::tevent /* event */, bool & handled) +{ + DBG_GUI_E << "label click" << std::endl; + + if (!get_link_aware()) { + return ; // without marking event as "handled". + } + + if (!desktop::open_object_is_supported()) { + gui2::show_message(get_window()->video(), "", _("Opening links is not supported, contact your packager."), gui2::tmessage::auto_close); + handled = true; + return; + } + + get_window()->mouse_capture(); + + tpoint mouse = get_mouse_position(); + + mouse.x -= get_x(); + mouse.y -= get_y(); + + std::string link = get_label_link(mouse); + + if (link.length() == 0) { + return ; // without marking event as "handled" + } + + DBG_GUI_E << "Clicked Link:\"" << link << "\"\n"; + + const int res = gui2::show_message(get_window()->video(), "", _("Do you want to open this link?") + std::string("\n") + link, gui2::tmessage::yes_no_buttons); + if(res != gui2::twindow::CANCEL) { + desktop::open_object(link); + } + + handled = true; +} + +void tlabel::signal_handler_right_button_click(const event::tevent /* event */, bool & handled) +{ + DBG_GUI_E << "label right click" << std::endl; + + if (!get_link_aware()) { + return ; // without marking event as "handled". + } + + get_window()->mouse_capture(); + + tpoint mouse = get_mouse_position(); + + mouse.x -= get_x(); + mouse.y -= get_y(); + + std::string link = get_label_link(mouse); + + if (link.length() == 0) { + return ; // without marking event as "handled" + } + + DBG_GUI_E << "Right Clicked Link:\"" << link << "\"\n"; + + desktop::clipboard::copy_to_clipboard(link, false); + + gui2::show_message(get_window()->video(), "", _("Copied link!"), gui2::tmessage::auto_close); + + handled = true; +} + } // namespace gui2 diff --git a/src/gui/widgets/label.hpp b/src/gui/widgets/label.hpp index 93560dd374ae..586ddf414e98 100644 --- a/src/gui/widgets/label.hpp +++ b/src/gui/widgets/label.hpp @@ -24,13 +24,7 @@ namespace gui2 class tlabel : public tcontrol { public: - tlabel() - : tcontrol(COUNT) - , state_(ENABLED) - , can_wrap_(false) - , characters_per_line_(0) - { - } + tlabel(); /** See @ref twidget::can_wrap. */ virtual bool can_wrap() const OVERRIDE; @@ -38,6 +32,12 @@ class tlabel : public tcontrol /** See @ref tcontrol::get_characters_per_line. */ virtual unsigned get_characters_per_line() const OVERRIDE; + /** See @ref tcontrol::get_link_aware. */ + virtual bool get_link_aware() const OVERRIDE; + + /** See @ref tcontrol::get_link_aware. */ + virtual std::string get_link_color() const OVERRIDE; + /** See @ref tcontrol::set_active. */ virtual void set_active(const bool active) OVERRIDE; @@ -59,6 +59,10 @@ class tlabel : public tcontrol void set_characters_per_line(const unsigned set_characters_per_line); + void set_link_aware(bool l); + + void set_link_color(const std::string & color); + private: /** * Possible states of the widget. @@ -91,8 +95,34 @@ class tlabel : public tcontrol */ unsigned characters_per_line_; + /** + * Whether the label is link aware, rendering links with special formatting + * and handling click events. + */ + bool link_aware_; + + /** + * What color links will be rendered in. + */ + std::string link_color_; + /** See @ref tcontrol::get_control_type. */ virtual const std::string& get_control_type() const OVERRIDE; + + /** Inherited from tcontrol. */ + void load_config_extra(); + + /***** ***** ***** signal handlers ***** ****** *****/ + + /** + * Left click signal handler: checks if we clicked on a hyperlink + */ + void signal_handler_left_button_click(const event::tevent event, bool & handled); + + /** + * Right click signal handler: checks if we clicked on a hyperlink, copied to clipboard + */ + void signal_handler_right_button_click(const event::tevent event, bool & handled); }; } // namespace gui2 diff --git a/src/text.cpp b/src/text.cpp index 7ed258a3aba6..725e2c458a6c 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -68,6 +68,8 @@ const unsigned ttext::STYLE_BOLD = TTF_STYLE_BOLD; const unsigned ttext::STYLE_ITALIC = TTF_STYLE_ITALIC; const unsigned ttext::STYLE_UNDERLINE = TTF_STYLE_UNDERLINE; +static bool looks_like_url(const std::string & token); + std::string escape_text(const std::string& text) { std::string result; @@ -99,6 +101,7 @@ ttext::ttext() : #endif text_(), markedup_text_(false), + link_aware_(false), font_size_(14), font_style_(STYLE_NORMAL), foreground_color_(0xFFFFFFFF), // solid white @@ -269,6 +272,53 @@ gui2::tpoint ttext::get_cursor_position( return gui2::tpoint(PANGO_PIXELS(rect.x), PANGO_PIXELS(rect.y)); } +std::string ttext::get_token(const gui2::tpoint & position, const char * delim) const +{ + recalculate(); + + // Get the index of the character. + int index, trailing; + if (!pango_layout_xy_to_index(layout_, position.x * PANGO_SCALE, + position.y * PANGO_SCALE, &index, &trailing)) { + return ""; + } + + std::string txt = pango_layout_get_text(layout_); + + std::string d(delim); + + if (index < 0 || (static_cast(index) >= txt.size()) || d.find(txt.at(index)) != std::string::npos) { + return ""; // if the index is out of bounds, or the index character is a delimiter, return nothing + } + + size_t l = index; + while (l > 0 && (d.find(txt.at(l-1)) == std::string::npos)) { + --l; + } + + size_t r = index + 1; + while (r < txt.size() && (d.find(txt.at(r)) == std::string::npos)) { + ++r; + } + + return txt.substr(l,r-l); +} + +std::string ttext::get_link(const gui2::tpoint & position) const +{ + if (!link_aware_) { + return ""; + } + + std::string tok = get_token(position, " \n\r\t"); + + if (looks_like_url(tok)) { + return tok; + } else { + return ""; + } +} + gui2::tpoint ttext::get_column_line(const gui2::tpoint& position) const { recalculate(); @@ -477,6 +527,27 @@ ttext& ttext::set_maximum_length(const size_t maximum_length) return *this; } +ttext& ttext::set_link_aware(bool b) +{ + if (link_aware_ != b) { + calculation_dirty_ = true; + surface_dirty_ = true; + link_aware_ = b; + } + return *this; +} + +ttext& ttext::set_link_color(const std::string & color) +{ + if(color != link_color_) { + link_color_ = color; + calculation_dirty_ = true; + surface_dirty_ = true; + } + + return *this; +} + namespace { /** Small helper class to make sure the font object is destroyed properly. */ @@ -726,7 +797,47 @@ void ttext::create_surface_buffer(const size_t size) const memset(surface_buffer_, 0, size); } -bool ttext::set_markup(const std::string& text) +bool ttext::set_markup(const std::string & text) { + if (!link_aware_) { + return set_markup_helper(text); + } else { + std::string delim = " \n\r\t"; + + // Tokenize according to these delimiters, and stream the results of `handle_token` on each token to get the new text. + + std::stringstream ss; + + int last_delim = -1; + for (size_t index = 0; index < text.size(); ++index) { + if (delim.find(text.at(index)) != std::string::npos) { + ss << handle_token(text.substr(last_delim + 1, index - last_delim - 1)); // want to include chars from range since last token, dont want to include any delimiters + ss << text.at(index); + last_delim = index; + } + } + if (last_delim < static_cast(text.size()) - 1) { + ss << handle_token(text.substr(last_delim + 1, text.size() - last_delim - 1)); + } + + return set_markup_helper(ss.str()); + } +} + +static bool looks_like_url(const std::string & str) +{ + return (str.size() >= 8) && ((str.substr(0,7) == "http://") || (str.substr(0,8) == "https://")); +} + +std::string ttext::handle_token(const std::string & token) const +{ + if (looks_like_url(token)) { + return "" + token + ""; + } else { + return token; + } +} + +bool ttext::set_markup_helper(const std::string& text) { if(pango_parse_markup(text.c_str(), text.size() , 0, NULL, NULL, NULL, NULL)) { diff --git a/src/text.hpp b/src/text.hpp index c49a1f69ad7c..73912e3c0f54 100644 --- a/src/text.hpp +++ b/src/text.hpp @@ -162,6 +162,27 @@ class ttext gui2::tpoint get_cursor_position( const unsigned column, const unsigned line = 0) const; + /** + * Gets the largest collection of characters, including the token at position, + * and not including any characters from the delimiters set. + * + * @param position The pixel position in the text area. + * + * @returns The token containing position, and none of the + * delimiter characters. If position is out of bounds, + * it returns the empty string. + */ + std::string get_token(const gui2::tpoint & position, const char * delimiters = " \n\r\t") const; + + /** + * Checks if position points to a character in a link in the text, returns it + * if so, empty string otherwise. Link-awareness must be enabled to get results. + * @param position The pixel position in the text area. + * + * @returns The link if one is found, the empty string otherwise. + */ + std::string get_link(const gui2::tpoint & position) const; + /** * Gets the column of line of the character at the position. * @@ -219,6 +240,11 @@ class ttext ttext& set_maximum_length(const size_t maximum_length); + bool link_aware() const { return link_aware_; } + + ttext& set_link_aware(bool b); + + ttext& set_link_color(const std::string & color); private: /***** ***** ***** ***** Pango variables ***** ***** ***** *****/ @@ -244,6 +270,12 @@ class ttext /** Is the text markedup if so the markedup render routines need to be used. */ bool markedup_text_; + /** Are hyperlinks in the text marked-up, and will get_link return them. */ + bool link_aware_; + + /** The color to render links in. */ + std::string link_color_; + /** The font size to draw. */ unsigned font_size_; @@ -369,6 +401,9 @@ class ttext */ bool set_markup(const std::string& text); + bool set_markup_helper(const std::string & text); + + std::string handle_token(const std::string & token) const; }; } // namespace font