retrofit
[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.
20c416a0
KL
167 inSelection = true;
168 selectionColumn0 = leftColumn + mouse.getX();
169 selectionLine0 = topLine + mouse.getY();
170 selectionColumn1 = selectionColumn0;
171 selectionLine1 = selectionLine0;
101bc589
KL
172
173 // Set the row and column
174 int newLine = topLine + mouse.getY();
175 int newX = leftColumn + mouse.getX();
176 if (newLine > document.getLineCount() - 1) {
177 // Go to the end
178 document.setLineNumber(document.getLineCount() - 1);
179 document.end();
180 if (newLine > document.getLineCount() - 1) {
181 setCursorY(document.getLineCount() - 1 - topLine);
182 } else {
183 setCursorY(mouse.getY());
184 }
185 alignCursor();
186 if (inSelection) {
187 selectionColumn1 = document.getCursor();
188 selectionLine1 = document.getLineNumber();
101bc589
KL
189 }
190 return;
191 }
192
193 document.setLineNumber(newLine);
194 setCursorY(mouse.getY());
195 if (newX >= document.getCurrentLine().getDisplayLength()) {
196 document.end();
197 alignCursor();
198 } else {
199 document.setCursor(newX);
200 setCursorX(mouse.getX());
201 }
202 if (inSelection) {
203 selectionColumn1 = document.getCursor();
204 selectionLine1 = document.getLineNumber();
101bc589
KL
205 }
206 return;
207 } else {
208 inSelection = false;
209 }
210
211 // Pass to children
212 super.onMouseDown(mouse);
213 }
214
215 /**
216 * Handle mouse motion events.
217 *
218 * @param mouse mouse motion event
219 */
220 @Override
221 public void onMouseMotion(final TMouseEvent mouse) {
222
223 if (mouse.isMouse1()) {
8d3480c7
KL
224 // Set the row and column
225 int newLine = topLine + mouse.getY();
226 int newX = leftColumn + mouse.getX();
227 if ((newLine < 0) || (newX < 0)) {
228 return;
229 }
230
101bc589
KL
231 // Selection.
232 if (inSelection) {
8d3480c7
KL
233 selectionColumn1 = newX;
234 selectionLine1 = newLine;
20c416a0 235 } else {
101bc589 236 inSelection = true;
8d3480c7
KL
237 selectionColumn0 = newX;
238 selectionLine0 = newLine;
101bc589
KL
239 selectionColumn1 = selectionColumn0;
240 selectionLine1 = selectionLine0;
101bc589
KL
241 }
242
df602ccf 243 if (newLine > document.getLineCount() - 1) {
e8a11f98
KL
244 // Go to the end
245 document.setLineNumber(document.getLineCount() - 1);
246 document.end();
df602ccf
KL
247 if (newLine > document.getLineCount() - 1) {
248 setCursorY(document.getLineCount() - 1 - topLine);
e8a11f98 249 } else {
df602ccf 250 setCursorY(mouse.getY());
e8a11f98
KL
251 }
252 alignCursor();
101bc589
KL
253 if (inSelection) {
254 selectionColumn1 = document.getCursor();
255 selectionLine1 = document.getLineNumber();
101bc589 256 }
e8a11f98
KL
257 return;
258 }
e8a11f98
KL
259 document.setLineNumber(newLine);
260 setCursorY(mouse.getY());
4297b49b 261 if (newX >= document.getCurrentLine().getDisplayLength()) {
e8a11f98
KL
262 document.end();
263 alignCursor();
264 } else {
df602ccf 265 document.setCursor(newX);
e8a11f98
KL
266 setCursorX(mouse.getX());
267 }
101bc589
KL
268 if (inSelection) {
269 selectionColumn1 = document.getCursor();
270 selectionLine1 = document.getLineNumber();
101bc589 271 }
e8a11f98
KL
272 return;
273 }
12b55d76
KL
274
275 // Pass to children
276 super.onMouseDown(mouse);
277 }
278
279 /**
280 * Handle keystrokes.
281 *
282 * @param keypress keystroke event
283 */
284 @Override
285 public void onKeypress(final TKeypressEvent keypress) {
43ee96da 286 if (keypress.getKey().isShift()) {
101bc589
KL
287 // Selection.
288 if (!inSelection) {
289 inSelection = true;
290 selectionColumn0 = document.getCursor();
291 selectionLine0 = document.getLineNumber();
292 selectionColumn1 = selectionColumn0;
293 selectionLine1 = selectionLine0;
101bc589
KL
294 }
295 } else {
43ee96da
KL
296 if (keypress.equals(kbLeft)
297 || keypress.equals(kbRight)
298 || keypress.equals(kbUp)
299 || keypress.equals(kbDown)
300 || keypress.equals(kbPgDn)
301 || keypress.equals(kbPgUp)
302 || keypress.equals(kbHome)
303 || keypress.equals(kbEnd)
304 ) {
305 // Non-shifted navigation keys disable selection.
306 inSelection = false;
307 }
101bc589
KL
308 }
309
310 if (keypress.equals(kbLeft)
311 || keypress.equals(kbShiftLeft)
312 ) {
df602ccf
KL
313 document.left();
314 alignTopLine(false);
101bc589
KL
315 } else if (keypress.equals(kbRight)
316 || keypress.equals(kbShiftRight)
317 ) {
df602ccf
KL
318 document.right();
319 alignTopLine(true);
e23989a4
KL
320 } else if (keypress.equals(kbAltLeft)
321 || keypress.equals(kbCtrlLeft)
43ee96da
KL
322 || keypress.equals(kbAltShiftLeft)
323 || keypress.equals(kbCtrlShiftLeft)
e23989a4
KL
324 ) {
325 document.backwardsWord();
326 alignTopLine(false);
327 } else if (keypress.equals(kbAltRight)
328 || keypress.equals(kbCtrlRight)
43ee96da
KL
329 || keypress.equals(kbAltShiftRight)
330 || keypress.equals(kbCtrlShiftRight)
e23989a4
KL
331 ) {
332 document.forwardsWord();
333 alignTopLine(true);
101bc589
KL
334 } else if (keypress.equals(kbUp)
335 || keypress.equals(kbShiftUp)
336 ) {
71a389c9
KL
337 document.up();
338 alignTopLine(false);
101bc589
KL
339 } else if (keypress.equals(kbDown)
340 || keypress.equals(kbShiftDown)
341 ) {
71a389c9
KL
342 document.down();
343 alignTopLine(true);
43ee96da
KL
344 } else if (keypress.equals(kbPgUp)
345 || keypress.equals(kbShiftPgUp)
346 ) {
71a389c9
KL
347 document.up(getHeight() - 1);
348 alignTopLine(false);
43ee96da
KL
349 } else if (keypress.equals(kbPgDn)
350 || keypress.equals(kbShiftPgDn)
351 ) {
71a389c9
KL
352 document.down(getHeight() - 1);
353 alignTopLine(true);
43ee96da
KL
354 } else if (keypress.equals(kbHome)
355 || keypress.equals(kbShiftHome)
356 ) {
e8a11f98
KL
357 if (document.home()) {
358 leftColumn = 0;
359 if (leftColumn < 0) {
360 leftColumn = 0;
361 }
362 setCursorX(0);
363 }
43ee96da
KL
364 } else if (keypress.equals(kbEnd)
365 || keypress.equals(kbShiftEnd)
366 ) {
e8a11f98
KL
367 if (document.end()) {
368 alignCursor();
369 }
43ee96da
KL
370 } else if (keypress.equals(kbCtrlHome)
371 || keypress.equals(kbCtrlShiftHome)
372 ) {
12b55d76
KL
373 document.setLineNumber(0);
374 document.home();
e8a11f98
KL
375 topLine = 0;
376 leftColumn = 0;
377 setCursorX(0);
378 setCursorY(0);
43ee96da
KL
379 } else if (keypress.equals(kbCtrlEnd)
380 || keypress.equals(kbCtrlShiftEnd)
381 ) {
12b55d76
KL
382 document.setLineNumber(document.getLineCount() - 1);
383 document.end();
71a389c9 384 alignTopLine(false);
12b55d76
KL
385 } else if (keypress.equals(kbIns)) {
386 document.setOverwrite(!document.getOverwrite());
387 } else if (keypress.equals(kbDel)) {
43ee96da
KL
388 if (inSelection) {
389 deleteSelection();
390 } else {
391 document.del();
392 }
71a389c9 393 alignCursor();
39e86397
KL
394 } else if (keypress.equals(kbBackspace)
395 || keypress.equals(kbBackspaceDel)
396 ) {
43ee96da
KL
397 if (inSelection) {
398 deleteSelection();
399 } else {
400 document.backspace();
401 }
df602ccf
KL
402 alignTopLine(false);
403 } else if (keypress.equals(kbTab)) {
43ee96da 404 deleteSelection();
101bc589 405 // Add spaces until we hit modulo 8.
df602ccf
KL
406 for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
407 document.addChar(' ');
408 }
e8a11f98 409 alignCursor();
71a389c9 410 } else if (keypress.equals(kbEnter)) {
43ee96da 411 deleteSelection();
df602ccf
KL
412 document.enter();
413 alignTopLine(true);
12b55d76
KL
414 } else if (!keypress.getKey().isFnKey()
415 && !keypress.getKey().isAlt()
416 && !keypress.getKey().isCtrl()
417 ) {
418 // Plain old keystroke, process it
43ee96da 419 deleteSelection();
2d3f60d8 420 document.addChar(keypress.getKey().getChar());
71a389c9 421 alignCursor();
12b55d76
KL
422 } else {
423 // Pass other keys (tab etc.) on to TWidget
424 super.onKeypress(keypress);
425 }
101bc589
KL
426
427 if (inSelection) {
428 selectionColumn1 = document.getCursor();
429 selectionLine1 = document.getLineNumber();
101bc589 430 }
12b55d76
KL
431 }
432
e8a11f98
KL
433 /**
434 * Method that subclasses can override to handle window/screen resize
435 * events.
436 *
437 * @param resize resize event
438 */
439 @Override
440 public void onResize(final TResizeEvent resize) {
441 // Change my width/height, and pull the cursor in as needed.
442 if (resize.getType() == TResizeEvent.Type.WIDGET) {
443 setWidth(resize.getWidth());
444 setHeight(resize.getHeight());
445 // See if the cursor is now outside the window, and if so move
446 // things.
447 if (getCursorX() >= getWidth()) {
448 leftColumn += getCursorX() - (getWidth() - 1);
449 setCursorX(getWidth() - 1);
450 }
451 if (getCursorY() >= getHeight()) {
452 topLine += getCursorY() - (getHeight() - 1);
453 setCursorY(getHeight() - 1);
454 }
455 } else {
456 // Let superclass handle it
457 super.onResize(resize);
458 }
459 }
460
101bc589
KL
461 /**
462 * Handle posted command events.
463 *
464 * @param command command event
465 */
466 @Override
467 public void onCommand(final TCommandEvent command) {
468 if (command.equals(cmCut)) {
469 // Copy text to clipboard, and then remove it.
43ee96da 470 copySelection();
101bc589
KL
471 deleteSelection();
472 return;
473 }
474
475 if (command.equals(cmCopy)) {
476 // Copy text to clipboard.
43ee96da 477 copySelection();
101bc589
KL
478 return;
479 }
480
481 if (command.equals(cmPaste)) {
482 // Delete selected text, then paste text from clipboard.
483 deleteSelection();
484
485 String text = getClipboard().pasteText();
486 if (text != null) {
487 for (int i = 0; i < text.length(); ) {
488 int ch = text.codePointAt(i);
489 onKeypress(new TKeypressEvent(false, 0, ch, false, false,
490 false));
491 i += Character.charCount(ch);
492 }
493 }
494 return;
495 }
496
497 if (command.equals(cmClear)) {
498 // Remove text.
499 deleteSelection();
500 return;
501 }
502
503 }
504
505 // ------------------------------------------------------------------------
506 // TWidget ----------------------------------------------------------------
507 // ------------------------------------------------------------------------
508
509 /**
510 * Draw the text box.
511 */
512 @Override
513 public void draw() {
43ee96da
KL
514 CellAttributes selectedColor = getTheme().getColor("teditor.selected");
515
516 int startCol = selectionColumn0;
517 int startRow = selectionLine0;
518 int endCol = selectionColumn1;
519 int endRow = selectionLine1;
520
521 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
522 && (selectionLine1 == selectionLine0))
523 || (selectionLine1 < selectionLine0)
43ee96da 524 ) {
8d3480c7
KL
525 // The user selected from bottom-to-top and/or right-to-left.
526 // Reverse the coordinates for the inverted section.
43ee96da
KL
527 startCol = selectionColumn1;
528 startRow = selectionLine1;
529 endCol = selectionColumn0;
530 endRow = selectionLine0;
531 }
532
101bc589
KL
533 for (int i = 0; i < getHeight(); i++) {
534 // Background line
535 getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
536
537 // Now draw document's line
538 if (topLine + i < document.getLineCount()) {
539 Line line = document.getLine(topLine + i);
540 int x = 0;
541 for (Word word: line.getWords()) {
542 // For now, we are cheating: draw outside the left region
543 // if needed and let screen do the clipping.
544 getScreen().putStringXY(x - leftColumn, i, word.getText(),
545 word.getColor());
546 x += word.getDisplayLength();
547 if (x - leftColumn > getWidth()) {
548 break;
549 }
550 }
101bc589 551
43ee96da
KL
552 // Highlight selected region
553 if (inSelection) {
554 if (startRow == endRow) {
555 if (topLine + i == startRow) {
556 for (x = startCol; x <= endCol; x++) {
557 putAttrXY(x - leftColumn, i, selectedColor);
558 }
559 }
560 } else {
561 if (topLine + i == startRow) {
562 for (x = startCol; x < line.getDisplayLength(); x++) {
563 putAttrXY(x - leftColumn, i, selectedColor);
564 }
565 } else if (topLine + i == endRow) {
566 for (x = 0; x <= endCol; x++) {
567 putAttrXY(x - leftColumn, i, selectedColor);
568 }
569 } else if ((topLine + i >= startRow)
570 && (topLine + i <= endRow)
571 ) {
572 for (x = 0; x < getWidth(); x++) {
573 putAttrXY(x, i, selectedColor);
574 }
575 }
576 }
577 }
578
579 }
101bc589
KL
580 }
581 }
582
615a0d99
KL
583 // ------------------------------------------------------------------------
584 // TEditorWidget ----------------------------------------------------------
585 // ------------------------------------------------------------------------
586
587 /**
588 * Align visible area with document current line.
589 *
590 * @param topLineIsTop if true, make the top visible line the document
591 * current line if it was off-screen. If false, make the bottom visible
592 * line the document current line.
593 */
594 private void alignTopLine(final boolean topLineIsTop) {
595 int line = document.getLineNumber();
596
597 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
598 // Need to move topLine to bring document back into view.
599 if (topLineIsTop) {
600 topLine = line - (getHeight() - 1);
601 if (topLine < 0) {
602 topLine = 0;
603 }
604 assert (topLine >= 0);
605 } else {
606 topLine = line;
607 assert (topLine >= 0);
608 }
609 }
610
611 /*
612 System.err.println("line " + line + " topLine " + topLine);
613 */
614
615 // Document is in view, let's set cursorY
616 assert (line >= topLine);
617 setCursorY(line - topLine);
618 alignCursor();
619 }
620
621 /**
622 * Align document current line with visible area.
623 *
624 * @param topLineIsTop if true, make the top visible line the document
625 * current line if it was off-screen. If false, make the bottom visible
626 * line the document current line.
627 */
628 private void alignDocument(final boolean topLineIsTop) {
629 int line = document.getLineNumber();
630 int cursor = document.getCursor();
631
632 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
633 // Need to move document to ensure it fits view.
634 if (topLineIsTop) {
635 document.setLineNumber(topLine);
636 } else {
637 document.setLineNumber(topLine + (getHeight() - 1));
638 }
639 if (cursor < document.getCurrentLine().getDisplayLength()) {
640 document.setCursor(cursor);
641 }
642 }
643
644 /*
645 System.err.println("getLineNumber() " + document.getLineNumber() +
646 " topLine " + topLine);
647 */
648
649 // Document is in view, let's set cursorY
650 setCursorY(document.getLineNumber() - topLine);
651 alignCursor();
652 }
653
654 /**
655 * Align visible cursor with document cursor.
656 */
657 private void alignCursor() {
658 int width = getWidth();
659
660 int desiredX = document.getCursor() - leftColumn;
661 if (desiredX < 0) {
662 // We need to push the screen to the left.
663 leftColumn = document.getCursor();
664 } else if (desiredX > width - 1) {
665 // We need to push the screen to the right.
666 leftColumn = document.getCursor() - (width - 1);
667 }
668
669 /*
670 System.err.println("document cursor " + document.getCursor() +
671 " leftColumn " + leftColumn);
672 */
673
674
675 setCursorX(document.getCursor() - leftColumn);
676 }
677
71a389c9
KL
678 /**
679 * Get the number of lines in the underlying Document.
680 *
681 * @return the number of lines
682 */
683 public int getLineCount() {
684 return document.getLineCount();
685 }
686
b6faeac0
KL
687 /**
688 * Get the current visible top row number. 1-based.
689 *
690 * @return the visible top row number. Row 1 is the first row.
691 */
692 public int getVisibleRowNumber() {
693 return topLine + 1;
694 }
695
696 /**
697 * Set the current visible row number. 1-based.
698 *
699 * @param row the new visible row number. Row 1 is the first row.
700 */
701 public void setVisibleRowNumber(final int row) {
702 assert (row > 0);
703 if ((row > 0) && (row < document.getLineCount())) {
704 topLine = row - 1;
705 alignDocument(true);
706 }
707 }
708
71a389c9
KL
709 /**
710 * Get the current editing row number. 1-based.
711 *
712 * @return the editing row number. Row 1 is the first row.
713 */
714 public int getEditingRowNumber() {
715 return document.getLineNumber() + 1;
716 }
717
718 /**
719 * Set the current editing row number. 1-based.
720 *
721 * @param row the new editing row number. Row 1 is the first row.
722 */
723 public void setEditingRowNumber(final int row) {
fe0770f9
KL
724 assert (row > 0);
725 if ((row > 0) && (row < document.getLineCount())) {
726 document.setLineNumber(row - 1);
727 alignTopLine(true);
728 }
71a389c9
KL
729 }
730
1bc08f18
KL
731 /**
732 * Set the current visible column number. 1-based.
733 *
734 * @return the visible column number. Column 1 is the first column.
735 */
736 public int getVisibleColumnNumber() {
737 return leftColumn + 1;
738 }
739
740 /**
741 * Set the current visible column number. 1-based.
742 *
743 * @param column the new visible column number. Column 1 is the first
744 * column.
745 */
746 public void setVisibleColumnNumber(final int column) {
747 assert (column > 0);
748 if ((column > 0) && (column < document.getLineLengthMax())) {
749 leftColumn = column - 1;
750 alignDocument(true);
751 }
752 }
753
71a389c9
KL
754 /**
755 * Get the current editing column number. 1-based.
756 *
757 * @return the editing column number. Column 1 is the first column.
758 */
759 public int getEditingColumnNumber() {
760 return document.getCursor() + 1;
761 }
762
763 /**
764 * Set the current editing column number. 1-based.
765 *
766 * @param column the new editing column number. Column 1 is the first
767 * column.
768 */
769 public void setEditingColumnNumber(final int column) {
fe0770f9
KL
770 if ((column > 0) && (column < document.getLineLength())) {
771 document.setCursor(column - 1);
772 alignCursor();
773 }
71a389c9
KL
774 }
775
776 /**
777 * Get the maximum possible row number. 1-based.
778 *
779 * @return the maximum row number. Row 1 is the first row.
780 */
781 public int getMaximumRowNumber() {
782 return document.getLineCount() + 1;
783 }
784
785 /**
786 * Get the maximum possible column number. 1-based.
787 *
788 * @return the maximum column number. Column 1 is the first column.
789 */
790 public int getMaximumColumnNumber() {
791 return document.getLineLengthMax() + 1;
792 }
793
ed76cd41
KL
794 /**
795 * Get the current editing row plain text. 1-based.
796 *
797 * @param row the editing row number. Row 1 is the first row.
798 * @return the plain text of the row
799 */
800 public String getEditingRawLine(final int row) {
801 Line line = document.getLine(row - 1);
802 return line.getRawString();
803 }
804
71a389c9
KL
805 /**
806 * Get the dirty value.
807 *
808 * @return true if the buffer is dirty
809 */
810 public boolean isDirty() {
811 return document.isDirty();
812 }
813
ed76cd41
KL
814 /**
815 * Unset the dirty flag.
816 */
817 public void setNotDirty() {
818 document.setNotDirty();
819 }
820
71a389c9
KL
821 /**
822 * Save contents to file.
823 *
824 * @param filename file to save to
825 * @throws IOException if a java.io operation throws
826 */
827 public void saveToFilename(final String filename) throws IOException {
828 document.saveToFilename(filename);
829 }
830
101bc589
KL
831 /**
832 * Delete text within the selection bounds.
833 */
834 private void deleteSelection() {
835 if (inSelection == false) {
836 return;
837 }
43ee96da
KL
838 inSelection = false;
839
840 int startCol = selectionColumn0;
841 int startRow = selectionLine0;
842 int endCol = selectionColumn1;
843 int endRow = selectionLine1;
844
8d3480c7
KL
845 /*
846 System.err.println("INITIAL: " + startRow + " " + startCol + " " +
847 endRow + " " + endCol + " " +
848 document.getLineNumber() + " " + document.getCursor());
849 */
850
43ee96da 851 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
852 && (selectionLine1 == selectionLine0))
853 || (selectionLine1 < selectionLine0)
43ee96da 854 ) {
8d3480c7
KL
855 // The user selected from bottom-to-top and/or right-to-left.
856 // Reverse the coordinates for the inverted section.
43ee96da
KL
857 startCol = selectionColumn1;
858 startRow = selectionLine1;
859 endCol = selectionColumn0;
860 endRow = selectionLine0;
8d3480c7
KL
861
862 if (endRow >= document.getLineCount()) {
863 // The selection started beyond EOF, trim it to EOF.
864 endRow = document.getLineCount() - 1;
865 endCol = document.getLine(endRow).getDisplayLength();
866 } else if (endRow == document.getLineCount() - 1) {
867 // The selection started beyond EOF, trim it to EOF.
868 if (endCol >= document.getLine(endRow).getDisplayLength()) {
869 endCol = document.getLine(endRow).getDisplayLength() - 1;
870 }
871 }
872 }
873 /*
874 System.err.println("FLIP: " + startRow + " " + startCol + " " +
875 endRow + " " + endCol + " " +
876 document.getLineNumber() + " " + document.getCursor());
877 System.err.println(" --END: " + endRow + " " + document.getLineCount() +
878 " " + document.getLine(endRow).getDisplayLength());
879 */
880
881 assert (endRow < document.getLineCount());
882 if (endCol >= document.getLine(endRow).getDisplayLength()) {
883 endCol = document.getLine(endRow).getDisplayLength() - 1;
884 }
885 if (endCol < 0) {
886 endCol = 0;
887 }
888 if (startCol >= document.getLine(startRow).getDisplayLength()) {
889 startCol = document.getLine(startRow).getDisplayLength() - 1;
890 }
891 if (startCol < 0) {
892 startCol = 0;
43ee96da
KL
893 }
894
895 // Place the cursor on the selection end, and "press backspace" until
896 // the cursor matches the selection start.
8d3480c7
KL
897 /*
898 System.err.println("BEFORE: " + startRow + " " + startCol + " " +
899 endRow + " " + endCol + " " +
900 document.getLineNumber() + " " + document.getCursor());
901 */
43ee96da
KL
902 document.setLineNumber(endRow);
903 document.setCursor(endCol + 1);
904 while (!((document.getLineNumber() == startRow)
905 && (document.getCursor() == startCol))
906 ) {
8d3480c7
KL
907 /*
908 System.err.println("DURING: " + startRow + " " + startCol + " " +
909 endRow + " " + endCol + " " +
910 document.getLineNumber() + " " + document.getCursor());
911 */
912
43ee96da
KL
913 document.backspace();
914 }
915 alignTopLine(true);
916 }
917
918 /**
919 * Copy text within the selection bounds to clipboard.
920 */
921 private void copySelection() {
922 if (inSelection == false) {
923 return;
924 }
925
926 int startCol = selectionColumn0;
927 int startRow = selectionLine0;
928 int endCol = selectionColumn1;
929 int endRow = selectionLine1;
930
931 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
932 && (selectionLine1 == selectionLine0))
933 || (selectionLine1 < selectionLine0)
43ee96da 934 ) {
8d3480c7
KL
935 // The user selected from bottom-to-top and/or right-to-left.
936 // Reverse the coordinates for the inverted section.
43ee96da
KL
937 startCol = selectionColumn1;
938 startRow = selectionLine1;
939 endCol = selectionColumn0;
940 endRow = selectionLine0;
941 }
942
943 StringBuilder sb = new StringBuilder();
944
945 if (endRow > startRow) {
946 // First line
947 String line = document.getLine(startRow).getRawString();
948 int x = 0;
949 for (int i = 0; i < line.length(); ) {
950 int ch = line.codePointAt(i);
951
952 if (x >= startCol) {
953 sb.append(Character.toChars(ch));
954 }
955 x += StringUtils.width(ch);
956 i += Character.charCount(ch);
957 }
958 sb.append("\n");
959
960 // Middle lines
961 for (int y = startRow + 1; y < endRow; y++) {
962 sb.append(document.getLine(y).getRawString());
963 sb.append("\n");
964 }
965
966 // Final line
967 line = document.getLine(endRow).getRawString();
968 x = 0;
969 for (int i = 0; i < line.length(); ) {
970 int ch = line.codePointAt(i);
971
972 if (x > endCol) {
973 break;
974 }
975
976 sb.append(Character.toChars(ch));
977 x += StringUtils.width(ch);
978 i += Character.charCount(ch);
979 }
980 } else {
981 assert (startRow == endRow);
982
983 // Only one line
984 String line = document.getLine(startRow).getRawString();
985 int x = 0;
986 for (int i = 0; i < line.length(); ) {
987 int ch = line.codePointAt(i);
988
989 if ((x >= startCol) && (x <= endCol)) {
990 sb.append(Character.toChars(ch));
991 }
992
993 x += StringUtils.width(ch);
994 i += Character.charCount(ch);
995 }
996 }
101bc589 997
43ee96da 998 getClipboard().copyText(sb.toString());
101bc589
KL
999 }
1000
ed76cd41
KL
1001 /**
1002 * Set the selection.
1003 *
1004 * @param startRow the starting row number. 0-based: row 0 is the first
1005 * row.
1006 * @param startColumn the starting column number. 0-based: column 0 is
1007 * the first column.
1008 * @param endRow the ending row number. 0-based: row 0 is the first row.
1009 * @param endColumn the ending column number. 0-based: column 0 is the
1010 * first column.
1011 */
1012 public void setSelection(final int startRow, final int startColumn,
1013 final int endRow, final int endColumn) {
1014
1015 inSelection = true;
1016 selectionLine0 = startRow;
1017 selectionColumn0 = startColumn;
1018 selectionLine1 = endRow;
1019 selectionColumn1 = endColumn;
1020 }
1021
1022 /**
1023 * Unset the selection.
1024 */
1025 public void unsetSelection() {
1026 inSelection = false;
1027 }
1028
1029 /**
1030 * Replace whatever is being selected with new text. If not in
1031 * selection, nothing is replaced.
1032 *
1033 * @param text the new replacement text
1034 */
1035 public void replaceSelection(final String text) {
1036 if (!inSelection) {
1037 return;
1038 }
1039
1040 // Delete selected text, then paste text from clipboard.
1041 deleteSelection();
1042
1043 for (int i = 0; i < text.length(); ) {
1044 int ch = text.codePointAt(i);
1045 onKeypress(new TKeypressEvent(false, 0, ch, false, false,
1046 false));
1047 i += Character.charCount(ch);
1048 }
1049 }
1050
1051 /**
1052 * Get the entire contents of the editor as one string.
1053 *
1054 * @return the editor contents
1055 */
1056 public String getText() {
1057 return document.getText();
1058 }
1059
1060 /**
1061 * Set the entire contents of the editor from one string.
1062 *
1063 * @param text the new contents
1064 */
1065 public void setText(final String text) {
1066 document = new Document(text, defaultColor);
1067 unsetSelection();
1068 topLine = 0;
1069 leftColumn = 0;
1070 }
1071
101bc589
KL
1072 // ------------------------------------------------------------------------
1073 // EditMenuUser -----------------------------------------------------------
1074 // ------------------------------------------------------------------------
1075
1076 /**
1077 * Check if the cut menu item should be enabled.
1078 *
1079 * @return true if the cut menu item should be enabled
1080 */
1081 public boolean isEditMenuCut() {
1082 return true;
1083 }
1084
1085 /**
1086 * Check if the copy menu item should be enabled.
1087 *
1088 * @return true if the copy menu item should be enabled
1089 */
1090 public boolean isEditMenuCopy() {
1091 return true;
1092 }
1093
1094 /**
1095 * Check if the paste menu item should be enabled.
1096 *
1097 * @return true if the paste menu item should be enabled
1098 */
1099 public boolean isEditMenuPaste() {
1100 return true;
1101 }
1102
1103 /**
1104 * Check if the clear menu item should be enabled.
1105 *
1106 * @return true if the clear menu item should be enabled
1107 */
1108 public boolean isEditMenuClear() {
1109 return true;
1110 }
1111
12b55d76 1112}