8c444a86ff8a167b344a0e4bd8a2efceef2f0145
[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 backbufferGraphics.setColor(colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false));
273 backbufferGraphics.fillRect(0, 0, getWidth(), getHeight());
274
275 if(isTextAntiAliased()) {
276 backbufferGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
277 backbufferGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
278 }
279
280 // Draw line by line, character by character
281 // Initiate the blink state to whatever the cursor is using, since if the cursor is blinking then we always want
282 // to do the blink repaint
283 boolean foundBlinkingCharacters = deviceConfiguration.isCursorBlinking();
284 int rowIndex = 0;
285 for(List<TextCharacter> row: virtualTerminal.getLines()) {
286 for(int columnIndex = 0; columnIndex < row.size(); columnIndex++) {
287 //Any extra characters from the virtual terminal that doesn't fit can be discarded
288 if(columnIndex >= terminalSize.getColumns()) {
289 continue;
290 }
291
292 TextCharacter character = row.get(columnIndex);
293 boolean atCursorLocation = translatedCursorPosition.equals(columnIndex, rowIndex);
294 //If next position is the cursor location and this is a CJK character (i.e. cursor is on the padding),
295 //consider this location the cursor position since otherwise the cursor will be skipped
296 if(!atCursorLocation &&
297 translatedCursorPosition.getColumn() == columnIndex + 1 &&
298 translatedCursorPosition.getRow() == rowIndex &&
299 TerminalTextUtils.isCharCJK(character.getCharacter())) {
300 atCursorLocation = true;
301 }
302 int characterWidth = fontWidth * (TerminalTextUtils.isCharCJK(character.getCharacter()) ? 2 : 1);
303
304 Color foregroundColor = deriveTrueForegroundColor(character, atCursorLocation);
305 Color backgroundColor = deriveTrueBackgroundColor(character, atCursorLocation);
306
307 boolean drawCursor = atCursorLocation &&
308 (!deviceConfiguration.isCursorBlinking() || //Always draw if the cursor isn't blinking
309 (deviceConfiguration.isCursorBlinking() && blinkOn)); //If the cursor is blinking, only draw when blinkOn is true
310
311 CharacterState characterState = new CharacterState(character, foregroundColor, backgroundColor, drawCursor);
312 //if(!characterState.equals(visualState[rowIndex][columnIndex]) || terminalResized) {
313 drawCharacter(backbufferGraphics,
314 character,
315 columnIndex,
316 rowIndex,
317 foregroundColor,
318 backgroundColor,
319 fontWidth,
320 fontHeight,
321 characterWidth,
322 drawCursor);
323 visualState[rowIndex][columnIndex] = characterState;
324 if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
325 visualState[rowIndex][columnIndex+1] = characterState;
326 }
327 //}
328
329 if(character.getModifiers().contains(SGR.BLINK)) {
330 foundBlinkingCharacters = true;
331 }
332 if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
333 columnIndex++; //Skip the trailing space after a CJK character
334 }
335 }
336 rowIndex++;
337 }
338
339 // Take care of the left-over area at the bottom and right of the component where no character can fit
340 int leftoverHeight = getHeight() % fontHeight;
341 int leftoverWidth = getWidth() % fontWidth;
342 backbufferGraphics.setColor(Color.BLACK);
343 if(leftoverWidth > 0) {
344 backbufferGraphics.fillRect(getWidth() - leftoverWidth, 0, leftoverWidth, getHeight());
345 }
346 if(leftoverHeight > 0) {
347 backbufferGraphics.fillRect(0, getHeight() - leftoverHeight, getWidth(), leftoverHeight);
348 }
349 backbufferGraphics.dispose();
350
351 // Update the blink status according to if there were any blinking characters or not
352 this.hasBlinkingText = foundBlinkingCharacters;
353 }
354
355 private void ensureBackbufferHasRightSize() {
356 if(backbuffer == null) {
357 backbuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
358 }
359 if(backbuffer.getWidth() < getWidth() || backbuffer.getWidth() > getWidth() * 4 ||
360 backbuffer.getHeight() < getHeight() || backbuffer.getHeight() > getHeight() * 4) {
361 BufferedImage newBackbuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
362 Graphics2D graphics = newBackbuffer.createGraphics();
363 graphics.drawImage(backbuffer, 0, 0, null);
364 graphics.dispose();
365 backbuffer = newBackbuffer;
366 }
367 }
368
369 private void ensureVisualStateHasRightSize(TerminalSize terminalSize) {
370 if(visualState == null) {
371 visualState = new CharacterState[terminalSize.getRows() * 2][terminalSize.getColumns() * 2];
372 }
373 if(visualState.length < terminalSize.getRows() || visualState.length > Math.max(terminalSize.getRows(), 1) * 4) {
374 visualState = Arrays.copyOf(visualState, terminalSize.getRows() * 2);
375 }
376 for(int rowIndex = 0; rowIndex < visualState.length; rowIndex++) {
377 CharacterState[] row = visualState[rowIndex];
378 if(row == null) {
379 row = new CharacterState[terminalSize.getColumns() * 2];
380 visualState[rowIndex] = row;
381 }
382 if(row.length < terminalSize.getColumns() || row.length > Math.max(terminalSize.getColumns(), 1) * 4) {
383 row = Arrays.copyOf(row, terminalSize.getColumns() * 2);
384 visualState[rowIndex] = row;
385 }
386
387 // Make sure all items outside the 'real' terminal size are null
388 if(rowIndex < terminalSize.getRows()) {
389 Arrays.fill(row, terminalSize.getColumns(), row.length, null);
390 }
391 else {
392 Arrays.fill(row, null);
393 }
394 }
395 }
396
397 private void drawCharacter(
398 Graphics g,
399 TextCharacter character,
400 int columnIndex,
401 int rowIndex,
402 Color foregroundColor,
403 Color backgroundColor,
404 int fontWidth,
405 int fontHeight,
406 int characterWidth,
407 boolean drawCursor) {
408
409 int x = columnIndex * fontWidth;
410 int y = rowIndex * fontHeight;
411 g.setColor(backgroundColor);
412 g.setClip(x, y, characterWidth, fontHeight);
413 g.fillRect(x, y, characterWidth, fontHeight);
414
415 g.setColor(foregroundColor);
416 Font font = getFontForCharacter(character);
417 g.setFont(font);
418 FontMetrics fontMetrics = g.getFontMetrics();
419 g.drawString(Character.toString(character.getCharacter()), x, ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent());
420
421 if(character.isCrossedOut()) {
422 int lineStartX = x;
423 int lineStartY = y + (fontHeight / 2);
424 int lineEndX = lineStartX + characterWidth;
425 g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
426 }
427 if(character.isUnderlined()) {
428 int lineStartX = x;
429 int lineStartY = ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent() + 1;
430 int lineEndX = lineStartX + characterWidth;
431 g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
432 }
433
434 if(drawCursor) {
435 if(deviceConfiguration.getCursorColor() == null) {
436 g.setColor(foregroundColor);
437 }
438 else {
439 g.setColor(colorConfiguration.toAWTColor(deviceConfiguration.getCursorColor(), false, false));
440 }
441 if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.UNDER_BAR) {
442 g.fillRect(x, y + fontHeight - 3, characterWidth, 2);
443 }
444 else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.VERTICAL_BAR) {
445 g.fillRect(x, y + 1, 2, fontHeight - 2);
446 }
447 }
448 }
449
450
451 private Color deriveTrueForegroundColor(TextCharacter character, boolean atCursorLocation) {
452 TextColor foregroundColor = character.getForegroundColor();
453 TextColor backgroundColor = character.getBackgroundColor();
454 boolean reverse = character.isReversed();
455 boolean blink = character.isBlinking();
456
457 if(cursorIsVisible && atCursorLocation) {
458 if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
459 (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
460 reverse = true;
461 }
462 }
463
464 if(reverse && (!blink || !blinkOn)) {
465 return colorConfiguration.toAWTColor(backgroundColor, backgroundColor != TextColor.ANSI.DEFAULT, character.isBold());
466 }
467 else if(!reverse && blink && blinkOn) {
468 return colorConfiguration.toAWTColor(backgroundColor, false, character.isBold());
469 }
470 else {
471 return colorConfiguration.toAWTColor(foregroundColor, true, character.isBold());
472 }
473 }
474
475 private Color deriveTrueBackgroundColor(TextCharacter character, boolean atCursorLocation) {
476 TextColor foregroundColor = character.getForegroundColor();
477 TextColor backgroundColor = character.getBackgroundColor();
478 boolean reverse = character.isReversed();
479
480 if(cursorIsVisible && atCursorLocation) {
481 if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
482 (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
483 reverse = true;
484 }
485 else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.FIXED_BACKGROUND) {
486 backgroundColor = deviceConfiguration.getCursorColor();
487 }
488 }
489
490 if(reverse) {
491 return colorConfiguration.toAWTColor(foregroundColor, backgroundColor == TextColor.ANSI.DEFAULT, character.isBold());
492 }
493 else {
494 return colorConfiguration.toAWTColor(backgroundColor, false, false);
495 }
496 }
497
498 ///////////
499 // Then delegate all Terminal interface methods to the virtual terminal implementation
500 //
501 // Some of these methods we need to pass to the AWT-thread, which makes the call asynchronous. Hopefully this isn't
502 // causing too much problem...
503 ///////////
504 @Override
505 public KeyStroke pollInput() {
506 return keyQueue.poll();
507 }
508
509 @Override
510 public KeyStroke readInput() throws IOException {
511 try {
512 return keyQueue.take();
513 }
514 catch(InterruptedException ignore) {
515 throw new IOException("Blocking input was interrupted");
516 }
517 }
518
519 @Override
520 public synchronized void enterPrivateMode() {
521 virtualTerminal.switchToPrivateMode();
522 clearBackBufferAndVisualState();
523 flush();
524 }
525
526 @Override
527 public synchronized void exitPrivateMode() {
528 virtualTerminal.switchToNormalMode();
529 clearBackBufferAndVisualState();
530 flush();
531 }
532
533 @Override
534 public synchronized void clearScreen() {
535 virtualTerminal.clear();
536 clearBackBufferAndVisualState();
537 flush();
538 }
539
540 /**
541 * Clears out the back buffer and the resets the visual state so next paint operation will do a full repaint of
542 * everything
543 */
544 protected void clearBackBufferAndVisualState() {
545 // Manually clear the backbuffer and visual state
546 if(backbuffer != null) {
547 Graphics2D graphics = backbuffer.createGraphics();
548 Color foregroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, true, false);
549 Color backgroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false);
550 graphics.setColor(backgroundColor);
551 graphics.fillRect(0, 0, getWidth(), getHeight());
552 graphics.dispose();
553
554 for(CharacterState[] line : visualState) {
555 Arrays.fill(line, new CharacterState(new TextCharacter(' '), foregroundColor, backgroundColor, false));
556 }
557 }
558 }
559
560 @Override
561 public synchronized void setCursorPosition(final int x, final int y) {
562 virtualTerminal.setCursorPosition(new TerminalPosition(x, y));
563 }
564
565 @Override
566 public void setCursorVisible(final boolean visible) {
567 cursorIsVisible = visible;
568 }
569
570 @Override
571 public synchronized void putCharacter(final char c) {
572 virtualTerminal.putCharacter(new TextCharacter(c, foregroundColor, backgroundColor, activeSGRs));
573 }
574
575 @Override
576 public TextGraphics newTextGraphics() throws IOException {
577 return new VirtualTerminalTextGraphics(virtualTerminal);
578 }
579
580 @Override
581 public void enableSGR(final SGR sgr) {
582 activeSGRs.add(sgr);
583 }
584
585 @Override
586 public void disableSGR(final SGR sgr) {
587 activeSGRs.remove(sgr);
588 }
589
590 @Override
591 public void resetColorAndSGR() {
592 foregroundColor = TextColor.ANSI.DEFAULT;
593 backgroundColor = TextColor.ANSI.DEFAULT;
594 activeSGRs.clear();
595 }
596
597 @Override
598 public void setForegroundColor(final TextColor color) {
599 foregroundColor = color;
600 }
601
602 @Override
603 public void setBackgroundColor(final TextColor color) {
604 backgroundColor = color;
605 }
606
607 @Override
608 public synchronized TerminalSize getTerminalSize() {
609 return virtualTerminal.getSize();
610 }
611
612 @Override
613 public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
614 return enquiryString.getBytes();
615 }
616
617 @Override
618 public void flush() {
619 flushed = true;
620 repaint();
621 }
622
623 @Override
624 public void addResizeListener(ResizeListener listener) {
625 resizeListeners.add(listener);
626 }
627
628 @Override
629 public void removeResizeListener(ResizeListener listener) {
630 resizeListeners.remove(listener);
631 }
632
633 ///////////
634 // Remaining are private internal classes used by SwingTerminal
635 ///////////
636 private static final Set<Character> TYPED_KEYS_TO_IGNORE = new HashSet<Character>(Arrays.asList('\n', '\t', '\r', '\b', '\33', (char)127));
637
638 /**
639 * Class that translates AWT key events into Lanterna {@link KeyStroke}
640 */
641 protected class TerminalInputListener extends KeyAdapter {
642 @Override
643 public void keyTyped(KeyEvent e) {
644 char character = e.getKeyChar();
645 boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
646 boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
647
648 if(!TYPED_KEYS_TO_IGNORE.contains(character)) {
649 if(ctrlDown) {
650 //We need to re-adjust the character if ctrl is pressed, just like for the AnsiTerminal
651 character = (char) ('a' - 1 + character);
652 }
653 keyQueue.add(new KeyStroke(character, ctrlDown, altDown));
654 }
655 }
656
657 @Override
658 public void keyPressed(KeyEvent e) {
659 boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
660 boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
661 if(e.getKeyCode() == KeyEvent.VK_ENTER) {
662 keyQueue.add(new KeyStroke(KeyType.Enter, ctrlDown, altDown));
663 }
664 else if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
665 keyQueue.add(new KeyStroke(KeyType.Escape, ctrlDown, altDown));
666 }
667 else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
668 keyQueue.add(new KeyStroke(KeyType.Backspace, ctrlDown, altDown));
669 }
670 else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
671 keyQueue.add(new KeyStroke(KeyType.ArrowLeft, ctrlDown, altDown));
672 }
673 else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
674 keyQueue.add(new KeyStroke(KeyType.ArrowRight, ctrlDown, altDown));
675 }
676 else if(e.getKeyCode() == KeyEvent.VK_UP) {
677 keyQueue.add(new KeyStroke(KeyType.ArrowUp, ctrlDown, altDown));
678 }
679 else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
680 keyQueue.add(new KeyStroke(KeyType.ArrowDown, ctrlDown, altDown));
681 }
682 else if(e.getKeyCode() == KeyEvent.VK_INSERT) {
683 keyQueue.add(new KeyStroke(KeyType.Insert, ctrlDown, altDown));
684 }
685 else if(e.getKeyCode() == KeyEvent.VK_DELETE) {
686 keyQueue.add(new KeyStroke(KeyType.Delete, ctrlDown, altDown));
687 }
688 else if(e.getKeyCode() == KeyEvent.VK_HOME) {
689 keyQueue.add(new KeyStroke(KeyType.Home, ctrlDown, altDown));
690 }
691 else if(e.getKeyCode() == KeyEvent.VK_END) {
692 keyQueue.add(new KeyStroke(KeyType.End, ctrlDown, altDown));
693 }
694 else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
695 keyQueue.add(new KeyStroke(KeyType.PageUp, ctrlDown, altDown));
696 }
697 else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
698 keyQueue.add(new KeyStroke(KeyType.PageDown, ctrlDown, altDown));
699 }
700 else if(e.getKeyCode() == KeyEvent.VK_F1) {
701 keyQueue.add(new KeyStroke(KeyType.F1, ctrlDown, altDown));
702 }
703 else if(e.getKeyCode() == KeyEvent.VK_F2) {
704 keyQueue.add(new KeyStroke(KeyType.F2, ctrlDown, altDown));
705 }
706 else if(e.getKeyCode() == KeyEvent.VK_F3) {
707 keyQueue.add(new KeyStroke(KeyType.F3, ctrlDown, altDown));
708 }
709 else if(e.getKeyCode() == KeyEvent.VK_F4) {
710 keyQueue.add(new KeyStroke(KeyType.F4, ctrlDown, altDown));
711 }
712 else if(e.getKeyCode() == KeyEvent.VK_F5) {
713 keyQueue.add(new KeyStroke(KeyType.F5, ctrlDown, altDown));
714 }
715 else if(e.getKeyCode() == KeyEvent.VK_F6) {
716 keyQueue.add(new KeyStroke(KeyType.F6, ctrlDown, altDown));
717 }
718 else if(e.getKeyCode() == KeyEvent.VK_F7) {
719 keyQueue.add(new KeyStroke(KeyType.F7, ctrlDown, altDown));
720 }
721 else if(e.getKeyCode() == KeyEvent.VK_F8) {
722 keyQueue.add(new KeyStroke(KeyType.F8, ctrlDown, altDown));
723 }
724 else if(e.getKeyCode() == KeyEvent.VK_F9) {
725 keyQueue.add(new KeyStroke(KeyType.F9, ctrlDown, altDown));
726 }
727 else if(e.getKeyCode() == KeyEvent.VK_F10) {
728 keyQueue.add(new KeyStroke(KeyType.F10, ctrlDown, altDown));
729 }
730 else if(e.getKeyCode() == KeyEvent.VK_F11) {
731 keyQueue.add(new KeyStroke(KeyType.F11, ctrlDown, altDown));
732 }
733 else if(e.getKeyCode() == KeyEvent.VK_F12) {
734 keyQueue.add(new KeyStroke(KeyType.F12, ctrlDown, altDown));
735 }
736 else if(e.getKeyCode() == KeyEvent.VK_TAB) {
737 if(e.isShiftDown()) {
738 keyQueue.add(new KeyStroke(KeyType.ReverseTab, ctrlDown, altDown));
739 }
740 else {
741 keyQueue.add(new KeyStroke(KeyType.Tab, ctrlDown, altDown));
742 }
743 }
744 else {
745 //keyTyped doesn't catch this scenario (for whatever reason...) so we have to do it here
746 if(altDown && ctrlDown && e.getKeyCode() >= 'A' && e.getKeyCode() <= 'Z') {
747 char asLowerCase = Character.toLowerCase((char) e.getKeyCode());
748 keyQueue.add(new KeyStroke(asLowerCase, true, true));
749 }
750 }
751 }
752 }
753
754 private static class CharacterState {
755 private final TextCharacter textCharacter;
756 private final Color foregroundColor;
757 private final Color backgroundColor;
758 private final boolean drawCursor;
759
760 CharacterState(TextCharacter textCharacter, Color foregroundColor, Color backgroundColor, boolean drawCursor) {
761 this.textCharacter = textCharacter;
762 this.foregroundColor = foregroundColor;
763 this.backgroundColor = backgroundColor;
764 this.drawCursor = drawCursor;
765 }
766
767 @Override
768 public boolean equals(Object o) {
769 if(this == o) {
770 return true;
771 }
772 if(o == null || getClass() != o.getClass()) {
773 return false;
774 }
775 CharacterState that = (CharacterState) o;
776 if(drawCursor != that.drawCursor) {
777 return false;
778 }
779 if(!textCharacter.equals(that.textCharacter)) {
780 return false;
781 }
782 if(!foregroundColor.equals(that.foregroundColor)) {
783 return false;
784 }
785 return backgroundColor.equals(that.backgroundColor);
786 }
787
788 @Override
789 public int hashCode() {
790 int result = textCharacter.hashCode();
791 result = 31 * result + foregroundColor.hashCode();
792 result = 31 * result + backgroundColor.hashCode();
793 result = 31 * result + (drawCursor ? 1 : 0);
794 return result;
795 }
796
797 @Override
798 public String toString() {
799 return "CharacterState{" +
800 "textCharacter=" + textCharacter +
801 ", foregroundColor=" + foregroundColor +
802 ", backgroundColor=" + backgroundColor +
803 ", drawCursor=" + drawCursor +
804 '}';
805 }
806 }
807 }