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