Update lanterna, fix bugs, implement save...
[jvcard.git] / src / com / googlecode / lanterna / terminal / swing / GraphicalTerminalImplementation.java
1 /*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19 package com.googlecode.lanterna.terminal.swing;
20
21 import com.googlecode.lanterna.*;
22 import com.googlecode.lanterna.graphics.TextGraphics;
23 import com.googlecode.lanterna.input.KeyStroke;
24 import com.googlecode.lanterna.input.KeyType;
25 import com.googlecode.lanterna.terminal.IOSafeTerminal;
26 import com.googlecode.lanterna.terminal.ResizeListener;
27
28 import java.awt.*;
29 import java.awt.event.InputEvent;
30 import java.awt.event.KeyAdapter;
31 import java.awt.event.KeyEvent;
32 import java.awt.image.BufferedImage;
33 import java.io.IOException;
34 import java.util.*;
35 import java.util.List;
36 import java.util.concurrent.BlockingQueue;
37 import java.util.concurrent.CopyOnWriteArrayList;
38 import java.util.concurrent.LinkedBlockingQueue;
39 import java.util.concurrent.TimeUnit;
40
41 /**
42 * This is the class that does the heavy lifting for both {@link AWTTerminal} and {@link SwingTerminal}. It maintains
43 * most of the external terminal state and also the main back buffer that is copied to the components area on draw
44 * operations.
45 *
46 * @author martin
47 */
48 @SuppressWarnings("serial")
49 abstract class GraphicalTerminalImplementation implements IOSafeTerminal {
50 private final TerminalEmulatorDeviceConfiguration deviceConfiguration;
51 private final TerminalEmulatorColorConfiguration colorConfiguration;
52 private final VirtualTerminal virtualTerminal;
53 private final BlockingQueue<KeyStroke> keyQueue;
54 private final List<ResizeListener> resizeListeners;
55
56 private final String enquiryString;
57 private final EnumSet<SGR> activeSGRs;
58 private TextColor foregroundColor;
59 private TextColor backgroundColor;
60
61 private volatile boolean cursorIsVisible;
62 private volatile Timer blinkTimer;
63 private volatile boolean hasBlinkingText;
64 private volatile boolean blinkOn;
65 private volatile boolean flushed;
66
67 // We use two different data structures to optimize drawing
68 // * A map (as a two-dimensional array) of all characters currently visible inside this component
69 // * A backbuffer with the graphics content
70 //
71 // The buffer is the most important one as it allows us to re-use what was drawn earlier. It is not reset on every
72 // drawing operation but updates just in those places where the map tells us the character has changed. Note that
73 // when the component is resized, we always update the whole buffer.
74 //
75 // DON'T RELY ON THESE FOR SIZE! We make it a big bigger than necessary to make resizing smoother. Use the AWT/Swing
76 // methods to get the correct dimensions or use {@code getTerminalSize()} to get the size in terminal space.
77 private CharacterState[][] visualState;
78 private BufferedImage backbuffer;
79
80 /**
81 * Creates a new GraphicalTerminalImplementation component using custom settings and a custom scroll controller. The
82 * scrolling controller will be notified when the terminal's history size grows and will be called when this class
83 * needs to figure out the current scrolling position.
84 * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
85 * of the component. If null, it will default to 80x25. If the AWT layout manager forces
86 * the component to a different size, the value of this parameter won't have any meaning
87 * @param deviceConfiguration Device configuration to use for this SwingTerminal
88 * @param colorConfiguration Color configuration to use for this SwingTerminal
89 * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
90 * scrollable area has changed
91 */
92 public GraphicalTerminalImplementation(
93 TerminalSize initialTerminalSize,
94 TerminalEmulatorDeviceConfiguration deviceConfiguration,
95 TerminalEmulatorColorConfiguration colorConfiguration,
96 TerminalScrollController scrollController) {
97
98 //This is kind of meaningless since we don't know how large the
99 //component is at this point, but we should set it to something
100 if(initialTerminalSize == null) {
101 initialTerminalSize = new TerminalSize(80, 24);
102 }
103 this.virtualTerminal = new VirtualTerminal(
104 deviceConfiguration.getLineBufferScrollbackSize(),
105 initialTerminalSize,
106 scrollController);
107 this.keyQueue = new LinkedBlockingQueue<KeyStroke>();
108 this.resizeListeners = new CopyOnWriteArrayList<ResizeListener>();
109 this.deviceConfiguration = deviceConfiguration;
110 this.colorConfiguration = colorConfiguration;
111
112 this.activeSGRs = EnumSet.noneOf(SGR.class);
113 this.foregroundColor = TextColor.ANSI.DEFAULT;
114 this.backgroundColor = TextColor.ANSI.DEFAULT;
115 this.cursorIsVisible = true; //Always start with an activate and visible cursor
116 this.enquiryString = "TerminalEmulator";
117 this.visualState = new CharacterState[48][160];
118 this.backbuffer = null; // We don't know the dimensions yet
119 this.blinkTimer = null;
120 this.hasBlinkingText = false; // Assume initial content doesn't have any blinking text
121 this.blinkOn = true;
122 this.flushed = false;
123
124 //Set the initial scrollable size
125 //scrollObserver.newScrollableLength(fontConfiguration.getFontHeight() * terminalSize.getRows());
126 }
127
128 ///////////
129 // First abstract methods that are implemented in AWTTerminalImplementation and SwingTerminalImplementation
130 ///////////
131
132 /**
133 * Used to find out the font height, in pixels
134 * @return Terminal font height in pixels
135 */
136 protected abstract int getFontHeight();
137
138 /**
139 * Used to find out the font width, in pixels
140 * @return Terminal font width in pixels
141 */
142 protected abstract int getFontWidth();
143
144 /**
145 * Used when requiring the total height of the terminal component, in pixels
146 * @return Height of the terminal component, in pixels
147 */
148 protected abstract int getHeight();
149
150 /**
151 * Used when requiring the total width of the terminal component, in pixels
152 * @return Width of the terminal component, in pixels
153 */
154 protected abstract int getWidth();
155
156 /**
157 * Returning the AWT font to use for the specific character. This might not always be the same, in case a we are
158 * trying to draw an unusual character (probably CJK) which isn't contained in the standard terminal font.
159 * @param character Character to get the font for
160 * @return Font to be used for this character
161 */
162 protected abstract Font getFontForCharacter(TextCharacter character);
163
164 /**
165 * Returns {@code true} if anti-aliasing is enabled, {@code false} otherwise
166 * @return {@code true} if anti-aliasing is enabled, {@code false} otherwise
167 */
168 protected abstract boolean isTextAntiAliased();
169
170 /**
171 * Called by the {@code GraphicalTerminalImplementation} when it would like the OS to schedule a repaint of the
172 * window
173 */
174 protected abstract void repaint();
175
176 /**
177 * Start the timer that triggers blinking
178 */
179 protected synchronized void startBlinkTimer() {
180 if(blinkTimer != null) {
181 // Already on!
182 return;
183 }
184 blinkTimer = new Timer("LanternaTerminalBlinkTimer", true);
185 blinkTimer.schedule(new TimerTask() {
186 @Override
187 public void run() {
188 blinkOn = !blinkOn;
189 if(hasBlinkingText) {
190 repaint();
191 }
192 }
193 }, deviceConfiguration.getBlinkLengthInMilliSeconds(), deviceConfiguration.getBlinkLengthInMilliSeconds());
194 }
195
196 /**
197 * Stops the timer the triggers blinking
198 */
199 protected synchronized void stopBlinkTimer() {
200 if(blinkTimer == null) {
201 // Already off!
202 return;
203 }
204 blinkTimer.cancel();
205 blinkTimer = null;
206 }
207
208 ///////////
209 // First implement all the Swing-related methods
210 ///////////
211 /**
212 * Calculates the preferred size of this terminal
213 * @return Preferred size of this terminal
214 */
215 synchronized Dimension getPreferredSize() {
216 return new Dimension(getFontWidth() * virtualTerminal.getSize().getColumns(),
217 getFontHeight() * virtualTerminal.getSize().getRows());
218 }
219
220 /**
221 * Updates the back buffer (if necessary) and draws it to the component's surface
222 * @param componentGraphics Object to use when drawing to the component's surface
223 */
224 protected synchronized void paintComponent(Graphics componentGraphics) {
225 //First, resize the buffer width/height if necessary
226 int fontWidth = getFontWidth();
227 int fontHeight = getFontHeight();
228 //boolean antiAliasing = fontConfiguration.isAntiAliased();
229 int widthInNumberOfCharacters = getWidth() / fontWidth;
230 int visibleRows = getHeight() / fontHeight;
231 boolean terminalResized = false;
232
233 //Don't let size be less than 1
234 widthInNumberOfCharacters = Math.max(1, widthInNumberOfCharacters);
235 visibleRows = Math.max(1, visibleRows);
236
237 //scrollObserver.updateModel(currentBuffer.getNumberOfLines(), visibleRows);
238 TerminalSize terminalSize = virtualTerminal.getSize().withColumns(widthInNumberOfCharacters).withRows(visibleRows);
239 if(!terminalSize.equals(virtualTerminal.getSize())) {
240 virtualTerminal.resize(terminalSize);
241 for(ResizeListener listener: resizeListeners) {
242 listener.onResized(this, terminalSize);
243 }
244 terminalResized = true;
245 ensureVisualStateHasRightSize(terminalSize);
246 }
247 ensureBackbufferHasRightSize();
248
249 // At this point, if the user hasn't asked for an explicit flush, just paint the backbuffer. It's prone to
250 // problems if the user isn't flushing properly but it reduces flickering when resizing the window and the code
251 // is asynchronously responding to the resize
252 if(flushed) {
253 updateBackBuffer(fontWidth, fontHeight, terminalResized, terminalSize);
254 flushed = false;
255 }
256
257 componentGraphics.drawImage(backbuffer, 0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
258
259 // Dispose the graphic objects
260 componentGraphics.dispose();
261
262 // Tell anyone waiting on us that drawing is complete
263 notifyAll();
264 }
265
266 private void updateBackBuffer(int fontWidth, int fontHeight, boolean terminalResized, TerminalSize terminalSize) {
267 //Retrieve the position of the cursor, relative to the scrolling state
268 TerminalPosition translatedCursorPosition = virtualTerminal.getTranslatedCursorPosition();
269
270 //Setup the graphics object
271 Graphics2D backbufferGraphics = backbuffer.createGraphics();
272
273 if(isTextAntiAliased()) {
274 backbufferGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
275 backbufferGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
276 }
277
278 // Draw line by line, character by character
279 // Initiate the blink state to whatever the cursor is using, since if the cursor is blinking then we always want
280 // to do the blink repaint
281 boolean foundBlinkingCharacters = deviceConfiguration.isCursorBlinking();
282 int rowIndex = 0;
283 for(List<TextCharacter> row: virtualTerminal.getLines()) {
284 for(int columnIndex = 0; columnIndex < row.size(); columnIndex++) {
285 //Any extra characters from the virtual terminal that doesn't fit can be discarded
286 if(columnIndex >= terminalSize.getColumns()) {
287 continue;
288 }
289
290 TextCharacter character = row.get(columnIndex);
291 boolean atCursorLocation = translatedCursorPosition.equals(columnIndex, rowIndex);
292 //If next position is the cursor location and this is a CJK character (i.e. cursor is on the padding),
293 //consider this location the cursor position since otherwise the cursor will be skipped
294 if(!atCursorLocation &&
295 translatedCursorPosition.getColumn() == columnIndex + 1 &&
296 translatedCursorPosition.getRow() == rowIndex &&
297 TerminalTextUtils.isCharCJK(character.getCharacter())) {
298 atCursorLocation = true;
299 }
300 int characterWidth = fontWidth * (TerminalTextUtils.isCharCJK(character.getCharacter()) ? 2 : 1);
301
302 Color foregroundColor = deriveTrueForegroundColor(character, atCursorLocation);
303 Color backgroundColor = deriveTrueBackgroundColor(character, atCursorLocation);
304
305 boolean drawCursor = atCursorLocation &&
306 (!deviceConfiguration.isCursorBlinking() || //Always draw if the cursor isn't blinking
307 (deviceConfiguration.isCursorBlinking() && blinkOn)); //If the cursor is blinking, only draw when blinkOn is true
308
309 CharacterState characterState = new CharacterState(character, foregroundColor, backgroundColor, drawCursor);
310 if(!characterState.equals(visualState[rowIndex][columnIndex]) || terminalResized) {
311 drawCharacter(backbufferGraphics,
312 character,
313 columnIndex,
314 rowIndex,
315 foregroundColor,
316 backgroundColor,
317 fontWidth,
318 fontHeight,
319 characterWidth,
320 drawCursor);
321 visualState[rowIndex][columnIndex] = characterState;
322 if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
323 visualState[rowIndex][columnIndex+1] = characterState;
324 }
325 }
326
327 if(character.getModifiers().contains(SGR.BLINK)) {
328 foundBlinkingCharacters = true;
329 }
330 if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
331 columnIndex++; //Skip the trailing space after a CJK character
332 }
333 }
334 rowIndex++;
335 }
336
337 // Take care of the left-over area at the bottom and right of the component where no character can fit
338 int leftoverHeight = getHeight() % fontHeight;
339 int leftoverWidth = getWidth() % fontWidth;
340 backbufferGraphics.setColor(Color.BLACK);
341 if(leftoverWidth > 0) {
342 backbufferGraphics.fillRect(getWidth() - leftoverWidth, 0, leftoverWidth, getHeight());
343 }
344 if(leftoverHeight > 0) {
345 backbufferGraphics.fillRect(0, getHeight() - leftoverHeight, getWidth(), leftoverHeight);
346 }
347 backbufferGraphics.dispose();
348
349 // Update the blink status according to if there were any blinking characters or not
350 this.hasBlinkingText = foundBlinkingCharacters;
351 }
352
353 private void ensureBackbufferHasRightSize() {
354 if(backbuffer == null) {
355 backbuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
356 }
357 if(backbuffer.getWidth() < getWidth() || backbuffer.getWidth() > getWidth() * 4 ||
358 backbuffer.getHeight() < getHeight() || backbuffer.getHeight() > getHeight() * 4) {
359 BufferedImage newBackbuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
360 Graphics2D graphics = newBackbuffer.createGraphics();
361 graphics.drawImage(backbuffer, 0, 0, null);
362 graphics.dispose();
363 backbuffer = newBackbuffer;
364 }
365 }
366
367 private void ensureVisualStateHasRightSize(TerminalSize terminalSize) {
368 if(visualState == null) {
369 visualState = new CharacterState[terminalSize.getRows() * 2][terminalSize.getColumns() * 2];
370 }
371 if(visualState.length < terminalSize.getRows() || visualState.length > Math.max(terminalSize.getRows(), 1) * 4) {
372 visualState = Arrays.copyOf(visualState, terminalSize.getRows() * 2);
373 }
374 for(int rowIndex = 0; rowIndex < visualState.length; rowIndex++) {
375 CharacterState[] row = visualState[rowIndex];
376 if(row == null) {
377 row = new CharacterState[terminalSize.getColumns() * 2];
378 visualState[rowIndex] = row;
379 }
380 if(row.length < terminalSize.getColumns() || row.length > Math.max(terminalSize.getColumns(), 1) * 4) {
381 row = Arrays.copyOf(row, terminalSize.getColumns() * 2);
382 visualState[rowIndex] = row;
383 }
384
385 // Make sure all items outside the 'real' terminal size are null
386 if(rowIndex < terminalSize.getRows()) {
387 Arrays.fill(row, terminalSize.getColumns(), row.length, null);
388 }
389 else {
390 Arrays.fill(row, null);
391 }
392 }
393 }
394
395 private void drawCharacter(
396 Graphics g,
397 TextCharacter character,
398 int columnIndex,
399 int rowIndex,
400 Color foregroundColor,
401 Color backgroundColor,
402 int fontWidth,
403 int fontHeight,
404 int characterWidth,
405 boolean drawCursor) {
406
407 int x = columnIndex * fontWidth;
408 int y = rowIndex * fontHeight;
409 g.setColor(backgroundColor);
410 g.setClip(x, y, characterWidth, fontHeight);
411 g.fillRect(x, y, characterWidth, fontHeight);
412
413 g.setColor(foregroundColor);
414 Font font = getFontForCharacter(character);
415 g.setFont(font);
416 FontMetrics fontMetrics = g.getFontMetrics();
417 g.drawString(Character.toString(character.getCharacter()), x, ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent());
418
419 if(character.isCrossedOut()) {
420 int lineStartX = x;
421 int lineStartY = y + (fontHeight / 2);
422 int lineEndX = lineStartX + characterWidth;
423 g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
424 }
425 if(character.isUnderlined()) {
426 int lineStartX = x;
427 int lineStartY = ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent() + 1;
428 int lineEndX = lineStartX + characterWidth;
429 g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
430 }
431
432 if(drawCursor) {
433 if(deviceConfiguration.getCursorColor() == null) {
434 g.setColor(foregroundColor);
435 }
436 else {
437 g.setColor(colorConfiguration.toAWTColor(deviceConfiguration.getCursorColor(), false, false));
438 }
439 if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.UNDER_BAR) {
440 g.fillRect(x, y + fontHeight - 3, characterWidth, 2);
441 }
442 else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.VERTICAL_BAR) {
443 g.fillRect(x, y + 1, 2, fontHeight - 2);
444 }
445 }
446 }
447
448
449 private Color deriveTrueForegroundColor(TextCharacter character, boolean atCursorLocation) {
450 TextColor foregroundColor = character.getForegroundColor();
451 TextColor backgroundColor = character.getBackgroundColor();
452 boolean reverse = character.isReversed();
453 boolean blink = character.isBlinking();
454
455 if(cursorIsVisible && atCursorLocation) {
456 if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
457 (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
458 reverse = true;
459 }
460 }
461
462 if(reverse && (!blink || !blinkOn)) {
463 return colorConfiguration.toAWTColor(backgroundColor, backgroundColor != TextColor.ANSI.DEFAULT, character.isBold());
464 }
465 else if(!reverse && blink && blinkOn) {
466 return colorConfiguration.toAWTColor(backgroundColor, false, character.isBold());
467 }
468 else {
469 return colorConfiguration.toAWTColor(foregroundColor, true, character.isBold());
470 }
471 }
472
473 private Color deriveTrueBackgroundColor(TextCharacter character, boolean atCursorLocation) {
474 TextColor foregroundColor = character.getForegroundColor();
475 TextColor backgroundColor = character.getBackgroundColor();
476 boolean reverse = character.isReversed();
477
478 if(cursorIsVisible && atCursorLocation) {
479 if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
480 (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
481 reverse = true;
482 }
483 else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.FIXED_BACKGROUND) {
484 backgroundColor = deviceConfiguration.getCursorColor();
485 }
486 }
487
488 if(reverse) {
489 return colorConfiguration.toAWTColor(foregroundColor, backgroundColor == TextColor.ANSI.DEFAULT, character.isBold());
490 }
491 else {
492 return colorConfiguration.toAWTColor(backgroundColor, false, false);
493 }
494 }
495
496 ///////////
497 // Then delegate all Terminal interface methods to the virtual terminal implementation
498 //
499 // Some of these methods we need to pass to the AWT-thread, which makes the call asynchronous. Hopefully this isn't
500 // causing too much problem...
501 ///////////
502 @Override
503 public KeyStroke pollInput() {
504 return keyQueue.poll();
505 }
506
507 @Override
508 public KeyStroke readInput() throws IOException {
509 try {
510 return keyQueue.take();
511 }
512 catch(InterruptedException ignore) {
513 throw new IOException("Blocking input was interrupted");
514 }
515 }
516
517 @Override
518 public synchronized void enterPrivateMode() {
519 virtualTerminal.switchToPrivateMode();
520 clearBackBufferAndVisualState();
521 flush();
522 }
523
524 @Override
525 public synchronized void exitPrivateMode() {
526 virtualTerminal.switchToNormalMode();
527 clearBackBufferAndVisualState();
528 flush();
529 }
530
531 @Override
532 public synchronized void clearScreen() {
533 virtualTerminal.clear();
534 clearBackBufferAndVisualState();
535 flush();
536 }
537
538 /**
539 * Clears out the back buffer and the resets the visual state so next paint operation will do a full repaint of
540 * everything
541 */
542 protected void clearBackBufferAndVisualState() {
543 // Manually clear the backbuffer and visual state
544 if(backbuffer != null) {
545 Graphics2D graphics = backbuffer.createGraphics();
546 Color foregroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, true, false);
547 Color backgroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false);
548 graphics.setColor(backgroundColor);
549 graphics.fillRect(0, 0, getWidth(), getHeight());
550 graphics.dispose();
551
552 for(CharacterState[] line : visualState) {
553 Arrays.fill(line, new CharacterState(new TextCharacter(' '), foregroundColor, backgroundColor, false));
554 }
555 }
556 }
557
558 @Override
559 public synchronized void setCursorPosition(final int x, final int y) {
560 virtualTerminal.setCursorPosition(new TerminalPosition(x, y));
561 }
562
563 @Override
564 public void setCursorVisible(final boolean visible) {
565 cursorIsVisible = visible;
566 }
567
568 @Override
569 public synchronized void putCharacter(final char c) {
570 virtualTerminal.putCharacter(new TextCharacter(c, foregroundColor, backgroundColor, activeSGRs));
571 }
572
573 @Override
574 public TextGraphics newTextGraphics() throws IOException {
575 return new VirtualTerminalTextGraphics(virtualTerminal);
576 }
577
578 @Override
579 public void enableSGR(final SGR sgr) {
580 activeSGRs.add(sgr);
581 }
582
583 @Override
584 public void disableSGR(final SGR sgr) {
585 activeSGRs.remove(sgr);
586 }
587
588 @Override
589 public void resetColorAndSGR() {
590 foregroundColor = TextColor.ANSI.DEFAULT;
591 backgroundColor = TextColor.ANSI.DEFAULT;
592 activeSGRs.clear();
593 }
594
595 @Override
596 public void setForegroundColor(final TextColor color) {
597 foregroundColor = color;
598 }
599
600 @Override
601 public void setBackgroundColor(final TextColor color) {
602 backgroundColor = color;
603 }
604
605 @Override
606 public synchronized TerminalSize getTerminalSize() {
607 return virtualTerminal.getSize();
608 }
609
610 @Override
611 public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
612 return enquiryString.getBytes();
613 }
614
615 @Override
616 public void flush() {
617 flushed = true;
618 repaint();
619 }
620
621 @Override
622 public void addResizeListener(ResizeListener listener) {
623 resizeListeners.add(listener);
624 }
625
626 @Override
627 public void removeResizeListener(ResizeListener listener) {
628 resizeListeners.remove(listener);
629 }
630
631 ///////////
632 // Remaining are private internal classes used by SwingTerminal
633 ///////////
634 private static final Set<Character> TYPED_KEYS_TO_IGNORE = new HashSet<Character>(Arrays.asList('\n', '\t', '\r', '\b', '\33', (char)127));
635
636 /**
637 * Class that translates AWT key events into Lanterna {@link KeyStroke}
638 */
639 protected class TerminalInputListener extends KeyAdapter {
640 @Override
641 public void keyTyped(KeyEvent e) {
642 char character = e.getKeyChar();
643 boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
644 boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
645
646 if(!TYPED_KEYS_TO_IGNORE.contains(character)) {
647 if(ctrlDown) {
648 //We need to re-adjust the character if ctrl is pressed, just like for the AnsiTerminal
649 character = (char) ('a' - 1 + character);
650 }
651 keyQueue.add(new KeyStroke(character, ctrlDown, altDown));
652 }
653 }
654
655 @Override
656 public void keyPressed(KeyEvent e) {
657 boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
658 boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
659 if(e.getKeyCode() == KeyEvent.VK_ENTER) {
660 keyQueue.add(new KeyStroke(KeyType.Enter, ctrlDown, altDown));
661 }
662 else if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
663 keyQueue.add(new KeyStroke(KeyType.Escape, ctrlDown, altDown));
664 }
665 else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
666 keyQueue.add(new KeyStroke(KeyType.Backspace, ctrlDown, altDown));
667 }
668 else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
669 keyQueue.add(new KeyStroke(KeyType.ArrowLeft, ctrlDown, altDown));
670 }
671 else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
672 keyQueue.add(new KeyStroke(KeyType.ArrowRight, ctrlDown, altDown));
673 }
674 else if(e.getKeyCode() == KeyEvent.VK_UP) {
675 keyQueue.add(new KeyStroke(KeyType.ArrowUp, ctrlDown, altDown));
676 }
677 else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
678 keyQueue.add(new KeyStroke(KeyType.ArrowDown, ctrlDown, altDown));
679 }
680 else if(e.getKeyCode() == KeyEvent.VK_INSERT) {
681 keyQueue.add(new KeyStroke(KeyType.Insert, ctrlDown, altDown));
682 }
683 else if(e.getKeyCode() == KeyEvent.VK_DELETE) {
684 keyQueue.add(new KeyStroke(KeyType.Delete, ctrlDown, altDown));
685 }
686 else if(e.getKeyCode() == KeyEvent.VK_HOME) {
687 keyQueue.add(new KeyStroke(KeyType.Home, ctrlDown, altDown));
688 }
689 else if(e.getKeyCode() == KeyEvent.VK_END) {
690 keyQueue.add(new KeyStroke(KeyType.End, ctrlDown, altDown));
691 }
692 else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
693 keyQueue.add(new KeyStroke(KeyType.PageUp, ctrlDown, altDown));
694 }
695 else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
696 keyQueue.add(new KeyStroke(KeyType.PageDown, ctrlDown, altDown));
697 }
698 else if(e.getKeyCode() == KeyEvent.VK_F1) {
699 keyQueue.add(new KeyStroke(KeyType.F1, ctrlDown, altDown));
700 }
701 else if(e.getKeyCode() == KeyEvent.VK_F2) {
702 keyQueue.add(new KeyStroke(KeyType.F2, ctrlDown, altDown));
703 }
704 else if(e.getKeyCode() == KeyEvent.VK_F3) {
705 keyQueue.add(new KeyStroke(KeyType.F3, ctrlDown, altDown));
706 }
707 else if(e.getKeyCode() == KeyEvent.VK_F4) {
708 keyQueue.add(new KeyStroke(KeyType.F4, ctrlDown, altDown));
709 }
710 else if(e.getKeyCode() == KeyEvent.VK_F5) {
711 keyQueue.add(new KeyStroke(KeyType.F5, ctrlDown, altDown));
712 }
713 else if(e.getKeyCode() == KeyEvent.VK_F6) {
714 keyQueue.add(new KeyStroke(KeyType.F6, ctrlDown, altDown));
715 }
716 else if(e.getKeyCode() == KeyEvent.VK_F7) {
717 keyQueue.add(new KeyStroke(KeyType.F7, ctrlDown, altDown));
718 }
719 else if(e.getKeyCode() == KeyEvent.VK_F8) {
720 keyQueue.add(new KeyStroke(KeyType.F8, ctrlDown, altDown));
721 }
722 else if(e.getKeyCode() == KeyEvent.VK_F9) {
723 keyQueue.add(new KeyStroke(KeyType.F9, ctrlDown, altDown));
724 }
725 else if(e.getKeyCode() == KeyEvent.VK_F10) {
726 keyQueue.add(new KeyStroke(KeyType.F10, ctrlDown, altDown));
727 }
728 else if(e.getKeyCode() == KeyEvent.VK_F11) {
729 keyQueue.add(new KeyStroke(KeyType.F11, ctrlDown, altDown));
730 }
731 else if(e.getKeyCode() == KeyEvent.VK_F12) {
732 keyQueue.add(new KeyStroke(KeyType.F12, ctrlDown, altDown));
733 }
734 else if(e.getKeyCode() == KeyEvent.VK_TAB) {
735 if(e.isShiftDown()) {
736 keyQueue.add(new KeyStroke(KeyType.ReverseTab, ctrlDown, altDown));
737 }
738 else {
739 keyQueue.add(new KeyStroke(KeyType.Tab, ctrlDown, altDown));
740 }
741 }
742 else {
743 //keyTyped doesn't catch this scenario (for whatever reason...) so we have to do it here
744 if(altDown && ctrlDown && e.getKeyCode() >= 'A' && e.getKeyCode() <= 'Z') {
745 char asLowerCase = Character.toLowerCase((char) e.getKeyCode());
746 keyQueue.add(new KeyStroke(asLowerCase, true, true));
747 }
748 }
749 }
750 }
751
752 private static class CharacterState {
753 private final TextCharacter textCharacter;
754 private final Color foregroundColor;
755 private final Color backgroundColor;
756 private final boolean drawCursor;
757
758 CharacterState(TextCharacter textCharacter, Color foregroundColor, Color backgroundColor, boolean drawCursor) {
759 this.textCharacter = textCharacter;
760 this.foregroundColor = foregroundColor;
761 this.backgroundColor = backgroundColor;
762 this.drawCursor = drawCursor;
763 }
764
765 @Override
766 public boolean equals(Object o) {
767 if(this == o) {
768 return true;
769 }
770 if(o == null || getClass() != o.getClass()) {
771 return false;
772 }
773 CharacterState that = (CharacterState) o;
774 if(drawCursor != that.drawCursor) {
775 return false;
776 }
777 if(!textCharacter.equals(that.textCharacter)) {
778 return false;
779 }
780 if(!foregroundColor.equals(that.foregroundColor)) {
781 return false;
782 }
783 return backgroundColor.equals(that.backgroundColor);
784 }
785
786 @Override
787 public int hashCode() {
788 int result = textCharacter.hashCode();
789 result = 31 * result + foregroundColor.hashCode();
790 result = 31 * result + backgroundColor.hashCode();
791 result = 31 * result + (drawCursor ? 1 : 0);
792 return result;
793 }
794
795 @Override
796 public String toString() {
797 return "CharacterState{" +
798 "textCharacter=" + textCharacter +
799 ", foregroundColor=" + foregroundColor +
800 ", backgroundColor=" + backgroundColor +
801 ", drawCursor=" + drawCursor +
802 '}';
803 }
804 }
805 }