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 | |
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 | } |