cdca714bb1cff6765144cc381c5bfef20ae6c16c
[nikiroo-utils.git] / src / jexer / io / SwingScreen.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer.io;
32
33 import java.awt.Color;
34 import java.awt.Cursor;
35 import java.awt.Font;
36 import java.awt.FontMetrics;
37 import java.awt.Graphics;
38 import java.awt.Insets;
39 import java.awt.Point;
40 import java.awt.Rectangle;
41 import java.awt.Toolkit;
42 import java.awt.geom.Rectangle2D;
43 import java.awt.image.BufferedImage;
44 import java.awt.image.BufferStrategy;
45 import java.io.InputStream;
46 import java.util.Date;
47 import javax.swing.JFrame;
48 import javax.swing.SwingUtilities;
49
50 import jexer.bits.Cell;
51 import jexer.bits.CellAttributes;
52 import jexer.session.SwingSessionInfo;
53
54 /**
55 * This Screen implementation draws to a Java Swing JFrame.
56 */
57 public final class SwingScreen extends Screen {
58
59 /**
60 * If true, use double buffering thread.
61 */
62 private static final boolean doubleBuffer = true;
63
64 /**
65 * Cursor style to draw.
66 */
67 public enum CursorStyle {
68 /**
69 * Use an underscore for the cursor.
70 */
71 UNDERLINE,
72
73 /**
74 * Use a solid block for the cursor.
75 */
76 BLOCK,
77
78 /**
79 * Use an outlined block for the cursor.
80 */
81 OUTLINE
82 }
83
84 private static Color MYBLACK;
85 private static Color MYRED;
86 private static Color MYGREEN;
87 private static Color MYYELLOW;
88 private static Color MYBLUE;
89 private static Color MYMAGENTA;
90 private static Color MYCYAN;
91 private static Color MYWHITE;
92
93 private static Color MYBOLD_BLACK;
94 private static Color MYBOLD_RED;
95 private static Color MYBOLD_GREEN;
96 private static Color MYBOLD_YELLOW;
97 private static Color MYBOLD_BLUE;
98 private static Color MYBOLD_MAGENTA;
99 private static Color MYBOLD_CYAN;
100 private static Color MYBOLD_WHITE;
101
102 private static boolean dosColors = false;
103
104 /**
105 * Setup Swing colors to match DOS color palette.
106 */
107 private static void setDOSColors() {
108 if (dosColors) {
109 return;
110 }
111 MYBLACK = new Color(0x00, 0x00, 0x00);
112 MYRED = new Color(0xa8, 0x00, 0x00);
113 MYGREEN = new Color(0x00, 0xa8, 0x00);
114 MYYELLOW = new Color(0xa8, 0x54, 0x00);
115 MYBLUE = new Color(0x00, 0x00, 0xa8);
116 MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
117 MYCYAN = new Color(0x00, 0xa8, 0xa8);
118 MYWHITE = new Color(0xa8, 0xa8, 0xa8);
119 MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
120 MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
121 MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
122 MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
123 MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
124 MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
125 MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
126 MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
127
128 dosColors = true;
129 }
130
131 /**
132 * SwingFrame is our top-level hook into the Swing system.
133 */
134 class SwingFrame extends JFrame {
135
136 /**
137 * Serializable version.
138 */
139 private static final long serialVersionUID = 1;
140
141 /**
142 * The terminus font resource filename.
143 */
144 private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
145
146 /**
147 * The BufferStrategy object needed for double-buffering.
148 */
149 private BufferStrategy bufferStrategy;
150
151 /**
152 * The TUI Screen data.
153 */
154 SwingScreen screen;
155
156 /**
157 * Width of a character cell.
158 */
159 private int textWidth = 1;
160
161 /**
162 * Height of a character cell.
163 */
164 private int textHeight = 1;
165
166 /**
167 * Descent of a character cell.
168 */
169 private int maxDescent = 0;
170
171 /**
172 * System-dependent Y adjustment for text in the character cell.
173 */
174 private int textAdjustY = 0;
175
176 /**
177 * System-dependent X adjustment for text in the character cell.
178 */
179 private int textAdjustX = 0;
180
181 /**
182 * Top pixel absolute location.
183 */
184 private int top = 30;
185
186 /**
187 * Left pixel absolute location.
188 */
189 private int left = 30;
190
191 /**
192 * The cursor style to draw.
193 */
194 private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
195
196 /**
197 * The number of millis to wait before switching the blink from
198 * visible to invisible.
199 */
200 private long blinkMillis = 500;
201
202 /**
203 * If true, the cursor should be visible right now based on the blink
204 * time.
205 */
206 private boolean cursorBlinkVisible = true;
207
208 /**
209 * The time that the blink last flipped from visible to invisible or
210 * from invisible to visible.
211 */
212 private long lastBlinkTime = 0;
213
214 /**
215 * Convert a CellAttributes foreground color to an Swing Color.
216 *
217 * @param attr the text attributes
218 * @return the Swing Color
219 */
220 private Color attrToForegroundColor(final CellAttributes attr) {
221 if (attr.isBold()) {
222 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
223 return MYBOLD_BLACK;
224 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
225 return MYBOLD_RED;
226 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
227 return MYBOLD_BLUE;
228 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
229 return MYBOLD_GREEN;
230 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
231 return MYBOLD_YELLOW;
232 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
233 return MYBOLD_CYAN;
234 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
235 return MYBOLD_MAGENTA;
236 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
237 return MYBOLD_WHITE;
238 }
239 } else {
240 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
241 return MYBLACK;
242 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
243 return MYRED;
244 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
245 return MYBLUE;
246 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
247 return MYGREEN;
248 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
249 return MYYELLOW;
250 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
251 return MYCYAN;
252 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
253 return MYMAGENTA;
254 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
255 return MYWHITE;
256 }
257 }
258 throw new IllegalArgumentException("Invalid color: " + attr.getForeColor().getValue());
259 }
260
261 /**
262 * Convert a CellAttributes background color to an Swing Color.
263 *
264 * @param attr the text attributes
265 * @return the Swing Color
266 */
267 private Color attrToBackgroundColor(final CellAttributes attr) {
268 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
269 return MYBLACK;
270 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
271 return MYRED;
272 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
273 return MYBLUE;
274 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
275 return MYGREEN;
276 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
277 return MYYELLOW;
278 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
279 return MYCYAN;
280 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
281 return MYMAGENTA;
282 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
283 return MYWHITE;
284 }
285 throw new IllegalArgumentException("Invalid color: " + attr.getBackColor().getValue());
286 }
287
288 /**
289 * Public constructor.
290 *
291 * @param screen the Screen that Backend talks to
292 */
293 public SwingFrame(final SwingScreen screen) {
294 this.screen = screen;
295 setDOSColors();
296
297 // Figure out my cursor style
298 String cursorStyleString = System.getProperty("jexer.Swing.cursorStyle",
299 "underline").toLowerCase();
300
301 if (cursorStyleString.equals("underline")) {
302 cursorStyle = CursorStyle.UNDERLINE;
303 } else if (cursorStyleString.equals("outline")) {
304 cursorStyle = CursorStyle.OUTLINE;
305 } else if (cursorStyleString.equals("block")) {
306 cursorStyle = CursorStyle.BLOCK;
307 }
308
309 setTitle("Jexer Application");
310 setBackground(Color.black);
311
312 try {
313 // Always try to use Terminus, the one decent font.
314 ClassLoader loader = Thread.currentThread().getContextClassLoader();
315 InputStream in = loader.getResourceAsStream(FONTFILE);
316 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
317 Font terminus = terminusRoot.deriveFont(Font.PLAIN, 22);
318 setFont(terminus);
319 } catch (Exception e) {
320 e.printStackTrace();
321 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
322 setFont(new Font(Font.MONOSPACED, Font.PLAIN, 24));
323 }
324 pack();
325
326 // Kill the X11 cursor
327 // Transparent 16 x 16 pixel cursor image.
328 BufferedImage cursorImg = new BufferedImage(16, 16,
329 BufferedImage.TYPE_INT_ARGB);
330 // Create a new blank cursor.
331 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
332 cursorImg, new Point(0, 0), "blank cursor");
333 setCursor(blankCursor);
334
335 // Be capable of seeing Tab / Shift-Tab
336 setFocusTraversalKeysEnabled(false);
337
338 // Save the text cell width/height
339 getFontDimensions();
340
341 // Setup double-buffering
342 if (SwingScreen.doubleBuffer) {
343 setIgnoreRepaint(true);
344 createBufferStrategy(2);
345 bufferStrategy = getBufferStrategy();
346 }
347 }
348
349 /**
350 * Figure out my font dimensions.
351 */
352 private void getFontDimensions() {
353 Graphics gr = getGraphics();
354 FontMetrics fm = gr.getFontMetrics();
355 maxDescent = fm.getMaxDescent();
356 Rectangle2D bounds = fm.getMaxCharBounds(gr);
357 int leading = fm.getLeading();
358 textWidth = (int)Math.round(bounds.getWidth());
359 textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
360 // This also produces the same number, but works better for ugly
361 // monospace.
362 textHeight = fm.getMaxAscent() + maxDescent - leading;
363
364 if (System.getProperty("os.name").startsWith("Windows")) {
365 textAdjustY = -1;
366 textAdjustX = 0;
367 }
368 }
369
370 /**
371 * Resize to font dimensions.
372 */
373 public void resizeToScreen() {
374 // Figure out the thickness of borders and use that to set the
375 // final size.
376 Insets insets = getInsets();
377 left = insets.left;
378 top = insets.top;
379
380 setSize(textWidth * screen.width + insets.left + insets.right,
381 textHeight * screen.height + insets.top + insets.bottom);
382 }
383
384 /**
385 * Update redraws the whole screen.
386 *
387 * @param gr the Swing Graphics context
388 */
389 @Override
390 public void update(final Graphics gr) {
391 // The default update clears the area. Don't do that, instead
392 // just paint it directly.
393 paint(gr);
394 }
395
396 /**
397 * Paint redraws the whole screen.
398 *
399 * @param gr the Swing Graphics context
400 */
401 @Override
402 public void paint(final Graphics gr) {
403 // Do nothing until the screen reference has been set.
404 if (screen == null) {
405 return;
406 }
407 if (screen.frame == null) {
408 return;
409 }
410
411 // See if it is time to flip the blink time.
412 long nowTime = (new Date()).getTime();
413 if (nowTime > blinkMillis + lastBlinkTime) {
414 lastBlinkTime = nowTime;
415 cursorBlinkVisible = !cursorBlinkVisible;
416 }
417
418 int xCellMin = 0;
419 int xCellMax = screen.width;
420 int yCellMin = 0;
421 int yCellMax = screen.height;
422
423 Rectangle bounds = gr.getClipBounds();
424 if (bounds != null) {
425 // Only update what is in the bounds
426 xCellMin = screen.textColumn(bounds.x);
427 xCellMax = screen.textColumn(bounds.x + bounds.width);
428 if (xCellMax > screen.width) {
429 xCellMax = screen.width;
430 }
431 if (xCellMin >= xCellMax) {
432 xCellMin = xCellMax - 2;
433 }
434 if (xCellMin < 0) {
435 xCellMin = 0;
436 }
437 yCellMin = screen.textRow(bounds.y);
438 yCellMax = screen.textRow(bounds.y + bounds.height);
439 if (yCellMax > screen.height) {
440 yCellMax = screen.height;
441 }
442 if (yCellMin >= yCellMax) {
443 yCellMin = yCellMax - 2;
444 }
445 if (yCellMin < 0) {
446 yCellMin = 0;
447 }
448 } else {
449 // We need a total repaint
450 reallyCleared = true;
451 }
452
453 // Prevent updates to the screen's data from the TApplication
454 // threads.
455 synchronized (screen) {
456 /*
457 System.err.printf("bounds %s X %d %d Y %d %d\n",
458 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
459 */
460
461 for (int y = yCellMin; y < yCellMax; y++) {
462 for (int x = xCellMin; x < xCellMax; x++) {
463
464 int xPixel = x * textWidth + left;
465 int yPixel = y * textHeight + top;
466
467 Cell lCell = screen.logical[x][y];
468 Cell pCell = screen.physical[x][y];
469
470 if (!lCell.equals(pCell)
471 || lCell.isBlink()
472 || reallyCleared) {
473
474 Cell lCellColor = new Cell();
475 lCellColor.setTo(lCell);
476
477 // Check for reverse
478 if (lCell.isReverse()) {
479 lCellColor.setForeColor(lCell.getBackColor());
480 lCellColor.setBackColor(lCell.getForeColor());
481 }
482
483 // Draw the background rectangle, then the
484 // foreground character.
485 gr.setColor(attrToBackgroundColor(lCellColor));
486 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
487
488 // Handle blink and underline
489 if (!lCell.isBlink()
490 || (lCell.isBlink() && cursorBlinkVisible)
491 ) {
492 gr.setColor(attrToForegroundColor(lCellColor));
493 char [] chars = new char[1];
494 chars[0] = lCell.getChar();
495 gr.drawChars(chars, 0, 1, xPixel + textAdjustX,
496 yPixel + textHeight - maxDescent
497 + textAdjustY);
498
499 if (lCell.isUnderline()) {
500 gr.fillRect(xPixel, yPixel + textHeight - 2,
501 textWidth, 2);
502 }
503 }
504
505 // Physical is always updated
506 physical[x][y].setTo(lCell);
507 }
508 }
509 }
510
511 // Draw the cursor if it is visible
512 if (cursorVisible
513 && (cursorY <= screen.height - 1)
514 && (cursorX <= screen.width - 1)
515 && cursorBlinkVisible
516 ) {
517 int xPixel = cursorX * textWidth + left;
518 int yPixel = cursorY * textHeight + top;
519 Cell lCell = screen.logical[cursorX][cursorY];
520 gr.setColor(attrToForegroundColor(lCell));
521 switch (cursorStyle) {
522 default:
523 // Fall through...
524 case UNDERLINE:
525 gr.fillRect(xPixel, yPixel + textHeight - 2,
526 textWidth, 2);
527 break;
528 case BLOCK:
529 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
530 break;
531 case OUTLINE:
532 gr.drawRect(xPixel, yPixel, textWidth - 1,
533 textHeight - 1);
534 break;
535 }
536 }
537
538 dirty = false;
539 reallyCleared = false;
540 } // synchronized (screen)
541 }
542
543 } // class SwingFrame
544
545 /**
546 * The raw Swing JFrame. Note package private access.
547 */
548 SwingFrame frame;
549
550 /**
551 * Restore terminal to normal state.
552 */
553 public void shutdown() {
554 frame.dispose();
555 }
556
557 /**
558 * Public constructor.
559 */
560 public SwingScreen() {
561 try {
562 SwingUtilities.invokeAndWait(new Runnable() {
563 public void run() {
564 SwingScreen.this.frame = new SwingFrame(SwingScreen.this);
565 SwingScreen.this.sessionInfo =
566 new SwingSessionInfo(SwingScreen.this.frame,
567 frame.textWidth,
568 frame.textHeight);
569
570 SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(),
571 sessionInfo.getWindowHeight());
572
573 SwingScreen.this.frame.resizeToScreen();
574 SwingScreen.this.frame.setVisible(true);
575 }
576 });
577 } catch (Exception e) {
578 e.printStackTrace();
579 }
580 }
581
582 /**
583 * The sessionInfo.
584 */
585 private SwingSessionInfo sessionInfo;
586
587 /**
588 * Create the SwingSessionInfo. Note package private access.
589 *
590 * @return the sessionInfo
591 */
592 SwingSessionInfo getSessionInfo() {
593 return sessionInfo;
594 }
595
596 /**
597 * Push the logical screen to the physical device.
598 */
599 @Override
600 public void flushPhysical() {
601
602 if (reallyCleared) {
603 // Really refreshed, do it all
604 if (SwingScreen.doubleBuffer) {
605 Graphics gr = frame.bufferStrategy.getDrawGraphics();
606 frame.paint(gr);
607 gr.dispose();
608 frame.bufferStrategy.show();
609 Toolkit.getDefaultToolkit().sync();
610 } else {
611 frame.repaint();
612 }
613 return;
614 }
615
616 // Do nothing if nothing happened.
617 if (!dirty) {
618 return;
619 }
620
621 // Request a repaint, let the frame's repaint/update methods do the
622 // right thing.
623
624 // Find the minimum-size damaged region.
625 int xMin = frame.getWidth();
626 int xMax = 0;
627 int yMin = frame.getHeight();
628 int yMax = 0;
629
630 synchronized (this) {
631 for (int y = 0; y < height; y++) {
632 for (int x = 0; x < width; x++) {
633 Cell lCell = logical[x][y];
634 Cell pCell = physical[x][y];
635
636 int xPixel = x * frame.textWidth + frame.left;
637 int yPixel = y * frame.textHeight + frame.top;
638
639 if (!lCell.equals(pCell)
640 || ((x == cursorX)
641 && (y == cursorY)
642 && cursorVisible)
643 || lCell.isBlink()
644 ) {
645 if (xPixel < xMin) {
646 xMin = xPixel;
647 }
648 if (xPixel + frame.textWidth > xMax) {
649 xMax = xPixel + frame.textWidth;
650 }
651 if (yPixel < yMin) {
652 yMin = yPixel;
653 }
654 if (yPixel + frame.textHeight > yMax) {
655 yMax = yPixel + frame.textHeight;
656 }
657 }
658 }
659 }
660 }
661 if (xMin + frame.textWidth >= xMax) {
662 xMax += frame.textWidth;
663 }
664 if (yMin + frame.textHeight >= yMax) {
665 yMax += frame.textHeight;
666 }
667
668 // Repaint the desired area
669 // System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
670 // yMin, yMax);
671 if (SwingScreen.doubleBuffer) {
672 Graphics gr = frame.bufferStrategy.getDrawGraphics();
673 Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
674 yMax - yMin);
675 gr.setClip(bounds);
676 frame.paint(gr);
677 gr.dispose();
678 frame.bufferStrategy.show();
679 Toolkit.getDefaultToolkit().sync();
680 } else {
681 frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
682 }
683 }
684
685 /**
686 * Put the cursor at (x,y).
687 *
688 * @param visible if true, the cursor should be visible
689 * @param x column coordinate to put the cursor on
690 * @param y row coordinate to put the cursor on
691 */
692 @Override
693 public void putCursor(final boolean visible, final int x, final int y) {
694
695 if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) {
696 // See if it is time to flip the blink time.
697 long nowTime = (new Date()).getTime();
698 if (nowTime < frame.blinkMillis + frame.lastBlinkTime) {
699 // Nothing has changed, so don't do anything.
700 return;
701 }
702 }
703
704 if (cursorVisible
705 && (cursorY <= height - 1)
706 && (cursorX <= width - 1)
707 ) {
708 // Make the current cursor position dirty
709 if (physical[cursorX][cursorY].getChar() == 'Q') {
710 physical[cursorX][cursorY].setChar('X');
711 } else {
712 physical[cursorX][cursorY].setChar('Q');
713 }
714 }
715
716 super.putCursor(visible, x, y);
717 }
718
719 /**
720 * Convert pixel column position to text cell column position.
721 *
722 * @param x pixel column position
723 * @return text cell column position
724 */
725 public int textColumn(final int x) {
726 return ((x - frame.left) / frame.textWidth);
727 }
728
729 /**
730 * Convert pixel row position to text cell row position.
731 *
732 * @param y pixel row position
733 * @return text cell row position
734 */
735 public int textRow(final int y) {
736 return ((y - frame.top) / frame.textHeight);
737 }
738
739 }