Example

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.

Figure C.1. Multiline Text Entry

Multiline Text Entry

Source code

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