Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 1614 lines (1339 sloc) 39.841 kB
#include <boost/bind.hpp>
#include <boost/function.hpp>
#include <boost/regex.hpp>
#include <boost/scoped_ptr.hpp>
#include <algorithm>
#include "asserts.hpp"
#include "graphics.hpp"
#include "clipboard.hpp"
#include "font.hpp"
#include "foreach.hpp"
#include "raster.hpp"
#include "string_utils.hpp"
#include "text_editor_widget.hpp"
#include "unit_test.hpp"
namespace gui {
namespace {
const int BorderSize = 3;
const int TabWidth = 4;
const int TabAdjust = TabWidth - 1;
typedef boost::shared_ptr<graphics::texture> char_texture_ptr;
std::vector<char_texture_ptr> char_textures;
struct CharArea {
GLfloat x1, y1, x2, y2;
};
std::map<int, std::map<char, CharArea> > all_char_to_area;
const CharArea& get_char_area(int font_size, char c)
{
std::map<char, CharArea>& char_to_area = all_char_to_area[font_size];
std::map<char, CharArea>::const_iterator i = char_to_area.find(c);
if(i != char_to_area.end()) {
return i->second;
}
const CharArea& result = char_to_area[c];
const int char_width = font::char_width(font_size);
const int char_height = font::char_height(font_size);
std::string str;
int row = 0, col = 0;
int nchars = 0;
for(std::map<char, CharArea>::iterator i = char_to_area.begin();
i != char_to_area.end(); ++i) {
str.push_back(i->first);
CharArea area = {col*char_width, row*char_height, (col+1)*char_width, (row+1)*char_height};
char_to_area[i->first] = area;
++col;
if(col == 128) {
str += "\n";
col = 0;
++row;
}
}
char_texture_ptr& char_texture = char_textures[font_size];
char_texture.reset(new graphics::texture(font::render_text(str, graphics::color_white(), font_size)));
for(std::map<char, CharArea>::iterator i = char_to_area.begin();
i != char_to_area.end(); ++i) {
CharArea& area = i->second;
area.x1 = char_texture->translate_coord_x(area.x1/GLfloat(char_texture->width()));
area.x2 = char_texture->translate_coord_x(area.x2/GLfloat(char_texture->width()));
area.y1 = char_texture->translate_coord_y(area.y1/GLfloat(char_texture->height()));
area.y2 = char_texture->translate_coord_y(area.y2/GLfloat(char_texture->height()));
}
return result;
}
void init_char_area(size_t font_size)
{
if(char_textures.size() <= font_size) {
char_textures.resize(font_size+1);
}
if(char_textures[font_size].get()) {
return;
}
std::map<char, CharArea>& char_to_area = all_char_to_area[font_size];
for(char c = 1; c < 127; ++c) {
if(util::c_isprint(c) && c != 'a') {
char_to_area[c] = CharArea();
}
}
get_char_area(font_size, 'a');
ASSERT_LOG(char_textures[font_size].get(), "DID NOT INIT CHAR TEXTURE\n");
}
}
text_editor_widget::text_editor_widget(int width, int height)
: last_op_type_(NULL),
font_size_(14),
char_width_(font::char_width(font_size_)),
char_height_(font::char_height(font_size_)),
select_(0,0), cursor_(0,0),
nrows_((height - BorderSize*2)/char_height_),
ncols_((width - 20 - BorderSize*2)/char_width_),
scroll_pos_(0),
begin_highlight_line_(-1), end_highlight_line_(-1),
has_focus_(false),
is_dragging_(false),
begin_enter_return_(true),
last_click_at_(-1),
consecutive_clicks_(0),
text_color_(255, 255, 255, 255)
{
set_environment();
if(height == 0) {
height = char_height_ + BorderSize*2;
nrows_ = 1;
ncols_ = width/char_width_;
widget::set_dim(width, height);
} else {
widget::set_dim(width - 20, height);
}
text_.push_back("");
init_clipboard();
}
text_editor_widget::text_editor_widget(const variant& v, game_logic::formula_callable* e)
: widget(v,e), scrollable_widget(v,e), last_op_type_(NULL), font_size_(14),
select_(0,0), cursor_(0,0), scroll_pos_(0),
begin_highlight_line_(-1), end_highlight_line_(-1),
has_focus_(false),
is_dragging_(false),
begin_enter_return_(true),
last_click_at_(-1),
consecutive_clicks_(0),
text_color_(255, 255, 255, 255)
{
ASSERT_LOG(get_environment() != 0, "You must specify a callable environment");
int width = v.has_key("width") ? v["width"].as_int() : 0;
int height = v.has_key("height") ? v["height"].as_int() : 0;
if(v.has_key("font_size")) {
font_size_ = v["font_size"].as_int();
}
if(v.has_key("color")) {
text_color_ = graphics::color(v["color"]);
} else if(v.has_key("colour")) {
text_color_ = graphics::color(v["colour"]);
}
if(v.has_key("on_change")) {
on_change_ = boost::bind(&text_editor_widget::change_delegate, this);
ffl_on_change_ = get_environment()->create_formula(v["on_change"]);
}
if(v.has_key("on_move_cursor")) {
on_move_cursor_ = boost::bind(&text_editor_widget::move_cursor_delegate, this);
ffl_on_move_cursor_ = get_environment()->create_formula(v["on_move_cursor"]);
}
if(v.has_key("on_enter")) {
on_enter_ = boost::bind(&text_editor_widget::enter_delegate, this);
ffl_on_enter_ = get_environment()->create_formula(v["on_enter"]);
}
if(v.has_key("on_tab")) {
on_tab_ = boost::bind(&text_editor_widget::tab_delegate, this);
ffl_on_tab_ = get_environment()->create_formula(v["on_tab"]);
}
if(v.has_key("on_escape")) {
on_escape_ = boost::bind(&text_editor_widget::escape_delegate, this);
ffl_on_escape_ = get_environment()->create_formula(v["on_escape"]);
}
if(v.has_key("on_begin_enter")) {
on_begin_enter_ = boost::bind(&text_editor_widget::begin_enter_delegate, this);
ffl_on_begin_enter_ = get_environment()->create_formula(v["on_begin_enter"]);
}
if(v.has_key("on_change_focus")) {
on_change_focus_ = boost::bind(&text_editor_widget::change_focus_delgate, this, _1);
ffl_on_change_focus_ = get_environment()->create_formula(v["on_change_focus"]);
}
char_width_= font::char_width(font_size_);
char_height_ = font::char_height(font_size_);
nrows_ = (height - BorderSize*2)/char_height_;
ncols_ = (width - 20 - BorderSize*2)/char_width_;
if(height == 0) {
height = char_height_ + BorderSize*2;
nrows_ = 1;
ncols_ = width/char_width_;
widget::set_dim(width, height);
} else {
widget::set_dim(width - 20, height);
}
if(v.has_key("text") && v["text"].is_string()) {
set_text(v["text"].as_string());
} else {
text_.push_back("");
}
init_clipboard();
}
text_editor_widget::~text_editor_widget()
{
}
std::string text_editor_widget::text() const
{
std::string result;
foreach(const std::string& line, text_) {
result += line;
result += "\n";
}
result.resize(result.size()-1);
return result;
}
void text_editor_widget::set_row_contents(int row, const std::string& value)
{
ASSERT_LOG(row >= 0 && size_t(row) < text_.size(), "ILLEGAL ROW SET: " << row << " / " << text_.size());
text_[row] = value;
refresh_scrollbar();
on_change();
}
void text_editor_widget::set_text(const std::string& value, bool reset_cursor)
{
std::string txt = value;
txt.erase(std::remove(txt.begin(), txt.end(), '\r'), txt.end());
text_ = util::split(txt, '\n', 0 /*don't remove empties or strip spaces*/);
if(text_.empty()) {
text_.push_back("");
}
if(reset_cursor) {
select_ = cursor_ = Loc(0,0);
scroll_pos_ = 0;
} else {
if(select_.row >= text_.size()) {
select_.row = text_.size() - 1;
}
if(cursor_.row >= text_.size()) {
cursor_.row = text_.size() - 1;
}
}
refresh_scrollbar();
on_change();
}
void text_editor_widget::set_font_size(int font_size)
{
if(font_size < 6) {
font_size = 6;
} else if(font_size > 28) {
font_size = 28;
}
font_size_ = font_size;
char_width_ = font::char_width(font_size_);
char_height_ = font::char_height(font_size_);
nrows_ = (height() - BorderSize*2)/char_height_;
ncols_ = (width() - BorderSize*2)/char_width_;
refresh_scrollbar();
}
void text_editor_widget::change_font_size(int amount)
{
set_font_size(font_size_ + amount);
}
void text_editor_widget::set_dim(int w, int h)
{
widget::set_dim(w - 20, h);
nrows_ = (height() - BorderSize*2)/char_height_;
ncols_ = (width() - BorderSize*2)/char_width_;
refresh_scrollbar();
}
namespace {
struct RectDraw {
rect area;
graphics::color col;
bool merge(RectDraw& o) {
if(o.col.value() != col.value()) {
return false;
}
if(o.area.y() != area.y() || o.area.x() > area.x() + area.w()) {
return false;
}
area = rect(area.x(), area.y(), area.w() + o.area.w(), area.h());
return true;
}
};
}
void text_editor_widget::handle_draw() const
{
init_char_area(font_size_);
std::vector<RectDraw> rects;
std::map<uint32_t, graphics::blit_queue> chars;
int begin_build = SDL_GetTicks();
const int xpos = x() + BorderSize;
const int ypos = y() + BorderSize;
int r = 0;
for(int n = scroll_pos_; n < text_.size() && r < nrows_; ++n, ++r) {
if(n >= begin_highlight_line_ && n <= end_highlight_line_) {
RectDraw rect_draw = { rect(xpos, ypos + r*char_height_, width(), char_height_), graphics::color(255, 255, 255, 32) };
rects.push_back(rect_draw);
}
int c = 0;
std::vector<std::pair<Loc, Loc> >::const_iterator search_itor = std::lower_bound(search_matches_.begin(), search_matches_.end(), std::pair<Loc,Loc>(Loc(n,0),Loc(n,0)));
for(int m = 0; m < text_[n].size(); ++m, ++c) {
if(c >= ncols_) {
++r;
c -= ncols_;
if(r == nrows_) {
break;
}
}
const int char_size = text_[n][m] == '\t' ? 4 : 1;
Loc pos(n, m);
Loc begin_select = select_;
Loc end_select = cursor_;
if(end_select < begin_select) {
std::swap(begin_select, end_select);
}
graphics::color col = get_character_color(n, m);
if(pos >= begin_select && pos < end_select) {
RectDraw rect_draw = { rect(xpos + c*char_width_, ypos + r*char_height_, char_width_*char_size, char_height_), col };
if(rects.empty() || !rects.back().merge(rect_draw)) {
rects.push_back(rect_draw);
}
col = graphics::color(0,0,0,255);
} else {
for(std::vector<std::pair<Loc,Loc> >::const_iterator i = search_itor; i != search_matches_.end() && i->first <= pos; ++i) {
if(pos >= i->first && pos < i->second) {
RectDraw rect_draw = { rect(xpos + c*char_width_, ypos + r*char_height_, char_width_*char_size, char_height_), graphics::color(255,255,0,128) };
if(rects.empty() || !rects.back().merge(rect_draw)) {
rects.push_back(rect_draw);
}
col = graphics::color(0,0,0,255);
}
}
}
if(!util::c_isspace(text_[n][m]) && util::c_isprint(text_[n][m])) {
const CharArea& area = get_char_area(font_size_, text_[n][m]);
const int x1 = xpos + c*char_width_;
const int y1 = ypos + r*char_height_;
const int x2 = x1 + char_width_;
const int y2 = y1 + char_height_;
graphics::blit_queue& q = chars[col.rgba()];
q.repeat_last();
q.add(x1, y1, area.x1, area.y1);
q.repeat_last();
q.add(x2, y1, area.x2, area.y1);
q.add(x1, y2, area.x1, area.y2);
q.add(x2, y2, area.x2, area.y2);
}
if(cursor_.row == n && cursor_.col == m &&
(SDL_GetTicks()%500 < 350 || !has_focus_)) {
RectDraw rect_draw = { rect(xpos + c*char_width_+1, ypos + r*char_height_, 1, char_height_), graphics::color(255,255,255,255) };
rects.push_back(rect_draw);
}
if(text_[n][m] == '\t') {
c += TabAdjust;
}
}
if(has_focus_ && cursor_.row == n && cursor_.col >= text_[n].size() && SDL_GetTicks()%500 < 350) {
RectDraw rect_draw = { rect(xpos + c*char_width_+1, ypos + r*char_height_, 1, char_height_), graphics::color(255,255,255,255) };
rects.push_back(rect_draw);
}
}
const int begin_draw = SDL_GetTicks();
foreach(const RectDraw& r, rects) {
graphics::draw_rect(r.area, r.col);
}
for(std::map<uint32_t, graphics::blit_queue>::iterator i = chars.begin(); i != chars.end(); ++i) {
graphics::color(i->first).set_as_current_color();
i->second.set_texture(char_textures[font_size_]->get_id());
i->second.do_blit();
}
SDL_Color border_color = graphics::color_white();
if(!has_focus_) {
border_color.r = 128;
border_color.g = 128;
border_color.b = 128;
}
SDL_Rect border = {x()+1, y()+1, width()-2, height()-2};
graphics::draw_hollow_rect(border, border_color);
scrollable_widget::handle_draw();
}
bool text_editor_widget::handle_event(const SDL_Event& event, bool claimed)
{
if(!claimed) {
claimed = clipboard_handle_event(event);
}
claimed = scrollable_widget::handle_event(event, claimed) || claimed;
switch(event.type) {
case SDL_KEYDOWN:
return handle_key_press(event.key) || claimed;
case SDL_MOUSEBUTTONDOWN:
return handle_mouse_button_down(event.button) || claimed;
case SDL_MOUSEBUTTONUP:
return handle_mouse_button_up(event.button) || claimed;
case SDL_MOUSEMOTION:
return handle_mouse_motion(event.motion) || claimed;
}
return false;
}
void text_editor_widget::set_focus(bool value)
{
if(has_focus_ != value && on_change_focus_) {
on_change_focus_(value);
}
has_focus_ = value;
if(value) {
SDL_EnableUNICODE(1);
SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
}
if(nrows_ == 1 && value) {
cursor_ = Loc(0, text_.front().size());
select_ = Loc(0, 0);
on_move_cursor();
}
}
void text_editor_widget::set_cursor(int row, int col)
{
if(row < 0) {
row = 0;
}
if(col < 0) {
col = 0;
}
if(row >= text_.size()) {
row = text_.size() - 1;
}
if(col > text_[row].size()) {
col = text_[row].size();
}
select_ = cursor_ = Loc(row, col);
on_move_cursor();
}
int text_editor_widget::row_col_to_text_pos(int row, int col) const
{
if(col > text_[row].size()) {
col = text_[row].size();
}
int result = 0;
for(int n = 0; n != row; ++n) {
result += text_[n].size() + 1;
}
return result + col;
}
std::pair<int,int> text_editor_widget::text_pos_to_row_col(int pos) const
{
int nrow = 0;
while(pos > text_[nrow].size()+1) {
pos -= text_[nrow].size()+1;
++nrow;
}
return std::pair<int,int>(nrow, pos);
}
void text_editor_widget::set_highlight_lines(int begin, int end)
{
begin_highlight_line_ = begin;
end_highlight_line_ = end;
}
void text_editor_widget::clear_highlight_lines()
{
set_highlight_lines(-1, -1);
}
bool text_editor_widget::handle_mouse_button_down(const SDL_MouseButtonEvent& event)
{
record_op();
if(event.x >= x() && event.x < x() + width() && event.y >= y() && event.y < y() + height()) {
if(event.button == SDL_BUTTON_WHEELUP) {
if(cursor_.row > 2) {
cursor_.row -= 3;
scroll_pos_ -= 3;
if( scroll_pos_ < 0 ){
scroll_pos_ = 0;
}
cursor_.col = find_equivalent_col(cursor_.col, cursor_.row+3, cursor_.row);
on_move_cursor();
}
return true;
} else if(event.button == SDL_BUTTON_WHEELDOWN) {
if(text_.size() > 2 && cursor_.row < text_.size()-3) {
cursor_.row += 3;
scroll_pos_ += 3;
if( scroll_pos_ > text_.size() ){
scroll_pos_ = text_.size();
}
cursor_.col = find_equivalent_col(cursor_.col, cursor_.row-3, cursor_.row);
on_move_cursor();
}
return true;
}
set_focus(true);
std::pair<int, int> pos = mouse_position_to_row_col(event.x, event.y);
if(pos.first != -1) {
cursor_.row = pos.first;
cursor_.col = pos.second;
on_move_cursor();
}
if(last_click_at_ != -1 && SDL_GetTicks() - last_click_at_ < 500) {
++consecutive_clicks_;
const int nclicks = consecutive_clicks_%3;
if(nclicks == 1) {
select_ = cursor_;
select_token(text_[cursor_.row], select_.row, cursor_.row, select_.col, cursor_.col);
} else if(nclicks == 2) {
select_ = Loc(cursor_.row, 0);
cursor_.col = text_[cursor_.row].size();
}
if(select_ != cursor_) {
//a mouse-based copy for X-style copy/paste
handle_copy(true);
}
} else {
consecutive_clicks_ = 0;
if(event.button == SDL_BUTTON_MIDDLE && clipboard_has_mouse_area()) {
std::string txt = copy_from_clipboard(true);
handle_paste(txt);
}
}
last_click_at_ = SDL_GetTicks();
is_dragging_ = true;
return true;
}
if(has_focus_ != false && on_change_focus_) {
on_change_focus_(false);
}
is_dragging_ = false;
has_focus_ = false;
return false;
}
bool text_editor_widget::handle_mouse_button_up(const SDL_MouseButtonEvent& event)
{
record_op();
is_dragging_ = false;
return false;
}
bool text_editor_widget::handle_mouse_motion(const SDL_MouseMotionEvent& event)
{
int mousex, mousey;
if(is_dragging_ && has_focus_ && SDL_GetMouseState(&mousex, &mousey)) {
std::pair<int, int> pos = mouse_position_to_row_col(event.x, event.y);
if(pos.first != -1) {
cursor_.row = pos.first;
cursor_.col = pos.second;
on_move_cursor(true /*don't check for shift, assume it is*/);
}
}
return false;
}
bool text_editor_widget::handle_key_press(const SDL_KeyboardEvent& event)
{
if(!has_focus_) {
return false;
}
if(event.keysym.sym == SDLK_z && (event.keysym.mod&KMOD_CTRL)) {
record_op();
undo();
return true;
}
if(event.keysym.sym == SDLK_y && (event.keysym.mod&KMOD_CTRL)) {
record_op();
redo();
return true;
}
if((event.keysym.sym == SDLK_c || event.keysym.sym == SDLK_x) && (event.keysym.mod&KMOD_CTRL)) {
record_op();
handle_copy();
if(event.keysym.sym == SDLK_x) {
save_undo_state();
delete_selection();
on_change();
}
return true;
} else if(event.keysym.sym == SDLK_v && (event.keysym.mod&KMOD_CTRL)) {
handle_paste(copy_from_clipboard(false));
return true;
}
if(event.keysym.mod&KMOD_CTRL) {
if(event.keysym.sym == SDLK_BACKSPACE) {
if(select_ == cursor_) {
//We delete the current word behind us.
truncate_col_position();
if(cursor_.col > 0) {
save_undo_state();
}
const std::string& line = text_[select_.row];
int col = select_.col;
while(col > 0 && !(util::c_isalnum(line[col-1]) || line[col-1] == '_')) {
--col;
}
while(col > 0 && (util::c_isalnum(line[col-1]) || line[col-1] == '_')) {
--col;
}
select_.col = col;
delete_selection();
record_op();
return true;
}
} else if(event.keysym.sym == SDLK_DELETE) {
if(select_ == cursor_) {
//We delete until end of line.
truncate_col_position();
if(cursor_.col < text_[select_.row].size()) {
save_undo_state();
}
select_ = Loc(select_.row, text_[select_.row].size());
delete_selection();
record_op();
return true;
}
} else {
record_op();
return false;
}
}
if(event.keysym.sym == SDLK_ESCAPE && on_escape_) {
on_escape_();
return true;
}
switch(event.keysym.sym) {
case SDLK_LEFT:
record_op();
if(cursor_ != select_ && !(SDL_GetModState()&KMOD_SHIFT)) {
//pressing left without shift while we have a selection moves us to the beginning of the selection
if(cursor_ < select_) {
select_ = cursor_;
} else {
cursor_ = select_;
}
} else {
if(cursor_.col > text_[cursor_.row].size()) {
cursor_.col = text_[cursor_.row].size();
}
--cursor_.col;
if(cursor_.col < 0) {
if(cursor_.row == 0) {
cursor_.col = 0;
} else {
--cursor_.row;
cursor_.col = text_[cursor_.row].size();
}
}
}
on_move_cursor();
break;
case SDLK_RIGHT:
record_op();
if(cursor_ != select_ && !(SDL_GetModState()&KMOD_SHIFT)) {
//pressing right without shift while we have a selection moves us to the end of the selection
if(cursor_ < select_) {
cursor_ = select_;
} else {
select_ = cursor_;
}
} else {
++cursor_.col;
if(cursor_.col > text_[cursor_.row].size()) {
if(cursor_.row == text_.size()-1) {
--cursor_.col;
} else if(cursor_.row < text_.size()-1) {
++cursor_.row;
cursor_.col = 0;
} else {
--cursor_.col;
}
}
}
on_move_cursor();
break;
case SDLK_UP:
record_op();
if(cursor_.row > 0) {
--cursor_.row;
cursor_.col = find_equivalent_col(cursor_.col, cursor_.row+1, cursor_.row);
}
on_move_cursor();
break;
case SDLK_DOWN:
record_op();
if(cursor_.row < text_.size()-1) {
++cursor_.row;
cursor_.col = find_equivalent_col(cursor_.col, cursor_.row-1, cursor_.row);
}
on_move_cursor();
break;
case SDLK_PAGEUP: {
record_op();
on_page_up();
bool move_cursor = false;
while(cursor_.row > scroll_pos_ && char_position_on_screen(cursor_.row, cursor_.col).first == -1) {
--cursor_.row;
cursor_.col = find_equivalent_col(cursor_.col, cursor_.row+1, cursor_.row);
move_cursor = true;
}
if(move_cursor) {
on_move_cursor();
}
if(!(SDL_GetModState()&KMOD_SHIFT)) {
select_ = cursor_;
}
break;
}
case SDLK_PAGEDOWN: {
record_op();
on_page_down();
bool move_cursor = false;
while(cursor_.row < scroll_pos_ && char_position_on_screen(cursor_.row, cursor_.col).first == -1) {
++cursor_.row;
cursor_.col = find_equivalent_col(cursor_.col, cursor_.row-1, cursor_.row);
move_cursor = true;
}
if(move_cursor) {
on_move_cursor();
}
if(!(SDL_GetModState()&KMOD_SHIFT)) {
select_ = cursor_;
}
break;
}
case SDLK_HOME:
record_op();
cursor_.col = 0;
on_move_cursor();
break;
case SDLK_END:
record_op();
cursor_.col = text_[cursor_.row].size();
on_move_cursor();
break;
case SDLK_DELETE:
case SDLK_BACKSPACE:
if(record_op("delete")) {
save_undo_state();
}
if(cursor_ == select_) {
if(event.keysym.sym == SDLK_BACKSPACE) {
//backspace is like delete but we move to the left first.
if(cursor_.col > text_[cursor_.row].size()) {
cursor_.col = text_[cursor_.row].size();
}
if(cursor_.row == 0 && cursor_.col == 0) {
break;
}
--cursor_.col;
if(cursor_.col < 0) {
--cursor_.row;
cursor_.col = text_[cursor_.row].size();
}
on_move_cursor();
}
if(cursor_.col >= text_[cursor_.row].size()) {
if(text_.size() > cursor_.row+1) {
cursor_.col = text_[cursor_.row].size();
text_[cursor_.row] += text_[cursor_.row+1];
text_.erase(text_.begin() + cursor_.row + 1);
}
} else {
text_[cursor_.row].erase(text_[cursor_.row].begin() + cursor_.col);
}
} else {
delete_selection();
}
refresh_scrollbar();
on_change();
break;
case SDLK_RETURN: {
if(record_op("enter")) {
save_undo_state();
}
if(nrows_ == 1) {
if(on_enter_) {
on_enter_();
}
break;
}
if(on_begin_enter_) {
if(!on_begin_enter_()) {
break;
}
}
delete_selection();
truncate_col_position();
std::string new_line(text_[cursor_.row].begin() + cursor_.col, text_[cursor_.row].end());
text_[cursor_.row].erase(text_[cursor_.row].begin() + cursor_.col, text_[cursor_.row].end());
std::string::iterator indent = text_[cursor_.row].begin();
while(indent != text_[cursor_.row].end() && strchr(" \t", *indent)) {
++indent;
}
new_line.insert(new_line.begin(), text_[cursor_.row].begin(), indent);
cursor_.col = indent - text_[cursor_.row].begin();
text_.insert(text_.begin() + cursor_.row + 1, new_line);
++cursor_.row;
select_ = cursor_;
refresh_scrollbar();
on_change();
on_move_cursor();
if(on_enter_) {
on_enter_();
}
break;
}
case SDLK_TAB: {
if(on_tab_) {
on_tab_();
}
if(nrows_ == 1) {
break;
}
}
default: {
const char c = event.keysym.unicode;
if(util::c_isprint(c) || c == '\t') {
if(record_op("chars")) {
save_undo_state();
}
delete_selection();
if(cursor_.col > text_[cursor_.row].size()) {
cursor_.col = text_[cursor_.row].size();
}
text_[cursor_.row].insert(text_[cursor_.row].begin() + cursor_.col, c);
++cursor_.col;
select_ = cursor_;
refresh_scrollbar();
on_change();
return true;
}
return false;
}
}
return true;
}
void text_editor_widget::handle_paste(std::string txt)
{
record_op();
save_undo_state();
delete_selection();
txt.erase(std::remove(txt.begin(), txt.end(), '\r'), txt.end());
std::vector<std::string> lines = util::split(txt, '\n', 0 /*don't remove empties or strip spaces*/);
truncate_col_position();
if(lines.size() == 1) {
text_[cursor_.row].insert(text_[cursor_.row].begin() + cursor_.col, lines.front().begin(), lines.front().end());
cursor_.col += lines.front().size();
refresh_scrollbar();
select_ = cursor_;
} else if(lines.size() >= 2) {
text_.insert(text_.begin() + cursor_.row + 1, lines.back() + std::string(text_[cursor_.row].begin() + cursor_.col, text_[cursor_.row].end()));
text_[cursor_.row] = std::string(text_[cursor_.row].begin(), text_[cursor_.row].begin() + cursor_.col) + lines.front();
text_.insert(text_.begin() + cursor_.row + 1, lines.begin()+1, lines.end()-1);
cursor_ = select_ = Loc(cursor_.row + lines.size() - 1, lines.back().size());
}
on_change();
}
void text_editor_widget::handle_copy(bool mouse_based)
{
if(mouse_based && !clipboard_has_mouse_area()) {
return;
}
Loc begin = cursor_;
Loc end = select_;
if(begin.col > text_[begin.row].size()) {
begin.col = text_[begin.row].size();
}
if(end.col > text_[end.row].size()) {
end.col = text_[end.row].size();
}
if(end < begin) {
std::swap(begin, end);
}
std::string str;
if(begin.row == end.row) {
str = std::string(text_[begin.row].begin() + begin.col, text_[begin.row].begin() + end.col);
} else {
str = std::string(text_[begin.row].begin() + begin.col, text_[begin.row].end());
while(++begin.row < end.row) {
str += "\n" + text_[begin.row];
}
str += "\n" + std::string(text_[end.row].begin(), text_[end.row].begin() + end.col);
}
copy_to_clipboard(str, mouse_based);
}
void text_editor_widget::delete_selection()
{
if(cursor_.col == select_.col && cursor_.row == select_.row) {
return;
}
if(cursor_.col > text_[cursor_.row].size()) {
cursor_.col = text_[cursor_.row].size();
}
if(select_.col > text_[select_.row].size()) {
select_.col = text_[select_.row].size();
}
if(select_ < cursor_) {
std::swap(cursor_, select_);
}
std::string& cursor_line = text_[cursor_.row];
std::string& select_line = text_[select_.row];
if(cursor_.row == select_.row) {
cursor_line.erase(cursor_line.begin() + cursor_.col, cursor_line.begin() + select_.col);
} else {
cursor_line = std::string(cursor_line.begin(), cursor_line.begin() + cursor_.col) + std::string(select_line.begin() + select_.col, select_line.end());
text_.erase(text_.begin() + cursor_.row + 1, text_.begin() + select_.row + 1);
}
select_ = cursor_;
}
graphics::color text_editor_widget::get_character_color(int row, int col) const
{
return text_color_;
}
std::pair<int, int> text_editor_widget::mouse_position_to_row_col(int xpos, int ypos) const
{
const int xloc = x() + BorderSize;
const int yloc = y() + BorderSize;
int r = 0;
for(int n = scroll_pos_; n < text_.size() && r < nrows_; ++n, ++r) {
int c = 0;
bool matches_row = ypos >= yloc + r*char_height_ && ypos < yloc + (r+1)*char_height_;
for(size_t m = 0; m < text_[n].size(); ++m, ++c) {
if(c >= ncols_) {
if(matches_row) {
break;
}
++r;
c -= ncols_;
matches_row = ypos >= yloc + r*char_height_ && ypos < yloc + (r+1)*char_height_;
if(r == nrows_) {
break;
}
}
const int char_size = text_[n][m] == '\t' ? TabWidth : 1;
if(matches_row && xpos >= xloc + c*char_width_ && xpos < xloc + (c+char_size)*char_width_) {
return std::pair<int, int>(n, m);
}
if(text_[n][m] == '\t') {
c += TabAdjust;
continue;
}
}
if(matches_row) {
return std::pair<int, int>(n, text_[n].size());
}
}
return std::pair<int, int>(-1,-1);
}
std::pair<int, int> text_editor_widget::char_position_on_screen(int row, int col) const
{
if(row < scroll_pos_) {
return std::pair<int, int>(-1, -1);
}
int r = 0;
for(size_t n = scroll_pos_; n < text_.size() && r < nrows_; ++n, ++r) {
int c = 0;
size_t m;
for(m = 0; m < text_[n].size(); ++m, ++c) {
if(c >= ncols_) {
++r;
c -= ncols_;
if(r == nrows_) {
break;
}
}
if(row == n && col == m) {
return std::pair<int, int>(BorderSize + r*char_height_, BorderSize + c*char_width_);
}
if(text_[n][m] == '\t') {
c += TabAdjust;
continue;
}
}
if(row == n && m == text_[n].size()) {
return std::pair<int, int>(BorderSize + r*char_height_, BorderSize + c*char_width_);
}
}
return std::pair<int, int>(-1,-1);
}
void text_editor_widget::on_page_up()
{
int leap = nrows_ - 1;
while(scroll_pos_ > 0 && leap > 0) {
--scroll_pos_;
--leap;
for(int n = int(text_[scroll_pos_].size()) - ncols_; n > 0; n -= ncols_) {
--leap;
}
}
refresh_scrollbar();
}
void text_editor_widget::on_page_down()
{
int leap = nrows_ - 1;
while(scroll_pos_ < int(text_.size())-2 && leap > 0) {
++scroll_pos_;
--leap;
for(int n = int(text_[scroll_pos_].size()) - ncols_; n > 0; n -= ncols_) {
--leap;
}
}
refresh_scrollbar();
}
void text_editor_widget::on_move_cursor(bool auto_shift)
{
const int start_pos = scroll_pos_;
if(cursor_.row < scroll_pos_) {
scroll_pos_ = cursor_.row;
} else {
while(scroll_pos_ < cursor_.row && char_position_on_screen(cursor_.row, cursor_.col).first == -1) {
++scroll_pos_;
}
}
if(start_pos != scroll_pos_) {
refresh_scrollbar();
}
if(!auto_shift && !(SDL_GetModState()&KMOD_SHIFT)) {
select_ = cursor_;
}
scrollable_widget::set_yscroll(scroll_pos_*char_height_);
if(select_ != cursor_) {
//a mouse-based copy for X-style copy/paste
handle_copy(true);
}
if(on_move_cursor_) {
on_move_cursor_();
}
}
int text_editor_widget::find_equivalent_col(int old_col, int old_row, int new_row) const
{
int actual_pos = old_col + std::count(text_[old_row].begin(), text_[old_row].end(), '\t')*TabAdjust;
for(int n = 0; n < actual_pos; ++n) {
if(n < text_[new_row].size() && text_[new_row][n] == '\t') {
actual_pos -= TabAdjust;
}
}
return actual_pos;
}
void text_editor_widget::on_set_yscroll(int old_pos, int new_pos)
{
scroll_pos_ = new_pos/char_height_;
}
void text_editor_widget::refresh_scrollbar()
{
int total_rows = 0;
//See if it can all fit without a scrollbar.
for(int n = 0; n != text_.size(); ++n) {
const int rows = 1 + text_[n].size()/ncols_;
total_rows += rows;
if(total_rows > nrows_) {
break;
}
}
if(total_rows <= nrows_) {
//no scrollbar needed.
set_virtual_height(height());
update_scrollbar();
return;
}
set_virtual_height(text_.size()*char_height_ + height() - char_height_);
set_scroll_step(char_height_);
set_yscroll(scroll_pos_*char_height_);
update_scrollbar();
}
void text_editor_widget::select_token(const std::string& row, int& begin_row, int& end_row, int& begin_col, int& end_col)
{
if(util::c_isdigit(row[begin_col]) || row[begin_col] == '.' && begin_col+1 < row.size() && util::c_isdigit(row[begin_col+1])) {
while(begin_col >= 0 && (util::c_isdigit(row[begin_col]) || row[begin_col] == '.')) {
--begin_col;
}
if(begin_col < 0 || row[begin_col] != '-') {
++begin_col;
}
while(end_col < row.size() && (util::c_isdigit(row[end_col]) || row[end_col] == '.')) {
++end_col;
}
} else if(util::c_isalnum(row[begin_col]) || row[begin_col] == '_') {
while(begin_col >= 0 && (util::c_isalnum(row[begin_col]) || row[begin_col] == '_')) {
--begin_col;
}
++begin_col;
while(end_col < row.size() && (util::c_isalnum(row[end_col]) || row[end_col] == '_')) {
++end_col;
}
} else if(end_col < row.size()) {
++end_col;
}
}
text_editor_widget_ptr text_editor_widget::clone() const
{
text_editor_widget_ptr result = new text_editor_widget(*this);
result->last_op_type_ = NULL;
return result;
}
void text_editor_widget::restore(const text_editor_widget* state)
{
*this = *state;
}
void text_editor_widget::save_undo_state()
{
redo_.clear();
undo_.push_back(text_editor_widget_ptr(clone()));
}
bool text_editor_widget::record_op(const char* type)
{
if(type == NULL || type != last_op_type_) {
last_op_type_ = type;
return true;
} else {
return false;
}
}
void text_editor_widget::undo()
{
if(undo_.empty()) {
return;
}
std::vector<text_editor_widget_ptr> redo_state = redo_;
save_undo_state();
redo_state.push_back(undo_.back());
undo_.pop_back();
//Save the state before restoring it so it doesn't get cleaned up
//while we're in the middle of the restore call.
text_editor_widget_ptr state = undo_.back();
restore(state.get());
redo_ = redo_state;
on_change();
}
void text_editor_widget::redo()
{
if(redo_.empty()) {
return;
}
std::vector<text_editor_widget_ptr> redo_state = redo_;
redo_state.pop_back();
//Save the state before restoring it so it doesn't get cleaned up
//while we're in the middle of the restore call.
text_editor_widget_ptr state = redo_.back();
restore(state.get());
redo_ = redo_state;
on_change();
}
void text_editor_widget::truncate_col_position()
{
if(cursor_.col > text_[cursor_.row].size()) {
cursor_.col = text_[cursor_.row].size();
}
if(select_.col > text_[select_.row].size()) {
select_.col = text_[select_.row].size();
}
}
void text_editor_widget::set_search(const std::string& term)
{
search_ = term;
calculate_search_matches();
if(search_matches_.empty()) {
return;
}
std::vector<std::pair<Loc, Loc> >::const_iterator search_itor =
std::lower_bound(search_matches_.begin(), search_matches_.end(),
std::pair<Loc,Loc>(cursor_, cursor_));
if(search_itor == search_matches_.end()) {
search_itor = search_matches_.begin();
}
select_ = cursor_ = search_itor->first;
on_move_cursor();
}
void text_editor_widget::next_search_match()
{
if(search_matches_.empty()) {
return;
}
cursor_.col++;
select_ = cursor_;
set_search(search_);
}
void text_editor_widget::calculate_search_matches()
{
search_matches_.clear();
if(search_.empty()) {
return;
}
try {
boost::regex re(search_, boost::regex::perl|boost::regex::icase);
for(int n = 0; n != text_.size(); ++n) {
boost::cmatch match;
const char* ptr = text_[n].c_str();
while(boost::regex_search(ptr, match, re)) {
const int base = ptr - text_[n].c_str();
const Loc begin(n, base + match.position());
const Loc end(n, base + match.position() + match.length());
search_matches_.push_back(std::pair<Loc,Loc>(begin,end));
const int advance = match.position() + match.length();
if(advance == 0) {
break;
}
ptr += advance;
}
}
} catch(boost::regex_error&) {
}
}
void text_editor_widget::replace(const std::string& replace_with)
{
record_op();
save_undo_state();
//we have to get the end itor here because some compilers don't
//support comparing a const and non-const reverse iterator
const std::vector<std::pair<Loc, Loc> >::const_reverse_iterator end_itor = search_matches_.rend();
for(std::vector<std::pair<Loc, Loc> >::const_reverse_iterator i = search_matches_.rbegin(); i != end_itor; ++i) {
const Loc& begin = i->first;
const Loc& end = i->second;
if(begin.row != end.row) {
continue;
}
text_[begin.row].erase(text_[begin.row].begin() + begin.col, text_[begin.row].begin() + end.col);
text_[begin.row].insert(text_[begin.row].begin() + begin.col, replace_with.begin(), replace_with.end());
}
on_change();
}
void text_editor_widget::on_change()
{
if(on_change_) {
on_change_();
}
calculate_search_matches();
}
void text_editor_widget::set_value(const std::string& key, const variant& v)
{
if(key == "text") {
set_text(v.as_string());
} else if(key == "begin_enter") {
begin_enter_return_ = v.as_bool();
} else if(key == "color" || key == "colour") {
text_color_ = graphics::color(v);
}
scrollable_widget::set_value(key, v);
}
variant text_editor_widget::get_value(const std::string& key) const
{
if(key == "text" ) {
return variant(text());
}
return scrollable_widget::get_value(key);
}
void text_editor_widget::change_delegate()
{
using namespace game_logic;
if(get_environment()) {
map_formula_callable_ptr callable = map_formula_callable_ptr(new map_formula_callable(get_environment()));
callable->add("text", variant(text()));
variant value = ffl_on_change_->execute(*callable);
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::change_delegate() called without environment!" << std::endl;
}
}
void text_editor_widget::move_cursor_delegate()
{
if(get_environment()) {
variant value = ffl_on_move_cursor_->execute(*get_environment());
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::move_cursor_delegate() called without environment!" << std::endl;
}
}
void text_editor_widget::enter_delegate()
{
using namespace game_logic;
if(get_environment()) {
map_formula_callable_ptr callable = map_formula_callable_ptr(new map_formula_callable(get_environment()));
callable->add("text", variant(text()));
variant value = ffl_on_enter_->execute(*callable);
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::enter_delegate() called without environment!" << std::endl;
}
}
void text_editor_widget::escape_delegate()
{
using namespace game_logic;
if(get_environment()) {
map_formula_callable_ptr callable = map_formula_callable_ptr(new map_formula_callable(get_environment()));
callable->add("text", variant(text()));
variant value = ffl_on_escape_->execute(*callable);
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::escape_delegate() called without environment!" << std::endl;
}
}
void text_editor_widget::tab_delegate()
{
using namespace game_logic;
if(get_environment()) {
map_formula_callable_ptr callable = map_formula_callable_ptr(new map_formula_callable(get_environment()));
callable->add("text", variant(text()));
variant value = ffl_on_tab_->execute(*callable);
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::tab_delegate() called without environment!" << std::endl;
}
}
bool text_editor_widget::begin_enter_delegate()
{
if(get_environment()) {
variant value = ffl_on_begin_enter_->execute(*get_environment());
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::begin_enter_delegate() called without environment!" << std::endl;
}
// XXX Need some way of doing the return value here.
return begin_enter_return_;
}
void text_editor_widget::change_focus_delgate(bool new_focus_value)
{
using namespace game_logic;
if(get_environment()) {
map_formula_callable_ptr callable = map_formula_callable_ptr(new map_formula_callable(get_environment()));
callable->add("focus", variant::from_bool(new_focus_value));
callable->add("text", variant(text()));
variant value = ffl_on_change_focus_->execute(*callable);
get_environment()->execute_command(value);
} else {
std::cerr << "text_editor_widget::tab_delegate() called without environment!" << std::endl;
}
}
}
#include "code_editor_widget.hpp"
#include "dialog.hpp"
#include "filesystem.hpp"
namespace {
void on_change_search(const gui::text_editor_widget_ptr search_entry, gui::text_editor_widget_ptr editor)
{
editor->set_search(search_entry->text());
}
}
UTILITY(textedit)
{
using namespace gui;
if(args.size() != 1) {
std::cerr << "textedit usage: <filename>\n";
return;
}
std::string file = args[0];
std::string contents = sys::read_file(file);
if(contents.empty()) {
std::cerr << "Could not read file (" << file << ")\n";
return;
}
text_editor_widget_ptr entry = new text_editor_widget(120);
text_editor_widget_ptr editor = new code_editor_widget(600, 400);
editor->set_text(contents);
entry->set_on_change_handler(boost::bind(on_change_search, entry, editor));
entry->set_on_enter_handler(boost::bind(&text_editor_widget::next_search_match, editor));
dialog d(0, 0, graphics::screen_width(), graphics::screen_height());
d.add_widget(widget_ptr(entry), 10, 10);
d.add_widget(widget_ptr(editor), 10, 30);
d.show_modal();
}
UNIT_TEST(test_regex)
{
std::string searching = "abcdefg";
boost::regex re("cde");
boost::cmatch matches;
const char* ptr = searching.c_str();
if(boost::regex_search(ptr, matches, re)) {
CHECK_EQ(matches.size(), 1);
CHECK_EQ(matches.position(), 2);
CHECK_EQ(matches.length(), 3);
}
}
Jump to Line
Something went wrong with that request. Please try again.