This example has a custom actor, based on the Clutter::Entry
implementation, using a PangoLayout to show text wrapped over multiple
lines.
Real-world applications will probably want to implement more text-editing features, such as the ability to move the cursor vertically, the ability to select and copy sections of text, the ability to show and manipulate right-to-left text, etc.
File: multiline_entry.h
#ifndef CLUTTER_TUTORIAL_MULTILINE_ENTRY_H
#define CLUTTER_TUTORIAL_MULTILINE_ENTRY_H
#include <cluttermm.h>
#include <pangomm.h>
namespace Tutorial
{
class MultilineEntry : public Clutter::Actor
{
public:
virtual ~MultilineEntry();
static Glib::RefPtr<MultilineEntry> create();
sigc::signal<void>& signal_text_changed() { return signal_text_changed_; }
sigc::signal<void, Clutter::Geometry&>& signal_cursor_event() { return signal_cursor_event_; }
sigc::signal<void>& signal_activate() { return signal_activate_; }
void set_text(const Glib::ustring& text);
Glib::ustring get_text() const;
void set_font_name(const Glib::ustring& font_name);
Glib::ustring get_font_name() const;
void set_color(const Clutter::Color& color);
Clutter::Color get_color() const;
bool handle_key_event(Clutter::KeyEvent* event);
protected:
MultilineEntry();
virtual void on_paint();
virtual void allocate_vfunc(const Clutter::ActorBox& box, bool absolute_origin_changed);
virtual void paint_cursor_vfunc();
virtual void on_text_changed();
virtual void on_cursor_event(Clutter::Geometry& geometry);
virtual void on_activate();
private:
sigc::signal<void> signal_text_changed_;
sigc::signal<void, Clutter::Geometry&> signal_cursor_event_;
sigc::signal<void> signal_activate_;
Glib::RefPtr<Pango::Context> context_;
Pango::FontDescription font_;
Clutter::Color fgcol_;
Glib::ustring text_;
int width_;
Glib::ustring::size_type position_;
int text_x_;
Pango::AttrList effective_attrs_;
Glib::RefPtr<Pango::Layout> layout_;
Clutter::Geometry cursor_pos_;
Glib::RefPtr<Clutter::Rectangle> cursor_;
void ensure_layout(int width);
void ensure_cursor_position();
void set_cursor_position(Glib::ustring::size_type position);
void insert_unichar(gunichar wc);
void delete_chars(Glib::ustring::size_type num);
void delete_text(Glib::ustring::size_type start_pos, Glib::ustring::size_type end_pos);
};
} // namespace Tutorial
#endif /* !CLUTTER_TUTORIAL_MULTILINE_ENTRY_H */
File: main.cc
#include "multiline_entry.h"
#include <cluttermm.h>
#include <pangomm/init.h>
int main(int argc, char** argv)
{
Pango::init();
Clutter::init(&argc, &argv);
// Get the stage and set its size and color:
const Glib::RefPtr<Clutter::Stage> stage = Clutter::Stage::get_default();
stage->set_size(400, 400);
stage->set_color(Clutter::Color(0x00, 0x00, 0x00, 0xFF)); // black
// Add our multiline entry to the stage
const Glib::RefPtr<Tutorial::MultilineEntry>
multiline = Tutorial::MultilineEntry::create();
multiline->set_text(
"And as I sat there brooding on the old, unknown world, I thought of "
"Gatsby's wonder when he first picked out the green light at the end of "
"Daisy's dock. He had come a long way to this blue lawn and his dream "
"must have seemed so close that he could hardly fail to grasp it. He did "
"not know that it was already behind him, somewhere back in that vast "
"obscurity beyond the city, where the dark fields of the republic rolled "
"on under the night.");
multiline->set_color(Clutter::Color(0xAE, 0xFF, 0x7F, 0xFF));
multiline->set_size(380, 380);
multiline->set_position(10, 10);
stage->add_actor(multiline);
multiline->show();
// Connect signal handlers to handle key presses on the stage:
stage->signal_key_press_event().connect(
sigc::mem_fun(*multiline.operator->(), &Tutorial::MultilineEntry::handle_key_event));
stage->show();
// Start the main loop, so we can respond to events:
Clutter::main();
return 0;
}
File: multiline_entry.cc
#include "multiline_entry.h"
#include <cogl/cogl.h>
#include <clutter/pangoclutter.h>
namespace
{
static const char *const default_font_name = "Sans 10";
enum { ENTRY_CURSOR_WIDTH = 1 };
static Glib::RefPtr<Pango::Context> ref_shared_context()
{
static void* context = 0;
PangoClutterFontMap *font_map = NULL;
if(context == 0)
{
double resolution = clutter_backend_get_resolution(clutter_get_default_backend());
if(resolution < 0.0)
resolution = 96.0; // fallback
font_map = PANGO_CLUTTER_FONT_MAP (pango_clutter_font_map_new ());
pango_clutter_font_map_set_resolution (font_map, resolution);
context = pango_clutter_font_map_create_context (font_map);
// Clear the pointer when the object is destroyed:
g_object_add_weak_pointer(static_cast<GObject*>(context), &context);
// Transfer ownership:
return Glib::wrap(static_cast<PangoContext*>(context), false);
}
// Increase reference count:
return Glib::wrap(static_cast<PangoContext*>(context), true);
}
} // anonymous namespace
namespace Tutorial
{
/*
* Example of a multi-line text entry actor, based on ClutterEntry.
*/
MultilineEntry::MultilineEntry()
:
context_ (ref_shared_context()),
font_ (default_font_name),
fgcol_ (0x00, 0x00, 0x00, 0xFF),
text_ (),
width_ (0),
position_ (Glib::ustring::npos),
text_x_ (0),
effective_attrs_ (),
layout_ (),
cursor_pos_ (),
cursor_ (Clutter::Rectangle::create(fgcol_))
{
signal_text_changed_.connect(sigc::mem_fun(*this, &MultilineEntry::on_text_changed));
signal_cursor_event_.connect(sigc::mem_fun(*this, &MultilineEntry::on_cursor_event));
signal_activate_ .connect(sigc::mem_fun(*this, &MultilineEntry::on_activate));
cursor_->set_parent(Glib::RefPtr<Clutter::Actor>((reference(), this)));
// We use the font size to set the default width and height, in case
// the user doesn't call Clutter::Actor::set_size().
const double font_size = font_.get_size() * context_->get_resolution() / (72.0 * Pango::SCALE);
set_size(20 * int(font_size), 50);
}
MultilineEntry::~MultilineEntry()
{}
Glib::RefPtr<MultilineEntry> MultilineEntry::create()
{
return Glib::RefPtr<MultilineEntry>(new MultilineEntry());
}
void MultilineEntry::set_text(const Glib::ustring& text)
{
text_ = text;
layout_.clear();
cursor_pos_.set_width(0);
if(is_visible())
queue_redraw();
signal_text_changed_.emit();
}
Glib::ustring MultilineEntry::get_text() const
{
return text_;
}
/*
* Sets font_name as the font used by entry. font_name must be a string
* containing the font name and its size, similarly to what you would feed
* to the Pango::FontDescription constructor.
*/
void MultilineEntry::set_font_name(const Glib::ustring& font_name)
{
Pango::FontDescription font ((font_name.empty()) ? Glib::ustring(default_font_name) : font_name);
if(font == font_)
return;
swap(font_, font);
if(!text_.empty())
{
layout_.clear();
if(is_visible())
queue_redraw();
}
}
Glib::ustring MultilineEntry::get_font_name() const
{
return font_.to_string();
}
void MultilineEntry::set_color(const Clutter::Color& color)
{
fgcol_ = color;
set_opacity(fgcol_.get_alpha());
cursor_->set_color(fgcol_);
if(is_visible())
queue_redraw();
}
Clutter::Color MultilineEntry::get_color() const
{
return fgcol_;
}
void MultilineEntry::on_text_changed()
{}
void MultilineEntry::on_cursor_event(Clutter::Geometry&)
{}
void MultilineEntry::on_activate()
{}
/*
* Characters are removed from before the current position of the cursor.
*/
void MultilineEntry::delete_chars(Glib::ustring::size_type num)
{
using Glib::ustring;
const ustring::size_type len = text_.length();
const ustring::size_type end = (position_ == ustring::npos) ? len : std::min(position_, len);
const ustring::size_type start = (num < end) ? end - num : 0;
set_text(ustring(text_).erase(start, end - start));
if(position_ != ustring::npos)
set_cursor_position(start);
}
void MultilineEntry::ensure_layout(int width)
{
if(!layout_)
{
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(context_);
layout->set_attributes(effective_attrs_);
layout->set_single_paragraph_mode(false);
layout->set_font_description(font_);
layout->set_wrap(Pango::WRAP_WORD);
layout->set_width((width > 0) ? width * Pango::SCALE : -1);
layout->set_text(text_);
swap(layout_, layout);
}
}
void MultilineEntry::ensure_cursor_position()
{
Glib::ustring::iterator pos = text_.begin();
if(position_ == Glib::ustring::npos)
pos = text_.end();
else
std::advance(pos, position_);
const Pango::Rectangle rect = layout_->get_cursor_strong_pos(pos.base() - text_.begin().base());
cursor_pos_.set_xy(rect.get_x() / Pango::SCALE, rect.get_y() / Pango::SCALE);
cursor_pos_.set_size(ENTRY_CURSOR_WIDTH, rect.get_height() / Pango::SCALE);
signal_cursor_event_.emit(sigc::ref(cursor_pos_));
}
/*
* Sets the position of the cursor. The position must be less than or
* equal to the number of characters in the entry. A value of npos indicates
* that the position should be set after the last character in the entry.
* Note that this position is in characters, not in bytes.
*/
void MultilineEntry::set_cursor_position(Glib::ustring::size_type position)
{
if(position < text_.length())
position_ = position;
else
position_ = Glib::ustring::npos;
cursor_pos_.set_width(0);
if(is_visible())
queue_redraw();
}
/*
* Insert a character to the right of the current position of the cursor,
* and update the position of the cursor.
*/
void MultilineEntry::insert_unichar(gunichar wc)
{
g_return_if_fail(Glib::Unicode::validate(wc));
if(wc == 0)
return;
using Glib::ustring;
if(position_ == ustring::npos)
set_text(text_ + wc);
else
set_text(ustring(text_).insert(position_, 1, wc));
if(position_ != ustring::npos)
set_cursor_position(position_ + 1);
}
/*
* Deletes a sequence of characters. The characters that are deleted are
* those characters at positions from start_pos up to, but not including,
* end_pos. If end_pos is npos, then the characters deleted will be
* those characters from start_pos to the end of the text.
*/
void MultilineEntry::delete_text(Glib::ustring::size_type start_pos,
Glib::ustring::size_type end_pos)
{
using Glib::ustring;
set_text(ustring(text_).erase(start_pos, (end_pos == ustring::npos) ? ustring::npos
: end_pos - start_pos));
}
void MultilineEntry::paint_cursor_vfunc()
{
cursor_->set_geometry(cursor_pos_);
cursor_->paint();
}
void MultilineEntry::on_paint()
{
const int width = (width_ < 0) ? get_width() : width_;
set_clip(0, 0, width, get_height());
int actor_width = width;
ensure_layout(actor_width);
ensure_cursor_position();
const Pango::Rectangle logical = layout_->get_logical_extents();
int text_width = logical.get_width() / Pango::SCALE;
if(actor_width < text_width)
{
// We need to do some scrolling:
const int cursor_x = cursor_pos_.get_x();
// If the cursor is at the begining or the end of the text, the placement
// is easy, however, if the cursor is in the middle somewhere, we need to
// make sure the text doesn't move until the cursor is either in the
// far left or far right.
if(position_ == 0)
{
text_x_ = 0;
}
else if(position_ == Glib::ustring::npos)
{
text_x_ = actor_width - text_width;
cursor_pos_.set_x(cursor_x + text_x_);
}
else
{
if(text_x_ <= 0)
{
const int diff = -text_x_;
if(cursor_x < diff)
text_x_ += diff - cursor_x;
else if(cursor_x > diff + actor_width)
text_x_ -= cursor_x - (diff + actor_width);
}
cursor_pos_.set_x(cursor_x + text_x_);
}
}
else
{
text_x_ = 0;
}
Clutter::Color color = fgcol_;
color.set_alpha( get_opacity() );
pango_clutter_render_layout(layout_->gobj(), text_x_, 0, color.gobj(), 0);
paint_cursor_vfunc();
}
void MultilineEntry::allocate_vfunc(const Clutter::ActorBox& box, bool absolute_origin_changed)
{
const int width = CLUTTER_UNITS_TO_DEVICE(box.get_x2() - box.get_x1());
if(width_ != width)
{
layout_.clear();
ensure_layout(width);
width_ = width;
}
Clutter::Actor::allocate_vfunc(box, absolute_origin_changed);
}
/*
* This method will handle a Clutter::KeyEvent, like those returned in a
* key-press/release-event, and will translate it for the entry. This includes
* non-alphanumeric keys, such as the arrows keys, which will move the
* input cursor. You should use this function inside a handler for the
* Clutter::Stage::signal_key_press_event() or
* Clutter::Stage::signal_key_release_event().
*/
bool MultilineEntry::handle_key_event(Clutter::KeyEvent* event)
{
switch(Clutter::key_event_symbol(event))
{
case CLUTTER_Escape:
case CLUTTER_Shift_L:
case CLUTTER_Shift_R:
// Ignore these - Don't try to insert them as characters:
return false;
case CLUTTER_BackSpace:
// Delete the current character:
if(position_ != 0 && !text_.empty())
delete_chars(1);
break;
case CLUTTER_Delete:
case CLUTTER_KP_Delete:
// Delete the current character:
if(!text_.empty() && position_ != Glib::ustring::npos)
delete_text(position_, position_ + 1);
break;
case CLUTTER_Left:
case CLUTTER_KP_Left:
// Move the cursor one character left:
if(position_ != 0 && !text_.empty())
set_cursor_position(((position_ == Glib::ustring::npos) ? text_.length() : position_) - 1);
break;
case CLUTTER_Right:
case CLUTTER_KP_Right:
// Move the cursor one character right:
if(position_ != Glib::ustring::npos && !text_.empty() && position_ < text_.length())
set_cursor_position(position_ + 1);
break;
case CLUTTER_Up:
case CLUTTER_KP_Up:
// TODO: Calculate the index of the position on the line above,
// and set the cursor to it.
break;
case CLUTTER_Down:
case CLUTTER_KP_Down:
// TODO: Calculate the index of the position on the line below,
// and set the cursor to it.
break;
case CLUTTER_End:
case CLUTTER_KP_End:
// Move the cursor to the end of the text:
set_cursor_position(Glib::ustring::npos);
break;
case CLUTTER_Begin:
case CLUTTER_Home:
case CLUTTER_KP_Home:
// Move the cursor to the start of the text:
set_cursor_position(0);
break;
default:
insert_unichar(Clutter::key_event_unicode(event));
break;
}
return true;
}
} // namespace Tutorial