stub maven support
[nikiroo-utils.git] / src / jexer / io / SwingScreen.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.io;
30
31 import java.awt.Color;
32 import java.awt.Cursor;
33 import java.awt.Font;
34 import java.awt.FontMetrics;
35 import java.awt.Graphics;
36 import java.awt.Graphics2D;
37 import java.awt.Insets;
38 import java.awt.Point;
39 import java.awt.Rectangle;
40 import java.awt.Toolkit;
41 import java.awt.geom.Rectangle2D;
42 import java.awt.image.BufferedImage;
43 import java.awt.image.BufferStrategy;
44 import java.io.InputStream;
45 import java.util.Date;
46 import java.util.HashMap;
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 triple buffering thread.
61 */
62 private static boolean tripleBuffer = 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 triple-buffering.
148 */
149 private BufferStrategy bufferStrategy;
150
151 /**
152 * A cache of previously-rendered glyphs for blinking text, when it
153 * is not visible.
154 */
155 private HashMap<Cell, BufferedImage> glyphCacheBlink;
156
157 /**
158 * A cache of previously-rendered glyphs for non-blinking, or
159 * blinking-and-visible, text.
160 */
161 private HashMap<Cell, BufferedImage> glyphCache;
162
163 /**
164 * The TUI Screen data.
165 */
166 SwingScreen screen;
167
168 /**
169 * If true, we were successful getting Terminus.
170 */
171 private boolean gotTerminus = false;
172
173 /**
174 * Width of a character cell.
175 */
176 private int textWidth = 1;
177
178 /**
179 * Height of a character cell.
180 */
181 private int textHeight = 1;
182
183 /**
184 * Descent of a character cell.
185 */
186 private int maxDescent = 0;
187
188 /**
189 * System-dependent Y adjustment for text in the character cell.
190 */
191 private int textAdjustY = 0;
192
193 /**
194 * System-dependent X adjustment for text in the character cell.
195 */
196 private int textAdjustX = 0;
197
198 /**
199 * Top pixel absolute location.
200 */
201 private int top = 30;
202
203 /**
204 * Left pixel absolute location.
205 */
206 private int left = 30;
207
208 /**
209 * The cursor style to draw.
210 */
211 private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
212
213 /**
214 * The number of millis to wait before switching the blink from
215 * visible to invisible.
216 */
217 private long blinkMillis = 500;
218
219 /**
220 * If true, the cursor should be visible right now based on the blink
221 * time.
222 */
223 private boolean cursorBlinkVisible = true;
224
225 /**
226 * The time that the blink last flipped from visible to invisible or
227 * from invisible to visible.
228 */
229 private long lastBlinkTime = 0;
230
231 /**
232 * Convert a CellAttributes foreground color to an Swing Color.
233 *
234 * @param attr the text attributes
235 * @return the Swing Color
236 */
237 private Color attrToForegroundColor(final CellAttributes attr) {
238 if (attr.isBold()) {
239 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
240 return MYBOLD_BLACK;
241 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
242 return MYBOLD_RED;
243 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
244 return MYBOLD_BLUE;
245 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
246 return MYBOLD_GREEN;
247 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
248 return MYBOLD_YELLOW;
249 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
250 return MYBOLD_CYAN;
251 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
252 return MYBOLD_MAGENTA;
253 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
254 return MYBOLD_WHITE;
255 }
256 } else {
257 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
258 return MYBLACK;
259 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
260 return MYRED;
261 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
262 return MYBLUE;
263 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
264 return MYGREEN;
265 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
266 return MYYELLOW;
267 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
268 return MYCYAN;
269 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
270 return MYMAGENTA;
271 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
272 return MYWHITE;
273 }
274 }
275 throw new IllegalArgumentException("Invalid color: " +
276 attr.getForeColor().getValue());
277 }
278
279 /**
280 * Convert a CellAttributes background color to an Swing Color.
281 *
282 * @param attr the text attributes
283 * @return the Swing Color
284 */
285 private Color attrToBackgroundColor(final CellAttributes attr) {
286 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
287 return MYBLACK;
288 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
289 return MYRED;
290 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
291 return MYBLUE;
292 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
293 return MYGREEN;
294 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
295 return MYYELLOW;
296 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
297 return MYCYAN;
298 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
299 return MYMAGENTA;
300 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
301 return MYWHITE;
302 }
303 throw new IllegalArgumentException("Invalid color: " +
304 attr.getBackColor().getValue());
305 }
306
307 /**
308 * Public constructor.
309 *
310 * @param screen the Screen that Backend talks to
311 * @param fontSize the size in points. Good values to pick are: 16,
312 * 20, 22, and 24.
313 */
314 public SwingFrame(final SwingScreen screen, final int fontSize) {
315 this.screen = screen;
316 setDOSColors();
317
318 // Figure out my cursor style
319 String cursorStyleString = System.getProperty(
320 "jexer.Swing.cursorStyle", "underline").toLowerCase();
321
322 if (cursorStyleString.equals("underline")) {
323 cursorStyle = CursorStyle.UNDERLINE;
324 } else if (cursorStyleString.equals("outline")) {
325 cursorStyle = CursorStyle.OUTLINE;
326 } else if (cursorStyleString.equals("block")) {
327 cursorStyle = CursorStyle.BLOCK;
328 }
329
330 if (System.getProperty("jexer.Swing.tripleBuffer") != null) {
331 if (System.getProperty("jexer.Swing.tripleBuffer").
332 equals("false")) {
333
334 SwingScreen.tripleBuffer = false;
335 }
336 }
337
338 setTitle("Jexer Application");
339 setBackground(Color.black);
340
341 try {
342 // Always try to use Terminus, the one decent font.
343 ClassLoader loader = Thread.currentThread().
344 getContextClassLoader();
345 InputStream in = loader.getResourceAsStream(FONTFILE);
346 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
347 Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
348 setFont(terminus);
349 gotTerminus = true;
350 } catch (Exception e) {
351 e.printStackTrace();
352 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
353 setFont(new Font(Font.MONOSPACED, Font.PLAIN, fontSize));
354 }
355 pack();
356
357 // Kill the X11 cursor
358 // Transparent 16 x 16 pixel cursor image.
359 BufferedImage cursorImg = new BufferedImage(16, 16,
360 BufferedImage.TYPE_INT_ARGB);
361 // Create a new blank cursor.
362 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
363 cursorImg, new Point(0, 0), "blank cursor");
364 setCursor(blankCursor);
365
366 // Be capable of seeing Tab / Shift-Tab
367 setFocusTraversalKeysEnabled(false);
368
369 // Save the text cell width/height
370 getFontDimensions();
371
372 // Cache glyphs as they are rendered
373 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
374 glyphCache = new HashMap<Cell, BufferedImage>();
375
376 // Setup triple-buffering
377 if (SwingScreen.tripleBuffer) {
378 setIgnoreRepaint(true);
379 createBufferStrategy(3);
380 bufferStrategy = getBufferStrategy();
381 }
382 }
383
384 /**
385 * Figure out what textAdjustX and textAdjustY should be, based on
386 * the location of a vertical bar (to find textAdjustY) and a
387 * horizontal bar (to find textAdjustX).
388 *
389 * @return true if textAdjustX and textAdjustY were guessed at
390 * correctly
391 */
392 private boolean getFontAdjustments() {
393 BufferedImage image = null;
394
395 // What SHOULD happen is that the topmost/leftmost white pixel is
396 // at position (gr2x, gr2y). But it might also be off by a pixel
397 // in either direction.
398
399 Graphics2D gr2 = null;
400 int gr2x = 3;
401 int gr2y = 3;
402 image = new BufferedImage(textWidth * 2, textHeight * 2,
403 BufferedImage.TYPE_INT_ARGB);
404
405 gr2 = image.createGraphics();
406 gr2.setFont(getFont());
407 gr2.setColor(java.awt.Color.BLACK);
408 gr2.fillRect(0, 0, textWidth * 2, textHeight * 2);
409 gr2.setColor(java.awt.Color.WHITE);
410 char [] chars = new char[1];
411 chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR;
412 gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent);
413 gr2.dispose();
414
415 for (int x = 0; x < textWidth; x++) {
416 for (int y = 0; y < textHeight; y++) {
417
418 /*
419 System.err.println("X: " + x + " Y: " + y + " " +
420 image.getRGB(x, y));
421 */
422
423 if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
424 textAdjustY = (gr2y - y);
425
426 // System.err.println("textAdjustY: " + textAdjustY);
427 x = textWidth;
428 break;
429 }
430 }
431 }
432
433 gr2 = image.createGraphics();
434 gr2.setFont(getFont());
435 gr2.setColor(java.awt.Color.BLACK);
436 gr2.fillRect(0, 0, textWidth * 2, textHeight * 2);
437 gr2.setColor(java.awt.Color.WHITE);
438 chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR;
439 gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent);
440 gr2.dispose();
441
442 for (int x = 0; x < textWidth; x++) {
443 for (int y = 0; y < textHeight; y++) {
444
445 /*
446 System.err.println("X: " + x + " Y: " + y + " " +
447 image.getRGB(x, y));
448 */
449
450 if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
451 textAdjustX = (gr2x - x);
452
453 // System.err.println("textAdjustX: " + textAdjustX);
454 return true;
455 }
456 }
457 }
458
459 // Something weird happened, don't rely on this function.
460 // System.err.println("getFontAdjustments: false");
461 return false;
462 }
463
464
465 /**
466 * Figure out my font dimensions.
467 */
468 private void getFontDimensions() {
469 Graphics gr = getGraphics();
470 FontMetrics fm = gr.getFontMetrics();
471 maxDescent = fm.getMaxDescent();
472 Rectangle2D bounds = fm.getMaxCharBounds(gr);
473 int leading = fm.getLeading();
474 textWidth = (int)Math.round(bounds.getWidth());
475 // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
476
477 // This produces the same number, but works better for ugly
478 // monospace.
479 textHeight = fm.getMaxAscent() + maxDescent - leading;
480
481 if (gotTerminus == true) {
482 textHeight++;
483 }
484
485 if (getFontAdjustments() == false) {
486 // We were unable to programmatically determine textAdjustX
487 // and textAdjustY, so try some guesses based on VM vendor.
488 String runtime = System.getProperty("java.runtime.name");
489 if ((runtime != null) && (runtime.contains("Java(TM)"))) {
490 textAdjustY = -1;
491 textAdjustX = 0;
492 }
493 }
494 }
495
496 /**
497 * Resize to font dimensions.
498 */
499 public void resizeToScreen() {
500 // Figure out the thickness of borders and use that to set the
501 // final size.
502 Insets insets = getInsets();
503 left = insets.left;
504 top = insets.top;
505
506 setSize(textWidth * screen.width + insets.left + insets.right,
507 textHeight * screen.height + insets.top + insets.bottom);
508 }
509
510 /**
511 * Update redraws the whole screen.
512 *
513 * @param gr the Swing Graphics context
514 */
515 @Override
516 public void update(final Graphics gr) {
517 // The default update clears the area. Don't do that, instead
518 // just paint it directly.
519 paint(gr);
520 }
521
522 /**
523 * Draw one glyph to the screen.
524 *
525 * @param gr the Swing Graphics context
526 * @param cell the Cell to draw
527 * @param xPixel the x-coordinate to render to. 0 means the
528 * left-most pixel column.
529 * @param yPixel the y-coordinate to render to. 0 means the top-most
530 * pixel row.
531 */
532 private void drawGlyph(final Graphics gr, final Cell cell,
533 final int xPixel, final int yPixel) {
534
535 BufferedImage image = null;
536 if (cell.isBlink() && !cursorBlinkVisible) {
537 image = glyphCacheBlink.get(cell);
538 } else {
539 image = glyphCache.get(cell);
540 }
541 if (image != null) {
542 gr.drawImage(image, xPixel, yPixel, this);
543 return;
544 }
545
546 // Generate glyph and draw it.
547 Graphics2D gr2 = null;
548 int gr2x = xPixel;
549 int gr2y = yPixel;
550 if (tripleBuffer) {
551 image = new BufferedImage(textWidth, textHeight,
552 BufferedImage.TYPE_INT_ARGB);
553 gr2 = image.createGraphics();
554 gr2.setFont(getFont());
555 gr2x = 0;
556 gr2y = 0;
557 } else {
558 gr2 = (Graphics2D) gr;
559 }
560
561 Cell cellColor = new Cell();
562 cellColor.setTo(cell);
563
564 // Check for reverse
565 if (cell.isReverse()) {
566 cellColor.setForeColor(cell.getBackColor());
567 cellColor.setBackColor(cell.getForeColor());
568 }
569
570 // Draw the background rectangle, then the foreground character.
571 gr2.setColor(attrToBackgroundColor(cellColor));
572 gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
573
574 // Handle blink and underline
575 if (!cell.isBlink()
576 || (cell.isBlink() && cursorBlinkVisible)
577 ) {
578 gr2.setColor(attrToForegroundColor(cellColor));
579 char [] chars = new char[1];
580 chars[0] = cell.getChar();
581 gr2.drawChars(chars, 0, 1, gr2x + textAdjustX,
582 gr2y + textHeight - maxDescent + textAdjustY);
583
584 if (cell.isUnderline()) {
585 gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
586 }
587 }
588
589 if (tripleBuffer) {
590 gr2.dispose();
591
592 // We need a new key that will not be mutated by
593 // invertCell().
594 Cell key = new Cell();
595 key.setTo(cell);
596 if (cell.isBlink() && !cursorBlinkVisible) {
597 glyphCacheBlink.put(key, image);
598 } else {
599 glyphCache.put(key, image);
600 }
601
602 gr.drawImage(image, xPixel, yPixel, this);
603 }
604
605 }
606
607 /**
608 * Check if the cursor is visible, and if so draw it.
609 *
610 * @param gr the Swing Graphics context
611 */
612 private void drawCursor(final Graphics gr) {
613
614 if (cursorVisible
615 && (cursorY <= screen.height - 1)
616 && (cursorX <= screen.width - 1)
617 && cursorBlinkVisible
618 ) {
619 int xPixel = cursorX * textWidth + left;
620 int yPixel = cursorY * textHeight + top;
621 Cell lCell = screen.logical[cursorX][cursorY];
622 gr.setColor(attrToForegroundColor(lCell));
623 switch (cursorStyle) {
624 default:
625 // Fall through...
626 case UNDERLINE:
627 gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2);
628 break;
629 case BLOCK:
630 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
631 break;
632 case OUTLINE:
633 gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1);
634 break;
635 }
636 }
637 }
638
639 /**
640 * Paint redraws the whole screen.
641 *
642 * @param gr the Swing Graphics context
643 */
644 @Override
645 public void paint(final Graphics gr) {
646 // Do nothing until the screen reference has been set.
647 if (screen == null) {
648 return;
649 }
650 if (screen.frame == null) {
651 return;
652 }
653
654 // See if it is time to flip the blink time.
655 long nowTime = (new Date()).getTime();
656 if (nowTime > blinkMillis + lastBlinkTime) {
657 lastBlinkTime = nowTime;
658 cursorBlinkVisible = !cursorBlinkVisible;
659 }
660
661 int xCellMin = 0;
662 int xCellMax = screen.width;
663 int yCellMin = 0;
664 int yCellMax = screen.height;
665
666 Rectangle bounds = gr.getClipBounds();
667 if (bounds != null) {
668 // Only update what is in the bounds
669 xCellMin = screen.textColumn(bounds.x);
670 xCellMax = screen.textColumn(bounds.x + bounds.width);
671 if (xCellMax > screen.width) {
672 xCellMax = screen.width;
673 }
674 if (xCellMin >= xCellMax) {
675 xCellMin = xCellMax - 2;
676 }
677 if (xCellMin < 0) {
678 xCellMin = 0;
679 }
680 yCellMin = screen.textRow(bounds.y);
681 yCellMax = screen.textRow(bounds.y + bounds.height);
682 if (yCellMax > screen.height) {
683 yCellMax = screen.height;
684 }
685 if (yCellMin >= yCellMax) {
686 yCellMin = yCellMax - 2;
687 }
688 if (yCellMin < 0) {
689 yCellMin = 0;
690 }
691 } else {
692 // We need a total repaint
693 reallyCleared = true;
694 }
695
696 // Prevent updates to the screen's data from the TApplication
697 // threads.
698 synchronized (screen) {
699 /*
700 System.err.printf("bounds %s X %d %d Y %d %d\n",
701 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
702 */
703
704 for (int y = yCellMin; y < yCellMax; y++) {
705 for (int x = xCellMin; x < xCellMax; x++) {
706
707 int xPixel = x * textWidth + left;
708 int yPixel = y * textHeight + top;
709
710 Cell lCell = screen.logical[x][y];
711 Cell pCell = screen.physical[x][y];
712
713 if (!lCell.equals(pCell)
714 || lCell.isBlink()
715 || reallyCleared) {
716
717 drawGlyph(gr, lCell, xPixel, yPixel);
718
719 // Physical is always updated
720 physical[x][y].setTo(lCell);
721 }
722 }
723 }
724 drawCursor(gr);
725
726 dirty = false;
727 reallyCleared = false;
728 } // synchronized (screen)
729 }
730
731 } // class SwingFrame
732
733 /**
734 * The raw Swing JFrame. Note package private access.
735 */
736 SwingFrame frame;
737
738 /**
739 * Restore terminal to normal state.
740 */
741 public void shutdown() {
742 frame.dispose();
743 }
744
745 /**
746 * Public constructor.
747 *
748 * @param windowWidth the number of text columns to start with
749 * @param windowHeight the number of text rows to start with
750 * @param fontSize the size in points. Good values to pick are: 16, 20,
751 * 22, and 24.
752 */
753 public SwingScreen(final int windowWidth, final int windowHeight,
754 final int fontSize) {
755
756 try {
757 SwingUtilities.invokeAndWait(new Runnable() {
758 public void run() {
759 SwingScreen.this.frame = new SwingFrame(SwingScreen.this,
760 fontSize);
761 SwingScreen.this.sessionInfo =
762 new SwingSessionInfo(SwingScreen.this.frame,
763 frame.textWidth, frame.textHeight,
764 windowWidth, windowHeight);
765
766 SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(),
767 sessionInfo.getWindowHeight());
768
769 SwingScreen.this.frame.resizeToScreen();
770 SwingScreen.this.frame.setVisible(true);
771 }
772 });
773 } catch (Exception e) {
774 e.printStackTrace();
775 }
776 }
777
778 /**
779 * The sessionInfo.
780 */
781 private SwingSessionInfo sessionInfo;
782
783 /**
784 * Create the SwingSessionInfo. Note package private access.
785 *
786 * @return the sessionInfo
787 */
788 SwingSessionInfo getSessionInfo() {
789 return sessionInfo;
790 }
791
792 /**
793 * Push the logical screen to the physical device.
794 */
795 @Override
796 public void flushPhysical() {
797
798 /*
799 System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n",
800 reallyCleared, dirty);
801 */
802
803 // If reallyCleared is set, we have to draw everything.
804 if ((frame.bufferStrategy != null) && (reallyCleared == true)) {
805 // Triple-buffering: we have to redraw everything on this thread.
806 Graphics gr = frame.bufferStrategy.getDrawGraphics();
807 frame.paint(gr);
808 gr.dispose();
809 frame.bufferStrategy.show();
810 // sync() doesn't seem to help the tearing for me.
811 // Toolkit.getDefaultToolkit().sync();
812 return;
813 } else if ((frame.bufferStrategy == null) && (reallyCleared == true)) {
814 // Repaint everything on the Swing thread.
815 frame.repaint();
816 return;
817 }
818
819 // Do nothing if nothing happened.
820 if (!dirty) {
821 return;
822 }
823
824 if (frame.bufferStrategy != null) {
825 // See if it is time to flip the blink time.
826 long nowTime = (new Date()).getTime();
827 if (nowTime > frame.blinkMillis + frame.lastBlinkTime) {
828 frame.lastBlinkTime = nowTime;
829 frame.cursorBlinkVisible = !frame.cursorBlinkVisible;
830 }
831
832 Graphics gr = frame.bufferStrategy.getDrawGraphics();
833
834 synchronized (this) {
835 for (int y = 0; y < height; y++) {
836 for (int x = 0; x < width; x++) {
837 Cell lCell = logical[x][y];
838 Cell pCell = physical[x][y];
839
840 int xPixel = x * frame.textWidth + frame.left;
841 int yPixel = y * frame.textHeight + frame.top;
842
843 if (!lCell.equals(pCell)
844 || ((x == cursorX)
845 && (y == cursorY)
846 && cursorVisible)
847 || (lCell.isBlink())
848 ) {
849 frame.drawGlyph(gr, lCell, xPixel, yPixel);
850 physical[x][y].setTo(lCell);
851 }
852 }
853 }
854 frame.drawCursor(gr);
855 } // synchronized (this)
856
857 gr.dispose();
858 frame.bufferStrategy.show();
859 // sync() doesn't seem to help the tearing for me.
860 // Toolkit.getDefaultToolkit().sync();
861 return;
862 }
863
864 // Swing thread version: request a repaint, but limit it to the area
865 // that has changed.
866
867 // Find the minimum-size damaged region.
868 int xMin = frame.getWidth();
869 int xMax = 0;
870 int yMin = frame.getHeight();
871 int yMax = 0;
872
873 synchronized (this) {
874 for (int y = 0; y < height; y++) {
875 for (int x = 0; x < width; x++) {
876 Cell lCell = logical[x][y];
877 Cell pCell = physical[x][y];
878
879 int xPixel = x * frame.textWidth + frame.left;
880 int yPixel = y * frame.textHeight + frame.top;
881
882 if (!lCell.equals(pCell)
883 || ((x == cursorX)
884 && (y == cursorY)
885 && cursorVisible)
886 || lCell.isBlink()
887 ) {
888 if (xPixel < xMin) {
889 xMin = xPixel;
890 }
891 if (xPixel + frame.textWidth > xMax) {
892 xMax = xPixel + frame.textWidth;
893 }
894 if (yPixel < yMin) {
895 yMin = yPixel;
896 }
897 if (yPixel + frame.textHeight > yMax) {
898 yMax = yPixel + frame.textHeight;
899 }
900 }
901 }
902 }
903 }
904 if (xMin + frame.textWidth >= xMax) {
905 xMax += frame.textWidth;
906 }
907 if (yMin + frame.textHeight >= yMax) {
908 yMax += frame.textHeight;
909 }
910
911 // Repaint the desired area
912 /*
913 System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
914 yMin, yMax);
915 */
916 if (frame.bufferStrategy != null) {
917 // This path should never be taken, but is left here for
918 // completeness.
919 Graphics gr = frame.bufferStrategy.getDrawGraphics();
920 Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
921 yMax - yMin);
922 gr.setClip(bounds);
923 frame.paint(gr);
924 gr.dispose();
925 frame.bufferStrategy.show();
926 // sync() doesn't seem to help the tearing for me.
927 // Toolkit.getDefaultToolkit().sync();
928 } else {
929 // Repaint on the Swing thread.
930 frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
931 }
932 }
933
934 /**
935 * Put the cursor at (x,y).
936 *
937 * @param visible if true, the cursor should be visible
938 * @param x column coordinate to put the cursor on
939 * @param y row coordinate to put the cursor on
940 */
941 @Override
942 public void putCursor(final boolean visible, final int x, final int y) {
943
944 if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) {
945 // See if it is time to flip the blink time.
946 long nowTime = (new Date()).getTime();
947 if (nowTime < frame.blinkMillis + frame.lastBlinkTime) {
948 // Nothing has changed, so don't do anything.
949 return;
950 }
951 }
952
953 if (cursorVisible
954 && (cursorY <= height - 1)
955 && (cursorX <= width - 1)
956 ) {
957 // Make the current cursor position dirty
958 if (physical[cursorX][cursorY].getChar() == 'Q') {
959 physical[cursorX][cursorY].setChar('X');
960 } else {
961 physical[cursorX][cursorY].setChar('Q');
962 }
963 }
964
965 super.putCursor(visible, x, y);
966 }
967
968 /**
969 * Convert pixel column position to text cell column position.
970 *
971 * @param x pixel column position
972 * @return text cell column position
973 */
974 public int textColumn(final int x) {
975 return ((x - frame.left) / frame.textWidth);
976 }
977
978 /**
979 * Convert pixel row position to text cell row position.
980 *
981 * @param y pixel row position
982 * @return text cell row position
983 */
984 public int textRow(final int y) {
985 return ((y - frame.top) / frame.textHeight);
986 }
987
988 /**
989 * Set the window title.
990 *
991 * @param title the new title
992 */
993 public void setTitle(final String title) {
994 frame.setTitle(title);
995 }
996
997 }