textbox.cpp

Go to the documentation of this file.
00001 /* $Id: textbox.cpp 26170 2008-04-27 15:09:20Z mordante $ */
00002 /*
00003    Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
00004    Part of the Battle for Wesnoth Project http://www.wesnoth.org/
00005 
00006    This program is free software; you can redistribute it and/or modify
00007    it under the terms of the GNU General Public License version 2
00008    or at your option any later version.
00009    This program is distributed in the hope that it will be useful,
00010    but WITHOUT ANY WARRANTY.
00011 
00012    See the COPYING file for more details.
00013 */
00014 
00015 #include "global.hpp"
00016 
00017 #include "widgets/textbox.hpp"
00018 #include "clipboard.hpp"
00019 #include "font.hpp"
00020 #include "language.hpp"
00021 #include "log.hpp"
00022 #include "sdl_utils.hpp"
00023 #include "serialization/string_utils.hpp"
00024 #include "util.hpp"
00025 #include "video.hpp"
00026 
00027 #include "SDL.h"
00028 
00029 #include <algorithm>
00030 #include <cctype>
00031 #include <cstring>
00032 
00033 #define WRN_DISPLAY LOG_STREAM(warn, display)
00034 #define DBG_G LOG_STREAM(debug, general)
00035 
00036 namespace gui {
00037 
00038 #ifdef USE_TINY_GUI
00039 const int font_size = font::SIZE_TINY;
00040 #else
00041 const int font_size = font::SIZE_PLUS;
00042 #endif
00043 
00044 textbox::textbox(CVideo &video, int width, const std::string& text, bool editable, size_t max_size, double alpha, double alpha_focus, const bool auto_join)
00045        : scrollarea(video, auto_join), max_size_(max_size), text_(utils::string_to_wstring(text)),
00046          cursor_(text_.size()), selstart_(-1), selend_(-1),
00047          grabmouse_(false), text_pos_(0), editable_(editable),
00048          show_cursor_(true), show_cursor_at_(0), text_image_(NULL),
00049          wrap_(false), line_height_(0), yscroll_(0), alpha_(alpha),
00050           alpha_focus_(alpha_focus)
00051 {
00052     // static const SDL_Rect area = d.screen_area();
00053     // const int height = font::draw_text(NULL,area,font_size,font::NORMAL_COLOUR,"ABCD",0,0).h;
00054     set_measurements(width, font::get_max_height(font_size));
00055     set_scroll_rate(font::get_max_height(font_size) / 2);
00056     update_text_cache(true);
00057 }
00058 
00059 textbox::~textbox()
00060 {
00061 }
00062 
00063 void textbox::set_inner_location(SDL_Rect const &rect)
00064 {
00065     bg_register(rect);
00066 }
00067 
00068 const std::string textbox::text() const
00069 {
00070     const std::string &ret = utils::wstring_to_string(text_);
00071     return ret;
00072 }
00073 
00074 // set_text does not respect max_size_
00075 void textbox::set_text(const std::string& text)
00076 {
00077     text_ = utils::string_to_wstring(text);
00078     cursor_ = text_.size();
00079     text_pos_ = 0;
00080     selstart_ = -1;
00081     selend_ = -1;
00082     set_dirty(true);
00083     update_text_cache(true);
00084     handle_text_changed(text_);
00085 }
00086 
00087 void textbox::append_text(const std::string& text, bool auto_scroll)
00088 {
00089     if(text_image_.get() == NULL) {
00090         set_text(text);
00091         return;
00092     }
00093 
00094     //disallow adding multi-line text to a single-line text box
00095     if(wrap_ == false && std::find_if(text.begin(),text.end(),utils::isnewline) != text.end()) {
00096         return;
00097     }
00098     const bool is_at_bottom = get_position() == get_max_position();
00099     const wide_string& wtext = utils::string_to_wstring(text);
00100 
00101     const surface new_text = add_text_line(wtext);
00102     const surface new_surface = create_compatible_surface(text_image_,maximum<size_t>(text_image_->w,new_text->w),text_image_->h+new_text->h);
00103 
00104     SDL_SetAlpha(new_text.get(),0,0);
00105     SDL_SetAlpha(text_image_.get(),0,0);
00106 
00107     SDL_BlitSurface(text_image_,NULL,new_surface,NULL);
00108 
00109     SDL_Rect target = {0,text_image_->h,new_text->w,new_text->h};
00110     SDL_BlitSurface(new_text,NULL,new_surface,&target);
00111     text_image_.assign(new_surface);
00112 
00113     text_.resize(text_.size() + wtext.size());
00114     std::copy(wtext.begin(),wtext.end(),text_.end()-wtext.size());
00115 
00116     set_dirty(true);
00117     update_text_cache(false);
00118     if(auto_scroll && is_at_bottom) scroll_to_bottom();
00119     handle_text_changed(text_);
00120 }
00121 
00122 void textbox::clear()
00123 {
00124     text_.clear();
00125     cursor_ = 0;
00126     cursor_pos_ = 0;
00127     text_pos_ = 0;
00128     selstart_ = -1;
00129     selend_ = -1;
00130     set_dirty(true);
00131     update_text_cache(true);
00132     handle_text_changed(text_);
00133 }
00134 
00135 void textbox::draw_cursor(int pos, CVideo &video) const
00136 {
00137     if(show_cursor_ && editable_) {
00138         SDL_Rect rect = {location().x + pos, location().y, 1, location().h };
00139         surface const frame_buffer = video.getSurface();
00140         SDL_FillRect(frame_buffer,&rect,SDL_MapRGB(frame_buffer->format,255,255,255));
00141     }
00142 }
00143 
00144 void textbox::draw_contents()
00145 {
00146     SDL_Rect const &loc = inner_location();
00147 
00148     surface surf = video().getSurface();
00149     draw_solid_tinted_rectangle(loc.x,loc.y,loc.w,loc.h,0,0,0,
00150                     focus(NULL) ? alpha_focus_ : alpha_, surf);
00151 
00152     SDL_Rect src;
00153 
00154     if(text_image_ == NULL) {
00155         update_text_cache(true);
00156     }
00157 
00158     if(text_image_ != NULL) {
00159         src.y = yscroll_;
00160         src.w = minimum<size_t>(loc.w,text_image_->w);
00161         src.h = minimum<size_t>(loc.h,text_image_->h);
00162         src.x = text_pos_;
00163         SDL_Rect dest = screen_area();
00164         dest.x = loc.x;
00165         dest.y = loc.y;
00166 
00167         // Fills the selected area
00168         if(is_selection()) {
00169             const int start = minimum<int>(selstart_,selend_);
00170             const int end = maximum<int>(selstart_,selend_);
00171             int startx = char_x_[start];
00172             int starty = char_y_[start];
00173             const int endx = char_x_[end];
00174             const int endy = char_y_[end];
00175 
00176             while(starty <= endy) {
00177                 const size_t right = starty == endy ? endx : text_image_->w;
00178                 if(right <= size_t(startx)) {
00179                     break;
00180                 }
00181 
00182                 SDL_Rect rect = { loc.x + startx, loc.y + starty - src.y, right - startx, line_height_ };
00183 
00184                 const clip_rect_setter clipper(surf, loc);
00185 
00186                 Uint32 colour = SDL_MapRGB(surf->format, 0, 0, 160);
00187                 fill_rect_alpha(rect, colour, 140, surf);
00188 
00189                 starty += int(line_height_);
00190                 startx = 0;
00191             }
00192         }
00193 
00194         SDL_BlitSurface(text_image_, &src, surf, &dest);
00195     }
00196 
00197     draw_cursor((cursor_pos_ == 0 ? 0 : cursor_pos_ - 1), video());
00198 
00199     update_rect(loc);
00200 }
00201 
00202 void textbox::process()
00203 {
00204     if(editable_) {
00205         if(focus(NULL)) {
00206             const int ticks = SDL_GetTicks();
00207             if(ticks > show_cursor_at_+500) {
00208                 show_cursor_ = !show_cursor_;
00209                 show_cursor_at_ = ticks;
00210                 set_dirty();
00211             }
00212         } else if(show_cursor_ == true) {
00213             show_cursor_ = false;
00214             set_dirty();
00215         }
00216     }
00217 
00218     draw();
00219 }
00220 
00221 void textbox::set_editable(bool value)
00222 {
00223     editable_ = value;
00224 }
00225 
00226 bool textbox::editable() const
00227 {
00228     return editable_;
00229 }
00230 
00231 void textbox::scroll_to_bottom()
00232 {
00233     set_position(get_max_position());
00234 }
00235 
00236 void textbox::set_wrap(bool val)
00237 {
00238     if(wrap_ != val) {
00239         wrap_ = val;
00240         update_text_cache(true);
00241         set_dirty(true);
00242     }
00243 }
00244 
00245 void textbox::set_location(const SDL_Rect& rect)
00246 {
00247     text_pos_ = 0;
00248 
00249     scrollarea::set_location(rect);
00250     set_shown_size(location().h);
00251 }
00252 
00253 void textbox::scroll(unsigned int pos)
00254 {
00255     yscroll_ = pos;
00256     set_dirty(true);
00257 }
00258 
00259 surface textbox::add_text_line(const wide_string& text)
00260 {
00261     line_height_ = font::get_max_height(font_size);
00262 
00263     if(char_y_.empty()) {
00264         char_y_.push_back(0);
00265     } else {
00266         char_y_.push_back(char_y_.back() + line_height_);
00267     }
00268 
00269     char_x_.push_back(0);
00270 
00271     // Re-calculate the position of each glyph. We approximate this by asking the
00272     // width of each substring, but this is a flawed assumption which won't work with
00273     // some more complex scripts (that is, RTL languages). This part of the work should
00274     // actually be done by the font-rendering system.
00275     std::string visible_string;
00276     wide_string wrapped_text;
00277 
00278     wide_string::const_iterator backup_itor = text.end();
00279 
00280     wide_string::const_iterator itor = text.begin();
00281     while(itor != text.end()) {
00282         //If this is a space, save copies of the current state so we can roll back
00283         if(char(*itor) == ' ') {
00284             backup_itor = itor;
00285         }
00286         visible_string.append(utils::wchar_to_string(*itor));
00287 
00288         if(char(*itor) == '\n') {
00289             backup_itor = text.end();
00290             visible_string = "";
00291         }
00292 
00293         int w = font::line_width(visible_string, font_size);
00294 
00295         if(wrap_ && w >= inner_location().w) {
00296             if(backup_itor != text.end()) {
00297                 int backup = itor - backup_itor;
00298                 itor = backup_itor + 1;
00299                 if(backup > 0) {
00300                     char_x_.erase(char_x_.end()-backup, char_x_.end());
00301                     char_y_.erase(char_y_.end()-backup, char_y_.end());
00302                     wrapped_text.erase(wrapped_text.end()-backup, wrapped_text.end());
00303                 }
00304             }
00305             backup_itor = text.end();
00306             wrapped_text.push_back(wchar_t('\n'));
00307             char_x_.push_back(0);
00308             char_y_.push_back(char_y_.back() + line_height_);
00309             visible_string = "";
00310         } else {
00311             wrapped_text.push_back(*itor);
00312             char_x_.push_back(w);
00313             char_y_.push_back(char_y_.back() + (char(*itor) == '\n' ? line_height_ : 0));
00314             ++itor;
00315         }
00316     }
00317 
00318     const std::string s = utils::wstring_to_string(wrapped_text);
00319     const surface res(font::get_rendered_text(s, font_size, font::NORMAL_COLOUR));
00320 
00321     return res;
00322 }
00323 
00324 
00325 void textbox::update_text_cache(bool changed)
00326 {
00327     if(changed) {
00328         char_x_.clear();
00329         char_y_.clear();
00330 
00331         text_image_.assign(add_text_line(text_));
00332     }
00333 
00334     int cursor_x = char_x_[cursor_];
00335 
00336     if(cursor_x - text_pos_ > location().w) {
00337         text_pos_ = cursor_x - location().w;
00338     } else if(cursor_x - text_pos_ < 0) {
00339         text_pos_ = cursor_x;
00340     }
00341     cursor_pos_ = cursor_x - text_pos_;
00342 
00343     if (!text_image_.null()) {
00344         set_full_size(text_image_->h);
00345         set_shown_size(location().h);
00346     }
00347 }
00348 
00349 bool textbox::is_selection()
00350 {
00351     return (selstart_ != -1) && (selend_ != -1) && (selstart_ != selend_);
00352 }
00353 
00354 void textbox::erase_selection()
00355 {
00356     if(!is_selection())
00357         return;
00358 
00359     wide_string::iterator itor = text_.begin() + minimum(selstart_, selend_);
00360     text_.erase(itor, itor + abs(selend_ - selstart_));
00361     cursor_ = minimum(selstart_, selend_);
00362     selstart_ = selend_ = -1;
00363 }
00364 
00365 namespace {
00366     const unsigned int copypaste_modifier =
00367 #ifdef __APPLE__
00368         KMOD_LMETA | KMOD_RMETA
00369 #else
00370         KMOD_CTRL
00371 #endif
00372         ;
00373 }
00374 
00375 bool textbox::requires_event_focus(const SDL_Event* event) const
00376 {
00377     if(!focus_ || !editable_ || hidden()) {
00378         return false;
00379     }
00380     if(event == NULL) {
00381         //when event is not specified, signal that focus may be desired later
00382         return true;
00383     }
00384 
00385     if(event->type == SDL_KEYDOWN) {
00386         SDLKey key = event->key.keysym.sym;
00387         switch(key) {
00388         case SDLK_UP:
00389         case SDLK_DOWN:
00390         case SDLK_PAGEUP:
00391         case SDLK_PAGEDOWN:
00392             //in the future we may need to check for input history or multi-line support
00393             //for now, just return false since these events are not handled.
00394             return false;
00395         default:
00396             return true;
00397         }
00398     }
00399     //mouse events are processed regardless of focus
00400     return false;
00401 }
00402 
00403 void textbox::handle_event(const SDL_Event& event)
00404 {
00405     scrollarea::handle_event(event);
00406     if(hidden())
00407         return;
00408 
00409     bool changed = false;
00410 
00411     const int old_selstart = selstart_;
00412     const int old_selend = selend_;
00413 
00414     //Sanity check: verify that selection start and end are within text
00415     //boundaries
00416     if(is_selection() && !(size_t(selstart_) <= text_.size() && size_t(selend_) <= text_.size())) {
00417         WRN_DISPLAY << "out-of-boundary selection\n";
00418         selstart_ = selend_ = -1;
00419     }
00420 
00421     int mousex, mousey;
00422     const Uint8 mousebuttons = SDL_GetMouseState(&mousex,&mousey);
00423     if(!(mousebuttons & SDL_BUTTON(1))) {
00424         grabmouse_ = false;
00425     }
00426 
00427     SDL_Rect const &loc = inner_location();
00428     bool clicked_inside = (event.type == SDL_MOUSEBUTTONDOWN
00429                        && (mousebuttons & SDL_BUTTON(1))
00430                        && point_in_rect(mousex, mousey, loc));
00431     if(clicked_inside) {
00432         set_focus(true);
00433     }
00434     if ((grabmouse_ && (event.type == SDL_MOUSEMOTION)) || clicked_inside) {
00435         const int x = mousex - loc.x + text_pos_;
00436         const int y = mousey - loc.y;
00437         int pos = 0;
00438         int distance = x;
00439 
00440         for(unsigned int i = 1; i < char_x_.size(); ++i) {
00441             if(static_cast<int>(yscroll_) + y < char_y_[i]) {
00442                 break;
00443             }
00444 
00445             // Check individually each distance (if, one day, we support
00446             // RTL languages, char_x_[c] may not be monotonous.)
00447             if(abs(x - char_x_[i]) < distance && yscroll_ + y < char_y_[i] + line_height_) {
00448                 pos = i;
00449                 distance = abs(x - char_x_[i]);
00450             }
00451         }
00452 
00453         cursor_ = pos;
00454 
00455         if(grabmouse_)
00456             selend_ = cursor_;
00457 
00458         update_text_cache(false);
00459 
00460         if(!grabmouse_ && mousebuttons & SDL_BUTTON(1)) {
00461             grabmouse_ = true;
00462             selstart_ = selend_ = cursor_;
00463         } else if (! (mousebuttons & SDL_BUTTON(1))) {
00464             grabmouse_ = false;
00465         }
00466 
00467         set_dirty();
00468     }
00469 
00470     if(editable_ == false) {
00471         return;
00472     }
00473 
00474     //if we don't have the focus, then see if we gain the focus,
00475     //otherwise return
00476     if(focus(&event) == false) {
00477         if (event.type == SDL_MOUSEMOTION && point_in_rect(mousex, mousey, loc))
00478             events::focus_handler(this);
00479 
00480         return;
00481     }
00482 
00483     if(event.type != SDL_KEYDOWN || focus(&event) != true) {
00484         draw();
00485         return;
00486     }
00487 
00488     const SDL_keysym& key = reinterpret_cast<const SDL_KeyboardEvent&>(event).keysym;
00489     const SDLMod modifiers = SDL_GetModState();
00490 
00491     const int c = key.sym;
00492     const int old_cursor = cursor_;
00493 
00494     if(c == SDLK_LEFT && cursor_ > 0)
00495         --cursor_;
00496 
00497     if(c == SDLK_RIGHT && cursor_ < static_cast<int>(text_.size()))
00498         ++cursor_;
00499 
00500     // ctrl-a, ctrl-e and ctrl-u are readline style shortcuts, even on Macs
00501     if(c == SDLK_END || (c == SDLK_e && (modifiers & KMOD_CTRL)))
00502         cursor_ = text_.size();
00503 
00504     if(c == SDLK_HOME || (c == SDLK_a && (modifiers & KMOD_CTRL)))
00505         cursor_ = 0;
00506 
00507     if((old_cursor != cursor_) && (modifiers & KMOD_SHIFT)) {
00508         if(selstart_ == -1)
00509             selstart_ = old_cursor;
00510         selend_ = cursor_;
00511     }
00512 
00513     if(c == SDLK_BACKSPACE) {
00514         changed = true;
00515         if(is_selection()) {
00516             erase_selection();
00517         } else if(cursor_ > 0) {
00518             --cursor_;
00519             text_.erase(text_.begin()+cursor_);
00520         }
00521     }
00522 
00523     if(c == SDLK_u && (modifiers & KMOD_CTRL)) { // clear line
00524         changed = true;
00525         cursor_ = 0;
00526         text_.resize(0);
00527     }
00528 
00529     if(c == SDLK_DELETE && !text_.empty()) {
00530         changed = true;
00531         if(is_selection()) {
00532             erase_selection();
00533         } else {
00534             if(cursor_ < static_cast<int>(text_.size())) {
00535                 text_.erase(text_.begin()+cursor_);
00536             }
00537         }
00538     }
00539 
00540     wchar_t character = key.unicode;
00541 
00542     //movement characters may have a "Unicode" field on some platforms, so ignore it.
00543     if(!(c == SDLK_UP || c == SDLK_DOWN || c == SDLK_LEFT || c == SDLK_RIGHT ||
00544        c == SDLK_DELETE || c == SDLK_BACKSPACE || c == SDLK_END || c == SDLK_HOME ||
00545        c == SDLK_PAGEUP || c == SDLK_PAGEDOWN)) {
00546         if(character != 0) {
00547             DBG_G << "Char: " << character << ", c = " << c << "\n";
00548         }
00549         if(event.key.keysym.mod & copypaste_modifier) {
00550             switch(c) {
00551             case SDLK_v: // paste
00552                 {
00553                 changed = true;
00554                 if(is_selection())
00555                     erase_selection();
00556 
00557                 std::string str = copy_from_clipboard(false);
00558 
00559                 //cut off anything after the first newline
00560                 str.erase(std::find_if(str.begin(),str.end(),utils::isnewline),str.end());
00561 
00562                 wide_string s = utils::string_to_wstring(str);
00563 
00564                 if(text_.size() < max_size_) {
00565                     if(s.size() + text_.size() > max_size_) {
00566                         s.resize(max_size_ - text_.size());
00567                     }
00568                     text_.insert(text_.begin()+cursor_, s.begin(), s.end());
00569                     cursor_ += s.size();
00570                 }
00571 
00572                 }
00573 
00574                 break;
00575 
00576             case SDLK_c: // copy
00577                 {
00578                 const size_t beg = minimum<size_t>(size_t(selstart_),size_t(selend_));
00579                 const size_t end = maximum<size_t>(size_t(selstart_),size_t(selend_));
00580 
00581                 wide_string ws = wide_string(text_.begin() + beg, text_.begin() + end);
00582                 std::string s = utils::wstring_to_string(ws);
00583                 copy_to_clipboard(s, false);
00584                 }
00585                 break;
00586             }
00587         } else {
00588             if(character >= 32 && character != 127) {
00589                 changed = true;
00590                 if(is_selection())
00591                     erase_selection();
00592 
00593                 if(text_.size() + 1 <= max_size_) {
00594                     text_.insert(text_.begin()+cursor_,character);
00595                     ++cursor_;
00596                 }
00597             }
00598         }
00599     }
00600 
00601     if(is_selection() && (selend_ != cursor_))
00602         selstart_ = selend_ = -1;
00603 
00604     //since there has been cursor activity, make the cursor appear for
00605     //at least the next 500ms.
00606     show_cursor_ = true;
00607     show_cursor_at_ = SDL_GetTicks();
00608 
00609     if(changed || old_cursor != cursor_ || old_selstart != selstart_ || old_selend != selend_) {
00610         text_image_ = NULL;
00611         handle_text_changed(text_);
00612     }
00613 
00614     set_dirty(true);
00615 }
00616 
00617 } //end namespace gui

Generated by doxygen 1.5.5 on 23 May 2008 for The Battle for Wesnoth
Gna! | Forum | Wiki | CIA | devdocs