b7c8410993ce8647e23eacc8c5f8aad3545ece8f
[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) 2016 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 final 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 * Width of a character cell.
170 */
171 private int textWidth = 1;
172
173 /**
174 * Height of a character cell.
175 */
176 private int textHeight = 1;
177
178 /**
179 * Descent of a character cell.
180 */
181 private int maxDescent = 0;
182
183 /**
184 * System-dependent Y adjustment for text in the character cell.
185 */
186 private int textAdjustY = 0;
187
188 /**
189 * System-dependent X adjustment for text in the character cell.
190 */
191 private int textAdjustX = 0;
192
193 /**
194 * Top pixel absolute location.
195 */
196 private int top = 30;
197
198 /**
199 * Left pixel absolute location.
200 */
201 private int left = 30;
202
203 /**
204 * The cursor style to draw.
205 */
206 private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
207
208 /**
209 * The number of millis to wait before switching the blink from
210 * visible to invisible.
211 */
212 private long blinkMillis = 500;
213
214 /**
215 * If true, the cursor should be visible right now based on the blink
216 * time.
217 */
218 private boolean cursorBlinkVisible = true;
219
220 /**
221 * The time that the blink last flipped from visible to invisible or
222 * from invisible to visible.
223 */
224 private long lastBlinkTime = 0;
225
226 /**
227 * Convert a CellAttributes foreground color to an Swing Color.
228 *
229 * @param attr the text attributes
230 * @return the Swing Color
231 */
232 private Color attrToForegroundColor(final CellAttributes attr) {
233 if (attr.isBold()) {
234 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
235 return MYBOLD_BLACK;
236 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
237 return MYBOLD_RED;
238 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
239 return MYBOLD_BLUE;
240 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
241 return MYBOLD_GREEN;
242 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
243 return MYBOLD_YELLOW;
244 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
245 return MYBOLD_CYAN;
246 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
247 return MYBOLD_MAGENTA;
248 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
249 return MYBOLD_WHITE;
250 }
251 } else {
252 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
253 return MYBLACK;
254 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
255 return MYRED;
256 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
257 return MYBLUE;
258 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
259 return MYGREEN;
260 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
261 return MYYELLOW;
262 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
263 return MYCYAN;
264 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
265 return MYMAGENTA;
266 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
267 return MYWHITE;
268 }
269 }
270 throw new IllegalArgumentException("Invalid color: " +
271 attr.getForeColor().getValue());
272 }
273
274 /**
275 * Convert a CellAttributes background color to an Swing Color.
276 *
277 * @param attr the text attributes
278 * @return the Swing Color
279 */
280 private Color attrToBackgroundColor(final CellAttributes attr) {
281 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
282 return MYBLACK;
283 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
284 return MYRED;
285 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
286 return MYBLUE;
287 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
288 return MYGREEN;
289 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
290 return MYYELLOW;
291 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
292 return MYCYAN;
293 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
294 return MYMAGENTA;
295 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
296 return MYWHITE;
297 }
298 throw new IllegalArgumentException("Invalid color: " +
299 attr.getBackColor().getValue());
300 }
301
302 /**
303 * Public constructor.
304 *
305 * @param screen the Screen that Backend talks to
306 */
307 public SwingFrame(final SwingScreen screen) {
308 this.screen = screen;
309 setDOSColors();
310
311 // Figure out my cursor style
312 String cursorStyleString = System.getProperty(
313 "jexer.Swing.cursorStyle", "underline").toLowerCase();
314
315 if (cursorStyleString.equals("underline")) {
316 cursorStyle = CursorStyle.UNDERLINE;
317 } else if (cursorStyleString.equals("outline")) {
318 cursorStyle = CursorStyle.OUTLINE;
319 } else if (cursorStyleString.equals("block")) {
320 cursorStyle = CursorStyle.BLOCK;
321 }
322
323 setTitle("Jexer Application");
324 setBackground(Color.black);
325
326 try {
327 // Always try to use Terminus, the one decent font.
328 ClassLoader loader = Thread.currentThread().
329 getContextClassLoader();
330 InputStream in = loader.getResourceAsStream(FONTFILE);
331 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
332 Font terminus = terminusRoot.deriveFont(Font.PLAIN, 22);
333 setFont(terminus);
334 } catch (Exception e) {
335 e.printStackTrace();
336 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
337 setFont(new Font(Font.MONOSPACED, Font.PLAIN, 24));
338 }
339 pack();
340
341 // Kill the X11 cursor
342 // Transparent 16 x 16 pixel cursor image.
343 BufferedImage cursorImg = new BufferedImage(16, 16,
344 BufferedImage.TYPE_INT_ARGB);
345 // Create a new blank cursor.
346 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
347 cursorImg, new Point(0, 0), "blank cursor");
348 setCursor(blankCursor);
349
350 // Be capable of seeing Tab / Shift-Tab
351 setFocusTraversalKeysEnabled(false);
352
353 // Save the text cell width/height
354 getFontDimensions();
355
356 // Cache glyphs as they are rendered
357 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
358 glyphCache = new HashMap<Cell, BufferedImage>();
359
360 // Setup triple-buffering
361 if (SwingScreen.tripleBuffer) {
362 setIgnoreRepaint(true);
363 createBufferStrategy(3);
364 bufferStrategy = getBufferStrategy();
365 }
366 }
367
368 /**
369 * Figure out my font dimensions.
370 */
371 private void getFontDimensions() {
372 Graphics gr = getGraphics();
373 FontMetrics fm = gr.getFontMetrics();
374 maxDescent = fm.getMaxDescent();
375 Rectangle2D bounds = fm.getMaxCharBounds(gr);
376 int leading = fm.getLeading();
377 textWidth = (int)Math.round(bounds.getWidth());
378 textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
379 // This also produces the same number, but works better for ugly
380 // monospace.
381 textHeight = fm.getMaxAscent() + maxDescent - leading;
382
383 if (System.getProperty("os.name").startsWith("Windows")) {
384 textAdjustY = -1;
385 textAdjustX = 0;
386 }
387 }
388
389 /**
390 * Resize to font dimensions.
391 */
392 public void resizeToScreen() {
393 // Figure out the thickness of borders and use that to set the
394 // final size.
395 Insets insets = getInsets();
396 left = insets.left;
397 top = insets.top;
398
399 setSize(textWidth * screen.width + insets.left + insets.right,
400 textHeight * screen.height + insets.top + insets.bottom);
401 }
402
403 /**
404 * Update redraws the whole screen.
405 *
406 * @param gr the Swing Graphics context
407 */
408 @Override
409 public void update(final Graphics gr) {
410 // The default update clears the area. Don't do that, instead
411 // just paint it directly.
412 paint(gr);
413 }
414
415 /**
416 * Draw one glyph to the screen.
417 *
418 * @param gr the Swing Graphics context
419 * @param cell the Cell to draw
420 * @param xPixel the x-coordinate to render to. 0 means the
421 * left-most pixel column.
422 * @param yPixel the y-coordinate to render to. 0 means the top-most
423 * pixel row.
424 */
425 private void drawGlyph(final Graphics gr, final Cell cell,
426 final int xPixel, final int yPixel) {
427
428 BufferedImage image = null;
429 if (cell.isBlink() && !cursorBlinkVisible) {
430 image = glyphCacheBlink.get(cell);
431 } else {
432 image = glyphCache.get(cell);
433 }
434 if (image != null) {
435 gr.drawImage(image, xPixel, yPixel, this);
436 return;
437 }
438
439 // Generate glyph and draw it.
440
441 image = new BufferedImage(textWidth, textHeight,
442 BufferedImage.TYPE_INT_ARGB);
443 Graphics2D gr2 = image.createGraphics();
444 gr2.setFont(getFont());
445
446 Cell cellColor = new Cell();
447 cellColor.setTo(cell);
448
449 // Check for reverse
450 if (cell.isReverse()) {
451 cellColor.setForeColor(cell.getBackColor());
452 cellColor.setBackColor(cell.getForeColor());
453 }
454
455 // Draw the background rectangle, then the foreground character.
456 gr2.setColor(attrToBackgroundColor(cellColor));
457 gr2.fillRect(0, 0, textWidth, textHeight);
458
459 // Handle blink and underline
460 if (!cell.isBlink()
461 || (cell.isBlink() && cursorBlinkVisible)
462 ) {
463 gr2.setColor(attrToForegroundColor(cellColor));
464 char [] chars = new char[1];
465 chars[0] = cell.getChar();
466 gr2.drawChars(chars, 0, 1, 0 + textAdjustX,
467 0 + textHeight - maxDescent + textAdjustY);
468
469 if (cell.isUnderline()) {
470 gr2.fillRect(0, 0 + textHeight - 2, textWidth, 2);
471 }
472 }
473 gr2.dispose();
474
475 // We need a new key that will not be mutated by invertCell().
476 Cell key = new Cell();
477 key.setTo(cell);
478 if (cell.isBlink() && !cursorBlinkVisible) {
479 glyphCacheBlink.put(key, image);
480 } else {
481 glyphCache.put(key, image);
482 }
483
484 gr.drawImage(image, xPixel, yPixel, this);
485 }
486
487 /**
488 * Check if the cursor is visible, and if so draw it.
489 *
490 * @param gr the Swing Graphics context
491 */
492 private void drawCursor(final Graphics gr) {
493
494 if (cursorVisible
495 && (cursorY <= screen.height - 1)
496 && (cursorX <= screen.width - 1)
497 && cursorBlinkVisible
498 ) {
499 int xPixel = cursorX * textWidth + left;
500 int yPixel = cursorY * textHeight + top;
501 Cell lCell = screen.logical[cursorX][cursorY];
502 gr.setColor(attrToForegroundColor(lCell));
503 switch (cursorStyle) {
504 default:
505 // Fall through...
506 case UNDERLINE:
507 gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2);
508 break;
509 case BLOCK:
510 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
511 break;
512 case OUTLINE:
513 gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1);
514 break;
515 }
516 }
517 }
518
519 /**
520 * Paint redraws the whole screen.
521 *
522 * @param gr the Swing Graphics context
523 */
524 @Override
525 public void paint(final Graphics gr) {
526 // Do nothing until the screen reference has been set.
527 if (screen == null) {
528 return;
529 }
530 if (screen.frame == null) {
531 return;
532 }
533
534 // See if it is time to flip the blink time.
535 long nowTime = (new Date()).getTime();
536 if (nowTime > blinkMillis + lastBlinkTime) {
537 lastBlinkTime = nowTime;
538 cursorBlinkVisible = !cursorBlinkVisible;
539 }
540
541 int xCellMin = 0;
542 int xCellMax = screen.width;
543 int yCellMin = 0;
544 int yCellMax = screen.height;
545
546 Rectangle bounds = gr.getClipBounds();
547 if (bounds != null) {
548 // Only update what is in the bounds
549 xCellMin = screen.textColumn(bounds.x);
550 xCellMax = screen.textColumn(bounds.x + bounds.width);
551 if (xCellMax > screen.width) {
552 xCellMax = screen.width;
553 }
554 if (xCellMin >= xCellMax) {
555 xCellMin = xCellMax - 2;
556 }
557 if (xCellMin < 0) {
558 xCellMin = 0;
559 }
560 yCellMin = screen.textRow(bounds.y);
561 yCellMax = screen.textRow(bounds.y + bounds.height);
562 if (yCellMax > screen.height) {
563 yCellMax = screen.height;
564 }
565 if (yCellMin >= yCellMax) {
566 yCellMin = yCellMax - 2;
567 }
568 if (yCellMin < 0) {
569 yCellMin = 0;
570 }
571 } else {
572 // We need a total repaint
573 reallyCleared = true;
574 }
575
576 // Prevent updates to the screen's data from the TApplication
577 // threads.
578 synchronized (screen) {
579 /*
580 System.err.printf("bounds %s X %d %d Y %d %d\n",
581 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
582 */
583
584 for (int y = yCellMin; y < yCellMax; y++) {
585 for (int x = xCellMin; x < xCellMax; x++) {
586
587 int xPixel = x * textWidth + left;
588 int yPixel = y * textHeight + top;
589
590 Cell lCell = screen.logical[x][y];
591 Cell pCell = screen.physical[x][y];
592
593 if (!lCell.equals(pCell)
594 || lCell.isBlink()
595 || reallyCleared) {
596
597 drawGlyph(gr, lCell, xPixel, yPixel);
598
599 // Physical is always updated
600 physical[x][y].setTo(lCell);
601 }
602 }
603 }
604 drawCursor(gr);
605
606 dirty = false;
607 reallyCleared = false;
608 } // synchronized (screen)
609 }
610
611 } // class SwingFrame
612
613 /**
614 * The raw Swing JFrame. Note package private access.
615 */
616 SwingFrame frame;
617
618 /**
619 * Restore terminal to normal state.
620 */
621 public void shutdown() {
622 frame.dispose();
623 }
624
625 /**
626 * Public constructor.
627 */
628 public SwingScreen() {
629 try {
630 SwingUtilities.invokeAndWait(new Runnable() {
631 public void run() {
632 SwingScreen.this.frame = new SwingFrame(SwingScreen.this);
633 SwingScreen.this.sessionInfo =
634 new SwingSessionInfo(SwingScreen.this.frame,
635 frame.textWidth,
636 frame.textHeight);
637
638 SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(),
639 sessionInfo.getWindowHeight());
640
641 SwingScreen.this.frame.resizeToScreen();
642 SwingScreen.this.frame.setVisible(true);
643 }
644 });
645 } catch (Exception e) {
646 e.printStackTrace();
647 }
648 }
649
650 /**
651 * The sessionInfo.
652 */
653 private SwingSessionInfo sessionInfo;
654
655 /**
656 * Create the SwingSessionInfo. Note package private access.
657 *
658 * @return the sessionInfo
659 */
660 SwingSessionInfo getSessionInfo() {
661 return sessionInfo;
662 }
663
664 /**
665 * Push the logical screen to the physical device.
666 */
667 @Override
668 public void flushPhysical() {
669
670 /*
671 System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n",
672 reallyCleared, dirty);
673 */
674
675 // If reallyCleared is set, we have to draw everything.
676 if ((frame.bufferStrategy != null) && (reallyCleared == true)) {
677 // Triple-buffering: we have to redraw everything on this thread.
678 Graphics gr = frame.bufferStrategy.getDrawGraphics();
679 frame.paint(gr);
680 gr.dispose();
681 frame.bufferStrategy.show();
682 // sync() doesn't seem to help the tearing for me.
683 // Toolkit.getDefaultToolkit().sync();
684 return;
685 } else if ((frame.bufferStrategy == null) && (reallyCleared == true)) {
686 // Repaint everything on the Swing thread.
687 frame.repaint();
688 return;
689 }
690
691 // Do nothing if nothing happened.
692 if (!dirty) {
693 return;
694 }
695
696 if (frame.bufferStrategy != null) {
697 // See if it is time to flip the blink time.
698 long nowTime = (new Date()).getTime();
699 if (nowTime > frame.blinkMillis + frame.lastBlinkTime) {
700 frame.lastBlinkTime = nowTime;
701 frame.cursorBlinkVisible = !frame.cursorBlinkVisible;
702 }
703
704 Graphics gr = frame.bufferStrategy.getDrawGraphics();
705
706 synchronized (this) {
707 for (int y = 0; y < height; y++) {
708 for (int x = 0; x < width; x++) {
709 Cell lCell = logical[x][y];
710 Cell pCell = physical[x][y];
711
712 int xPixel = x * frame.textWidth + frame.left;
713 int yPixel = y * frame.textHeight + frame.top;
714
715 if (!lCell.equals(pCell)
716 || ((x == cursorX)
717 && (y == cursorY)
718 && cursorVisible)
719 || (lCell.isBlink())
720 ) {
721 frame.drawGlyph(gr, lCell, xPixel, yPixel);
722 physical[x][y].setTo(lCell);
723 }
724 }
725 }
726 frame.drawCursor(gr);
727 } // synchronized (this)
728
729 gr.dispose();
730 frame.bufferStrategy.show();
731 // sync() doesn't seem to help the tearing for me.
732 // Toolkit.getDefaultToolkit().sync();
733 return;
734 }
735
736 // Swing thread version: request a repaint, but limit it to the area
737 // that has changed.
738
739 // Find the minimum-size damaged region.
740 int xMin = frame.getWidth();
741 int xMax = 0;
742 int yMin = frame.getHeight();
743 int yMax = 0;
744
745 synchronized (this) {
746 for (int y = 0; y < height; y++) {
747 for (int x = 0; x < width; x++) {
748 Cell lCell = logical[x][y];
749 Cell pCell = physical[x][y];
750
751 int xPixel = x * frame.textWidth + frame.left;
752 int yPixel = y * frame.textHeight + frame.top;
753
754 if (!lCell.equals(pCell)
755 || ((x == cursorX)
756 && (y == cursorY)
757 && cursorVisible)
758 || lCell.isBlink()
759 ) {
760 if (xPixel < xMin) {
761 xMin = xPixel;
762 }
763 if (xPixel + frame.textWidth > xMax) {
764 xMax = xPixel + frame.textWidth;
765 }
766 if (yPixel < yMin) {
767 yMin = yPixel;
768 }
769 if (yPixel + frame.textHeight > yMax) {
770 yMax = yPixel + frame.textHeight;
771 }
772 }
773 }
774 }
775 }
776 if (xMin + frame.textWidth >= xMax) {
777 xMax += frame.textWidth;
778 }
779 if (yMin + frame.textHeight >= yMax) {
780 yMax += frame.textHeight;
781 }
782
783 // Repaint the desired area
784 /*
785 System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
786 yMin, yMax);
787 */
788 if (frame.bufferStrategy != null) {
789 // This path should never be taken, but is left here for
790 // completeness.
791 Graphics gr = frame.bufferStrategy.getDrawGraphics();
792 Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
793 yMax - yMin);
794 gr.setClip(bounds);
795 frame.paint(gr);
796 gr.dispose();
797 frame.bufferStrategy.show();
798 // sync() doesn't seem to help the tearing for me.
799 // Toolkit.getDefaultToolkit().sync();
800 } else {
801 // Repaint on the Swing thread.
802 frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
803 }
804 }
805
806 /**
807 * Put the cursor at (x,y).
808 *
809 * @param visible if true, the cursor should be visible
810 * @param x column coordinate to put the cursor on
811 * @param y row coordinate to put the cursor on
812 */
813 @Override
814 public void putCursor(final boolean visible, final int x, final int y) {
815
816 if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) {
817 // See if it is time to flip the blink time.
818 long nowTime = (new Date()).getTime();
819 if (nowTime < frame.blinkMillis + frame.lastBlinkTime) {
820 // Nothing has changed, so don't do anything.
821 return;
822 }
823 }
824
825 if (cursorVisible
826 && (cursorY <= height - 1)
827 && (cursorX <= width - 1)
828 ) {
829 // Make the current cursor position dirty
830 if (physical[cursorX][cursorY].getChar() == 'Q') {
831 physical[cursorX][cursorY].setChar('X');
832 } else {
833 physical[cursorX][cursorY].setChar('Q');
834 }
835 }
836
837 super.putCursor(visible, x, y);
838 }
839
840 /**
841 * Convert pixel column position to text cell column position.
842 *
843 * @param x pixel column position
844 * @return text cell column position
845 */
846 public int textColumn(final int x) {
847 return ((x - frame.left) / frame.textWidth);
848 }
849
850 /**
851 * Convert pixel row position to text cell row position.
852 *
853 * @param y pixel row position
854 * @return text cell row position
855 */
856 public int textRow(final int y) {
857 return ((y - frame.top) / frame.textHeight);
858 }
859
860 }