Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | |
bcb54330 | 252 | if(flushed) { |
a3b510ab NR |
253 | updateBackBuffer(fontWidth, fontHeight, terminalResized, terminalSize); |
254 | flushed = false; | |
bcb54330 | 255 | } |
a3b510ab NR |
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(); | |
a3b510ab NR |
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); | |
bcb54330 | 310 | if(!characterState.equals(visualState[rowIndex][columnIndex]) || terminalResized) { |
a3b510ab NR |
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 | } | |
bcb54330 | 325 | } |
a3b510ab NR |
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 | } |