Merge pull request #300 from cbeck88/clickable_labels

Clickable hyperlinks in gui2 labels
This commit is contained in:
Chris Beck 2014-10-18 21:11:46 -04:00
commit 455f6f7209
11 changed files with 430 additions and 8 deletions

View file

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

View file

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

View file

@ -1270,6 +1270,12 @@ private:
/** The text markup switch of the text. */
tformula<bool> text_markup_;
/** The link aware switch of the text. */
tformula<bool> link_aware_;
/** The link color of the text. */
tformula<std::string> link_color_;
/** The maximum width for the text. */
tformula<int> maximum_width_;
@ -1303,6 +1309,10 @@ private:
* 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_)

View file

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

View file

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

View file

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

View file

@ -141,6 +141,29 @@ public:
*/
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 @@ protected:
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
/**

View file

@ -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 <boost/bind.hpp>
#include <string>
#include <sstream>
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<event::LEFT_BUTTON_CLICK>(boost::bind(&tlabel::signal_handler_left_button_click, this, _2, _3));
connect_signal<event::RIGHT_BUTTON_CLICK>(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<const tlabel_definition::tresolution>
conf = boost::dynamic_pointer_cast<const tlabel_definition::tresolution>(
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

View file

@ -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 @@ public:
/** 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 @@ public:
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 @@ private:
*/
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

View file

@ -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<size_t>(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<int>(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 "<span underline=\'single\' color=\'" + link_color_ + "\'>" + token + "</span>";
} 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)) {

View file

@ -162,6 +162,27 @@ public:
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 @@ public:
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 @@ private:
/** 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 @@ private:
*/
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