help system
[fanfix.git] / src / jexer / TEditorWidget.java
CommitLineData
12b55d76
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
a69ed767 6 * Copyright (C) 2019 Kevin Lamonte
12b55d76
KL
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29package jexer;
30
71a389c9
KL
31import java.io.IOException;
32
12b55d76 33import jexer.bits.CellAttributes;
43ee96da 34import jexer.bits.StringUtils;
101bc589 35import jexer.event.TCommandEvent;
12b55d76
KL
36import jexer.event.TKeypressEvent;
37import jexer.event.TMouseEvent;
e8a11f98 38import jexer.event.TResizeEvent;
12b55d76
KL
39import jexer.teditor.Document;
40import jexer.teditor.Line;
41import jexer.teditor.Word;
101bc589 42import static jexer.TCommand.*;
12b55d76
KL
43import static jexer.TKeypress.*;
44
45/**
46 * TEditorWidget displays an editable text document. It is unaware of
47 * scrolling behavior, but can respond to mouse and keyboard events.
48 */
101bc589 49public class TEditorWidget extends TWidget implements EditMenuUser {
12b55d76 50
615a0d99
KL
51 // ------------------------------------------------------------------------
52 // Constants --------------------------------------------------------------
53 // ------------------------------------------------------------------------
54
b1a8acec
KL
55 /**
56 * The number of lines to scroll on mouse wheel up/down.
57 */
58 private static final int wheelScrollSize = 3;
59
615a0d99
KL
60 // ------------------------------------------------------------------------
61 // Variables --------------------------------------------------------------
62 // ------------------------------------------------------------------------
63
12b55d76
KL
64 /**
65 * The document being edited.
66 */
67 private Document document;
68
e8a11f98
KL
69 /**
70 * The default color for the TEditor class.
71 */
72 private CellAttributes defaultColor = null;
73
74 /**
75 * The topmost line number in the visible area. 0-based.
76 */
77 private int topLine = 0;
78
79 /**
80 * The leftmost column number in the visible area. 0-based.
81 */
82 private int leftColumn = 0;
83
101bc589
KL
84 /**
85 * If true, the mouse is dragging a selection.
86 */
87 private boolean inSelection = false;
88
89 /**
90 * Selection starting column.
91 */
92 private int selectionColumn0;
93
94 /**
95 * Selection starting line.
96 */
97 private int selectionLine0;
98
99 /**
100 * Selection ending column.
101 */
102 private int selectionColumn1;
103
104 /**
105 * Selection ending line.
106 */
107 private int selectionLine1;
108
615a0d99
KL
109 // ------------------------------------------------------------------------
110 // Constructors -----------------------------------------------------------
111 // ------------------------------------------------------------------------
112
12b55d76
KL
113 /**
114 * Public constructor.
115 *
116 * @param parent parent widget
117 * @param text text on the screen
118 * @param x column relative to parent
119 * @param y row relative to parent
120 * @param width width of text area
121 * @param height height of text area
122 */
123 public TEditorWidget(final TWidget parent, final String text, final int x,
124 final int y, final int width, final int height) {
125
126 // Set parent and window
127 super(parent, x, y, width, height);
128
129 setCursorVisible(true);
e8a11f98
KL
130
131 defaultColor = getTheme().getColor("teditor");
132 document = new Document(text, defaultColor);
12b55d76
KL
133 }
134
615a0d99 135 // ------------------------------------------------------------------------
101bc589 136 // Event handlers ---------------------------------------------------------
615a0d99
KL
137 // ------------------------------------------------------------------------
138
12b55d76
KL
139 /**
140 * Handle mouse press events.
141 *
142 * @param mouse mouse button press event
143 */
144 @Override
145 public void onMouseDown(final TMouseEvent mouse) {
146 if (mouse.isMouseWheelUp()) {
b1a8acec
KL
147 for (int i = 0; i < wheelScrollSize; i++) {
148 if (topLine > 0) {
149 topLine--;
150 alignDocument(false);
151 }
e8a11f98 152 }
12b55d76
KL
153 return;
154 }
155 if (mouse.isMouseWheelDown()) {
b1a8acec
KL
156 for (int i = 0; i < wheelScrollSize; i++) {
157 if (topLine < document.getLineCount() - 1) {
158 topLine++;
159 alignDocument(true);
160 }
e8a11f98 161 }
12b55d76
KL
162 return;
163 }
164
e8a11f98 165 if (mouse.isMouse1()) {
101bc589 166 // Selection.
9240f032
KL
167 int newLine = topLine + mouse.getY();
168 int newX = leftColumn + mouse.getX();
169
20c416a0 170 inSelection = true;
9240f032
KL
171 if (newLine > document.getLineCount() - 1) {
172 selectionLine0 = document.getLineCount() - 1;
173 } else {
174 selectionLine0 = topLine + mouse.getY();
175 }
3c5921e6
KL
176 selectionColumn0 = leftColumn + mouse.getX();
177 selectionColumn0 = Math.max(0, Math.min(selectionColumn0,
178 document.getLine(selectionLine0).getDisplayLength() - 1));
20c416a0
KL
179 selectionColumn1 = selectionColumn0;
180 selectionLine1 = selectionLine0;
101bc589
KL
181
182 // Set the row and column
101bc589
KL
183 if (newLine > document.getLineCount() - 1) {
184 // Go to the end
185 document.setLineNumber(document.getLineCount() - 1);
186 document.end();
187 if (newLine > document.getLineCount() - 1) {
188 setCursorY(document.getLineCount() - 1 - topLine);
189 } else {
190 setCursorY(mouse.getY());
191 }
192 alignCursor();
193 if (inSelection) {
194 selectionColumn1 = document.getCursor();
195 selectionLine1 = document.getLineNumber();
101bc589
KL
196 }
197 return;
198 }
199
200 document.setLineNumber(newLine);
201 setCursorY(mouse.getY());
202 if (newX >= document.getCurrentLine().getDisplayLength()) {
203 document.end();
204 alignCursor();
205 } else {
206 document.setCursor(newX);
207 setCursorX(mouse.getX());
208 }
209 if (inSelection) {
210 selectionColumn1 = document.getCursor();
211 selectionLine1 = document.getLineNumber();
101bc589
KL
212 }
213 return;
214 } else {
215 inSelection = false;
216 }
217
218 // Pass to children
219 super.onMouseDown(mouse);
220 }
221
222 /**
223 * Handle mouse motion events.
224 *
225 * @param mouse mouse motion event
226 */
227 @Override
228 public void onMouseMotion(final TMouseEvent mouse) {
229
230 if (mouse.isMouse1()) {
8d3480c7
KL
231 // Set the row and column
232 int newLine = topLine + mouse.getY();
233 int newX = leftColumn + mouse.getX();
234 if ((newLine < 0) || (newX < 0)) {
235 return;
236 }
237
101bc589
KL
238 // Selection.
239 if (inSelection) {
8d3480c7
KL
240 selectionColumn1 = newX;
241 selectionLine1 = newLine;
20c416a0 242 } else {
101bc589 243 inSelection = true;
8d3480c7
KL
244 selectionColumn0 = newX;
245 selectionLine0 = newLine;
101bc589
KL
246 selectionColumn1 = selectionColumn0;
247 selectionLine1 = selectionLine0;
101bc589
KL
248 }
249
df602ccf 250 if (newLine > document.getLineCount() - 1) {
e8a11f98
KL
251 // Go to the end
252 document.setLineNumber(document.getLineCount() - 1);
253 document.end();
df602ccf
KL
254 if (newLine > document.getLineCount() - 1) {
255 setCursorY(document.getLineCount() - 1 - topLine);
e8a11f98 256 } else {
df602ccf 257 setCursorY(mouse.getY());
e8a11f98
KL
258 }
259 alignCursor();
101bc589
KL
260 if (inSelection) {
261 selectionColumn1 = document.getCursor();
262 selectionLine1 = document.getLineNumber();
101bc589 263 }
e8a11f98
KL
264 return;
265 }
e8a11f98
KL
266 document.setLineNumber(newLine);
267 setCursorY(mouse.getY());
4297b49b 268 if (newX >= document.getCurrentLine().getDisplayLength()) {
e8a11f98
KL
269 document.end();
270 alignCursor();
271 } else {
df602ccf 272 document.setCursor(newX);
e8a11f98
KL
273 setCursorX(mouse.getX());
274 }
101bc589
KL
275 if (inSelection) {
276 selectionColumn1 = document.getCursor();
277 selectionLine1 = document.getLineNumber();
101bc589 278 }
e8a11f98
KL
279 return;
280 }
12b55d76
KL
281
282 // Pass to children
283 super.onMouseDown(mouse);
284 }
285
286 /**
287 * Handle keystrokes.
288 *
289 * @param keypress keystroke event
290 */
291 @Override
292 public void onKeypress(final TKeypressEvent keypress) {
43ee96da 293 if (keypress.getKey().isShift()) {
101bc589
KL
294 // Selection.
295 if (!inSelection) {
296 inSelection = true;
297 selectionColumn0 = document.getCursor();
298 selectionLine0 = document.getLineNumber();
299 selectionColumn1 = selectionColumn0;
300 selectionLine1 = selectionLine0;
101bc589
KL
301 }
302 } else {
43ee96da
KL
303 if (keypress.equals(kbLeft)
304 || keypress.equals(kbRight)
305 || keypress.equals(kbUp)
306 || keypress.equals(kbDown)
307 || keypress.equals(kbPgDn)
308 || keypress.equals(kbPgUp)
309 || keypress.equals(kbHome)
310 || keypress.equals(kbEnd)
311 ) {
312 // Non-shifted navigation keys disable selection.
313 inSelection = false;
314 }
4941d2d6
KL
315 if ((selectionColumn0 == selectionColumn1)
316 && (selectionLine0 == selectionLine1)
317 ) {
318 // The user clicked a spot and started typing.
319 inSelection = false;
320 }
101bc589
KL
321 }
322
323 if (keypress.equals(kbLeft)
324 || keypress.equals(kbShiftLeft)
325 ) {
df602ccf
KL
326 document.left();
327 alignTopLine(false);
101bc589
KL
328 } else if (keypress.equals(kbRight)
329 || keypress.equals(kbShiftRight)
330 ) {
df602ccf
KL
331 document.right();
332 alignTopLine(true);
e23989a4
KL
333 } else if (keypress.equals(kbAltLeft)
334 || keypress.equals(kbCtrlLeft)
43ee96da
KL
335 || keypress.equals(kbAltShiftLeft)
336 || keypress.equals(kbCtrlShiftLeft)
e23989a4
KL
337 ) {
338 document.backwardsWord();
339 alignTopLine(false);
340 } else if (keypress.equals(kbAltRight)
341 || keypress.equals(kbCtrlRight)
43ee96da
KL
342 || keypress.equals(kbAltShiftRight)
343 || keypress.equals(kbCtrlShiftRight)
e23989a4
KL
344 ) {
345 document.forwardsWord();
346 alignTopLine(true);
101bc589
KL
347 } else if (keypress.equals(kbUp)
348 || keypress.equals(kbShiftUp)
349 ) {
71a389c9
KL
350 document.up();
351 alignTopLine(false);
101bc589
KL
352 } else if (keypress.equals(kbDown)
353 || keypress.equals(kbShiftDown)
354 ) {
71a389c9
KL
355 document.down();
356 alignTopLine(true);
43ee96da
KL
357 } else if (keypress.equals(kbPgUp)
358 || keypress.equals(kbShiftPgUp)
359 ) {
71a389c9
KL
360 document.up(getHeight() - 1);
361 alignTopLine(false);
43ee96da
KL
362 } else if (keypress.equals(kbPgDn)
363 || keypress.equals(kbShiftPgDn)
364 ) {
71a389c9
KL
365 document.down(getHeight() - 1);
366 alignTopLine(true);
43ee96da
KL
367 } else if (keypress.equals(kbHome)
368 || keypress.equals(kbShiftHome)
369 ) {
e8a11f98
KL
370 if (document.home()) {
371 leftColumn = 0;
372 if (leftColumn < 0) {
373 leftColumn = 0;
374 }
375 setCursorX(0);
376 }
43ee96da
KL
377 } else if (keypress.equals(kbEnd)
378 || keypress.equals(kbShiftEnd)
379 ) {
e8a11f98
KL
380 if (document.end()) {
381 alignCursor();
382 }
43ee96da
KL
383 } else if (keypress.equals(kbCtrlHome)
384 || keypress.equals(kbCtrlShiftHome)
385 ) {
12b55d76
KL
386 document.setLineNumber(0);
387 document.home();
e8a11f98
KL
388 topLine = 0;
389 leftColumn = 0;
390 setCursorX(0);
391 setCursorY(0);
43ee96da
KL
392 } else if (keypress.equals(kbCtrlEnd)
393 || keypress.equals(kbCtrlShiftEnd)
394 ) {
12b55d76
KL
395 document.setLineNumber(document.getLineCount() - 1);
396 document.end();
71a389c9 397 alignTopLine(false);
12b55d76
KL
398 } else if (keypress.equals(kbIns)) {
399 document.setOverwrite(!document.getOverwrite());
400 } else if (keypress.equals(kbDel)) {
43ee96da
KL
401 if (inSelection) {
402 deleteSelection();
403 } else {
404 document.del();
405 }
71a389c9 406 alignCursor();
39e86397
KL
407 } else if (keypress.equals(kbBackspace)
408 || keypress.equals(kbBackspaceDel)
409 ) {
43ee96da
KL
410 if (inSelection) {
411 deleteSelection();
412 } else {
413 document.backspace();
414 }
df602ccf
KL
415 alignTopLine(false);
416 } else if (keypress.equals(kbTab)) {
43ee96da 417 deleteSelection();
101bc589 418 // Add spaces until we hit modulo 8.
df602ccf
KL
419 for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
420 document.addChar(' ');
421 }
e8a11f98 422 alignCursor();
71a389c9 423 } else if (keypress.equals(kbEnter)) {
43ee96da 424 deleteSelection();
df602ccf
KL
425 document.enter();
426 alignTopLine(true);
12b55d76
KL
427 } else if (!keypress.getKey().isFnKey()
428 && !keypress.getKey().isAlt()
429 && !keypress.getKey().isCtrl()
430 ) {
431 // Plain old keystroke, process it
43ee96da 432 deleteSelection();
2d3f60d8 433 document.addChar(keypress.getKey().getChar());
71a389c9 434 alignCursor();
12b55d76
KL
435 } else {
436 // Pass other keys (tab etc.) on to TWidget
437 super.onKeypress(keypress);
438 }
101bc589
KL
439
440 if (inSelection) {
441 selectionColumn1 = document.getCursor();
442 selectionLine1 = document.getLineNumber();
101bc589 443 }
12b55d76
KL
444 }
445
e8a11f98
KL
446 /**
447 * Method that subclasses can override to handle window/screen resize
448 * events.
449 *
450 * @param resize resize event
451 */
452 @Override
453 public void onResize(final TResizeEvent resize) {
454 // Change my width/height, and pull the cursor in as needed.
455 if (resize.getType() == TResizeEvent.Type.WIDGET) {
456 setWidth(resize.getWidth());
457 setHeight(resize.getHeight());
458 // See if the cursor is now outside the window, and if so move
459 // things.
460 if (getCursorX() >= getWidth()) {
461 leftColumn += getCursorX() - (getWidth() - 1);
462 setCursorX(getWidth() - 1);
463 }
464 if (getCursorY() >= getHeight()) {
465 topLine += getCursorY() - (getHeight() - 1);
466 setCursorY(getHeight() - 1);
467 }
468 } else {
469 // Let superclass handle it
470 super.onResize(resize);
471 }
472 }
473
101bc589
KL
474 /**
475 * Handle posted command events.
476 *
477 * @param command command event
478 */
479 @Override
480 public void onCommand(final TCommandEvent command) {
481 if (command.equals(cmCut)) {
482 // Copy text to clipboard, and then remove it.
43ee96da 483 copySelection();
101bc589
KL
484 deleteSelection();
485 return;
486 }
487
488 if (command.equals(cmCopy)) {
489 // Copy text to clipboard.
43ee96da 490 copySelection();
101bc589
KL
491 return;
492 }
493
494 if (command.equals(cmPaste)) {
495 // Delete selected text, then paste text from clipboard.
496 deleteSelection();
497
498 String text = getClipboard().pasteText();
499 if (text != null) {
500 for (int i = 0; i < text.length(); ) {
501 int ch = text.codePointAt(i);
502 onKeypress(new TKeypressEvent(false, 0, ch, false, false,
503 false));
504 i += Character.charCount(ch);
505 }
506 }
507 return;
508 }
509
510 if (command.equals(cmClear)) {
511 // Remove text.
512 deleteSelection();
513 return;
514 }
515
516 }
517
518 // ------------------------------------------------------------------------
519 // TWidget ----------------------------------------------------------------
520 // ------------------------------------------------------------------------
521
522 /**
523 * Draw the text box.
524 */
525 @Override
526 public void draw() {
43ee96da
KL
527 CellAttributes selectedColor = getTheme().getColor("teditor.selected");
528
3c5921e6
KL
529 boolean drawSelection = true;
530
43ee96da
KL
531 int startCol = selectionColumn0;
532 int startRow = selectionLine0;
533 int endCol = selectionColumn1;
534 int endRow = selectionLine1;
535
536 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
537 && (selectionLine1 == selectionLine0))
538 || (selectionLine1 < selectionLine0)
43ee96da 539 ) {
8d3480c7
KL
540 // The user selected from bottom-to-top and/or right-to-left.
541 // Reverse the coordinates for the inverted section.
43ee96da
KL
542 startCol = selectionColumn1;
543 startRow = selectionLine1;
544 endCol = selectionColumn0;
545 endRow = selectionLine0;
546 }
3c5921e6
KL
547 if ((startCol == endCol) && (startRow == endRow)) {
548 drawSelection = false;
549 }
43ee96da 550
101bc589
KL
551 for (int i = 0; i < getHeight(); i++) {
552 // Background line
553 getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
554
555 // Now draw document's line
556 if (topLine + i < document.getLineCount()) {
557 Line line = document.getLine(topLine + i);
558 int x = 0;
559 for (Word word: line.getWords()) {
560 // For now, we are cheating: draw outside the left region
561 // if needed and let screen do the clipping.
562 getScreen().putStringXY(x - leftColumn, i, word.getText(),
563 word.getColor());
564 x += word.getDisplayLength();
565 if (x - leftColumn > getWidth()) {
566 break;
567 }
568 }
101bc589 569
43ee96da 570 // Highlight selected region
3c5921e6 571 if (inSelection && drawSelection) {
43ee96da
KL
572 if (startRow == endRow) {
573 if (topLine + i == startRow) {
574 for (x = startCol; x <= endCol; x++) {
575 putAttrXY(x - leftColumn, i, selectedColor);
576 }
577 }
578 } else {
579 if (topLine + i == startRow) {
580 for (x = startCol; x < line.getDisplayLength(); x++) {
581 putAttrXY(x - leftColumn, i, selectedColor);
582 }
583 } else if (topLine + i == endRow) {
584 for (x = 0; x <= endCol; x++) {
585 putAttrXY(x - leftColumn, i, selectedColor);
586 }
587 } else if ((topLine + i >= startRow)
588 && (topLine + i <= endRow)
589 ) {
590 for (x = 0; x < getWidth(); x++) {
591 putAttrXY(x, i, selectedColor);
592 }
593 }
594 }
595 }
596
597 }
101bc589
KL
598 }
599 }
600
615a0d99
KL
601 // ------------------------------------------------------------------------
602 // TEditorWidget ----------------------------------------------------------
603 // ------------------------------------------------------------------------
604
605 /**
606 * Align visible area with document current line.
607 *
608 * @param topLineIsTop if true, make the top visible line the document
609 * current line if it was off-screen. If false, make the bottom visible
610 * line the document current line.
611 */
612 private void alignTopLine(final boolean topLineIsTop) {
613 int line = document.getLineNumber();
614
615 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
616 // Need to move topLine to bring document back into view.
617 if (topLineIsTop) {
618 topLine = line - (getHeight() - 1);
619 if (topLine < 0) {
620 topLine = 0;
621 }
622 assert (topLine >= 0);
623 } else {
624 topLine = line;
625 assert (topLine >= 0);
626 }
627 }
628
629 /*
630 System.err.println("line " + line + " topLine " + topLine);
631 */
632
633 // Document is in view, let's set cursorY
634 assert (line >= topLine);
635 setCursorY(line - topLine);
636 alignCursor();
637 }
638
639 /**
640 * Align document current line with visible area.
641 *
642 * @param topLineIsTop if true, make the top visible line the document
643 * current line if it was off-screen. If false, make the bottom visible
644 * line the document current line.
645 */
646 private void alignDocument(final boolean topLineIsTop) {
647 int line = document.getLineNumber();
648 int cursor = document.getCursor();
649
650 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
651 // Need to move document to ensure it fits view.
652 if (topLineIsTop) {
653 document.setLineNumber(topLine);
654 } else {
655 document.setLineNumber(topLine + (getHeight() - 1));
656 }
657 if (cursor < document.getCurrentLine().getDisplayLength()) {
658 document.setCursor(cursor);
659 }
660 }
661
662 /*
663 System.err.println("getLineNumber() " + document.getLineNumber() +
664 " topLine " + topLine);
665 */
666
667 // Document is in view, let's set cursorY
668 setCursorY(document.getLineNumber() - topLine);
669 alignCursor();
670 }
671
672 /**
673 * Align visible cursor with document cursor.
674 */
675 private void alignCursor() {
676 int width = getWidth();
677
678 int desiredX = document.getCursor() - leftColumn;
679 if (desiredX < 0) {
680 // We need to push the screen to the left.
681 leftColumn = document.getCursor();
682 } else if (desiredX > width - 1) {
683 // We need to push the screen to the right.
684 leftColumn = document.getCursor() - (width - 1);
685 }
686
687 /*
688 System.err.println("document cursor " + document.getCursor() +
689 " leftColumn " + leftColumn);
690 */
691
692
693 setCursorX(document.getCursor() - leftColumn);
694 }
695
71a389c9
KL
696 /**
697 * Get the number of lines in the underlying Document.
698 *
699 * @return the number of lines
700 */
701 public int getLineCount() {
702 return document.getLineCount();
703 }
704
b6faeac0
KL
705 /**
706 * Get the current visible top row number. 1-based.
707 *
708 * @return the visible top row number. Row 1 is the first row.
709 */
710 public int getVisibleRowNumber() {
711 return topLine + 1;
712 }
713
714 /**
715 * Set the current visible row number. 1-based.
716 *
717 * @param row the new visible row number. Row 1 is the first row.
718 */
719 public void setVisibleRowNumber(final int row) {
720 assert (row > 0);
721 if ((row > 0) && (row < document.getLineCount())) {
722 topLine = row - 1;
723 alignDocument(true);
724 }
725 }
726
71a389c9
KL
727 /**
728 * Get the current editing row number. 1-based.
729 *
730 * @return the editing row number. Row 1 is the first row.
731 */
732 public int getEditingRowNumber() {
733 return document.getLineNumber() + 1;
734 }
735
736 /**
737 * Set the current editing row number. 1-based.
738 *
739 * @param row the new editing row number. Row 1 is the first row.
740 */
741 public void setEditingRowNumber(final int row) {
fe0770f9
KL
742 assert (row > 0);
743 if ((row > 0) && (row < document.getLineCount())) {
744 document.setLineNumber(row - 1);
745 alignTopLine(true);
746 }
71a389c9
KL
747 }
748
1bc08f18
KL
749 /**
750 * Set the current visible column number. 1-based.
751 *
752 * @return the visible column number. Column 1 is the first column.
753 */
754 public int getVisibleColumnNumber() {
755 return leftColumn + 1;
756 }
757
758 /**
759 * Set the current visible column number. 1-based.
760 *
761 * @param column the new visible column number. Column 1 is the first
762 * column.
763 */
764 public void setVisibleColumnNumber(final int column) {
765 assert (column > 0);
766 if ((column > 0) && (column < document.getLineLengthMax())) {
767 leftColumn = column - 1;
768 alignDocument(true);
769 }
770 }
771
71a389c9
KL
772 /**
773 * Get the current editing column number. 1-based.
774 *
775 * @return the editing column number. Column 1 is the first column.
776 */
777 public int getEditingColumnNumber() {
778 return document.getCursor() + 1;
779 }
780
781 /**
782 * Set the current editing column number. 1-based.
783 *
784 * @param column the new editing column number. Column 1 is the first
785 * column.
786 */
787 public void setEditingColumnNumber(final int column) {
fe0770f9
KL
788 if ((column > 0) && (column < document.getLineLength())) {
789 document.setCursor(column - 1);
790 alignCursor();
791 }
71a389c9
KL
792 }
793
794 /**
795 * Get the maximum possible row number. 1-based.
796 *
797 * @return the maximum row number. Row 1 is the first row.
798 */
799 public int getMaximumRowNumber() {
800 return document.getLineCount() + 1;
801 }
802
803 /**
804 * Get the maximum possible column number. 1-based.
805 *
806 * @return the maximum column number. Column 1 is the first column.
807 */
808 public int getMaximumColumnNumber() {
809 return document.getLineLengthMax() + 1;
810 }
811
ed76cd41
KL
812 /**
813 * Get the current editing row plain text. 1-based.
814 *
815 * @param row the editing row number. Row 1 is the first row.
816 * @return the plain text of the row
817 */
818 public String getEditingRawLine(final int row) {
819 Line line = document.getLine(row - 1);
820 return line.getRawString();
821 }
822
71a389c9
KL
823 /**
824 * Get the dirty value.
825 *
826 * @return true if the buffer is dirty
827 */
828 public boolean isDirty() {
829 return document.isDirty();
830 }
831
ed76cd41
KL
832 /**
833 * Unset the dirty flag.
834 */
835 public void setNotDirty() {
836 document.setNotDirty();
837 }
838
71a389c9
KL
839 /**
840 * Save contents to file.
841 *
842 * @param filename file to save to
843 * @throws IOException if a java.io operation throws
844 */
845 public void saveToFilename(final String filename) throws IOException {
846 document.saveToFilename(filename);
847 }
848
101bc589
KL
849 /**
850 * Delete text within the selection bounds.
851 */
852 private void deleteSelection() {
3c5921e6 853 if (!inSelection) {
101bc589
KL
854 return;
855 }
43ee96da
KL
856 inSelection = false;
857
858 int startCol = selectionColumn0;
859 int startRow = selectionLine0;
860 int endCol = selectionColumn1;
861 int endRow = selectionLine1;
862
8d3480c7
KL
863 /*
864 System.err.println("INITIAL: " + startRow + " " + startCol + " " +
865 endRow + " " + endCol + " " +
866 document.getLineNumber() + " " + document.getCursor());
867 */
868
43ee96da 869 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
870 && (selectionLine1 == selectionLine0))
871 || (selectionLine1 < selectionLine0)
43ee96da 872 ) {
8d3480c7
KL
873 // The user selected from bottom-to-top and/or right-to-left.
874 // Reverse the coordinates for the inverted section.
43ee96da
KL
875 startCol = selectionColumn1;
876 startRow = selectionLine1;
877 endCol = selectionColumn0;
878 endRow = selectionLine0;
8d3480c7
KL
879
880 if (endRow >= document.getLineCount()) {
881 // The selection started beyond EOF, trim it to EOF.
882 endRow = document.getLineCount() - 1;
883 endCol = document.getLine(endRow).getDisplayLength();
884 } else if (endRow == document.getLineCount() - 1) {
885 // The selection started beyond EOF, trim it to EOF.
886 if (endCol >= document.getLine(endRow).getDisplayLength()) {
887 endCol = document.getLine(endRow).getDisplayLength() - 1;
888 }
889 }
890 }
891 /*
892 System.err.println("FLIP: " + startRow + " " + startCol + " " +
893 endRow + " " + endCol + " " +
894 document.getLineNumber() + " " + document.getCursor());
895 System.err.println(" --END: " + endRow + " " + document.getLineCount() +
896 " " + document.getLine(endRow).getDisplayLength());
897 */
898
899 assert (endRow < document.getLineCount());
900 if (endCol >= document.getLine(endRow).getDisplayLength()) {
901 endCol = document.getLine(endRow).getDisplayLength() - 1;
902 }
903 if (endCol < 0) {
904 endCol = 0;
905 }
906 if (startCol >= document.getLine(startRow).getDisplayLength()) {
907 startCol = document.getLine(startRow).getDisplayLength() - 1;
908 }
909 if (startCol < 0) {
910 startCol = 0;
43ee96da
KL
911 }
912
913 // Place the cursor on the selection end, and "press backspace" until
914 // the cursor matches the selection start.
8d3480c7
KL
915 /*
916 System.err.println("BEFORE: " + startRow + " " + startCol + " " +
917 endRow + " " + endCol + " " +
918 document.getLineNumber() + " " + document.getCursor());
919 */
43ee96da
KL
920 document.setLineNumber(endRow);
921 document.setCursor(endCol + 1);
922 while (!((document.getLineNumber() == startRow)
923 && (document.getCursor() == startCol))
924 ) {
8d3480c7
KL
925 /*
926 System.err.println("DURING: " + startRow + " " + startCol + " " +
927 endRow + " " + endCol + " " +
928 document.getLineNumber() + " " + document.getCursor());
929 */
930
43ee96da
KL
931 document.backspace();
932 }
933 alignTopLine(true);
934 }
935
936 /**
937 * Copy text within the selection bounds to clipboard.
938 */
939 private void copySelection() {
3c5921e6 940 if (!inSelection) {
43ee96da
KL
941 return;
942 }
3c5921e6
KL
943 getClipboard().copyText(getSelection());
944 }
945
946 /**
947 * Set the selection.
948 *
949 * @param startRow the starting row number. 0-based: row 0 is the first
950 * row.
951 * @param startColumn the starting column number. 0-based: column 0 is
952 * the first column.
953 * @param endRow the ending row number. 0-based: row 0 is the first row.
954 * @param endColumn the ending column number. 0-based: column 0 is the
955 * first column.
956 */
957 public void setSelection(final int startRow, final int startColumn,
958 final int endRow, final int endColumn) {
959
960 inSelection = true;
961 selectionLine0 = startRow;
962 selectionColumn0 = startColumn;
963 selectionLine1 = endRow;
964 selectionColumn1 = endColumn;
965 }
966
967 /**
968 * Copy text within the selection bounds to a string.
969 *
970 * @return the selection as a string, or null if there is no selection
971 */
972 public String getSelection() {
973 if (!inSelection) {
974 return null;
975 }
43ee96da
KL
976
977 int startCol = selectionColumn0;
978 int startRow = selectionLine0;
979 int endCol = selectionColumn1;
980 int endRow = selectionLine1;
981
982 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
983 && (selectionLine1 == selectionLine0))
984 || (selectionLine1 < selectionLine0)
43ee96da 985 ) {
8d3480c7
KL
986 // The user selected from bottom-to-top and/or right-to-left.
987 // Reverse the coordinates for the inverted section.
43ee96da
KL
988 startCol = selectionColumn1;
989 startRow = selectionLine1;
990 endCol = selectionColumn0;
991 endRow = selectionLine0;
992 }
993
994 StringBuilder sb = new StringBuilder();
995
996 if (endRow > startRow) {
997 // First line
998 String line = document.getLine(startRow).getRawString();
999 int x = 0;
1000 for (int i = 0; i < line.length(); ) {
1001 int ch = line.codePointAt(i);
1002
1003 if (x >= startCol) {
1004 sb.append(Character.toChars(ch));
1005 }
1006 x += StringUtils.width(ch);
1007 i += Character.charCount(ch);
1008 }
1009 sb.append("\n");
1010
1011 // Middle lines
1012 for (int y = startRow + 1; y < endRow; y++) {
1013 sb.append(document.getLine(y).getRawString());
1014 sb.append("\n");
1015 }
1016
1017 // Final line
1018 line = document.getLine(endRow).getRawString();
1019 x = 0;
1020 for (int i = 0; i < line.length(); ) {
1021 int ch = line.codePointAt(i);
1022
1023 if (x > endCol) {
1024 break;
1025 }
1026
1027 sb.append(Character.toChars(ch));
1028 x += StringUtils.width(ch);
1029 i += Character.charCount(ch);
1030 }
1031 } else {
1032 assert (startRow == endRow);
1033
1034 // Only one line
1035 String line = document.getLine(startRow).getRawString();
1036 int x = 0;
1037 for (int i = 0; i < line.length(); ) {
1038 int ch = line.codePointAt(i);
1039
1040 if ((x >= startCol) && (x <= endCol)) {
1041 sb.append(Character.toChars(ch));
1042 }
1043
1044 x += StringUtils.width(ch);
1045 i += Character.charCount(ch);
1046 }
1047 }
3c5921e6
KL
1048 return sb.toString();
1049 }
1050
1051 /**
1052 * Get the selection starting row number.
1053 *
1054 * @return the starting row number, or -1 if there is no selection.
1055 * 0-based: row 0 is the first row.
1056 */
1057 public int getSelectionStartRow() {
1058 if (!inSelection) {
1059 return -1;
1060 }
1061
1062 int startCol = selectionColumn0;
1063 int startRow = selectionLine0;
1064 int endCol = selectionColumn1;
1065 int endRow = selectionLine1;
101bc589 1066
3c5921e6
KL
1067 if (((selectionColumn1 < selectionColumn0)
1068 && (selectionLine1 == selectionLine0))
1069 || (selectionLine1 < selectionLine0)
1070 ) {
1071 // The user selected from bottom-to-top and/or right-to-left.
1072 // Reverse the coordinates for the inverted section.
1073 startCol = selectionColumn1;
1074 startRow = selectionLine1;
1075 endCol = selectionColumn0;
1076 endRow = selectionLine0;
1077 }
1078 return startRow;
101bc589
KL
1079 }
1080
ed76cd41 1081 /**
3c5921e6 1082 * Get the selection starting column number.
ed76cd41 1083 *
3c5921e6
KL
1084 * @return the starting column number, or -1 if there is no selection.
1085 * 0-based: column 0 is the first column.
ed76cd41 1086 */
3c5921e6
KL
1087 public int getSelectionStartColumn() {
1088 if (!inSelection) {
1089 return -1;
1090 }
ed76cd41 1091
3c5921e6
KL
1092 int startCol = selectionColumn0;
1093 int startRow = selectionLine0;
1094 int endCol = selectionColumn1;
1095 int endRow = selectionLine1;
1096
1097 if (((selectionColumn1 < selectionColumn0)
1098 && (selectionLine1 == selectionLine0))
1099 || (selectionLine1 < selectionLine0)
1100 ) {
1101 // The user selected from bottom-to-top and/or right-to-left.
1102 // Reverse the coordinates for the inverted section.
1103 startCol = selectionColumn1;
1104 startRow = selectionLine1;
1105 endCol = selectionColumn0;
1106 endRow = selectionLine0;
1107 }
1108 return startCol;
1109 }
1110
1111 /**
1112 * Get the selection ending row number.
1113 *
1114 * @return the ending row number, or -1 if there is no selection.
1115 * 0-based: row 0 is the first row.
1116 */
1117 public int getSelectionEndRow() {
1118 if (!inSelection) {
1119 return -1;
1120 }
1121
1122 int startCol = selectionColumn0;
1123 int startRow = selectionLine0;
1124 int endCol = selectionColumn1;
1125 int endRow = selectionLine1;
1126
1127 if (((selectionColumn1 < selectionColumn0)
1128 && (selectionLine1 == selectionLine0))
1129 || (selectionLine1 < selectionLine0)
1130 ) {
1131 // The user selected from bottom-to-top and/or right-to-left.
1132 // Reverse the coordinates for the inverted section.
1133 startCol = selectionColumn1;
1134 startRow = selectionLine1;
1135 endCol = selectionColumn0;
1136 endRow = selectionLine0;
1137 }
1138 return endRow;
1139 }
1140
1141 /**
1142 * Get the selection ending column number.
1143 *
1144 * @return the ending column number, or -1 if there is no selection.
1145 * 0-based: column 0 is the first column.
1146 */
1147 public int getSelectionEndColumn() {
1148 if (!inSelection) {
1149 return -1;
1150 }
1151
1152 int startCol = selectionColumn0;
1153 int startRow = selectionLine0;
1154 int endCol = selectionColumn1;
1155 int endRow = selectionLine1;
1156
1157 if (((selectionColumn1 < selectionColumn0)
1158 && (selectionLine1 == selectionLine0))
1159 || (selectionLine1 < selectionLine0)
1160 ) {
1161 // The user selected from bottom-to-top and/or right-to-left.
1162 // Reverse the coordinates for the inverted section.
1163 startCol = selectionColumn1;
1164 startRow = selectionLine1;
1165 endCol = selectionColumn0;
1166 endRow = selectionLine0;
1167 }
1168 return endCol;
ed76cd41
KL
1169 }
1170
1171 /**
1172 * Unset the selection.
1173 */
1174 public void unsetSelection() {
1175 inSelection = false;
1176 }
1177
1178 /**
1179 * Replace whatever is being selected with new text. If not in
1180 * selection, nothing is replaced.
1181 *
1182 * @param text the new replacement text
1183 */
1184 public void replaceSelection(final String text) {
1185 if (!inSelection) {
1186 return;
1187 }
1188
1189 // Delete selected text, then paste text from clipboard.
1190 deleteSelection();
1191
1192 for (int i = 0; i < text.length(); ) {
1193 int ch = text.codePointAt(i);
1194 onKeypress(new TKeypressEvent(false, 0, ch, false, false,
1195 false));
1196 i += Character.charCount(ch);
1197 }
1198 }
1199
3c5921e6
KL
1200 /**
1201 * Check if selection is available.
1202 *
1203 * @return true if a selection has been made
1204 */
1205 public boolean hasSelection() {
1206 return inSelection;
1207 }
1208
ed76cd41
KL
1209 /**
1210 * Get the entire contents of the editor as one string.
1211 *
1212 * @return the editor contents
1213 */
1214 public String getText() {
1215 return document.getText();
1216 }
1217
1218 /**
1219 * Set the entire contents of the editor from one string.
1220 *
1221 * @param text the new contents
1222 */
1223 public void setText(final String text) {
1224 document = new Document(text, defaultColor);
1225 unsetSelection();
1226 topLine = 0;
1227 leftColumn = 0;
1228 }
1229
101bc589
KL
1230 // ------------------------------------------------------------------------
1231 // EditMenuUser -----------------------------------------------------------
1232 // ------------------------------------------------------------------------
1233
1234 /**
1235 * Check if the cut menu item should be enabled.
1236 *
1237 * @return true if the cut menu item should be enabled
1238 */
1239 public boolean isEditMenuCut() {
1240 return true;
1241 }
1242
1243 /**
1244 * Check if the copy menu item should be enabled.
1245 *
1246 * @return true if the copy menu item should be enabled
1247 */
1248 public boolean isEditMenuCopy() {
1249 return true;
1250 }
1251
1252 /**
1253 * Check if the paste menu item should be enabled.
1254 *
1255 * @return true if the paste menu item should be enabled
1256 */
1257 public boolean isEditMenuPaste() {
1258 return true;
1259 }
1260
1261 /**
1262 * Check if the clear menu item should be enabled.
1263 *
1264 * @return true if the clear menu item should be enabled
1265 */
1266 public boolean isEditMenuClear() {
1267 return true;
1268 }
1269
12b55d76 1270}