diff options
Diffstat (limited to 'editbox.cc')
-rw-r--r-- | editbox.cc | 1909 |
1 files changed, 1909 insertions, 0 deletions
diff --git a/editbox.cc b/editbox.cc new file mode 100644 index 0000000..e298049 --- /dev/null +++ b/editbox.cc @@ -0,0 +1,1909 @@ +// Copyright (C) 2003 Mooffie <mooffie@typo.co.il> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA. + +#include <config.h> + +#include <algorithm> + +#include "editbox.h" +#include "scrollbar.h" +#include "transtbl.h" +#include "univalues.h" +#include "themes.h" +#include "dbg.h" + +TranslationTable EditBox::transtbl; +TranslationTable EditBox::reprtbl; +TranslationTable EditBox::altkbdtbl; + +// DOS's EOP is actually two characters: CR + LF, but we represent it +// internally as one character to make processing much simpler. The value we +// choose for it is in Unicode's Private Area block. + +#define DOS_PS 0xF880 +#define MAC_PS 0x0D +#define UNIX_PS 0x0A + +// Initialization {{{ + +EditBox::EditBox() +{ + create_window(); + + status_listener = NULL; + error_listener = NULL; + margin_before = 0; + margin_after = 1; + primary_mark.para = -1; + optimal_vis_column = -1; + bidi_enabled = true; + visual_cursor_movement = false; + modified = false; + scroll_step = 4; + update_region = rgnAll; + alt_kbd = false; + auto_justify = false; + justification_column = 72; + auto_indent = false; + translation_mode = false; + read_only = false; + smart_typing = false; + wrap_type = wrpAtWhiteSpace; + dir_algo = algoContextRTL; + tab_width = 8; + show_tabs = true; + show_explicits = true; + show_paragraph_endings = true; + rtl_nsm_display = rtlnsmTransliterated; + maqaf_display = mqfAsis; + syn_hlt = synhltOff; + underline_hlt = false; + rfc2646_trailing_space = true; + data_transfer.in_transfer = false; + old_width = -1; + prev_command_type = current_command_type = cmdtpUnknown; + paragraphs.push_back(new Paragraph()); +} + +EditBox::~EditBox() +{ + for (int i = 0; i < parags_count(); i++) + delete paragraphs[i]; +} + +void EditBox::new_document() +{ + for (int i = 0; i < parags_count(); i++) + delete paragraphs[i]; + paragraphs.clear(); + paragraphs.push_back(new Paragraph()); + + undo_stack.clear(); + unset_primary_mark(); + set_modified(false); + cursor.zero(); + scroll_to_cursor_line(); + request_update(rgnAll); +} + +void EditBox::sync_scrollbar(Scrollbar *scrollbar) +{ + scrollbar->set_total_size(parags_count()); + scrollbar->set_page_size(window_height()); + scrollbar->set_page_pos(top_line.para); +} + +// }}} + +// Utility methods dealing with End-Of-Paragraphs {{{ + +static inline bool is_eop(unichar ch) { + return (ch == DOS_PS || ch == MAC_PS || ch == UNIX_PS || ch == UNICODE_PS); +} + +static inline eop_t get_eop_type(unichar ch) +{ + switch (ch) { + case DOS_PS: return eopDOS; + case MAC_PS: return eopMac; + case UNIX_PS: return eopUnix; + case UNICODE_PS: return eopUnicode; + default: return eopNone; + } +} + +unichar EditBox::get_eop_char(eop_t eop) +{ + switch (eop) { + case eopDOS: return DOS_PS; + case eopMac: return MAC_PS; + case eopUnix: return UNIX_PS; + case eopUnicode: return UNICODE_PS; + default: return UNIX_PS; // we shouldn't arrive here + } +} + +// get_curr_eop_char() returns the character corresponding to the current +// paragraph's EOP. +// +// When creating new paragraphs, e.g. as a reslt of pressing ENTER or of +// justifying lines, we copy the EOP of the current paragraph to the new +// paragraphs. Since our document may contain differenet EOPs (for different +// paragraphs), this seems to be the most sensible solution. +// +// When the current paragraph has no EOP (when it's the last in the buffer), +// we look at the previous one. As a last resort we use Unix's EOP. + +unichar EditBox::get_curr_eop_char() +{ + if (curr_para()->eop == eopNone) { + if (cursor.para > 0) + return get_eop_char(paragraphs[cursor.para - 1]->eop); + else + return UNIX_PS; // default + } else { + return get_eop_char(curr_para()->eop); + } +} + +// toggle_eops() is an interactive command to change all EOPs in the buffer. +// +// It'd be especially appreciated by Unix users who would like to convert +// DOS line ends to Unix line ends, but it was written to toggle among all +// possible EOPs. + +void EditBox::set_eops(eop_t new_eop) +{ + if (read_only) { + NOTIFY_ERROR(read_only); + return; + } + + for (int i = 0; i < parags_count() - 1; i++) + paragraphs[i]->eop = new_eop; + set_modified(true); + + request_update(rgnAll); +} + +INTERACTIVE void EditBox::toggle_eops() +{ + eop_t new_eop = eopNone; // silence the compiler + switch (paragraphs[0]->eop) { + case eopUnix: new_eop = eopDOS; break; + case eopDOS: new_eop = eopMac; break; + case eopMac: new_eop = eopUnicode; break; + case eopUnicode: new_eop = eopUnix; break; + case eopNone: return; // no eops in document + } + set_eops(new_eop); +} + +// }}} + +// Setters / Getters / Togglers {{{ + +// The following are trivial set_xxx methods. + +void EditBox::set_read_only(bool value) { + read_only = value; + NOTIFY_CHANGE(read_only); +} + +void EditBox::set_smart_typing(bool val) { + smart_typing = val; + NOTIFY_CHANGE(smart_typing); +} + +void EditBox::set_modified(bool value) { + modified = value; + NOTIFY_CHANGE(modification); +} + +void EditBox::set_auto_indent(bool value) { + auto_indent = value; + NOTIFY_CHANGE(auto_indent); +} + +void EditBox::set_auto_justify(bool value) { + auto_justify = value; + NOTIFY_CHANGE(auto_justify); +} + +void EditBox::set_justification_column(int value) { + if (value > 0) + justification_column = value; +} + +void EditBox::set_scroll_step(int value) { + if (value > 0) + scroll_step = value; +} + +void EditBox::set_primary_mark(const Point &point) { + primary_mark = point; + NOTIFY_CHANGE(selection); +} + +void EditBox::unset_primary_mark() { + if (is_primary_mark_set()) { + primary_mark.para = -1; + request_update(rgnAll); + NOTIFY_CHANGE(selection); + } +} + +INTERACTIVE void EditBox::toggle_primary_mark() { + if (is_primary_mark_set()) + unset_primary_mark(); + else + set_primary_mark(); +} + +// }}} + +// Misc {{{ + +// calc_distance() - count the number of characters, counting EOP as 1, +// between two points. + +int EditBox::calc_distance(Point p1, Point p2) +{ + int dist; + if (p2 < p1) + p2.swap(p1); + + if (p1.para == p2.para) { + dist = p2.pos - p1.pos; + } else { + dist = paragraphs[p1.para]->str.len() - p1.pos + 1; + while (++p1.para < p2.para) + dist += paragraphs[p1.para]->str.len() + 1; + dist += p2.pos; + } + return dist; +} + +// calc_inner_line() returns the inner-line on which the cursor stands. +// In our terminology, "inner-line" is a paragraph screen-line, zero-based. + +int EditBox::calc_inner_line() +{ + for (int line_num = 0; line_num < curr_para()->breaks_count(); line_num++) { + idx_t line_break = curr_para()->line_breaks[line_num]; + if (cursor.pos < line_break + || (cursor.pos == line_break + && cursor.pos == curr_para()->str.len())) + return line_num; + } + return 0; +} + +// }}} + +// Clipboard {{{ + +INTERACTIVE void EditBox::copy() { + cut_or_copy(true); +} + +INTERACTIVE void EditBox::cut() { + cut_or_copy(false); +} + +INTERACTIVE void EditBox::paste() { + insert_text(clipboard); +} + +// cut_or_copy() -- cut and/or copies text into the clipboard. It does it by +// calling copy_text() and delete_text(). +// +// @param just_copy - don't delete the text. + +void EditBox::cut_or_copy(bool just_copy) +{ + if (is_primary_mark_set()) { + int len = calc_distance(cursor, primary_mark); + // copy_text() and delete_text() advance the cursor -- + // that's why this code is a bit confusing: we restore the + // cursor position after each call. + Point orig_cursor = cursor; + if (cursor > primary_mark) + cursor = primary_mark; + if (just_copy) { + copy_text(len); + cursor = orig_cursor; + } else { + Point tmp = cursor; + copy_text(len); + cursor = tmp; + delete_text(len); + } + unset_primary_mark(); + } else { + NOTIFY_ERROR(no_selection); + } +} + +// copy_text() -- copies "len" characters to the clipboard. advances the +// cursor. +// if the 'append' parameter is true, the characters don't replace the +// clipboard but are appended to it. + +void EditBox::copy_text(int len, bool append) +{ + if (!append) + clipboard.clear(); + while (len > 0) { + if (cursor.pos == curr_para()->str.len()) { + if (cursor.para < parags_count() - 1) { + clipboard.push_back(get_curr_eop_char()); + cursor.para++; + cursor.pos = 0; + } + len--; + } else { + int to_copy = MIN(curr_para()->str.len() - cursor.pos, len); + clipboard.append(curr_para()->str.begin() + cursor.pos, to_copy); + cursor.pos += to_copy; + len -= to_copy; + } + } +} + +// }}} + +// Undo {{{ + +INTERACTIVE void EditBox::undo() +{ + undo_op(undo_stack.get_prev_op()); + // If we undo all the changes to the buffer, then we return it to its + // initial state. clear the "modified" flag. + if (!undo_stack.is_undo_available() + && !undo_stack.disabled() && !undo_stack.was_truncated()) + set_modified(false); +} + +INTERACTIVE void EditBox::redo() +{ + redo_op(undo_stack.get_next_op()); +} + +// undo_op() -- undoes en operation. For example, if the operation was to +// delete some text then we re-insert the deleted text. +// +// There are three fundamental operations: inserting text, deleting text and +// replacing text. + +void EditBox::undo_op(UndoOp *op) +{ + if (!op) + return; + + switch (op->type) { + case opDelete: + cursor = op->point; + insert_text(op->deleted_text, true); + break; + case opInsert: + cursor = op->point; + delete_text(op->inserted_text.len(), true); + break; + case opReplace: + cursor = op->point; + replace_text(op->deleted_text, op->inserted_text.len(), true); + break; + } +} + +void EditBox::redo_op(UndoOp *op) +{ + if (!op) + return; + + switch (op->type) { + case opDelete: + cursor = op->point; + delete_text(op->deleted_text.len(), true); + break; + case opInsert: + cursor = op->point; + insert_text(op->inserted_text, true); + break; + case opReplace: + cursor = op->point; + replace_text(op->inserted_text, op->deleted_text.len(), true); + break; + } +} + +INTERACTIVE void EditBox::toggle_key_for_key_undo() +{ + set_key_for_key_undo(!is_key_for_key_undo()); +} + +// }}} + +// Data Transfer {{{ + +// start_data_transfer() - must be called to start any data transfer; it +// initializes the data tranfser state (the "data_transfer" struct). +// +// @param dir - the direction in which the data is transferred. Either into +// EditBox (dataTransferIn) or out of EditBox (dataTransferOut). +// +// @param new_doc - clear the buffer before transferring data into EditBox. +// +// @param selection_only - when transferring data out, transfer only the +// selection, not the whole buffer. + +void EditBox::start_data_transfer(data_transfer_direction dir, + bool new_doc, + bool selection_only) +{ + data_transfer.dir = dir; + data_transfer.in_transfer = true; + + if (dir == dataTransferIn) { + // Loading a file + if (new_doc) { + // "Open new file" + new_document(); + data_transfer.skip_undo = true; + data_transfer.clear_modified_flag = true; + } else { + // "Insert file" + data_transfer.skip_undo = false; + data_transfer.clear_modified_flag = false; + } + data_transfer.prev_is_cr = false; + data_transfer.cursor_origin = cursor; + } else { + // Saving a file + data_transfer.cursor_origin = cursor; + if (selection_only && is_primary_mark_set()) { + data_transfer.ntransferred_out = 0; + data_transfer.ntransferred_out_max = calc_distance(cursor, primary_mark); + if (cursor > primary_mark) + cursor = primary_mark; + } else { + data_transfer.ntransferred_out_max = -1; + cursor.zero(); + } + data_transfer.at_eof = false; + // An error may occur during saving, so only a an object that knows + // the IO result may clear the modification flag. + data_transfer.clear_modified_flag = false; + } +} + +bool EditBox::is_in_data_transfer() const { + return data_transfer.in_transfer; +} + +int EditBox::transfer_data(unichar *buf, int len) +{ + if (data_transfer.dir == dataTransferIn) + return transfer_data_in(buf, len); + else + return transfer_data_out(buf, len); +} + +// transfer_data_out() - transfers data out of EditBox. returns the number of +// characters placed in "buf". when it returns 0, we know we've reached end +// of buffer. + +int EditBox::transfer_data_out(unichar *buf, int len) +{ + if (data_transfer.at_eof) + return 0; // end-of-buffer: no more chars to write + + int nwritten = 0; // number of chars we've written to buf + while (nwritten < len && !data_transfer.at_eof) { + if (cursor.pos == curr_para()->str.len()) { + + // write EOP + if (curr_para()->eop != eopNone) { + if (curr_para()->eop == eopDOS) { + // DOS's EOP is two characters. If there's not + // enough space in buf, we exit and do it in the + // next call. + if (len - nwritten < 2) + break; + *buf++ = '\r'; + *buf++ = '\n'; + nwritten += 2; + } else { + *buf++ = get_curr_eop_char(); + nwritten++; + } + data_transfer.ntransferred_out++; + if (data_transfer.ntransferred_out_max != -1 + && data_transfer.ntransferred_out + >= data_transfer.ntransferred_out_max) + data_transfer.at_eof = true; + } + + if (cursor.para < parags_count() - 1) { + // advance to next paragraph + cursor.para++; + cursor.pos = 0; + } else { + // we've reached end of buffer + data_transfer.at_eof = true; + } + } else { + // write the [cursor, end-of-paragraph) segment into buf. + int to_copy = MIN(curr_para()->str.len() - cursor.pos, (len - nwritten)); + if (data_transfer.ntransferred_out_max != -1 + && data_transfer.ntransferred_out + to_copy + > data_transfer.ntransferred_out_max) { + to_copy = MAX(data_transfer.ntransferred_out_max + - data_transfer.ntransferred_out, 0); + } + unichar *src = curr_para()->str.begin() + cursor.pos; + cursor.pos += to_copy; + nwritten += to_copy; + data_transfer.ntransferred_out += to_copy; + while (to_copy--) + *buf++ = *src++; + if (data_transfer.ntransferred_out_max != -1 + && data_transfer.ntransferred_out + >= data_transfer.ntransferred_out_max) + data_transfer.at_eof = true; + } + } + return nwritten; +} + +#define INSERT_DOS_PS() \ + do { \ + unichar ch = DOS_PS; \ + insert_text(&ch, 1, data_transfer.skip_undo); \ + } while (0) +#define INSERT_CR() \ + do { \ + unichar ch = '\r'; \ + insert_text(&ch, 1, data_transfer.skip_undo); \ + } while (0) + +// transfer_data_in() - transfers data into EditBox. + +int EditBox::transfer_data_in(unichar *data, int len) +{ + // What makes the implementation complicated is that we need to convert + // DOS's EOP (CR + LF) to one pseudo character. Since these two characters + // may be passed to this method in two separate calls, we have to maintain + // state (prev_is_cr = "last character was CR"). + int start = 0; + if (data_transfer.prev_is_cr && len) { + if (data[0] == '\n') { + INSERT_DOS_PS(); + start = 1; + } else { + INSERT_CR(); + } + data_transfer.prev_is_cr = false; + } + for (int i = start; i < len; i++) { + if (data[i] == '\r') { + insert_text(data + start, i - start, data_transfer.skip_undo); + start = i; + if (i + 1 < len) { + if (data[i + 1] == '\n') { + INSERT_DOS_PS(); + start += 2; + i++; + } + } else { + data_transfer.prev_is_cr = true; + start++; + } + } + } + insert_text(data + start, len - start, data_transfer.skip_undo); + return 0; // return value is meaningless +} + +// end_data_transfer() -- must be called when transfer is completed. + +void EditBox::end_data_transfer() +{ + if (data_transfer.dir == dataTransferIn) { + if (data_transfer.prev_is_cr) + INSERT_CR(); + } + + if (data_transfer.clear_modified_flag) + set_modified(false); + data_transfer.in_transfer = false; + + cursor = data_transfer.cursor_origin; + if (data_transfer.dir == dataTransferIn) + last_modification = cursor; + scroll_to_cursor_line(); + request_update(rgnAll); +} + +#undef INSERT_DOS_PS +#undef INSERT_CR + +// }}} + +// Scrolling {{{ + +// add_rows_to_line() - adds screen lines to a CombinedLine. For example, if +// we advance the cursor forward by one screen line, we could find the new +// line using: +// +// CombinedLine line(cursor.para, calc_inner_line()); +// add_rows_to_line(line, 1); +// +// @params rows - screen rows to add. can be negative. + +void EditBox::add_rows_to_line(CombinedLine &combline, int rows) +{ + if (rows > 0) { + int remaining_line_breaks = paragraphs[combline.para]->breaks_count() + - combline.inner_line - 1; + if (rows <= remaining_line_breaks) { + // Our final position is inside the paragraph. + // We zero rows to finish the loop. + combline.inner_line += rows; + rows = 0; + } else { + if (combline.para < parags_count() - 1) { + // We have way to go. Move to the next paragraph, + // decrease rows, and let the loop handle the rest. + combline.para++; + combline.inner_line = 0; + rows -= remaining_line_breaks + 1; + } else { + // We are at the last paragraph. Move to the last line. + combline.inner_line = paragraphs[combline.para]->breaks_count() - 1; + rows = 0; + } + } + + if (rows != 0) // way to go? + add_rows_to_line(combline, rows); + } + + if (rows < 0) { + if (-rows <= combline.inner_line) { + // Our final position is inside the paragraph. + // We zero rows to finish the loop. + combline.inner_line -= -rows; + rows = 0; + } else { + if (combline.para > 0) { + // We have way to go. Move to the previous paragraph, increase + // rows (towards zero), and let the loop handle the rest. + combline.para--; + rows += combline.inner_line + 1; + combline.inner_line = paragraphs[combline.para]->breaks_count() - 1; + } else { + // We are at the first paragraph. Move to the first line. + combline.inner_line = 0; + rows = 0; + } + } + + if (rows != 0) // way to go? + add_rows_to_line(combline, rows); + } +} + +// lines_diff() - calculates the difference, in screen lines, between +// two CombinedLines. + +int EditBox::lines_diff(CombinedLine L1, CombinedLine L2) +{ + if (L2 < L1) + L2.swap(L1); + + if (L1.para == L2.para) + return L2.inner_line - L1.inner_line; + + int rows = paragraphs[L1.para]->breaks_count() - L1.inner_line; + L1.para++; + while (L1.para < L2.para) { + rows += paragraphs[L1.para]->breaks_count(); + L1.para++; + } + rows += L2.inner_line; + + return rows; +} + +// scroll_to_cursor_line() - makes sure the cursor line (=current line) is +// visible on screen. If not, it scrolls. It is called at the end of various +// movement commands. + +void EditBox::scroll_to_cursor_line() +{ + CombinedLine curr(cursor.para, calc_inner_line()); + + if (curr < top_line) { + // if the current line is smaller than top_line, it means it's outside + // the window view and we have to scroll up to make it visible. + + // calculate the new top line + CombinedLine old_top_line = top_line; + top_line = curr; + add_rows_to_line(top_line, -(get_effective_scroll_step() - 1)); + + // Performance: we can either repaint all the window (rgnAll), + // or use curses' wscrl() and paint only the lines that aren't already + // on screen. + if (top_line.is_near(old_top_line)) { + int diff = lines_diff(top_line, old_top_line); + scrollok(wnd, TRUE); + wscrl(wnd, -diff); + scrollok(wnd, FALSE); + request_update(top_line.para, old_top_line.para - 1); + } else { + request_update(rgnAll); + } + + return; + } + + // We don't need to scroll up. Now we check whether we need to scroll down. + + // calculate the bottom line + CombinedLine bottom_line = top_line; + add_rows_to_line(bottom_line, window_height() - 1); + + if (curr > bottom_line) { + // if the current line is past bottom_line, we scroll down to make + // it visible. + + // calculate the new bottom line + bottom_line = curr; + add_rows_to_line(bottom_line, get_effective_scroll_step() - 1); + + // and the new top line + CombinedLine old_top_line = top_line; + top_line = bottom_line; + add_rows_to_line(top_line, -(window_height() - 1)); + + // Performance: either repaint everything or just the lines that + // aren't already on screen. + if (top_line.is_near(old_top_line)) { + int diff = lines_diff(top_line, old_top_line); + CombinedLine from = bottom_line; + add_rows_to_line(from, -(diff - 1)); + scrollok(wnd, TRUE); + wscrl(wnd, diff); + scrollok(wnd, FALSE); + request_update(from.para, bottom_line.para); + } else { + request_update(rgnAll); + } + } +} + +// move_forward_page() - interactive command called in response to the +// "Page Down" key. It mimics emacs. + +INTERACTIVE void EditBox::move_forward_page() +{ + // First, check whether the end of the buffer is already on screen (by + // calculating the bottom line). If so, move to the last line and quit. + + CombinedLine bottom_line = top_line; + add_rows_to_line(bottom_line, window_height() - 1); + + if (lines_diff(top_line, bottom_line) < window_height() - 1) { + move_relative_line(window_height()); + return; + } + + // No, the end of the buffer is not on screen. Advance top_line by + // window_height() lines (but keep 2 lines of context). + add_rows_to_line(top_line, window_height() - 2); + + // if the cursor is now outside the view ... + if (CombinedLine(cursor.para, calc_inner_line()) < top_line) { + // ... then move it to the top line. + cursor.para = top_line.para; + cursor.pos = (top_line.inner_line > 0) + ? curr_para()->line_breaks[top_line.inner_line - 1] + : 0; + invalidate_optimal_vis_column(); + } + + post_vertical_movement(); + request_update(rgnAll); +} + +// move_backward_page() - interactive command called in response to the +// "Page Up" key. It mimics emacs. + +INTERACTIVE void EditBox::move_backward_page() +{ + // First, check whether the first line of the buffer if already shown. If + // so, move to that first line and quit. + + if (top_line.para == 0 && top_line.inner_line == 0) { + move_relative_line(-window_height()); + return; + } + + // No, we can go back. Substract window_height() lines (minus 2 lines of + // context) from top_line. + add_rows_to_line(top_line, -(window_height() - 2)); + + // Calculate the new bottom line + CombinedLine bottom_line = top_line; + add_rows_to_line(bottom_line, window_height() - 1); + + // if the cursor is now past the bottom line ... + if (CombinedLine(cursor.para, calc_inner_line()) > bottom_line) { + // ... then move it to the bottom line. + cursor.para = bottom_line.para; + cursor.pos = (bottom_line.inner_line > 0) + ? curr_para()->line_breaks[bottom_line.inner_line - 1] + : 0; + invalidate_optimal_vis_column(); + } + + post_vertical_movement(); + request_update(rgnAll); +} + +// center_line() - interactive command used to center the current line. +// (like emacs's C-l) + +INTERACTIVE void EditBox::center_line() +{ + // Calculate new top line + CombinedLine new_top_line(cursor.para, calc_inner_line()); + add_rows_to_line(new_top_line, -(window_height() / 2)); + top_line = new_top_line; + request_update(rgnAll); +} + +// }}} + +// Movement commands {{{ + +// post_horizontal_movement() -- should be called at the end of horizontal +// movement commands. It makes sure the cursor is on screen and notify our +// listeners. + +void EditBox::post_horizontal_movement() +{ + scroll_to_cursor_line(); + request_update(rgnCursor); + invalidate_optimal_vis_column(); + NOTIFY_CHANGE(position); +} + +// post_vertical_movement() -- should be called at the end of vertical +// movement commands. + +void EditBox::post_vertical_movement() +{ + scroll_to_cursor_line(); + request_update(rgnCursor); + NOTIFY_CHANGE(position); +} + +// move_forward_char() - interactive command to move to the next logical +// character. + +INTERACTIVE void EditBox::move_forward_char() +{ + if (cursor.pos < curr_para()->str.len()) { + cursor.pos++; + } else if (cursor.para < parags_count() - 1) { + cursor.para++; + cursor.pos = 0; + } + post_horizontal_movement(); +} + +// move_backward_char() - interactive command to move the the previous logical +// character. + +INTERACTIVE void EditBox::move_backward_char() +{ + if (cursor.pos > 0) { + cursor.pos--; + } else if (cursor.para > 0) { + cursor.para--; + cursor.pos = curr_para()->str.len(); + } + post_horizontal_movement(); +} + +// key_left() - responds to the "left arrow" key. + +INTERACTIVE void EditBox::key_left() +{ + if (curr_para()->is_rtl()) { + if (visual_cursor_movement) + move_forward_visual_char(); + else + move_forward_char(); + } else { + if (visual_cursor_movement) + move_backward_visual_char(); + else + move_backward_char(); + } +} + +// key_left() - responds to the "right arrow" key. + +INTERACTIVE void EditBox::key_right() +{ + if (curr_para()->is_rtl()) { + if (visual_cursor_movement) + move_backward_visual_char(); + else + move_backward_char(); + } else { + if (visual_cursor_movement) + move_forward_visual_char(); + else + move_forward_char(); + } +} + +bool EditBox::is_at_end_of_buffer() { + return cursor.para == parags_count() - 1 + && cursor.pos == curr_para()->str.len(); +} + +bool EditBox::is_at_beginning_of_buffer() { + return cursor.para == 0 && cursor.pos == 0; +} + +unichar EditBox::get_current_char() +{ + if (cursor.pos < curr_para()->str.len()) + return curr_para()->str[cursor.pos]; + else + return get_curr_eop_char(); +} + +// move_forward_word() -- interactive command to move to the end of the +// current or next word. + +INTERACTIVE void EditBox::move_forward_word() +{ + while (!BiDi::is_wordch(get_current_char()) && !is_at_end_of_buffer()) + move_forward_char(); + while (BiDi::is_wordch(get_current_char()) && !is_at_end_of_buffer()) + move_forward_char(); +} + +bool EditBox::is_at_beginning_of_word() +{ + if (cursor.pos == curr_para()->str.len()) + return false; + if (cursor.pos == 0) + return BiDi::is_wordch(curr_para()->str[0]); + return (BiDi::is_wordch(curr_para()->str[cursor.pos]) + && !BiDi::is_wordch(curr_para()->str[cursor.pos-1])); +} + +// move_backward_word() -- interactive command to move to the start of the +// current or previous word. + +INTERACTIVE void EditBox::move_backward_word() +{ + if (is_at_beginning_of_word()) + move_backward_char(); + while (!is_at_beginning_of_word() && !is_at_beginning_of_buffer()) + move_backward_char(); +} + +// move_beginning_of_buffer() -- interactive command to move to the beginning +// of the buffer + +INTERACTIVE void EditBox::move_beginning_of_buffer() +{ + cursor.zero(); + post_vertical_movement(); +} + +// move_end_of_buffer() -- interactive command to move to the end of the +// buffer + +INTERACTIVE void EditBox::move_end_of_buffer() +{ + cursor.para = parags_count() - 1; + cursor.pos = 0; + post_vertical_movement(); +} + +// move_beginning_of_line() - interactive command to move to the beginning of +// the current screen line. + +INTERACTIVE void EditBox::move_beginning_of_line() +{ + idx_t prev_line_break = 0; + for (int line_num = 0; line_num < curr_para()->breaks_count(); line_num++) { + idx_t line_break = curr_para()->line_breaks[line_num]; + if (cursor.pos < line_break + || (cursor.pos == line_break + && cursor.pos == curr_para()->str.len())) { + cursor.pos = prev_line_break; + break; + } + prev_line_break = line_break; + } + post_horizontal_movement(); +} + +// move_end_of_line() - interactive command to move to the end of +// the current screen line. + +INTERACTIVE void EditBox::move_end_of_line() +{ + for (int line_num = 0; line_num < curr_para()->breaks_count(); line_num++) { + idx_t line_break = curr_para()->line_breaks[line_num]; + if (cursor.pos < line_break) { + cursor.pos = line_break; + if (line_num != curr_para()->breaks_count() - 1) + cursor.pos--; + break; + } + } + post_horizontal_movement(); +} + +INTERACTIVE void EditBox::move_last_modification() +{ + if (last_modification.para || last_modification.pos) + set_cursor_position(last_modification); +} + +// set_cursor_position(Point) - allow other objects to position the cursor +// at a specific paragraph and column. + +void EditBox::set_cursor_position(const Point &point) +{ + cursor.para = MIN(MAX(point.para, 0), parags_count() - 1); + cursor.pos = MIN(MAX(point.pos, 0), curr_para()->str.len()); + post_horizontal_movement(); +} + +// move_absolute_line() -- jumps to a specific paragraph (used by the user when +// he knows where he wants to jump to, i.e. when reading compiler error messages). + +void EditBox::move_absolute_line(int line) +{ + set_cursor_position(Point(line, 0)); +} + +// move_relative_line(diff) - moves the cursor diff lines up or down. + +void EditBox::move_relative_line(int diff) +{ + if (!valid_optimal_vis_column()) + optimal_vis_column = calc_vis_column(); + + CombinedLine curr(cursor.para, calc_inner_line()); + add_rows_to_line(curr, diff); + cursor.para = curr.para; + // move the cursor to the beginning of inner_line + cursor.pos = (curr.inner_line > 0) + ? curr_para()->line_breaks[curr.inner_line - 1] + : 0; + // reposition the cursor at the right column + move_to_vis_column(optimal_vis_column); + post_vertical_movement(); +} + +// move_next_line() - interactive command to move the cursor to the next +// screen line (e.g. in response to "key down") + +INTERACTIVE void EditBox::move_next_line() +{ + move_relative_line(1); +} + +// move_previous_line() - interactive command to move the cursor to the +// previous screen line (e.g. in response to "key up") + +INTERACTIVE void EditBox::move_previous_line() +{ + move_relative_line(-1); +} + +// search_forward() - a simple method for searching. It's not adequate for +// most uses: it can't ignore non-spacing marks and there's no corresponding +// method for searching backwards. I'll have to work on that. +// +// returns true if the string was found. + +bool EditBox::search_forward(unistring str) +{ + str = str.toupper_ascii(); + const unichar *first, *last; + for (int i = cursor.para; i < parags_count(); i++) { + const unistring para = paragraphs[i]->str.toupper_ascii(); + first = para.begin(); + last = para.end(); + if (i == cursor.para) { + first += cursor.pos; + // skip the character under the cursor + if (first != last) + first++; + } + const unichar *pos = std::search(first, last, str.begin(), str.end()); + if (pos != last) { + cursor.pos = pos - para.begin(); + cursor.para = i; + post_horizontal_movement(); + return true; + } + } + return false; +} + +// move_first_char(ch) - Moves to the first ch in the buffer. When saving the +// buffer to a file the charset conversion may fail. This method is used to +// position the cursor on the offending character. + +void EditBox::move_first_char(unichar ch) +{ + for (int i = 0; i < parags_count(); i++) { + Paragraph ¶ = *paragraphs[i]; + idx_t len = para.str.len(); + for (int pos = 0; pos <= len; pos++) + if ( + (pos < len && para.str[pos] == ch) || + (pos == len && para.eop != eopNone + && get_eop_char(para.eop) == ch) + ) + { + cursor.para = i; + cursor.pos = pos; + post_horizontal_movement(); + return; + } + } +} + +// }}} + +// Visual movement commands {{{ + +void EditBox::set_visual_cursor_movement(bool v) +{ + visual_cursor_movement = v; + NOTIFY_CHANGE(selection); // I don't have a special notification for this event. +} + +INTERACTIVE void EditBox::toggle_visual_cursor_movement() +{ + set_visual_cursor_movement(!get_visual_cursor_movement()); +} + +bool EditBox::is_at_end_of_screen_line() +{ + idx_t end_pos = curr_para()->str.len(); + for (int line_num = 0; line_num < curr_para()->breaks_count(); line_num++) { + idx_t line_break = curr_para()->line_breaks[line_num]; + if (cursor.pos < line_break) { + end_pos = line_break; + if (line_num != curr_para()->breaks_count() - 1) + end_pos--; + break; + } + } + return cursor.pos == end_pos; +} + +bool EditBox::is_at_first_screen_line() +{ + return cursor.para == 0 && calc_inner_line() == 0; +} + +bool EditBox::is_at_last_screen_line() +{ + return (cursor.para == parags_count()-1) + && (calc_inner_line() == curr_para()->breaks_count()-1); +} + +// move_forward_visual_char() - moves a char forward, visually. + +INTERACTIVE void EditBox::move_forward_visual_char() +{ + int vis_column = calc_vis_column(); + if (is_at_end_of_screen_line()) { + // move to visual start of next screen line + if (!is_at_last_screen_line()) { + move_relative_line(1); + move_to_vis_column(0); + } + } else { + move_to_vis_column(vis_column+1); + if (calc_vis_column() == vis_column) { + // We're stuck on a tab or on a wide character. + // Ugly hack, sorry :-( + move_forward_char(); + } + } + post_horizontal_movement(); +} + +// move_backward_visual_char() - moves a char backward, visually. + +INTERACTIVE void EditBox::move_backward_visual_char() +{ + int vis_column = calc_vis_column(); + if (vis_column == 0 // we need to move up a line, + && !is_at_first_screen_line()) // but only if we're not + // already at start of buffer + { + move_relative_line(-1); + move_end_of_line(); + } else { + move_to_vis_column(vis_column-1); + } + post_horizontal_movement(); +} + +INTERACTIVE void EditBox::move_beginning_of_visual_line() +{ + move_to_vis_column(0); + post_horizontal_movement(); +} + +INTERACTIVE void EditBox::key_home() +{ + if (visual_cursor_movement) + move_beginning_of_visual_line(); + else + move_beginning_of_line(); +} + +// }}} + +// Typing related: justification / indentation / alt_kbd / translation {{{ + +// insert_char() - inserts a character into the buffer. It assumes the +// character was typed by the user, so it also handles auto_justify. +// If you want to insert a character without this extra processing, use +// insert_text() instead. + +void EditBox::insert_char(unichar ch) +{ + insert_text(&ch, 1); + + if (auto_justify && cursor.pos > justification_column) { + // find previous whitespace + idx_t wspos = cursor.pos - 1; + while (wspos > 0 && !BiDi::is_space(curr_para()->str[wspos])) + --wspos; + if (wspos > 0) { + idx_t new_cursor_pos = cursor.pos - wspos - 1; + cursor.pos = wspos; + ch = get_curr_eop_char(); + if (rfc2646_trailing_space) { + // keep the space at the end of the line + cursor.pos++; + insert_text(&ch, 1); + cursor.pos = new_cursor_pos; + } else { + // overwrite the space with a new-line + replace_text(&ch, 1, 1); + cursor.pos = new_cursor_pos; + } + scroll_to_cursor_line(); + } + } + + last_modification = cursor; +} + +// key_enter() - called when the user hits Enter + +void EditBox::key_enter() +{ + if (auto_indent) { + unistring indent; + unistring ¶ = curr_para()->str; + indent.push_back(get_curr_eop_char()); + for (idx_t i = 0; i < cursor.pos && BiDi::is_space(para[i]); i++) + indent.push_back(para[i]); + insert_text(indent); + } else { + insert_char(get_curr_eop_char()); + } +} + +// key_enter() - interactive command to insert maqaf into the buffer + +INTERACTIVE void EditBox::insert_maqaf() +{ + insert_char(UNI_HEB_MAQAF); +} + +// key_dash() - called when the user types '-' + +void EditBox::key_dash() +{ + // if the previous [non-NSM] char is RTL and we're in smart_typing mode, + // insert maqaf. + if (smart_typing && cursor.pos > 0 + && (BiDi::is_rtl(curr_para()->str[cursor.pos-1]) + || (BiDi::is_nsm(curr_para()->str[cursor.pos-1]) + && cursor.pos > 1 + && BiDi::is_rtl(curr_para()->str[cursor.pos-2]))) ) + insert_maqaf(); + else + insert_char('-'); +} + +void EditBox::set_translation_mode(bool value) +{ + if (!transtbl.empty()) { + translation_mode = value; + NOTIFY_CHANGE(translation_mode); + } else { + NOTIFY_ERROR(no_translation_table); + } +} + +void EditBox::set_alt_kbd(bool val) +{ + if (!altkbdtbl.empty()) { + alt_kbd = val; + NOTIFY_CHANGE(alt_kbd); + } +} + +INTERACTIVE void EditBox::toggle_alt_kbd() +{ + if (!altkbdtbl.empty()) { + set_alt_kbd(!get_alt_kbd()); + } else { + NOTIFY_ERROR(no_alt_kbd); + } +} + +// handle_event() - deals with literal keys. Special keys (control & alt +// combinations, function keys, arrows, etc) are dealt by the base class, +// Dispatcher. +// +// When "tranlation mode" is active, it translates the character according to +// the "transtbl" table. When "alt_kbd" is active -- according to the +// "altkbdtbl" table. + +bool EditBox::handle_event(const Event &evt) +{ + current_command_type = cmdtpUnknown; + if (evt.is_literal()) { + unichar ch = evt.ch; + if (in_translation_mode()) { + transtbl.translate_char(ch); + set_translation_mode(false); + } else if (alt_kbd) { + altkbdtbl.translate_char(ch); + } + switch (ch) { + case 13: key_enter(); break; + case '-': key_dash(); break; + default: + insert_char(ch); + } + prev_command_type = current_command_type; + return true; + } else { + bool ret = Dispatcher::handle_event(evt); + prev_command_type = current_command_type; + return ret; + } +} + +static bool is_empty_or_starts_with_space(const unistring &str) { + return (str.empty() || BiDi::is_space(str[0])); +} + +static bool is_blank(const unistring &str) { + for (idx_t i = 0; i < str.len(); i++) + if (!BiDi::is_space(str[i])) + return false; + return true; +} + +// justify() - interactive command to justify the current "paragraph". Here +// "paragraph" means a bunch of Paragraphs (what a user thinks of as "lines") +// separated by blank or indented Paragraphs. + +INTERACTIVE void EditBox::justify() +{ + // Step 1. + // Move to a non-blank line + + while (cursor.para < parags_count() - 1 + && is_blank(curr_para()->str)) + move_next_line(); + + // Step 2. + // Find the min and max lines that constitute this paragraph. + + int min_para, max_para; + min_para = max_para = cursor.para; + // paragraphs are separated by blank lines or indentations. + while (min_para > 0 + && !is_empty_or_starts_with_space(paragraphs[min_para - 1]->str)) + --min_para; + while (max_para < parags_count() - 1 + && !is_empty_or_starts_with_space(paragraphs[max_para + 1]->str)) + ++max_para; + // if we're an indented paragraph, include the indented line. + if (min_para > 0 + && !is_empty_or_starts_with_space(paragraphs[min_para]->str) + && !is_blank(paragraphs[min_para - 1]->str)) + --min_para; + + // Step 3. + // Collect all the lines into 'text' + + unistring text; + for (int i = min_para; i <= max_para; i++) { + if (!text.empty() && !BiDi::is_space(text.back())) + text.push_back(' '); + text.append(paragraphs[i]->str); + } + + // Step 4. + // Justify 'text' into 'justified' + + unistring justified; + while (!text.empty()) { + if (!justified.empty()) { + if (rfc2646_trailing_space) + justified.push_back(' '); + justified.push_back(get_curr_eop_char()); + } + int wspos = justification_column; + if (wspos >= text.len()) { + justified.append(text); + text.clear(); + } else { + while (wspos > 0 && !BiDi::is_space(text[wspos])) + --wspos; + if (wspos > 0) { + justified.append(text, wspos); + text.erase_head(wspos + 1); + } else { + justified.append(text, justification_column); + text.erase_head(justification_column); + } + } + } + + // Step 5. + // Delete the unjustified lines from the buffer and insert + // the 'justified' string instead. + + cursor.pos = 0; + cursor.para = min_para; + int delete_len = calc_distance(cursor, + Point(max_para, paragraphs[max_para]->str.len())); + replace_text(justified, delete_len); + + // Move past this paragraph, so that the next "justify" command + // justifies the next paragraph and so on. + move_next_line(); + move_beginning_of_line(); +} + +// }}} + +// Paragraph Base Direction {{{ + +void EditBox::set_dir_algo(diralgo_t value) +{ + dir_algo = value; + for (int i = 0; i < parags_count(); i++) + paragraphs[i]->determine_base_dir(dir_algo); + if (dir_algo == algoContextStrong || dir_algo == algoContextRTL) + calc_contextual_dirs(0, parags_count() - 1, false); + request_update(rgnAll); + NOTIFY_CHANGE(dir_algo); +} + +// toggle_dir_algo() - interactive command to toggle the directionality +// algorithm used. + +INTERACTIVE void EditBox::toggle_dir_algo() { + switch (dir_algo) { + case algoUnicode: set_dir_algo(algoContextStrong); break; + case algoContextStrong: set_dir_algo(algoContextRTL); break; + case algoContextRTL: set_dir_algo(algoForceLTR); break; + case algoForceLTR: set_dir_algo(algoForceRTL); break; + case algoForceRTL: set_dir_algo(algoUnicode); break; + } +} + +// calc_contextual_dirs() - this method implements the "contextual" algorithm. +// Other algorithms are implemented in BiDi::determine_base_dir(), but the +// BiDi class doesn't know about the context of the paragraph, so we have to +// do most of the work in EditBox. +// +// The contextual algorithm has the following effect: +// +// 1. If there's an RTL character in the paragraph, set the base +// direction to RTL; +// 2. Else: if there's an LTR character in the paragraph, set the base +// direction to LTR; +// 3. Else: the paragraph is neutral; set the base direction to the +// base direction of the previous non-neutral paragraph. If all +// previous paragraphs are neuteral, set it to the base direction +// of the next non-neutral paragraph. If all paragraphs are +// neutral, set it to LTR. +// +// #1 and #2 are implemented in BiDi::determine_base_dir(). #3 is implemented +// here. + +void EditBox::calc_contextual_dirs(int min_para, int max_para, + bool update_display) +{ + // find the previous strong paragraph + while (min_para > 0) { + min_para--; + if (paragraphs[min_para]->individual_base_dir != dirN) + break; + } + // find the next strong paragraph + while (max_para < parags_count() - 1) { + max_para++; + if (paragraphs[max_para]->individual_base_dir != dirN) + break; + } + + // loop from min_para to max_para and whenever you find a neutral + // paragraph, set its base direction to the last strong direction you + // encountered (prev_strong). + direction_t prev_strong = dirN; + for (int i = min_para; i <= max_para; i++) { + Paragraph &p = *paragraphs[i]; + if (p.individual_base_dir == dirN) { + set_contextual_dir(p, prev_strong, update_display); + } else { + set_contextual_dir(p, p.individual_base_dir, update_display); + prev_strong = p.individual_base_dir; + } + } + + // Do the same but now loop backwards: from max_para to min_para. + prev_strong = dirN; + for (int i = max_para; i >= min_para; i--) { + Paragraph &p = *paragraphs[i]; + if (p.contextual_base_dir == dirN) { + set_contextual_dir(p, prev_strong, update_display); + } else { + prev_strong = p.contextual_base_dir; + } + } + + // If there're any neutral paragraphs left, set their base direction to + // LTR + for (int i = min_para; i <= max_para; i++) { + Paragraph &p = *paragraphs[i]; + if (p.contextual_base_dir == dirN) + set_contextual_dir(p, dirLTR, update_display); + } +} + +inline void EditBox::set_contextual_dir(Paragraph &p, direction_t dir, + bool update_display) +{ + if (!update_display) + p.contextual_base_dir = dir; + else { + if (p.contextual_base_dir != dir) { + // if only the current paragraph has changed its direction, + // we don't need to repaint all the window. + p.contextual_base_dir = dir; + if (&p == curr_para()) + request_update(rgnCurrent); + else + request_update(rgnAll); + } + } +} + +// }}} + +// Modification {{{ + +// post_modification() - should be called at the end of any primitive command +// that changes the buffer. It invalidates the selection, the optimal visual +// column and notifies our listeners. + +void EditBox::post_modification() +{ + invalidate_optimal_vis_column(); + last_modification = cursor; + cache.invalidate(); + if (is_primary_mark_set()) + unset_primary_mark(); + if (!is_modified()) + set_modified(true); + NOTIFY_CHANGE(position); +} + +// post_para_modification() - should be called for every paragraph that has +// been modified. It recalculates its base direcion and rewraps it. + +void EditBox::post_para_modification(Paragraph &p) +{ + p.determine_base_dir(dir_algo); + wrap_para(p); +} + +// delete_forward_char() - interactive command to delete the character +// we're on. Usually bound to the DELETE key. + +INTERACTIVE void EditBox::delete_forward_char() +{ + delete_text(1); +} + +// delete_backward_char() - interactive command to delete the previous +// character. Usually bound to the BACKSPACE key. + +INTERACTIVE void EditBox::delete_backward_char() +{ + if (!is_at_beginning_of_buffer()) { + move_backward_char(); + delete_forward_char(); + } +} + +// cut_end_of_paragraph() - interactive command to cut till +// end of paragraph. If the cursor is already at the end, it deletes +// the EOP (ala emacs). + +INTERACTIVE void EditBox::cut_end_of_paragraph() +{ + int count; + if (cursor.pos == curr_para()->str.len()) + count = 1; + else + count = curr_para()->str.len() - cursor.pos; + + Point orig_cursor = cursor; + copy_text(count, (prev_command_type == cmdtpKill)); + cursor = orig_cursor; + delete_text(count); + + current_command_type = cmdtpKill; +} + +// delete_paragraph() - interactive command to delete the current +// paragraph. + +INTERACTIVE void EditBox::delete_paragraph() +{ + cursor.pos = 0; + delete_text(curr_para()->str.len() + 1); +} + +// delete_forward_word() - interactive command to delete till the +// end of the current or next word. + +INTERACTIVE void EditBox::delete_forward_word() +{ + Point start = cursor; + move_forward_word(); + int len = calc_distance(cursor, start); + cursor = start; + delete_text(len); +} + +// delete_backward_word() - interactive command to delete till the +// start of the current or previous word. + +INTERACTIVE void EditBox::delete_backward_word() +{ + Point end = cursor; + move_backward_word(); + int len = calc_distance(cursor, end); + delete_text(len); +} + +// replace_text() - is a combination of delete_text() and insert_text(). we +// should use it instead of calling these two methods separately because it +// records these two operations on the undo stack as one atomic operation. + +void EditBox::replace_text(const unichar *str, int len, int delete_len, + bool skip_undo) +{ + unistring deleted; + Point point = cursor; + delete_text(delete_len, true, &deleted); + insert_text(str, len, true); + + if (!skip_undo) { + UndoOp op; + op.type = opReplace; + op.point = point; + op.deleted_text = deleted; + op.inserted_text.append(str, len); + undo_stack.record_op(op); + } +} + +// delete_text() - deletes len characters, starting at the cursor. +// +// @param skip_undo - don't record this operation on the undo stack. +// @param alt_deleted - copy the deleted text here. + +void EditBox::delete_text(int len, bool skip_undo, unistring *alt_deleted) +{ + if (read_only) { + NOTIFY_ERROR(read_only); + return; + } + + unistring _deleted, *deleted; + deleted = alt_deleted ? alt_deleted : &_deleted; + + int parags_deleted = 0; // we keep count of how many parags we delete. + + while (len > 0) { + if (cursor.pos == curr_para()->str.len()) { + if (cursor.para < parags_count() - 1) { + // Delete the EOP. that is, append the next + // paragraph to the current paragraph. + deleted->push_back(get_curr_eop_char()); + Paragraph *next_para = paragraphs[cursor.para + 1]; + curr_para()->str.append(next_para->str); + curr_para()->eop = next_para->eop; + delete next_para; + paragraphs.erase(paragraphs.begin() + cursor.para + 1); + } + len--; + parags_deleted++; + } else { + // delete the [cursor, end-of-paragraph) segment + int to_delete = MIN(curr_para()->str.len() - cursor.pos, len); + unichar *cursor_ptr = curr_para()->str.begin() + cursor.pos; + deleted->append(cursor_ptr, to_delete); + curr_para()->str.erase(cursor_ptr, cursor_ptr + to_delete); + len -= to_delete; + } + } + + // optimization: we save some values and latter check whether they changed. + int orig_num_lines = curr_para()->breaks_count(); + direction_t orig_individual_base_dir = curr_para()->individual_base_dir; + + post_para_modification(*curr_para()); + + // if no para was deleted, and the direction of the current para has not + // changed, it cannot affect the surrounding parags, so we don't call + // calc_contextual_dirs() + if (parags_deleted || + curr_para()->individual_base_dir != orig_individual_base_dir) + calc_contextual_dirs(cursor.para, cursor.para, true); + + // if no para was deleted, if no screen lines were deleted, we can + // update only rgnCurrent instead of the whole window. + if (!parags_deleted && curr_para()->breaks_count() == orig_num_lines) + request_update(rgnCurrent); + else + request_update(rgnAll); + + scroll_to_cursor_line(); + + if (!skip_undo) { + UndoOp op; + op.type = opDelete; + op.point = cursor; + op.deleted_text = *deleted; + undo_stack.record_op(op); + } + + post_modification(); +} + +// insert_text() - inserts text into the buffer. advances the cursor. +// +// @param skip_undo - don't record this operation on the undo stack. + +void EditBox::insert_text(const unichar *str, int len, bool skip_undo) +{ + if (read_only && !is_in_data_transfer()) { + if (len) + NOTIFY_ERROR_ARG(read_only, str[0]); + return; + } + + if (!skip_undo) { + UndoOp op; + op.type = opInsert; + op.point = cursor; + op.inserted_text.append(str, len); + undo_stack.record_op(op); + } + + int min_changed_para, max_changed_para; // which parags have changed? + min_changed_para = cursor.para; + + while (len > 0) { + if (is_eop(str[0])) { + // inserting EOP is like pressing Enter: split the current + // paragraph into two. + Paragraph *p = new Paragraph(); + p->str.insert(0, &curr_para()->str[cursor.pos], + curr_para()->str.end()); + p->eop = curr_para()->eop; + curr_para()->str.erase(&curr_para()->str[cursor.pos], + curr_para()->str.end()); + curr_para()->eop = get_eop_type(str[0]); + paragraphs.insert(paragraphs.begin() + cursor.para + 1, p); + cursor.para++; + cursor.pos = 0; + len--; + str++; + } else { + int line_len = 0; + while (line_len < len && !is_eop(str[line_len])) + line_len++; + curr_para()->str.insert(curr_para()->str.begin() + cursor.pos, + str, str + line_len); + cursor.pos += line_len; + len -= line_len; + str += line_len; + } + } + max_changed_para = cursor.para; + + // optimization: we save some values and latter check whether they changed. + int orig_num_lines = curr_para()->breaks_count(); + direction_t orig_individual_base_dir = curr_para()->individual_base_dir; + + // update state variables of the affected parags. + for (int i = min_changed_para; i <= max_changed_para; i++) + post_para_modification(*paragraphs[i]); + + // if only one para was modified, and its direction has not changed, + // it doesn't affect the surrounding parags, so we don't call + // calc_contextual_dirs() + if (!(min_changed_para == max_changed_para && + curr_para()->individual_base_dir == orig_individual_base_dir)) + calc_contextual_dirs(min_changed_para, max_changed_para, true); + + // if only one para was modified, and no screen lines were added, we can + // update only rgnCurrent instead of the whole window. + if (min_changed_para == max_changed_para + && curr_para()->breaks_count() == orig_num_lines) + request_update(rgnCurrent); + else + request_update(rgnAll); + + scroll_to_cursor_line(); + + post_modification(); +} + +// }}} + |