#37 don't resize except initial and font change
[fanfix.git] / src / jexer / backend / SwingTerminal.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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.backend;
30
31 import java.awt.BorderLayout;
32 import java.awt.Color;
33 import java.awt.Font;
34 import java.awt.FontMetrics;
35 import java.awt.Graphics2D;
36 import java.awt.Graphics;
37 import java.awt.Insets;
38 import java.awt.Rectangle;
39 import java.awt.Toolkit;
40 import java.awt.event.ComponentEvent;
41 import java.awt.event.ComponentListener;
42 import java.awt.event.KeyEvent;
43 import java.awt.event.KeyListener;
44 import java.awt.event.MouseEvent;
45 import java.awt.event.MouseListener;
46 import java.awt.event.MouseMotionListener;
47 import java.awt.event.MouseWheelEvent;
48 import java.awt.event.MouseWheelListener;
49 import java.awt.event.WindowEvent;
50 import java.awt.event.WindowListener;
51 import java.awt.geom.Rectangle2D;
52 import java.awt.image.BufferedImage;
53 import java.io.InputStream;
54 import java.util.ArrayList;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Map;
58 import javax.swing.JComponent;
59 import javax.swing.JFrame;
60 import javax.swing.ImageIcon;
61 import javax.swing.SwingUtilities;
62
63 import jexer.TKeypress;
64 import jexer.bits.Cell;
65 import jexer.bits.CellAttributes;
66 import jexer.event.TCommandEvent;
67 import jexer.event.TInputEvent;
68 import jexer.event.TKeypressEvent;
69 import jexer.event.TMouseEvent;
70 import jexer.event.TResizeEvent;
71 import static jexer.TCommand.*;
72 import static jexer.TKeypress.*;
73
74 /**
75 * This Screen backend reads keystrokes and mouse events and draws to either
76 * a Java Swing JFrame (potentially triple-buffered) or a JComponent.
77 *
78 * This class is a bit of an inversion of typical GUI classes. It performs
79 * all of the drawing logic from SwingTerminal (which is not a Swing class),
80 * and uses a SwingComponent wrapper class to call the JFrame or JComponent
81 * methods.
82 */
83 public class SwingTerminal extends LogicalScreen
84 implements TerminalReader,
85 ComponentListener, KeyListener,
86 MouseListener, MouseMotionListener,
87 MouseWheelListener, WindowListener {
88
89 // ------------------------------------------------------------------------
90 // Constants --------------------------------------------------------------
91 // ------------------------------------------------------------------------
92
93 /**
94 * The icon image location.
95 */
96 private static final String ICONFILE = "jexer_logo_128.png";
97
98 /**
99 * The terminus font resource filename.
100 */
101 public static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
102
103 /**
104 * Cursor style to draw.
105 */
106 public enum CursorStyle {
107 /**
108 * Use an underscore for the cursor.
109 */
110 UNDERLINE,
111
112 /**
113 * Use a solid block for the cursor.
114 */
115 BLOCK,
116
117 /**
118 * Use an outlined block for the cursor.
119 */
120 OUTLINE
121 }
122
123 // ------------------------------------------------------------------------
124 // Variables --------------------------------------------------------------
125 // ------------------------------------------------------------------------
126
127 // Colors to map DOS colors to AWT colors.
128 private static Color MYBLACK;
129 private static Color MYRED;
130 private static Color MYGREEN;
131 private static Color MYYELLOW;
132 private static Color MYBLUE;
133 private static Color MYMAGENTA;
134 private static Color MYCYAN;
135 private static Color MYWHITE;
136 private static Color MYBOLD_BLACK;
137 private static Color MYBOLD_RED;
138 private static Color MYBOLD_GREEN;
139 private static Color MYBOLD_YELLOW;
140 private static Color MYBOLD_BLUE;
141 private static Color MYBOLD_MAGENTA;
142 private static Color MYBOLD_CYAN;
143 private static Color MYBOLD_WHITE;
144
145 /**
146 * When true, all the MYBLACK, MYRED, etc. colors are set.
147 */
148 private static boolean dosColors = false;
149
150 /**
151 * The Swing component or frame to draw to.
152 */
153 private SwingComponent swing;
154
155 /**
156 * A cache of previously-rendered glyphs for blinking text, when it is
157 * not visible.
158 */
159 private Map<Cell, BufferedImage> glyphCacheBlink;
160
161 /**
162 * A cache of previously-rendered glyphs for non-blinking, or
163 * blinking-and-visible, text.
164 */
165 private Map<Cell, BufferedImage> glyphCache;
166
167 /**
168 * If true, we were successful at getting the font dimensions.
169 */
170 private boolean gotFontDimensions = false;
171
172 /**
173 * The currently selected font.
174 */
175 private Font font = null;
176
177 /**
178 * The currently selected font size in points.
179 */
180 private int fontSize = 16;
181
182 /**
183 * Width of a character cell in pixels.
184 */
185 private int textWidth = 1;
186
187 /**
188 * Height of a character cell in pixels.
189 */
190 private int textHeight = 1;
191
192 /**
193 * Width of a character cell in pixels, as reported by font.
194 */
195 private int fontTextWidth = 1;
196
197 /**
198 * Height of a character cell in pixels, as reported by font.
199 */
200 private int fontTextHeight = 1;
201
202 /**
203 * Descent of a character cell in pixels.
204 */
205 private int maxDescent = 0;
206
207 /**
208 * System-dependent Y adjustment for text in the character cell.
209 */
210 private int textAdjustY = 0;
211
212 /**
213 * System-dependent X adjustment for text in the character cell.
214 */
215 private int textAdjustX = 0;
216
217 /**
218 * System-dependent height adjustment for text in the character cell.
219 */
220 private int textAdjustHeight = 0;
221
222 /**
223 * System-dependent width adjustment for text in the character cell.
224 */
225 private int textAdjustWidth = 0;
226
227 /**
228 * Top pixel absolute location.
229 */
230 private int top = 30;
231
232 /**
233 * Left pixel absolute location.
234 */
235 private int left = 30;
236
237 /**
238 * The cursor style to draw.
239 */
240 private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
241
242 /**
243 * The number of millis to wait before switching the blink from visible
244 * to invisible. Set to 0 or negative to disable blinking.
245 */
246 private long blinkMillis = 500;
247
248 /**
249 * If true, the cursor should be visible right now based on the blink
250 * time.
251 */
252 private boolean cursorBlinkVisible = true;
253
254 /**
255 * The time that the blink last flipped from visible to invisible or
256 * from invisible to visible.
257 */
258 private long lastBlinkTime = 0;
259
260 /**
261 * The session information.
262 */
263 private SwingSessionInfo sessionInfo;
264
265 /**
266 * The listening object that run() wakes up on new input.
267 */
268 private Object listener;
269
270 /**
271 * The event queue, filled up by a thread reading on input.
272 */
273 private List<TInputEvent> eventQueue;
274
275 /**
276 * The last reported mouse X position.
277 */
278 private int oldMouseX = -1;
279
280 /**
281 * The last reported mouse Y position.
282 */
283 private int oldMouseY = -1;
284
285 /**
286 * true if mouse1 was down. Used to report mouse1 on the release event.
287 */
288 private boolean mouse1 = false;
289
290 /**
291 * true if mouse2 was down. Used to report mouse2 on the release event.
292 */
293 private boolean mouse2 = false;
294
295 /**
296 * true if mouse3 was down. Used to report mouse3 on the release event.
297 */
298 private boolean mouse3 = false;
299
300 // ------------------------------------------------------------------------
301 // Constructors -----------------------------------------------------------
302 // ------------------------------------------------------------------------
303
304 /**
305 * Static constructor.
306 */
307 static {
308 setDOSColors();
309 }
310
311 /**
312 * Public constructor creates a new JFrame to render to.
313 *
314 * @param windowWidth the number of text columns to start with
315 * @param windowHeight the number of text rows to start with
316 * @param fontSize the size in points. Good values to pick are: 16, 20,
317 * 22, and 24.
318 * @param listener the object this backend needs to wake up when new
319 * input comes in
320 */
321 public SwingTerminal(final int windowWidth, final int windowHeight,
322 final int fontSize, final Object listener) {
323
324 this.fontSize = fontSize;
325
326 reloadOptions();
327
328 try {
329 SwingUtilities.invokeAndWait(new Runnable() {
330 public void run() {
331
332 JFrame frame = new JFrame() {
333
334 /**
335 * Serializable version.
336 */
337 private static final long serialVersionUID = 1;
338
339 /**
340 * The code that performs the actual drawing.
341 */
342 public SwingTerminal screen = null;
343
344 /*
345 * Anonymous class initializer saves the screen
346 * reference, so that paint() and the like call out
347 * to SwingTerminal.
348 */
349 {
350 this.screen = SwingTerminal.this;
351 }
352
353 /**
354 * Update redraws the whole screen.
355 *
356 * @param gr the Swing Graphics context
357 */
358 @Override
359 public void update(final Graphics gr) {
360 // The default update clears the area. Don't do
361 // that, instead just paint it directly.
362 paint(gr);
363 }
364
365 /**
366 * Paint redraws the whole screen.
367 *
368 * @param gr the Swing Graphics context
369 */
370 @Override
371 public void paint(final Graphics gr) {
372 if (screen != null) {
373 screen.paint(gr);
374 }
375 }
376 };
377
378 // Set icon
379 ClassLoader loader = Thread.currentThread().
380 getContextClassLoader();
381 frame.setIconImage((new ImageIcon(loader.
382 getResource(ICONFILE))).getImage());
383
384 // Get the Swing component
385 SwingTerminal.this.swing = new SwingComponent(frame);
386
387 // Hang onto top and left for drawing.
388 Insets insets = SwingTerminal.this.swing.getInsets();
389 SwingTerminal.this.left = insets.left;
390 SwingTerminal.this.top = insets.top;
391
392 // Load the font so that we can set sessionInfo.
393 setDefaultFont();
394
395 // Get the default cols x rows and set component size
396 // accordingly.
397 SwingTerminal.this.sessionInfo =
398 new SwingSessionInfo(SwingTerminal.this.swing,
399 SwingTerminal.this.textWidth,
400 SwingTerminal.this.textHeight,
401 windowWidth, windowHeight);
402
403 SwingTerminal.this.setDimensions(sessionInfo.
404 getWindowWidth(), sessionInfo.getWindowHeight());
405
406 SwingTerminal.this.resizeToScreen(true);
407 SwingTerminal.this.swing.setVisible(true);
408 }
409 });
410 } catch (java.lang.reflect.InvocationTargetException e) {
411 e.printStackTrace();
412 } catch (InterruptedException e) {
413 e.printStackTrace();
414 }
415
416 this.listener = listener;
417 mouse1 = false;
418 mouse2 = false;
419 mouse3 = false;
420 eventQueue = new ArrayList<TInputEvent>();
421
422 // Add listeners to Swing.
423 swing.addKeyListener(this);
424 swing.addWindowListener(this);
425 swing.addComponentListener(this);
426 swing.addMouseListener(this);
427 swing.addMouseMotionListener(this);
428 swing.addMouseWheelListener(this);
429 }
430
431 /**
432 * Public constructor renders to an existing JComponent.
433 *
434 * @param component the Swing component to render to
435 * @param windowWidth the number of text columns to start with
436 * @param windowHeight the number of text rows to start with
437 * @param fontSize the size in points. Good values to pick are: 16, 20,
438 * 22, and 24.
439 * @param listener the object this backend needs to wake up when new
440 * input comes in
441 */
442 public SwingTerminal(final JComponent component, final int windowWidth,
443 final int windowHeight, final int fontSize, final Object listener) {
444
445 this.fontSize = fontSize;
446
447 reloadOptions();
448
449 try {
450 SwingUtilities.invokeAndWait(new Runnable() {
451 public void run() {
452
453 JComponent newComponent = new JComponent() {
454
455 /**
456 * Serializable version.
457 */
458 private static final long serialVersionUID = 1;
459
460 /**
461 * The code that performs the actual drawing.
462 */
463 public SwingTerminal screen = null;
464
465 /*
466 * Anonymous class initializer saves the screen
467 * reference, so that paint() and the like call out
468 * to SwingTerminal.
469 */
470 {
471 this.screen = SwingTerminal.this;
472 }
473
474 /**
475 * Update redraws the whole screen.
476 *
477 * @param gr the Swing Graphics context
478 */
479 @Override
480 public void update(final Graphics gr) {
481 // The default update clears the area. Don't do
482 // that, instead just paint it directly.
483 paint(gr);
484 }
485
486 /**
487 * Paint redraws the whole screen.
488 *
489 * @param gr the Swing Graphics context
490 */
491 @Override
492 public void paint(final Graphics gr) {
493 if (screen != null) {
494 screen.paint(gr);
495 }
496 }
497 };
498 component.setLayout(new BorderLayout());
499 component.add(newComponent);
500
501 // Allow key events to be received
502 component.setFocusable(true);
503
504 // Get the Swing component
505 SwingTerminal.this.swing = new SwingComponent(component);
506
507 // Hang onto top and left for drawing.
508 Insets insets = SwingTerminal.this.swing.getInsets();
509 SwingTerminal.this.left = insets.left;
510 SwingTerminal.this.top = insets.top;
511
512 // Load the font so that we can set sessionInfo.
513 setDefaultFont();
514
515 // Get the default cols x rows and set component size
516 // accordingly.
517 SwingTerminal.this.sessionInfo =
518 new SwingSessionInfo(SwingTerminal.this.swing,
519 SwingTerminal.this.textWidth,
520 SwingTerminal.this.textHeight);
521 }
522 });
523 } catch (java.lang.reflect.InvocationTargetException e) {
524 e.printStackTrace();
525 } catch (InterruptedException e) {
526 e.printStackTrace();
527 }
528
529 this.listener = listener;
530 mouse1 = false;
531 mouse2 = false;
532 mouse3 = false;
533 eventQueue = new ArrayList<TInputEvent>();
534
535 // Add listeners to Swing.
536 swing.addKeyListener(this);
537 swing.addWindowListener(this);
538 swing.addComponentListener(this);
539 swing.addMouseListener(this);
540 swing.addMouseMotionListener(this);
541 swing.addMouseWheelListener(this);
542 }
543
544 // ------------------------------------------------------------------------
545 // LogicalScreen ----------------------------------------------------------
546 // ------------------------------------------------------------------------
547
548 /**
549 * Set the window title.
550 *
551 * @param title the new title
552 */
553 @Override
554 public void setTitle(final String title) {
555 swing.setTitle(title);
556 }
557
558 /**
559 * Push the logical screen to the physical device.
560 */
561 @Override
562 public void flushPhysical() {
563 // See if it is time to flip the blink time.
564 long nowTime = System.currentTimeMillis();
565 if (nowTime >= blinkMillis + lastBlinkTime) {
566 lastBlinkTime = nowTime;
567 cursorBlinkVisible = !cursorBlinkVisible;
568 // System.err.println("New lastBlinkTime: " + lastBlinkTime);
569 }
570
571 if ((swing.getFrame() != null)
572 && (swing.getBufferStrategy() != null)
573 ) {
574 do {
575 do {
576 clearPhysical();
577 drawToSwing();
578 } while (swing.getBufferStrategy().contentsRestored());
579
580 swing.getBufferStrategy().show();
581 Toolkit.getDefaultToolkit().sync();
582 } while (swing.getBufferStrategy().contentsLost());
583
584 } else {
585 // Non-triple-buffered, call drawToSwing() once
586 drawToSwing();
587 }
588 }
589
590 // ------------------------------------------------------------------------
591 // TerminalReader ---------------------------------------------------------
592 // ------------------------------------------------------------------------
593
594 /**
595 * Check if there are events in the queue.
596 *
597 * @return if true, getEvents() has something to return to the backend
598 */
599 public boolean hasEvents() {
600 synchronized (eventQueue) {
601 return (eventQueue.size() > 0);
602 }
603 }
604
605 /**
606 * Return any events in the IO queue.
607 *
608 * @param queue list to append new events to
609 */
610 public void getEvents(final List<TInputEvent> queue) {
611 synchronized (eventQueue) {
612 if (eventQueue.size() > 0) {
613 synchronized (queue) {
614 queue.addAll(eventQueue);
615 }
616 eventQueue.clear();
617 }
618 }
619 }
620
621 /**
622 * Restore terminal to normal state.
623 */
624 public void closeTerminal() {
625 shutdown();
626 }
627
628 /**
629 * Set listener to a different Object.
630 *
631 * @param listener the new listening object that run() wakes up on new
632 * input
633 */
634 public void setListener(final Object listener) {
635 this.listener = listener;
636 }
637
638 /**
639 * Reload options from System properties.
640 */
641 public void reloadOptions() {
642 // Figure out my cursor style.
643 String cursorStyleString = System.getProperty(
644 "jexer.Swing.cursorStyle", "underline").toLowerCase();
645 if (cursorStyleString.equals("underline")) {
646 cursorStyle = CursorStyle.UNDERLINE;
647 } else if (cursorStyleString.equals("outline")) {
648 cursorStyle = CursorStyle.OUTLINE;
649 } else if (cursorStyleString.equals("block")) {
650 cursorStyle = CursorStyle.BLOCK;
651 }
652
653 // Pull the system property for triple buffering.
654 if (System.getProperty("jexer.Swing.tripleBuffer",
655 "true").equals("true")
656 ) {
657 SwingComponent.tripleBuffer = true;
658 } else {
659 SwingComponent.tripleBuffer = false;
660 }
661 }
662
663 // ------------------------------------------------------------------------
664 // SwingTerminal ----------------------------------------------------------
665 // ------------------------------------------------------------------------
666
667 /**
668 * Get the width of a character cell in pixels.
669 *
670 * @return the width in pixels of a character cell
671 */
672 public int getTextWidth() {
673 return textWidth;
674 }
675
676 /**
677 * Get the height of a character cell in pixels.
678 *
679 * @return the height in pixels of a character cell
680 */
681 public int getTextHeight() {
682 return textHeight;
683 }
684
685 /**
686 * Setup Swing colors to match DOS color palette.
687 */
688 private static void setDOSColors() {
689 if (dosColors) {
690 return;
691 }
692 MYBLACK = new Color(0x00, 0x00, 0x00);
693 MYRED = new Color(0xa8, 0x00, 0x00);
694 MYGREEN = new Color(0x00, 0xa8, 0x00);
695 MYYELLOW = new Color(0xa8, 0x54, 0x00);
696 MYBLUE = new Color(0x00, 0x00, 0xa8);
697 MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
698 MYCYAN = new Color(0x00, 0xa8, 0xa8);
699 MYWHITE = new Color(0xa8, 0xa8, 0xa8);
700 MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
701 MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
702 MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
703 MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
704 MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
705 MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
706 MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
707 MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
708
709 dosColors = true;
710 }
711
712 /**
713 * Get the number of millis to wait before switching the blink from
714 * visible to invisible.
715 *
716 * @return the number of milli to wait before switching the blink from
717 * visible to invisible
718 */
719 public long getBlinkMillis() {
720 return blinkMillis;
721 }
722
723 /**
724 * Get the current status of the blink flag.
725 *
726 * @return true if the cursor and blinking text should be visible
727 */
728 public boolean getCursorBlinkVisible() {
729 return cursorBlinkVisible;
730 }
731
732 /**
733 * Get the font size in points.
734 *
735 * @return font size in points
736 */
737 public int getFontSize() {
738 return fontSize;
739 }
740
741 /**
742 * Set the font size in points.
743 *
744 * @param fontSize font size in points
745 */
746 public void setFontSize(final int fontSize) {
747 this.fontSize = fontSize;
748 Font newFont = font.deriveFont((float) fontSize);
749 setFont(newFont);
750 }
751
752 /**
753 * Set to a new font, and resize the screen to match its dimensions.
754 *
755 * @param font the new font
756 */
757 public void setFont(final Font font) {
758 synchronized (this) {
759 this.font = font;
760 getFontDimensions();
761 swing.setFont(font);
762 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
763 glyphCache = new HashMap<Cell, BufferedImage>();
764 resizeToScreen(true);
765 }
766 }
767
768 /**
769 * Get the font this screen was last set to.
770 *
771 * @return the font
772 */
773 public Font getFont() {
774 return font;
775 }
776
777 /**
778 * Set the font to Terminus, the best all-around font for both CP437 and
779 * ISO8859-1.
780 */
781 public void setDefaultFont() {
782 try {
783 ClassLoader loader = Thread.currentThread().getContextClassLoader();
784 InputStream in = loader.getResourceAsStream(FONTFILE);
785 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
786 Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
787 font = terminus;
788 } catch (java.awt.FontFormatException e) {
789 e.printStackTrace();
790 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
791 } catch (java.io.IOException e) {
792 e.printStackTrace();
793 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
794 }
795
796 setFont(font);
797 }
798
799 /**
800 * Get the X text adjustment.
801 *
802 * @return X text adjustment
803 */
804 public int getTextAdjustX() {
805 return textAdjustX;
806 }
807
808 /**
809 * Set the X text adjustment.
810 *
811 * @param textAdjustX the X text adjustment
812 */
813 public void setTextAdjustX(final int textAdjustX) {
814 synchronized (this) {
815 this.textAdjustX = textAdjustX;
816 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
817 glyphCache = new HashMap<Cell, BufferedImage>();
818 clearPhysical();
819 }
820 }
821
822 /**
823 * Get the Y text adjustment.
824 *
825 * @return Y text adjustment
826 */
827 public int getTextAdjustY() {
828 return textAdjustY;
829 }
830
831 /**
832 * Set the Y text adjustment.
833 *
834 * @param textAdjustY the Y text adjustment
835 */
836 public void setTextAdjustY(final int textAdjustY) {
837 synchronized (this) {
838 this.textAdjustY = textAdjustY;
839 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
840 glyphCache = new HashMap<Cell, BufferedImage>();
841 clearPhysical();
842 }
843 }
844
845 /**
846 * Get the height text adjustment.
847 *
848 * @return height text adjustment
849 */
850 public int getTextAdjustHeight() {
851 return textAdjustHeight;
852 }
853
854 /**
855 * Set the height text adjustment.
856 *
857 * @param textAdjustHeight the height text adjustment
858 */
859 public void setTextAdjustHeight(final int textAdjustHeight) {
860 synchronized (this) {
861 this.textAdjustHeight = textAdjustHeight;
862 textHeight = fontTextHeight + textAdjustHeight;
863 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
864 glyphCache = new HashMap<Cell, BufferedImage>();
865 clearPhysical();
866 }
867 }
868
869 /**
870 * Get the width text adjustment.
871 *
872 * @return width text adjustment
873 */
874 public int getTextAdjustWidth() {
875 return textAdjustWidth;
876 }
877
878 /**
879 * Set the width text adjustment.
880 *
881 * @param textAdjustWidth the width text adjustment
882 */
883 public void setTextAdjustWidth(final int textAdjustWidth) {
884 synchronized (this) {
885 this.textAdjustWidth = textAdjustWidth;
886 textWidth = fontTextWidth + textAdjustWidth;
887 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
888 glyphCache = new HashMap<Cell, BufferedImage>();
889 clearPhysical();
890 }
891 }
892
893 /**
894 * Convert a CellAttributes foreground color to an Swing Color.
895 *
896 * @param attr the text attributes
897 * @return the Swing Color
898 */
899 public static Color attrToForegroundColor(final CellAttributes attr) {
900 int rgb = attr.getForeColorRGB();
901 if (rgb >= 0) {
902 int red = (rgb >> 16) & 0xFF;
903 int green = (rgb >> 8) & 0xFF;
904 int blue = rgb & 0xFF;
905
906 return new Color(red, green, blue);
907 }
908
909 if (attr.isBold()) {
910 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
911 return MYBOLD_BLACK;
912 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
913 return MYBOLD_RED;
914 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
915 return MYBOLD_BLUE;
916 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
917 return MYBOLD_GREEN;
918 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
919 return MYBOLD_YELLOW;
920 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
921 return MYBOLD_CYAN;
922 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
923 return MYBOLD_MAGENTA;
924 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
925 return MYBOLD_WHITE;
926 }
927 } else {
928 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
929 return MYBLACK;
930 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
931 return MYRED;
932 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
933 return MYBLUE;
934 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
935 return MYGREEN;
936 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
937 return MYYELLOW;
938 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
939 return MYCYAN;
940 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
941 return MYMAGENTA;
942 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
943 return MYWHITE;
944 }
945 }
946 throw new IllegalArgumentException("Invalid color: " +
947 attr.getForeColor().getValue());
948 }
949
950 /**
951 * Convert a CellAttributes background color to an Swing Color.
952 *
953 * @param attr the text attributes
954 * @return the Swing Color
955 */
956 public static Color attrToBackgroundColor(final CellAttributes attr) {
957 int rgb = attr.getBackColorRGB();
958 if (rgb >= 0) {
959 int red = (rgb >> 16) & 0xFF;
960 int green = (rgb >> 8) & 0xFF;
961 int blue = rgb & 0xFF;
962
963 return new Color(red, green, blue);
964 }
965
966 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
967 return MYBLACK;
968 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
969 return MYRED;
970 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
971 return MYBLUE;
972 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
973 return MYGREEN;
974 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
975 return MYYELLOW;
976 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
977 return MYCYAN;
978 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
979 return MYMAGENTA;
980 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
981 return MYWHITE;
982 }
983 throw new IllegalArgumentException("Invalid color: " +
984 attr.getBackColor().getValue());
985 }
986
987 /**
988 * Figure out what textAdjustX, textAdjustY, textAdjustHeight, and
989 * textAdjustWidth should be, based on the location of a vertical bar and
990 * a horizontal bar.
991 */
992 private void getFontAdjustments() {
993 BufferedImage image = null;
994
995 // What SHOULD happen is that the topmost/leftmost white pixel is at
996 // position (gr2x, gr2y). But it might also be off by a pixel in
997 // either direction.
998
999 Graphics2D gr2 = null;
1000 int gr2x = 3;
1001 int gr2y = 3;
1002 image = new BufferedImage(fontTextWidth * 2, fontTextHeight * 2,
1003 BufferedImage.TYPE_INT_ARGB);
1004
1005 gr2 = image.createGraphics();
1006 gr2.setFont(swing.getFont());
1007 gr2.setColor(java.awt.Color.BLACK);
1008 gr2.fillRect(0, 0, fontTextWidth * 2, fontTextHeight * 2);
1009 gr2.setColor(java.awt.Color.WHITE);
1010 char [] chars = new char[1];
1011 chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR;
1012 gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
1013 chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR;
1014 gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
1015 gr2.dispose();
1016
1017 int top = fontTextHeight * 2;
1018 int bottom = -1;
1019 int left = fontTextWidth * 2;
1020 int right = -1;
1021 textAdjustX = 0;
1022 textAdjustY = 0;
1023 textAdjustHeight = 0;
1024 textAdjustWidth = 0;
1025
1026 for (int x = 0; x < fontTextWidth * 2; x++) {
1027 for (int y = 0; y < fontTextHeight * 2; y++) {
1028
1029 /*
1030 System.err.println("H X: " + x + " Y: " + y + " " +
1031 image.getRGB(x, y));
1032 */
1033
1034 if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
1035 // Pixel is present.
1036 if (y < top) {
1037 top = y;
1038 }
1039 if (y > bottom) {
1040 bottom = y;
1041 }
1042 if (x < left) {
1043 left = x;
1044 }
1045 if (x > right) {
1046 right = x;
1047 }
1048 }
1049 }
1050 }
1051 if (left < right) {
1052 textAdjustX = (gr2x - left);
1053 textAdjustWidth = fontTextWidth - (right - left + 1);
1054 }
1055 if (top < bottom) {
1056 textAdjustY = (gr2y - top);
1057 textAdjustHeight = fontTextHeight - (bottom - top + 1);
1058 }
1059 // System.err.println("top " + top + " bottom " + bottom);
1060 // System.err.println("left " + left + " right " + right);
1061
1062 // Special case: do not believe fonts that claim to be wider than
1063 // they are tall.
1064 if (fontTextWidth >= fontTextHeight) {
1065 textAdjustX = 0;
1066 textAdjustWidth = 0;
1067 fontTextWidth = fontTextHeight / 2;
1068 }
1069 }
1070
1071 /**
1072 * Figure out my font dimensions. This code path works OK for the JFrame
1073 * case, and can be called immediately after JFrame creation.
1074 */
1075 private void getFontDimensions() {
1076 swing.setFont(font);
1077 Graphics gr = swing.getGraphics();
1078 if (gr == null) {
1079 return;
1080 }
1081 getFontDimensions(gr);
1082 }
1083
1084 /**
1085 * Figure out my font dimensions. This code path is needed to lazy-load
1086 * the information inside paint().
1087 *
1088 * @param gr Graphics object to use
1089 */
1090 private void getFontDimensions(final Graphics gr) {
1091 swing.setFont(font);
1092 FontMetrics fm = gr.getFontMetrics();
1093 maxDescent = fm.getMaxDescent();
1094 Rectangle2D bounds = fm.getMaxCharBounds(gr);
1095 int leading = fm.getLeading();
1096 fontTextWidth = (int)Math.round(bounds.getWidth());
1097 // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
1098
1099 // This produces the same number, but works better for ugly
1100 // monospace.
1101 fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
1102
1103 getFontAdjustments();
1104 textHeight = fontTextHeight + textAdjustHeight;
1105 textWidth = fontTextWidth + textAdjustWidth;
1106
1107 if (sessionInfo != null) {
1108 sessionInfo.setTextCellDimensions(textWidth, textHeight);
1109 }
1110 gotFontDimensions = true;
1111 }
1112
1113 /**
1114 * Resize the physical screen to match the logical screen dimensions.
1115 *
1116 * @param resizeComponent if true, resize the Swing component
1117 */
1118 private void resizeToScreen(final boolean resizeComponent) {
1119 if (resizeComponent) {
1120 swing.setDimensions(textWidth * width, textHeight * height);
1121 }
1122 clearPhysical();
1123 }
1124
1125 /**
1126 * Resize the physical screen to match the logical screen dimensions.
1127 */
1128 @Override
1129 public void resizeToScreen() {
1130 resizeToScreen(false);
1131 }
1132
1133 /**
1134 * Draw one cell's image to the screen.
1135 *
1136 * @param gr the Swing Graphics context
1137 * @param cell the Cell to draw
1138 * @param xPixel the x-coordinate to render to. 0 means the
1139 * left-most pixel column.
1140 * @param yPixel the y-coordinate to render to. 0 means the top-most
1141 * pixel row.
1142 */
1143 private void drawImage(final Graphics gr, final Cell cell,
1144 final int xPixel, final int yPixel) {
1145
1146 /*
1147 System.err.println("drawImage(): " + xPixel + " " + yPixel +
1148 " " + cell);
1149 */
1150
1151 // Draw the background rectangle, then the foreground character.
1152 assert (cell.isImage());
1153 gr.setColor(cell.getBackground());
1154 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
1155
1156 BufferedImage image = cell.getImage();
1157 if (image != null) {
1158 if (swing.getFrame() != null) {
1159 gr.drawImage(image, xPixel, yPixel, swing.getFrame());
1160 } else {
1161 gr.drawImage(image, xPixel, yPixel, swing.getComponent());
1162 }
1163 return;
1164 }
1165 }
1166
1167 /**
1168 * Draw one glyph to the screen.
1169 *
1170 * @param gr the Swing Graphics context
1171 * @param cell the Cell to draw
1172 * @param xPixel the x-coordinate to render to. 0 means the
1173 * left-most pixel column.
1174 * @param yPixel the y-coordinate to render to. 0 means the top-most
1175 * pixel row.
1176 */
1177 private void drawGlyph(final Graphics gr, final Cell cell,
1178 final int xPixel, final int yPixel) {
1179
1180 /*
1181 System.err.println("drawGlyph(): " + xPixel + " " + yPixel +
1182 " " + cell);
1183 */
1184
1185 BufferedImage image = null;
1186 if (cell.isBlink() && !cursorBlinkVisible) {
1187 image = glyphCacheBlink.get(cell);
1188 } else {
1189 image = glyphCache.get(cell);
1190 }
1191 if (image != null) {
1192 if (swing.getFrame() != null) {
1193 gr.drawImage(image, xPixel, yPixel, swing.getFrame());
1194 } else {
1195 gr.drawImage(image, xPixel, yPixel, swing.getComponent());
1196 }
1197 return;
1198 }
1199
1200 // Generate glyph and draw it.
1201 Graphics2D gr2 = null;
1202 int gr2x = xPixel;
1203 int gr2y = yPixel;
1204 if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
1205 image = new BufferedImage(textWidth, textHeight,
1206 BufferedImage.TYPE_INT_ARGB);
1207 gr2 = image.createGraphics();
1208 gr2.setFont(swing.getFont());
1209 gr2x = 0;
1210 gr2y = 0;
1211 } else {
1212 gr2 = (Graphics2D) gr;
1213 }
1214
1215 Cell cellColor = new Cell(cell);
1216
1217 // Check for reverse
1218 if (cell.isReverse()) {
1219 cellColor.setForeColor(cell.getBackColor());
1220 cellColor.setBackColor(cell.getForeColor());
1221 }
1222
1223 // Draw the background rectangle, then the foreground character.
1224 gr2.setColor(attrToBackgroundColor(cellColor));
1225 gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
1226
1227 // Handle blink and underline
1228 if (!cell.isBlink()
1229 || (cell.isBlink() && cursorBlinkVisible)
1230 ) {
1231 gr2.setColor(attrToForegroundColor(cellColor));
1232 char [] chars = new char[1];
1233 chars[0] = cell.getChar();
1234 gr2.drawChars(chars, 0, 1, gr2x + textAdjustX,
1235 gr2y + textHeight - maxDescent + textAdjustY);
1236
1237 if (cell.isUnderline()) {
1238 gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
1239 }
1240 }
1241
1242 if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
1243 gr2.dispose();
1244
1245 // We need a new key that will not be mutated by
1246 // invertCell().
1247 Cell key = new Cell(cell);
1248 if (cell.isBlink() && !cursorBlinkVisible) {
1249 glyphCacheBlink.put(key, image);
1250 } else {
1251 glyphCache.put(key, image);
1252 }
1253
1254 if (swing.getFrame() != null) {
1255 gr.drawImage(image, xPixel, yPixel, swing.getFrame());
1256 } else {
1257 gr.drawImage(image, xPixel, yPixel, swing.getComponent());
1258 }
1259 }
1260
1261 }
1262
1263 /**
1264 * Check if the cursor is visible, and if so draw it.
1265 *
1266 * @param gr the Swing Graphics context
1267 */
1268 private void drawCursor(final Graphics gr) {
1269
1270 if (cursorVisible
1271 && (cursorY >= 0)
1272 && (cursorX >= 0)
1273 && (cursorY <= height - 1)
1274 && (cursorX <= width - 1)
1275 && cursorBlinkVisible
1276 ) {
1277 int xPixel = cursorX * textWidth + left;
1278 int yPixel = cursorY * textHeight + top;
1279 Cell lCell = logical[cursorX][cursorY];
1280 int cursorWidth = textWidth;
1281 switch (lCell.getWidth()) {
1282 case SINGLE:
1283 // NOP
1284 break;
1285 case LEFT:
1286 cursorWidth *= 2;
1287 break;
1288 case RIGHT:
1289 cursorWidth *= 2;
1290 xPixel -= textWidth;
1291 break;
1292 }
1293 gr.setColor(attrToForegroundColor(lCell));
1294 switch (cursorStyle) {
1295 default:
1296 // Fall through...
1297 case UNDERLINE:
1298 gr.fillRect(xPixel, yPixel + textHeight - 2, cursorWidth, 2);
1299 break;
1300 case BLOCK:
1301 gr.fillRect(xPixel, yPixel, cursorWidth, textHeight);
1302 break;
1303 case OUTLINE:
1304 gr.drawRect(xPixel, yPixel, cursorWidth - 1, textHeight - 1);
1305 break;
1306 }
1307 }
1308 }
1309
1310 /**
1311 * Reset the blink timer.
1312 */
1313 private void resetBlinkTimer() {
1314 lastBlinkTime = System.currentTimeMillis();
1315 cursorBlinkVisible = true;
1316 }
1317
1318 /**
1319 * Paint redraws the whole screen.
1320 *
1321 * @param gr the Swing Graphics context
1322 */
1323 public void paint(final Graphics gr) {
1324
1325 if (gotFontDimensions == false) {
1326 // Lazy-load the text width/height
1327 getFontDimensions(gr);
1328 /*
1329 System.err.println("textWidth " + textWidth +
1330 " textHeight " + textHeight);
1331 System.err.println("FONT: " + swing.getFont() + " font " + font);
1332 */
1333 }
1334
1335 if ((swing.getFrame() != null)
1336 && (swing.getBufferStrategy() != null)
1337 && (SwingUtilities.isEventDispatchThread())
1338 ) {
1339 // System.err.println("paint(), skip first paint on swing thread");
1340 return;
1341 }
1342
1343 int xCellMin = 0;
1344 int xCellMax = width;
1345 int yCellMin = 0;
1346 int yCellMax = height;
1347
1348 Rectangle bounds = gr.getClipBounds();
1349 if (bounds != null) {
1350 // Only update what is in the bounds
1351 xCellMin = textColumn(bounds.x);
1352 xCellMax = textColumn(bounds.x + bounds.width);
1353 if (xCellMax > width) {
1354 xCellMax = width;
1355 }
1356 if (xCellMin >= xCellMax) {
1357 xCellMin = xCellMax - 2;
1358 }
1359 if (xCellMin < 0) {
1360 xCellMin = 0;
1361 }
1362 yCellMin = textRow(bounds.y);
1363 yCellMax = textRow(bounds.y + bounds.height);
1364 if (yCellMax > height) {
1365 yCellMax = height;
1366 }
1367 if (yCellMin >= yCellMax) {
1368 yCellMin = yCellMax - 2;
1369 }
1370 if (yCellMin < 0) {
1371 yCellMin = 0;
1372 }
1373 } else {
1374 // We need a total repaint
1375 reallyCleared = true;
1376 }
1377
1378 // Prevent updates to the screen's data from the TApplication
1379 // threads.
1380 synchronized (this) {
1381
1382 /*
1383 System.err.printf("bounds %s X %d %d Y %d %d\n",
1384 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
1385 */
1386
1387 for (int y = yCellMin; y < yCellMax; y++) {
1388 for (int x = xCellMin; x < xCellMax; x++) {
1389
1390 int xPixel = x * textWidth + left;
1391 int yPixel = y * textHeight + top;
1392
1393 Cell lCell = logical[x][y];
1394 Cell pCell = physical[x][y];
1395
1396 if (!lCell.equals(pCell)
1397 || lCell.isBlink()
1398 || reallyCleared
1399 || (swing.getFrame() == null)) {
1400
1401 if (lCell.isImage()) {
1402 drawImage(gr, lCell, xPixel, yPixel);
1403 } else {
1404 drawGlyph(gr, lCell, xPixel, yPixel);
1405 }
1406
1407 // Physical is always updated
1408 physical[x][y].setTo(lCell);
1409 }
1410 }
1411 }
1412 drawCursor(gr);
1413
1414 reallyCleared = false;
1415 } // synchronized (this)
1416 }
1417
1418 /**
1419 * Restore terminal to normal state.
1420 */
1421 public void shutdown() {
1422 swing.dispose();
1423 }
1424
1425 /**
1426 * Push the logical screen to the physical device.
1427 */
1428 private void drawToSwing() {
1429
1430 /*
1431 System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
1432 reallyCleared, dirty);
1433 */
1434
1435 // If reallyCleared is set, we have to draw everything.
1436 if ((swing.getFrame() != null)
1437 && (swing.getBufferStrategy() != null)
1438 && (reallyCleared == true)
1439 ) {
1440 // Triple-buffering: we have to redraw everything on this thread.
1441 Graphics gr = swing.getBufferStrategy().getDrawGraphics();
1442 swing.paint(gr);
1443 gr.dispose();
1444 swing.getBufferStrategy().show();
1445 Toolkit.getDefaultToolkit().sync();
1446 return;
1447 } else if (((swing.getFrame() != null)
1448 && (swing.getBufferStrategy() == null))
1449 || (reallyCleared == true)
1450 ) {
1451 // Repaint everything on the Swing thread.
1452 // System.err.println("REPAINT ALL");
1453 swing.repaint();
1454 return;
1455 }
1456
1457 if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
1458 Graphics gr = swing.getBufferStrategy().getDrawGraphics();
1459
1460 synchronized (this) {
1461 for (int y = 0; y < height; y++) {
1462 for (int x = 0; x < width; x++) {
1463 Cell lCell = logical[x][y];
1464 Cell pCell = physical[x][y];
1465
1466 int xPixel = x * textWidth + left;
1467 int yPixel = y * textHeight + top;
1468
1469 if (!lCell.equals(pCell)
1470 || ((x == cursorX)
1471 && (y == cursorY)
1472 && cursorVisible)
1473 || (lCell.isBlink())
1474 ) {
1475 if (lCell.isImage()) {
1476 drawImage(gr, lCell, xPixel, yPixel);
1477 } else {
1478 drawGlyph(gr, lCell, xPixel, yPixel);
1479 }
1480 physical[x][y].setTo(lCell);
1481 }
1482 }
1483 }
1484 drawCursor(gr);
1485 } // synchronized (this)
1486
1487 gr.dispose();
1488 swing.getBufferStrategy().show();
1489 Toolkit.getDefaultToolkit().sync();
1490 return;
1491 }
1492
1493 // Swing thread version: request a repaint, but limit it to the area
1494 // that has changed.
1495
1496 // Find the minimum-size damaged region.
1497 int xMin = swing.getWidth();
1498 int xMax = 0;
1499 int yMin = swing.getHeight();
1500 int yMax = 0;
1501
1502 synchronized (this) {
1503 for (int y = 0; y < height; y++) {
1504 for (int x = 0; x < width; x++) {
1505 Cell lCell = logical[x][y];
1506 Cell pCell = physical[x][y];
1507
1508 int xPixel = x * textWidth + left;
1509 int yPixel = y * textHeight + top;
1510
1511 if (!lCell.equals(pCell)
1512 || ((x == cursorX)
1513 && (y == cursorY)
1514 && cursorVisible)
1515 || lCell.isBlink()
1516 ) {
1517 if (xPixel < xMin) {
1518 xMin = xPixel;
1519 }
1520 if (xPixel + textWidth > xMax) {
1521 xMax = xPixel + textWidth;
1522 }
1523 if (yPixel < yMin) {
1524 yMin = yPixel;
1525 }
1526 if (yPixel + textHeight > yMax) {
1527 yMax = yPixel + textHeight;
1528 }
1529 }
1530 }
1531 }
1532 }
1533 if (xMin + textWidth >= xMax) {
1534 xMax += textWidth;
1535 }
1536 if (yMin + textHeight >= yMax) {
1537 yMax += textHeight;
1538 }
1539
1540 // Repaint the desired area
1541 /*
1542 System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
1543 yMin, yMax);
1544 */
1545
1546 if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
1547 // This path should never be taken, but is left here for
1548 // completeness.
1549 Graphics gr = swing.getBufferStrategy().getDrawGraphics();
1550 Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
1551 yMax - yMin);
1552 gr.setClip(bounds);
1553 swing.paint(gr);
1554 gr.dispose();
1555 swing.getBufferStrategy().show();
1556 Toolkit.getDefaultToolkit().sync();
1557 } else {
1558 // Repaint on the Swing thread.
1559 swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
1560 }
1561 }
1562
1563 /**
1564 * Convert pixel column position to text cell column position.
1565 *
1566 * @param x pixel column position
1567 * @return text cell column position
1568 */
1569 public int textColumn(final int x) {
1570 int column = ((x - left) / textWidth);
1571 if (column < 0) {
1572 column = 0;
1573 }
1574 if (column > width - 1) {
1575 column = width - 1;
1576 }
1577 return column;
1578 }
1579
1580 /**
1581 * Convert pixel row position to text cell row position.
1582 *
1583 * @param y pixel row position
1584 * @return text cell row position
1585 */
1586 public int textRow(final int y) {
1587 int row = ((y - top) / textHeight);
1588 if (row < 0) {
1589 row = 0;
1590 }
1591 if (row > height - 1) {
1592 row = height - 1;
1593 }
1594 return row;
1595 }
1596
1597 /**
1598 * Getter for sessionInfo.
1599 *
1600 * @return the SessionInfo
1601 */
1602 public SessionInfo getSessionInfo() {
1603 return sessionInfo;
1604 }
1605
1606 /**
1607 * Getter for the underlying Swing component.
1608 *
1609 * @return the SwingComponent
1610 */
1611 public SwingComponent getSwingComponent() {
1612 return swing;
1613 }
1614
1615 // ------------------------------------------------------------------------
1616 // KeyListener ------------------------------------------------------------
1617 // ------------------------------------------------------------------------
1618
1619 /**
1620 * Pass Swing keystrokes into the event queue.
1621 *
1622 * @param key keystroke received
1623 */
1624 public void keyReleased(final KeyEvent key) {
1625 // Ignore release events
1626 }
1627
1628 /**
1629 * Pass Swing keystrokes into the event queue.
1630 *
1631 * @param key keystroke received
1632 */
1633 public void keyTyped(final KeyEvent key) {
1634 // Ignore typed events
1635 }
1636
1637 /**
1638 * Pass Swing keystrokes into the event queue.
1639 *
1640 * @param key keystroke received
1641 */
1642 public void keyPressed(final KeyEvent key) {
1643 boolean alt = false;
1644 boolean shift = false;
1645 boolean ctrl = false;
1646 char ch = ' ';
1647 boolean isKey = false;
1648 if (key.isActionKey()) {
1649 isKey = true;
1650 } else {
1651 ch = key.getKeyChar();
1652 }
1653 alt = key.isAltDown();
1654 ctrl = key.isControlDown();
1655 shift = key.isShiftDown();
1656
1657 /*
1658 System.err.printf("Swing Key: %s\n", key);
1659 System.err.printf(" isKey: %s\n", isKey);
1660 System.err.printf(" alt: %s\n", alt);
1661 System.err.printf(" ctrl: %s\n", ctrl);
1662 System.err.printf(" shift: %s\n", shift);
1663 System.err.printf(" ch: %s\n", ch);
1664 */
1665
1666 // Special case: not return the bare modifier presses
1667 switch (key.getKeyCode()) {
1668 case KeyEvent.VK_ALT:
1669 return;
1670 case KeyEvent.VK_ALT_GRAPH:
1671 return;
1672 case KeyEvent.VK_CONTROL:
1673 return;
1674 case KeyEvent.VK_SHIFT:
1675 return;
1676 case KeyEvent.VK_META:
1677 return;
1678 default:
1679 break;
1680 }
1681
1682 TKeypress keypress = null;
1683 if (isKey) {
1684 switch (key.getKeyCode()) {
1685 case KeyEvent.VK_F1:
1686 keypress = new TKeypress(true, TKeypress.F1, ' ',
1687 alt, ctrl, shift);
1688 break;
1689 case KeyEvent.VK_F2:
1690 keypress = new TKeypress(true, TKeypress.F2, ' ',
1691 alt, ctrl, shift);
1692 break;
1693 case KeyEvent.VK_F3:
1694 keypress = new TKeypress(true, TKeypress.F3, ' ',
1695 alt, ctrl, shift);
1696 break;
1697 case KeyEvent.VK_F4:
1698 keypress = new TKeypress(true, TKeypress.F4, ' ',
1699 alt, ctrl, shift);
1700 break;
1701 case KeyEvent.VK_F5:
1702 keypress = new TKeypress(true, TKeypress.F5, ' ',
1703 alt, ctrl, shift);
1704 break;
1705 case KeyEvent.VK_F6:
1706 keypress = new TKeypress(true, TKeypress.F6, ' ',
1707 alt, ctrl, shift);
1708 break;
1709 case KeyEvent.VK_F7:
1710 keypress = new TKeypress(true, TKeypress.F7, ' ',
1711 alt, ctrl, shift);
1712 break;
1713 case KeyEvent.VK_F8:
1714 keypress = new TKeypress(true, TKeypress.F8, ' ',
1715 alt, ctrl, shift);
1716 break;
1717 case KeyEvent.VK_F9:
1718 keypress = new TKeypress(true, TKeypress.F9, ' ',
1719 alt, ctrl, shift);
1720 break;
1721 case KeyEvent.VK_F10:
1722 keypress = new TKeypress(true, TKeypress.F10, ' ',
1723 alt, ctrl, shift);
1724 break;
1725 case KeyEvent.VK_F11:
1726 keypress = new TKeypress(true, TKeypress.F11, ' ',
1727 alt, ctrl, shift);
1728 break;
1729 case KeyEvent.VK_F12:
1730 keypress = new TKeypress(true, TKeypress.F12, ' ',
1731 alt, ctrl, shift);
1732 break;
1733 case KeyEvent.VK_HOME:
1734 keypress = new TKeypress(true, TKeypress.HOME, ' ',
1735 alt, ctrl, shift);
1736 break;
1737 case KeyEvent.VK_END:
1738 keypress = new TKeypress(true, TKeypress.END, ' ',
1739 alt, ctrl, shift);
1740 break;
1741 case KeyEvent.VK_PAGE_UP:
1742 keypress = new TKeypress(true, TKeypress.PGUP, ' ',
1743 alt, ctrl, shift);
1744 break;
1745 case KeyEvent.VK_PAGE_DOWN:
1746 keypress = new TKeypress(true, TKeypress.PGDN, ' ',
1747 alt, ctrl, shift);
1748 break;
1749 case KeyEvent.VK_INSERT:
1750 keypress = new TKeypress(true, TKeypress.INS, ' ',
1751 alt, ctrl, shift);
1752 break;
1753 case KeyEvent.VK_DELETE:
1754 keypress = new TKeypress(true, TKeypress.DEL, ' ',
1755 alt, ctrl, shift);
1756 break;
1757 case KeyEvent.VK_RIGHT:
1758 keypress = new TKeypress(true, TKeypress.RIGHT, ' ',
1759 alt, ctrl, shift);
1760 break;
1761 case KeyEvent.VK_LEFT:
1762 keypress = new TKeypress(true, TKeypress.LEFT, ' ',
1763 alt, ctrl, shift);
1764 break;
1765 case KeyEvent.VK_UP:
1766 keypress = new TKeypress(true, TKeypress.UP, ' ',
1767 alt, ctrl, shift);
1768 break;
1769 case KeyEvent.VK_DOWN:
1770 keypress = new TKeypress(true, TKeypress.DOWN, ' ',
1771 alt, ctrl, shift);
1772 break;
1773 case KeyEvent.VK_TAB:
1774 // Special case: distinguish TAB vs BTAB
1775 if (shift) {
1776 keypress = kbShiftTab;
1777 } else {
1778 keypress = kbTab;
1779 }
1780 break;
1781 case KeyEvent.VK_ENTER:
1782 keypress = new TKeypress(true, TKeypress.ENTER, ' ',
1783 alt, ctrl, shift);
1784 break;
1785 case KeyEvent.VK_ESCAPE:
1786 keypress = new TKeypress(true, TKeypress.ESC, ' ',
1787 alt, ctrl, shift);
1788 break;
1789 case KeyEvent.VK_BACK_SPACE:
1790 keypress = kbBackspace;
1791 break;
1792 default:
1793 // Unsupported, ignore
1794 return;
1795 }
1796 }
1797
1798 if (keypress == null) {
1799 switch (ch) {
1800 case 0x08:
1801 // Disambiguate ^H from Backspace.
1802 if (KeyEvent.getKeyText(key.getKeyCode()).equals("H")) {
1803 // This is ^H.
1804 keypress = kbBackspace;
1805 } else {
1806 // We are emulating Xterm here, where the backspace key
1807 // on the keyboard returns ^?.
1808 keypress = kbBackspaceDel;
1809 }
1810 break;
1811 case 0x0A:
1812 keypress = kbEnter;
1813 break;
1814 case 0x1B:
1815 keypress = kbEsc;
1816 break;
1817 case 0x0D:
1818 keypress = kbEnter;
1819 break;
1820 case 0x09:
1821 if (shift) {
1822 keypress = kbShiftTab;
1823 } else {
1824 keypress = kbTab;
1825 }
1826 break;
1827 case 0x7F:
1828 keypress = kbDel;
1829 break;
1830 default:
1831 if (!alt && ctrl && !shift) {
1832 ch = KeyEvent.getKeyText(key.getKeyCode()).charAt(0);
1833 }
1834 // Not a special key, put it together
1835 keypress = new TKeypress(false, 0, ch, alt, ctrl, shift);
1836 }
1837 }
1838
1839 // Save it and we are done.
1840 synchronized (eventQueue) {
1841 eventQueue.add(new TKeypressEvent(keypress));
1842 resetBlinkTimer();
1843 }
1844 if (listener != null) {
1845 synchronized (listener) {
1846 listener.notifyAll();
1847 }
1848 }
1849 }
1850
1851 // ------------------------------------------------------------------------
1852 // WindowListener ---------------------------------------------------------
1853 // ------------------------------------------------------------------------
1854
1855 /**
1856 * Pass window events into the event queue.
1857 *
1858 * @param event window event received
1859 */
1860 public void windowActivated(final WindowEvent event) {
1861 // Force a total repaint
1862 synchronized (this) {
1863 clearPhysical();
1864 }
1865 }
1866
1867 /**
1868 * Pass window events into the event queue.
1869 *
1870 * @param event window event received
1871 */
1872 public void windowClosed(final WindowEvent event) {
1873 // Ignore
1874 }
1875
1876 /**
1877 * Pass window events into the event queue.
1878 *
1879 * @param event window event received
1880 */
1881 public void windowClosing(final WindowEvent event) {
1882 // Drop a cmBackendDisconnect and walk away
1883 synchronized (eventQueue) {
1884 eventQueue.add(new TCommandEvent(cmBackendDisconnect));
1885 resetBlinkTimer();
1886 }
1887 if (listener != null) {
1888 synchronized (listener) {
1889 listener.notifyAll();
1890 }
1891 }
1892 }
1893
1894 /**
1895 * Pass window events into the event queue.
1896 *
1897 * @param event window event received
1898 */
1899 public void windowDeactivated(final WindowEvent event) {
1900 // Ignore
1901 }
1902
1903 /**
1904 * Pass window events into the event queue.
1905 *
1906 * @param event window event received
1907 */
1908 public void windowDeiconified(final WindowEvent event) {
1909 // Ignore
1910 }
1911
1912 /**
1913 * Pass window events into the event queue.
1914 *
1915 * @param event window event received
1916 */
1917 public void windowIconified(final WindowEvent event) {
1918 // Ignore
1919 }
1920
1921 /**
1922 * Pass window events into the event queue.
1923 *
1924 * @param event window event received
1925 */
1926 public void windowOpened(final WindowEvent event) {
1927 // Ignore
1928 }
1929
1930 // ------------------------------------------------------------------------
1931 // ComponentListener ------------------------------------------------------
1932 // ------------------------------------------------------------------------
1933
1934 /**
1935 * Pass component events into the event queue.
1936 *
1937 * @param event component event received
1938 */
1939 public void componentHidden(final ComponentEvent event) {
1940 // Ignore
1941 }
1942
1943 /**
1944 * Pass component events into the event queue.
1945 *
1946 * @param event component event received
1947 */
1948 public void componentShown(final ComponentEvent event) {
1949 // Ignore
1950 }
1951
1952 /**
1953 * Pass component events into the event queue.
1954 *
1955 * @param event component event received
1956 */
1957 public void componentMoved(final ComponentEvent event) {
1958 // Ignore
1959 }
1960
1961 /**
1962 * Pass component events into the event queue.
1963 *
1964 * @param event component event received
1965 */
1966 public void componentResized(final ComponentEvent event) {
1967 if (gotFontDimensions == false) {
1968 // We are still waiting to get font information. Don't pass a
1969 // resize event up.
1970 // System.err.println("size " + swing.getComponent().getSize());
1971 return;
1972 }
1973
1974 if (sessionInfo == null) {
1975 // This is the initial component resize in construction, bail
1976 // out.
1977 return;
1978 }
1979
1980 // Drop a new TResizeEvent into the queue
1981 sessionInfo.queryWindowSize();
1982 synchronized (eventQueue) {
1983 TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
1984 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
1985 eventQueue.add(windowResize);
1986 resetBlinkTimer();
1987 /*
1988 System.err.println("Add resize event: " + windowResize.getWidth() +
1989 " x " + windowResize.getHeight());
1990 */
1991 }
1992 if (listener != null) {
1993 synchronized (listener) {
1994 listener.notifyAll();
1995 }
1996 }
1997 }
1998
1999 // ------------------------------------------------------------------------
2000 // MouseMotionListener ----------------------------------------------------
2001 // ------------------------------------------------------------------------
2002
2003 /**
2004 * Pass mouse events into the event queue.
2005 *
2006 * @param mouse mouse event received
2007 */
2008 public void mouseDragged(final MouseEvent mouse) {
2009 int modifiers = mouse.getModifiersEx();
2010 boolean eventMouse1 = false;
2011 boolean eventMouse2 = false;
2012 boolean eventMouse3 = false;
2013 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2014 eventMouse1 = true;
2015 }
2016 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2017 eventMouse2 = true;
2018 }
2019 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2020 eventMouse3 = true;
2021 }
2022 mouse1 = eventMouse1;
2023 mouse2 = eventMouse2;
2024 mouse3 = eventMouse3;
2025 int x = textColumn(mouse.getX());
2026 int y = textRow(mouse.getY());
2027
2028 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
2029 x, y, x, y, mouse1, mouse2, mouse3, false, false);
2030
2031 synchronized (eventQueue) {
2032 eventQueue.add(mouseEvent);
2033 resetBlinkTimer();
2034 }
2035 if (listener != null) {
2036 synchronized (listener) {
2037 listener.notifyAll();
2038 }
2039 }
2040 }
2041
2042 /**
2043 * Pass mouse events into the event queue.
2044 *
2045 * @param mouse mouse event received
2046 */
2047 public void mouseMoved(final MouseEvent mouse) {
2048 int x = textColumn(mouse.getX());
2049 int y = textRow(mouse.getY());
2050 if ((x == oldMouseX) && (y == oldMouseY)) {
2051 // Bail out, we've moved some pixels but not a whole text cell.
2052 return;
2053 }
2054 oldMouseX = x;
2055 oldMouseY = y;
2056
2057 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
2058 x, y, x, y, mouse1, mouse2, mouse3, false, false);
2059
2060 synchronized (eventQueue) {
2061 eventQueue.add(mouseEvent);
2062 resetBlinkTimer();
2063 }
2064 if (listener != null) {
2065 synchronized (listener) {
2066 listener.notifyAll();
2067 }
2068 }
2069 }
2070
2071 // ------------------------------------------------------------------------
2072 // MouseListener ----------------------------------------------------------
2073 // ------------------------------------------------------------------------
2074
2075 /**
2076 * Pass mouse events into the event queue.
2077 *
2078 * @param mouse mouse event received
2079 */
2080 public void mouseClicked(final MouseEvent mouse) {
2081 // Ignore
2082 }
2083
2084 /**
2085 * Pass mouse events into the event queue.
2086 *
2087 * @param mouse mouse event received
2088 */
2089 public void mouseEntered(final MouseEvent mouse) {
2090 // Ignore
2091 }
2092
2093 /**
2094 * Pass mouse events into the event queue.
2095 *
2096 * @param mouse mouse event received
2097 */
2098 public void mouseExited(final MouseEvent mouse) {
2099 // Ignore
2100 }
2101
2102 /**
2103 * Pass mouse events into the event queue.
2104 *
2105 * @param mouse mouse event received
2106 */
2107 public void mousePressed(final MouseEvent mouse) {
2108 int modifiers = mouse.getModifiersEx();
2109 boolean eventMouse1 = false;
2110 boolean eventMouse2 = false;
2111 boolean eventMouse3 = false;
2112 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2113 eventMouse1 = true;
2114 }
2115 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2116 eventMouse2 = true;
2117 }
2118 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2119 eventMouse3 = true;
2120 }
2121 mouse1 = eventMouse1;
2122 mouse2 = eventMouse2;
2123 mouse3 = eventMouse3;
2124 int x = textColumn(mouse.getX());
2125 int y = textRow(mouse.getY());
2126
2127 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
2128 x, y, x, y, mouse1, mouse2, mouse3, false, false);
2129
2130 synchronized (eventQueue) {
2131 eventQueue.add(mouseEvent);
2132 resetBlinkTimer();
2133 }
2134 if (listener != null) {
2135 synchronized (listener) {
2136 listener.notifyAll();
2137 }
2138 }
2139 }
2140
2141 /**
2142 * Pass mouse events into the event queue.
2143 *
2144 * @param mouse mouse event received
2145 */
2146 public void mouseReleased(final MouseEvent mouse) {
2147 int modifiers = mouse.getModifiersEx();
2148 boolean eventMouse1 = false;
2149 boolean eventMouse2 = false;
2150 boolean eventMouse3 = false;
2151 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2152 eventMouse1 = true;
2153 }
2154 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2155 eventMouse2 = true;
2156 }
2157 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2158 eventMouse3 = true;
2159 }
2160 if (mouse1) {
2161 mouse1 = false;
2162 eventMouse1 = true;
2163 }
2164 if (mouse2) {
2165 mouse2 = false;
2166 eventMouse2 = true;
2167 }
2168 if (mouse3) {
2169 mouse3 = false;
2170 eventMouse3 = true;
2171 }
2172 int x = textColumn(mouse.getX());
2173 int y = textRow(mouse.getY());
2174
2175 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP,
2176 x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false);
2177
2178 synchronized (eventQueue) {
2179 eventQueue.add(mouseEvent);
2180 resetBlinkTimer();
2181 }
2182 if (listener != null) {
2183 synchronized (listener) {
2184 listener.notifyAll();
2185 }
2186 }
2187 }
2188
2189 // ------------------------------------------------------------------------
2190 // MouseWheelListener -----------------------------------------------------
2191 // ------------------------------------------------------------------------
2192
2193 /**
2194 * Pass mouse events into the event queue.
2195 *
2196 * @param mouse mouse event received
2197 */
2198 public void mouseWheelMoved(final MouseWheelEvent mouse) {
2199 int modifiers = mouse.getModifiersEx();
2200 boolean eventMouse1 = false;
2201 boolean eventMouse2 = false;
2202 boolean eventMouse3 = false;
2203 boolean mouseWheelUp = false;
2204 boolean mouseWheelDown = false;
2205 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2206 eventMouse1 = true;
2207 }
2208 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2209 eventMouse2 = true;
2210 }
2211 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2212 eventMouse3 = true;
2213 }
2214 mouse1 = eventMouse1;
2215 mouse2 = eventMouse2;
2216 mouse3 = eventMouse3;
2217 int x = textColumn(mouse.getX());
2218 int y = textRow(mouse.getY());
2219 if (mouse.getWheelRotation() > 0) {
2220 mouseWheelDown = true;
2221 }
2222 if (mouse.getWheelRotation() < 0) {
2223 mouseWheelUp = true;
2224 }
2225
2226 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
2227 x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
2228
2229 synchronized (eventQueue) {
2230 eventQueue.add(mouseEvent);
2231 resetBlinkTimer();
2232 }
2233 if (listener != null) {
2234 synchronized (listener) {
2235 listener.notifyAll();
2236 }
2237 }
2238 }
2239
2240 }