#66 fix bounds on selection and deletion
[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
794 /**
795 * Get the dirty value.
796 *
797 * @return true if the buffer is dirty
798 */
799 public boolean isDirty() {
800 return document.isDirty();
801 }
802
803 /**
804 * Save contents to file.
805 *
806 * @param filename file to save to
807 * @throws IOException if a java.io operation throws
808 */
809 public void saveToFilename(final String filename) throws IOException {
810 document.saveToFilename(filename);
811 }
812
101bc589
KL
813 /**
814 * Delete text within the selection bounds.
815 */
816 private void deleteSelection() {
817 if (inSelection == false) {
818 return;
819 }
43ee96da
KL
820 inSelection = false;
821
822 int startCol = selectionColumn0;
823 int startRow = selectionLine0;
824 int endCol = selectionColumn1;
825 int endRow = selectionLine1;
826
8d3480c7
KL
827 /*
828 System.err.println("INITIAL: " + startRow + " " + startCol + " " +
829 endRow + " " + endCol + " " +
830 document.getLineNumber() + " " + document.getCursor());
831 */
832
43ee96da 833 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
834 && (selectionLine1 == selectionLine0))
835 || (selectionLine1 < selectionLine0)
43ee96da 836 ) {
8d3480c7
KL
837 // The user selected from bottom-to-top and/or right-to-left.
838 // Reverse the coordinates for the inverted section.
43ee96da
KL
839 startCol = selectionColumn1;
840 startRow = selectionLine1;
841 endCol = selectionColumn0;
842 endRow = selectionLine0;
8d3480c7
KL
843
844 if (endRow >= document.getLineCount()) {
845 // The selection started beyond EOF, trim it to EOF.
846 endRow = document.getLineCount() - 1;
847 endCol = document.getLine(endRow).getDisplayLength();
848 } else if (endRow == document.getLineCount() - 1) {
849 // The selection started beyond EOF, trim it to EOF.
850 if (endCol >= document.getLine(endRow).getDisplayLength()) {
851 endCol = document.getLine(endRow).getDisplayLength() - 1;
852 }
853 }
854 }
855 /*
856 System.err.println("FLIP: " + startRow + " " + startCol + " " +
857 endRow + " " + endCol + " " +
858 document.getLineNumber() + " " + document.getCursor());
859 System.err.println(" --END: " + endRow + " " + document.getLineCount() +
860 " " + document.getLine(endRow).getDisplayLength());
861 */
862
863 assert (endRow < document.getLineCount());
864 if (endCol >= document.getLine(endRow).getDisplayLength()) {
865 endCol = document.getLine(endRow).getDisplayLength() - 1;
866 }
867 if (endCol < 0) {
868 endCol = 0;
869 }
870 if (startCol >= document.getLine(startRow).getDisplayLength()) {
871 startCol = document.getLine(startRow).getDisplayLength() - 1;
872 }
873 if (startCol < 0) {
874 startCol = 0;
43ee96da
KL
875 }
876
877 // Place the cursor on the selection end, and "press backspace" until
878 // the cursor matches the selection start.
8d3480c7
KL
879 /*
880 System.err.println("BEFORE: " + startRow + " " + startCol + " " +
881 endRow + " " + endCol + " " +
882 document.getLineNumber() + " " + document.getCursor());
883 */
43ee96da
KL
884 document.setLineNumber(endRow);
885 document.setCursor(endCol + 1);
886 while (!((document.getLineNumber() == startRow)
887 && (document.getCursor() == startCol))
888 ) {
8d3480c7
KL
889 /*
890 System.err.println("DURING: " + startRow + " " + startCol + " " +
891 endRow + " " + endCol + " " +
892 document.getLineNumber() + " " + document.getCursor());
893 */
894
43ee96da
KL
895 document.backspace();
896 }
897 alignTopLine(true);
898 }
899
900 /**
901 * Copy text within the selection bounds to clipboard.
902 */
903 private void copySelection() {
904 if (inSelection == false) {
905 return;
906 }
907
908 int startCol = selectionColumn0;
909 int startRow = selectionLine0;
910 int endCol = selectionColumn1;
911 int endRow = selectionLine1;
912
913 if (((selectionColumn1 < selectionColumn0)
8d3480c7
KL
914 && (selectionLine1 == selectionLine0))
915 || (selectionLine1 < selectionLine0)
43ee96da 916 ) {
8d3480c7
KL
917 // The user selected from bottom-to-top and/or right-to-left.
918 // Reverse the coordinates for the inverted section.
43ee96da
KL
919 startCol = selectionColumn1;
920 startRow = selectionLine1;
921 endCol = selectionColumn0;
922 endRow = selectionLine0;
923 }
924
925 StringBuilder sb = new StringBuilder();
926
927 if (endRow > startRow) {
928 // First line
929 String line = document.getLine(startRow).getRawString();
930 int x = 0;
931 for (int i = 0; i < line.length(); ) {
932 int ch = line.codePointAt(i);
933
934 if (x >= startCol) {
935 sb.append(Character.toChars(ch));
936 }
937 x += StringUtils.width(ch);
938 i += Character.charCount(ch);
939 }
940 sb.append("\n");
941
942 // Middle lines
943 for (int y = startRow + 1; y < endRow; y++) {
944 sb.append(document.getLine(y).getRawString());
945 sb.append("\n");
946 }
947
948 // Final line
949 line = document.getLine(endRow).getRawString();
950 x = 0;
951 for (int i = 0; i < line.length(); ) {
952 int ch = line.codePointAt(i);
953
954 if (x > endCol) {
955 break;
956 }
957
958 sb.append(Character.toChars(ch));
959 x += StringUtils.width(ch);
960 i += Character.charCount(ch);
961 }
962 } else {
963 assert (startRow == endRow);
964
965 // Only one line
966 String line = document.getLine(startRow).getRawString();
967 int x = 0;
968 for (int i = 0; i < line.length(); ) {
969 int ch = line.codePointAt(i);
970
971 if ((x >= startCol) && (x <= endCol)) {
972 sb.append(Character.toChars(ch));
973 }
974
975 x += StringUtils.width(ch);
976 i += Character.charCount(ch);
977 }
978 }
101bc589 979
43ee96da 980 getClipboard().copyText(sb.toString());
101bc589
KL
981 }
982
983 // ------------------------------------------------------------------------
984 // EditMenuUser -----------------------------------------------------------
985 // ------------------------------------------------------------------------
986
987 /**
988 * Check if the cut menu item should be enabled.
989 *
990 * @return true if the cut menu item should be enabled
991 */
992 public boolean isEditMenuCut() {
993 return true;
994 }
995
996 /**
997 * Check if the copy menu item should be enabled.
998 *
999 * @return true if the copy menu item should be enabled
1000 */
1001 public boolean isEditMenuCopy() {
1002 return true;
1003 }
1004
1005 /**
1006 * Check if the paste menu item should be enabled.
1007 *
1008 * @return true if the paste menu item should be enabled
1009 */
1010 public boolean isEditMenuPaste() {
1011 return true;
1012 }
1013
1014 /**
1015 * Check if the clear menu item should be enabled.
1016 *
1017 * @return true if the clear menu item should be enabled
1018 */
1019 public boolean isEditMenuClear() {
1020 return true;
1021 }
1022
12b55d76 1023}