cjk support wip
[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();
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 drawToSwing();
577 } while (swing.getBufferStrategy().contentsRestored());
578
579 swing.getBufferStrategy().show();
580 Toolkit.getDefaultToolkit().sync();
581 } while (swing.getBufferStrategy().contentsLost());
582
583 } else {
584 // Non-triple-buffered, call drawToSwing() once
585 drawToSwing();
586 }
587 }
588
589 // ------------------------------------------------------------------------
590 // TerminalReader ---------------------------------------------------------
591 // ------------------------------------------------------------------------
592
593 /**
594 * Check if there are events in the queue.
595 *
596 * @return if true, getEvents() has something to return to the backend
597 */
598 public boolean hasEvents() {
599 synchronized (eventQueue) {
600 return (eventQueue.size() > 0);
601 }
602 }
603
604 /**
605 * Return any events in the IO queue.
606 *
607 * @param queue list to append new events to
608 */
609 public void getEvents(final List<TInputEvent> queue) {
610 synchronized (eventQueue) {
611 if (eventQueue.size() > 0) {
612 synchronized (queue) {
613 queue.addAll(eventQueue);
614 }
615 eventQueue.clear();
616 }
617 }
618 }
619
620 /**
621 * Restore terminal to normal state.
622 */
623 public void closeTerminal() {
624 shutdown();
625 }
626
627 /**
628 * Set listener to a different Object.
629 *
630 * @param listener the new listening object that run() wakes up on new
631 * input
632 */
633 public void setListener(final Object listener) {
634 this.listener = listener;
635 }
636
637 /**
638 * Reload options from System properties.
639 */
640 public void reloadOptions() {
641 // Figure out my cursor style.
642 String cursorStyleString = System.getProperty(
643 "jexer.Swing.cursorStyle", "underline").toLowerCase();
644 if (cursorStyleString.equals("underline")) {
645 cursorStyle = CursorStyle.UNDERLINE;
646 } else if (cursorStyleString.equals("outline")) {
647 cursorStyle = CursorStyle.OUTLINE;
648 } else if (cursorStyleString.equals("block")) {
649 cursorStyle = CursorStyle.BLOCK;
650 }
651
652 // Pull the system property for triple buffering.
653 if (System.getProperty("jexer.Swing.tripleBuffer",
654 "true").equals("true")
655 ) {
656 SwingComponent.tripleBuffer = true;
657 } else {
658 SwingComponent.tripleBuffer = false;
659 }
660 }
661
662 // ------------------------------------------------------------------------
663 // SwingTerminal ----------------------------------------------------------
664 // ------------------------------------------------------------------------
665
666 /**
667 * Get the width of a character cell in pixels.
668 *
669 * @return the width in pixels of a character cell
670 */
671 public int getTextWidth() {
672 return textWidth;
673 }
674
675 /**
676 * Get the height of a character cell in pixels.
677 *
678 * @return the height in pixels of a character cell
679 */
680 public int getTextHeight() {
681 return textHeight;
682 }
683
684 /**
685 * Setup Swing colors to match DOS color palette.
686 */
687 private static void setDOSColors() {
688 if (dosColors) {
689 return;
690 }
691 MYBLACK = new Color(0x00, 0x00, 0x00);
692 MYRED = new Color(0xa8, 0x00, 0x00);
693 MYGREEN = new Color(0x00, 0xa8, 0x00);
694 MYYELLOW = new Color(0xa8, 0x54, 0x00);
695 MYBLUE = new Color(0x00, 0x00, 0xa8);
696 MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
697 MYCYAN = new Color(0x00, 0xa8, 0xa8);
698 MYWHITE = new Color(0xa8, 0xa8, 0xa8);
699 MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
700 MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
701 MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
702 MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
703 MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
704 MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
705 MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
706 MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
707
708 dosColors = true;
709 }
710
711 /**
712 * Get the number of millis to wait before switching the blink from
713 * visible to invisible.
714 *
715 * @return the number of milli to wait before switching the blink from
716 * visible to invisible
717 */
718 public long getBlinkMillis() {
719 return blinkMillis;
720 }
721
722 /**
723 * Get the current status of the blink flag.
724 *
725 * @return true if the cursor and blinking text should be visible
726 */
727 public boolean getCursorBlinkVisible() {
728 return cursorBlinkVisible;
729 }
730
731 /**
732 * Get the font size in points.
733 *
734 * @return font size in points
735 */
736 public int getFontSize() {
737 return fontSize;
738 }
739
740 /**
741 * Set the font size in points.
742 *
743 * @param fontSize font size in points
744 */
745 public void setFontSize(final int fontSize) {
746 this.fontSize = fontSize;
747 Font newFont = font.deriveFont((float) fontSize);
748 setFont(newFont);
749 }
750
751 /**
752 * Set to a new font, and resize the screen to match its dimensions.
753 *
754 * @param font the new font
755 */
756 public void setFont(final Font font) {
757 synchronized (this) {
758 this.font = font;
759 getFontDimensions();
760 swing.setFont(font);
761 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
762 glyphCache = new HashMap<Cell, BufferedImage>();
763 resizeToScreen();
764 }
765 }
766
767 /**
768 * Get the font this screen was last set to.
769 *
770 * @return the font
771 */
772 public Font getFont() {
773 return font;
774 }
775
776 /**
777 * Set the font to Terminus, the best all-around font for both CP437 and
778 * ISO8859-1.
779 */
780 public void setDefaultFont() {
781 try {
782 ClassLoader loader = Thread.currentThread().getContextClassLoader();
783 InputStream in = loader.getResourceAsStream(FONTFILE);
784 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
785 Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
786 font = terminus;
787 } catch (java.awt.FontFormatException e) {
788 e.printStackTrace();
789 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
790 } catch (java.io.IOException e) {
791 e.printStackTrace();
792 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
793 }
794
795 setFont(font);
796 }
797
798 /**
799 * Get the X text adjustment.
800 *
801 * @return X text adjustment
802 */
803 public int getTextAdjustX() {
804 return textAdjustX;
805 }
806
807 /**
808 * Set the X text adjustment.
809 *
810 * @param textAdjustX the X text adjustment
811 */
812 public void setTextAdjustX(final int textAdjustX) {
813 synchronized (this) {
814 this.textAdjustX = textAdjustX;
815 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
816 glyphCache = new HashMap<Cell, BufferedImage>();
817 clearPhysical();
818 }
819 }
820
821 /**
822 * Get the Y text adjustment.
823 *
824 * @return Y text adjustment
825 */
826 public int getTextAdjustY() {
827 return textAdjustY;
828 }
829
830 /**
831 * Set the Y text adjustment.
832 *
833 * @param textAdjustY the Y text adjustment
834 */
835 public void setTextAdjustY(final int textAdjustY) {
836 synchronized (this) {
837 this.textAdjustY = textAdjustY;
838 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
839 glyphCache = new HashMap<Cell, BufferedImage>();
840 clearPhysical();
841 }
842 }
843
844 /**
845 * Get the height text adjustment.
846 *
847 * @return height text adjustment
848 */
849 public int getTextAdjustHeight() {
850 return textAdjustHeight;
851 }
852
853 /**
854 * Set the height text adjustment.
855 *
856 * @param textAdjustHeight the height text adjustment
857 */
858 public void setTextAdjustHeight(final int textAdjustHeight) {
859 synchronized (this) {
860 this.textAdjustHeight = textAdjustHeight;
861 textHeight = fontTextHeight + textAdjustHeight;
862 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
863 glyphCache = new HashMap<Cell, BufferedImage>();
864 clearPhysical();
865 }
866 }
867
868 /**
869 * Get the width text adjustment.
870 *
871 * @return width text adjustment
872 */
873 public int getTextAdjustWidth() {
874 return textAdjustWidth;
875 }
876
877 /**
878 * Set the width text adjustment.
879 *
880 * @param textAdjustWidth the width text adjustment
881 */
882 public void setTextAdjustWidth(final int textAdjustWidth) {
883 synchronized (this) {
884 this.textAdjustWidth = textAdjustWidth;
885 textWidth = fontTextWidth + textAdjustWidth;
886 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
887 glyphCache = new HashMap<Cell, BufferedImage>();
888 clearPhysical();
889 }
890 }
891
892 /**
893 * Convert a CellAttributes foreground color to an Swing Color.
894 *
895 * @param attr the text attributes
896 * @return the Swing Color
897 */
898 public static Color attrToForegroundColor(final CellAttributes attr) {
899 int rgb = attr.getForeColorRGB();
900 if (rgb >= 0) {
901 int red = (rgb >> 16) & 0xFF;
902 int green = (rgb >> 8) & 0xFF;
903 int blue = rgb & 0xFF;
904
905 return new Color(red, green, blue);
906 }
907
908 if (attr.isBold()) {
909 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
910 return MYBOLD_BLACK;
911 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
912 return MYBOLD_RED;
913 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
914 return MYBOLD_BLUE;
915 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
916 return MYBOLD_GREEN;
917 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
918 return MYBOLD_YELLOW;
919 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
920 return MYBOLD_CYAN;
921 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
922 return MYBOLD_MAGENTA;
923 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
924 return MYBOLD_WHITE;
925 }
926 } else {
927 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
928 return MYBLACK;
929 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
930 return MYRED;
931 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
932 return MYBLUE;
933 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
934 return MYGREEN;
935 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
936 return MYYELLOW;
937 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
938 return MYCYAN;
939 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
940 return MYMAGENTA;
941 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
942 return MYWHITE;
943 }
944 }
945 throw new IllegalArgumentException("Invalid color: " +
946 attr.getForeColor().getValue());
947 }
948
949 /**
950 * Convert a CellAttributes background color to an Swing Color.
951 *
952 * @param attr the text attributes
953 * @return the Swing Color
954 */
955 public static Color attrToBackgroundColor(final CellAttributes attr) {
956 int rgb = attr.getBackColorRGB();
957 if (rgb >= 0) {
958 int red = (rgb >> 16) & 0xFF;
959 int green = (rgb >> 8) & 0xFF;
960 int blue = rgb & 0xFF;
961
962 return new Color(red, green, blue);
963 }
964
965 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
966 return MYBLACK;
967 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
968 return MYRED;
969 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
970 return MYBLUE;
971 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
972 return MYGREEN;
973 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
974 return MYYELLOW;
975 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
976 return MYCYAN;
977 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
978 return MYMAGENTA;
979 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
980 return MYWHITE;
981 }
982 throw new IllegalArgumentException("Invalid color: " +
983 attr.getBackColor().getValue());
984 }
985
986 /**
987 * Figure out what textAdjustX, textAdjustY, textAdjustHeight, and
988 * textAdjustWidth should be, based on the location of a vertical bar and
989 * a horizontal bar.
990 */
991 private void getFontAdjustments() {
992 BufferedImage image = null;
993
994 // What SHOULD happen is that the topmost/leftmost white pixel is at
995 // position (gr2x, gr2y). But it might also be off by a pixel in
996 // either direction.
997
998 Graphics2D gr2 = null;
999 int gr2x = 3;
1000 int gr2y = 3;
1001 image = new BufferedImage(fontTextWidth * 2, fontTextHeight * 2,
1002 BufferedImage.TYPE_INT_ARGB);
1003
1004 gr2 = image.createGraphics();
1005 gr2.setFont(swing.getFont());
1006 gr2.setColor(java.awt.Color.BLACK);
1007 gr2.fillRect(0, 0, fontTextWidth * 2, fontTextHeight * 2);
1008 gr2.setColor(java.awt.Color.WHITE);
1009 char [] chars = new char[1];
1010 chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR;
1011 gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
1012 chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR;
1013 gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
1014 gr2.dispose();
1015
1016 int top = fontTextHeight * 2;
1017 int bottom = -1;
1018 int left = fontTextWidth * 2;
1019 int right = -1;
1020 textAdjustX = 0;
1021 textAdjustY = 0;
1022 textAdjustHeight = 0;
1023 textAdjustWidth = 0;
1024
1025 for (int x = 0; x < fontTextWidth * 2; x++) {
1026 for (int y = 0; y < fontTextHeight * 2; y++) {
1027
1028 /*
1029 System.err.println("H X: " + x + " Y: " + y + " " +
1030 image.getRGB(x, y));
1031 */
1032
1033 if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
1034 // Pixel is present.
1035 if (y < top) {
1036 top = y;
1037 }
1038 if (y > bottom) {
1039 bottom = y;
1040 }
1041 if (x < left) {
1042 left = x;
1043 }
1044 if (x > right) {
1045 right = x;
1046 }
1047 }
1048 }
1049 }
1050 if (left < right) {
1051 textAdjustX = (gr2x - left);
1052 textAdjustWidth = fontTextWidth - (right - left + 1);
1053 }
1054 if (top < bottom) {
1055 textAdjustY = (gr2y - top);
1056 textAdjustHeight = fontTextHeight - (bottom - top + 1);
1057 }
1058 // System.err.println("top " + top + " bottom " + bottom);
1059 // System.err.println("left " + left + " right " + right);
1060
1061 // Special case: do not believe fonts that claim to be wider than
1062 // they are tall.
1063 if (fontTextWidth >= fontTextHeight) {
1064 textAdjustX = 0;
1065 textAdjustWidth = 0;
1066 fontTextWidth = fontTextHeight / 2;
1067 }
1068 }
1069
1070 /**
1071 * Figure out my font dimensions. This code path works OK for the JFrame
1072 * case, and can be called immediately after JFrame creation.
1073 */
1074 private void getFontDimensions() {
1075 swing.setFont(font);
1076 Graphics gr = swing.getGraphics();
1077 if (gr == null) {
1078 return;
1079 }
1080 getFontDimensions(gr);
1081 }
1082
1083 /**
1084 * Figure out my font dimensions. This code path is needed to lazy-load
1085 * the information inside paint().
1086 *
1087 * @param gr Graphics object to use
1088 */
1089 private void getFontDimensions(final Graphics gr) {
1090 swing.setFont(font);
1091 FontMetrics fm = gr.getFontMetrics();
1092 maxDescent = fm.getMaxDescent();
1093 Rectangle2D bounds = fm.getMaxCharBounds(gr);
1094 int leading = fm.getLeading();
1095 fontTextWidth = (int)Math.round(bounds.getWidth());
1096 // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
1097
1098 // This produces the same number, but works better for ugly
1099 // monospace.
1100 fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
1101
1102 getFontAdjustments();
1103 textHeight = fontTextHeight + textAdjustHeight;
1104 textWidth = fontTextWidth + textAdjustWidth;
1105
1106 if (sessionInfo != null) {
1107 sessionInfo.setTextCellDimensions(textWidth, textHeight);
1108 }
1109 gotFontDimensions = true;
1110 }
1111
1112 /**
1113 * Resize the physical screen to match the logical screen dimensions.
1114 */
1115 @Override
1116 public void resizeToScreen() {
1117 swing.setDimensions(textWidth * width, textHeight * height);
1118 clearPhysical();
1119 }
1120
1121 /**
1122 * Draw one cell's image to the screen.
1123 *
1124 * @param gr the Swing Graphics context
1125 * @param cell the Cell to draw
1126 * @param xPixel the x-coordinate to render to. 0 means the
1127 * left-most pixel column.
1128 * @param yPixel the y-coordinate to render to. 0 means the top-most
1129 * pixel row.
1130 */
1131 private void drawImage(final Graphics gr, final Cell cell,
1132 final int xPixel, final int yPixel) {
1133
1134 /*
1135 System.err.println("drawImage(): " + xPixel + " " + yPixel +
1136 " " + cell);
1137 */
1138
1139 // Draw the background rectangle, then the foreground character.
1140 assert (cell.isImage());
1141 gr.setColor(cell.getBackground());
1142 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
1143
1144 BufferedImage image = cell.getImage();
1145 if (image != null) {
1146 if (swing.getFrame() != null) {
1147 gr.drawImage(image, xPixel, yPixel, swing.getFrame());
1148 } else {
1149 gr.drawImage(image, xPixel, yPixel, swing.getComponent());
1150 }
1151 return;
1152 }
1153 }
1154
1155 /**
1156 * Draw one glyph to the screen.
1157 *
1158 * @param gr the Swing Graphics context
1159 * @param cell the Cell to draw
1160 * @param xPixel the x-coordinate to render to. 0 means the
1161 * left-most pixel column.
1162 * @param yPixel the y-coordinate to render to. 0 means the top-most
1163 * pixel row.
1164 */
1165 private void drawGlyph(final Graphics gr, final Cell cell,
1166 final int xPixel, final int yPixel) {
1167
1168 /*
1169 System.err.println("drawGlyph(): " + xPixel + " " + yPixel +
1170 " " + cell);
1171 */
1172
1173 BufferedImage image = null;
1174 if (cell.isBlink() && !cursorBlinkVisible) {
1175 image = glyphCacheBlink.get(cell);
1176 } else {
1177 image = glyphCache.get(cell);
1178 }
1179 if (image != null) {
1180 if (swing.getFrame() != null) {
1181 gr.drawImage(image, xPixel, yPixel, swing.getFrame());
1182 } else {
1183 gr.drawImage(image, xPixel, yPixel, swing.getComponent());
1184 }
1185 return;
1186 }
1187
1188 // Generate glyph and draw it.
1189 Graphics2D gr2 = null;
1190 int gr2x = xPixel;
1191 int gr2y = yPixel;
1192 if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
1193 image = new BufferedImage(textWidth, textHeight,
1194 BufferedImage.TYPE_INT_ARGB);
1195 gr2 = image.createGraphics();
1196 gr2.setFont(swing.getFont());
1197 gr2x = 0;
1198 gr2y = 0;
1199 } else {
1200 gr2 = (Graphics2D) gr;
1201 }
1202
1203 Cell cellColor = new Cell(cell);
1204
1205 // Check for reverse
1206 if (cell.isReverse()) {
1207 cellColor.setForeColor(cell.getBackColor());
1208 cellColor.setBackColor(cell.getForeColor());
1209 }
1210
1211 // Draw the background rectangle, then the foreground character.
1212 gr2.setColor(attrToBackgroundColor(cellColor));
1213 gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
1214
1215 // Handle blink and underline
1216 if (!cell.isBlink()
1217 || (cell.isBlink() && cursorBlinkVisible)
1218 ) {
1219 gr2.setColor(attrToForegroundColor(cellColor));
1220 char [] chars = new char[1];
1221 chars[0] = cell.getChar();
1222 gr2.drawChars(chars, 0, 1, gr2x + textAdjustX,
1223 gr2y + textHeight - maxDescent + textAdjustY);
1224
1225 if (cell.isUnderline()) {
1226 gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
1227 }
1228 }
1229
1230 if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
1231 gr2.dispose();
1232
1233 // We need a new key that will not be mutated by
1234 // invertCell().
1235 Cell key = new Cell(cell);
1236 if (cell.isBlink() && !cursorBlinkVisible) {
1237 glyphCacheBlink.put(key, image);
1238 } else {
1239 glyphCache.put(key, image);
1240 }
1241
1242 if (swing.getFrame() != null) {
1243 gr.drawImage(image, xPixel, yPixel, swing.getFrame());
1244 } else {
1245 gr.drawImage(image, xPixel, yPixel, swing.getComponent());
1246 }
1247 }
1248
1249 }
1250
1251 /**
1252 * Check if the cursor is visible, and if so draw it.
1253 *
1254 * @param gr the Swing Graphics context
1255 */
1256 private void drawCursor(final Graphics gr) {
1257
1258 if (cursorVisible
1259 && (cursorY >= 0)
1260 && (cursorX >= 0)
1261 && (cursorY <= height - 1)
1262 && (cursorX <= width - 1)
1263 && cursorBlinkVisible
1264 ) {
1265 int xPixel = cursorX * textWidth + left;
1266 int yPixel = cursorY * textHeight + top;
1267 Cell lCell = logical[cursorX][cursorY];
1268 int cursorWidth = textWidth;
1269 switch (lCell.getWidth()) {
1270 case SINGLE:
1271 // NOP
1272 break;
1273 case LEFT:
1274 cursorWidth *= 2;
1275 break;
1276 case RIGHT:
1277 cursorWidth *= 2;
1278 xPixel -= textWidth;
1279 break;
1280 }
1281 gr.setColor(attrToForegroundColor(lCell));
1282 switch (cursorStyle) {
1283 default:
1284 // Fall through...
1285 case UNDERLINE:
1286 gr.fillRect(xPixel, yPixel + textHeight - 2, cursorWidth, 2);
1287 break;
1288 case BLOCK:
1289 gr.fillRect(xPixel, yPixel, cursorWidth, textHeight);
1290 break;
1291 case OUTLINE:
1292 gr.drawRect(xPixel, yPixel, cursorWidth - 1, textHeight - 1);
1293 break;
1294 }
1295 }
1296 }
1297
1298 /**
1299 * Reset the blink timer.
1300 */
1301 private void resetBlinkTimer() {
1302 lastBlinkTime = System.currentTimeMillis();
1303 cursorBlinkVisible = true;
1304 }
1305
1306 /**
1307 * Paint redraws the whole screen.
1308 *
1309 * @param gr the Swing Graphics context
1310 */
1311 public void paint(final Graphics gr) {
1312
1313 if (gotFontDimensions == false) {
1314 // Lazy-load the text width/height
1315 getFontDimensions(gr);
1316 /*
1317 System.err.println("textWidth " + textWidth +
1318 " textHeight " + textHeight);
1319 System.err.println("FONT: " + swing.getFont() + " font " + font);
1320 */
1321 }
1322
1323 if ((swing.getFrame() != null)
1324 && (swing.getBufferStrategy() != null)
1325 && (SwingUtilities.isEventDispatchThread())
1326 ) {
1327 // System.err.println("paint(), skip first paint on swing thread");
1328 return;
1329 }
1330
1331 int xCellMin = 0;
1332 int xCellMax = width;
1333 int yCellMin = 0;
1334 int yCellMax = height;
1335
1336 Rectangle bounds = gr.getClipBounds();
1337 if (bounds != null) {
1338 // Only update what is in the bounds
1339 xCellMin = textColumn(bounds.x);
1340 xCellMax = textColumn(bounds.x + bounds.width);
1341 if (xCellMax > width) {
1342 xCellMax = width;
1343 }
1344 if (xCellMin >= xCellMax) {
1345 xCellMin = xCellMax - 2;
1346 }
1347 if (xCellMin < 0) {
1348 xCellMin = 0;
1349 }
1350 yCellMin = textRow(bounds.y);
1351 yCellMax = textRow(bounds.y + bounds.height);
1352 if (yCellMax > height) {
1353 yCellMax = height;
1354 }
1355 if (yCellMin >= yCellMax) {
1356 yCellMin = yCellMax - 2;
1357 }
1358 if (yCellMin < 0) {
1359 yCellMin = 0;
1360 }
1361 } else {
1362 // We need a total repaint
1363 reallyCleared = true;
1364 }
1365
1366 // Prevent updates to the screen's data from the TApplication
1367 // threads.
1368 synchronized (this) {
1369
1370 /*
1371 System.err.printf("bounds %s X %d %d Y %d %d\n",
1372 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
1373 */
1374
1375 for (int y = yCellMin; y < yCellMax; y++) {
1376 for (int x = xCellMin; x < xCellMax; x++) {
1377
1378 int xPixel = x * textWidth + left;
1379 int yPixel = y * textHeight + top;
1380
1381 Cell lCell = logical[x][y];
1382 Cell pCell = physical[x][y];
1383
1384 if (!lCell.equals(pCell)
1385 || lCell.isBlink()
1386 || reallyCleared
1387 || (swing.getFrame() == null)) {
1388
1389 if (lCell.isImage()) {
1390 drawImage(gr, lCell, xPixel, yPixel);
1391 } else {
1392 drawGlyph(gr, lCell, xPixel, yPixel);
1393 }
1394
1395 // Physical is always updated
1396 physical[x][y].setTo(lCell);
1397 }
1398 }
1399 }
1400 drawCursor(gr);
1401
1402 reallyCleared = false;
1403 } // synchronized (this)
1404 }
1405
1406 /**
1407 * Restore terminal to normal state.
1408 */
1409 public void shutdown() {
1410 swing.dispose();
1411 }
1412
1413 /**
1414 * Push the logical screen to the physical device.
1415 */
1416 private void drawToSwing() {
1417
1418 /*
1419 System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
1420 reallyCleared, dirty);
1421 */
1422
1423 // If reallyCleared is set, we have to draw everything.
1424 if ((swing.getFrame() != null)
1425 && (swing.getBufferStrategy() != null)
1426 && (reallyCleared == true)
1427 ) {
1428 // Triple-buffering: we have to redraw everything on this thread.
1429 Graphics gr = swing.getBufferStrategy().getDrawGraphics();
1430 swing.paint(gr);
1431 gr.dispose();
1432 swing.getBufferStrategy().show();
1433 Toolkit.getDefaultToolkit().sync();
1434 return;
1435 } else if (((swing.getFrame() != null)
1436 && (swing.getBufferStrategy() == null))
1437 || (reallyCleared == true)
1438 ) {
1439 // Repaint everything on the Swing thread.
1440 // System.err.println("REPAINT ALL");
1441 swing.repaint();
1442 return;
1443 }
1444
1445 if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
1446 Graphics gr = swing.getBufferStrategy().getDrawGraphics();
1447
1448 synchronized (this) {
1449 for (int y = 0; y < height; y++) {
1450 for (int x = 0; x < width; x++) {
1451 Cell lCell = logical[x][y];
1452 Cell pCell = physical[x][y];
1453
1454 int xPixel = x * textWidth + left;
1455 int yPixel = y * textHeight + top;
1456
1457 if (!lCell.equals(pCell)
1458 || ((x == cursorX)
1459 && (y == cursorY)
1460 && cursorVisible)
1461 || (lCell.isBlink())
1462 ) {
1463 if (lCell.isImage()) {
1464 drawImage(gr, lCell, xPixel, yPixel);
1465 } else {
1466 drawGlyph(gr, lCell, xPixel, yPixel);
1467 }
1468 physical[x][y].setTo(lCell);
1469 }
1470 }
1471 }
1472 drawCursor(gr);
1473 } // synchronized (this)
1474
1475 gr.dispose();
1476 swing.getBufferStrategy().show();
1477 Toolkit.getDefaultToolkit().sync();
1478 return;
1479 }
1480
1481 // Swing thread version: request a repaint, but limit it to the area
1482 // that has changed.
1483
1484 // Find the minimum-size damaged region.
1485 int xMin = swing.getWidth();
1486 int xMax = 0;
1487 int yMin = swing.getHeight();
1488 int yMax = 0;
1489
1490 synchronized (this) {
1491 for (int y = 0; y < height; y++) {
1492 for (int x = 0; x < width; x++) {
1493 Cell lCell = logical[x][y];
1494 Cell pCell = physical[x][y];
1495
1496 int xPixel = x * textWidth + left;
1497 int yPixel = y * textHeight + top;
1498
1499 if (!lCell.equals(pCell)
1500 || ((x == cursorX)
1501 && (y == cursorY)
1502 && cursorVisible)
1503 || lCell.isBlink()
1504 ) {
1505 if (xPixel < xMin) {
1506 xMin = xPixel;
1507 }
1508 if (xPixel + textWidth > xMax) {
1509 xMax = xPixel + textWidth;
1510 }
1511 if (yPixel < yMin) {
1512 yMin = yPixel;
1513 }
1514 if (yPixel + textHeight > yMax) {
1515 yMax = yPixel + textHeight;
1516 }
1517 }
1518 }
1519 }
1520 }
1521 if (xMin + textWidth >= xMax) {
1522 xMax += textWidth;
1523 }
1524 if (yMin + textHeight >= yMax) {
1525 yMax += textHeight;
1526 }
1527
1528 // Repaint the desired area
1529 /*
1530 System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
1531 yMin, yMax);
1532 */
1533
1534 if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
1535 // This path should never be taken, but is left here for
1536 // completeness.
1537 Graphics gr = swing.getBufferStrategy().getDrawGraphics();
1538 Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
1539 yMax - yMin);
1540 gr.setClip(bounds);
1541 swing.paint(gr);
1542 gr.dispose();
1543 swing.getBufferStrategy().show();
1544 Toolkit.getDefaultToolkit().sync();
1545 } else {
1546 // Repaint on the Swing thread.
1547 swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
1548 }
1549 }
1550
1551 /**
1552 * Convert pixel column position to text cell column position.
1553 *
1554 * @param x pixel column position
1555 * @return text cell column position
1556 */
1557 public int textColumn(final int x) {
1558 int column = ((x - left) / textWidth);
1559 if (column < 0) {
1560 column = 0;
1561 }
1562 if (column > width - 1) {
1563 column = width - 1;
1564 }
1565 return column;
1566 }
1567
1568 /**
1569 * Convert pixel row position to text cell row position.
1570 *
1571 * @param y pixel row position
1572 * @return text cell row position
1573 */
1574 public int textRow(final int y) {
1575 int row = ((y - top) / textHeight);
1576 if (row < 0) {
1577 row = 0;
1578 }
1579 if (row > height - 1) {
1580 row = height - 1;
1581 }
1582 return row;
1583 }
1584
1585 /**
1586 * Getter for sessionInfo.
1587 *
1588 * @return the SessionInfo
1589 */
1590 public SessionInfo getSessionInfo() {
1591 return sessionInfo;
1592 }
1593
1594 /**
1595 * Getter for the underlying Swing component.
1596 *
1597 * @return the SwingComponent
1598 */
1599 public SwingComponent getSwingComponent() {
1600 return swing;
1601 }
1602
1603 // ------------------------------------------------------------------------
1604 // KeyListener ------------------------------------------------------------
1605 // ------------------------------------------------------------------------
1606
1607 /**
1608 * Pass Swing keystrokes into the event queue.
1609 *
1610 * @param key keystroke received
1611 */
1612 public void keyReleased(final KeyEvent key) {
1613 // Ignore release events
1614 }
1615
1616 /**
1617 * Pass Swing keystrokes into the event queue.
1618 *
1619 * @param key keystroke received
1620 */
1621 public void keyTyped(final KeyEvent key) {
1622 // Ignore typed events
1623 }
1624
1625 /**
1626 * Pass Swing keystrokes into the event queue.
1627 *
1628 * @param key keystroke received
1629 */
1630 public void keyPressed(final KeyEvent key) {
1631 boolean alt = false;
1632 boolean shift = false;
1633 boolean ctrl = false;
1634 char ch = ' ';
1635 boolean isKey = false;
1636 if (key.isActionKey()) {
1637 isKey = true;
1638 } else {
1639 ch = key.getKeyChar();
1640 }
1641 alt = key.isAltDown();
1642 ctrl = key.isControlDown();
1643 shift = key.isShiftDown();
1644
1645 /*
1646 System.err.printf("Swing Key: %s\n", key);
1647 System.err.printf(" isKey: %s\n", isKey);
1648 System.err.printf(" alt: %s\n", alt);
1649 System.err.printf(" ctrl: %s\n", ctrl);
1650 System.err.printf(" shift: %s\n", shift);
1651 System.err.printf(" ch: %s\n", ch);
1652 */
1653
1654 // Special case: not return the bare modifier presses
1655 switch (key.getKeyCode()) {
1656 case KeyEvent.VK_ALT:
1657 return;
1658 case KeyEvent.VK_ALT_GRAPH:
1659 return;
1660 case KeyEvent.VK_CONTROL:
1661 return;
1662 case KeyEvent.VK_SHIFT:
1663 return;
1664 case KeyEvent.VK_META:
1665 return;
1666 default:
1667 break;
1668 }
1669
1670 TKeypress keypress = null;
1671 if (isKey) {
1672 switch (key.getKeyCode()) {
1673 case KeyEvent.VK_F1:
1674 keypress = new TKeypress(true, TKeypress.F1, ' ',
1675 alt, ctrl, shift);
1676 break;
1677 case KeyEvent.VK_F2:
1678 keypress = new TKeypress(true, TKeypress.F2, ' ',
1679 alt, ctrl, shift);
1680 break;
1681 case KeyEvent.VK_F3:
1682 keypress = new TKeypress(true, TKeypress.F3, ' ',
1683 alt, ctrl, shift);
1684 break;
1685 case KeyEvent.VK_F4:
1686 keypress = new TKeypress(true, TKeypress.F4, ' ',
1687 alt, ctrl, shift);
1688 break;
1689 case KeyEvent.VK_F5:
1690 keypress = new TKeypress(true, TKeypress.F5, ' ',
1691 alt, ctrl, shift);
1692 break;
1693 case KeyEvent.VK_F6:
1694 keypress = new TKeypress(true, TKeypress.F6, ' ',
1695 alt, ctrl, shift);
1696 break;
1697 case KeyEvent.VK_F7:
1698 keypress = new TKeypress(true, TKeypress.F7, ' ',
1699 alt, ctrl, shift);
1700 break;
1701 case KeyEvent.VK_F8:
1702 keypress = new TKeypress(true, TKeypress.F8, ' ',
1703 alt, ctrl, shift);
1704 break;
1705 case KeyEvent.VK_F9:
1706 keypress = new TKeypress(true, TKeypress.F9, ' ',
1707 alt, ctrl, shift);
1708 break;
1709 case KeyEvent.VK_F10:
1710 keypress = new TKeypress(true, TKeypress.F10, ' ',
1711 alt, ctrl, shift);
1712 break;
1713 case KeyEvent.VK_F11:
1714 keypress = new TKeypress(true, TKeypress.F11, ' ',
1715 alt, ctrl, shift);
1716 break;
1717 case KeyEvent.VK_F12:
1718 keypress = new TKeypress(true, TKeypress.F12, ' ',
1719 alt, ctrl, shift);
1720 break;
1721 case KeyEvent.VK_HOME:
1722 keypress = new TKeypress(true, TKeypress.HOME, ' ',
1723 alt, ctrl, shift);
1724 break;
1725 case KeyEvent.VK_END:
1726 keypress = new TKeypress(true, TKeypress.END, ' ',
1727 alt, ctrl, shift);
1728 break;
1729 case KeyEvent.VK_PAGE_UP:
1730 keypress = new TKeypress(true, TKeypress.PGUP, ' ',
1731 alt, ctrl, shift);
1732 break;
1733 case KeyEvent.VK_PAGE_DOWN:
1734 keypress = new TKeypress(true, TKeypress.PGDN, ' ',
1735 alt, ctrl, shift);
1736 break;
1737 case KeyEvent.VK_INSERT:
1738 keypress = new TKeypress(true, TKeypress.INS, ' ',
1739 alt, ctrl, shift);
1740 break;
1741 case KeyEvent.VK_DELETE:
1742 keypress = new TKeypress(true, TKeypress.DEL, ' ',
1743 alt, ctrl, shift);
1744 break;
1745 case KeyEvent.VK_RIGHT:
1746 keypress = new TKeypress(true, TKeypress.RIGHT, ' ',
1747 alt, ctrl, shift);
1748 break;
1749 case KeyEvent.VK_LEFT:
1750 keypress = new TKeypress(true, TKeypress.LEFT, ' ',
1751 alt, ctrl, shift);
1752 break;
1753 case KeyEvent.VK_UP:
1754 keypress = new TKeypress(true, TKeypress.UP, ' ',
1755 alt, ctrl, shift);
1756 break;
1757 case KeyEvent.VK_DOWN:
1758 keypress = new TKeypress(true, TKeypress.DOWN, ' ',
1759 alt, ctrl, shift);
1760 break;
1761 case KeyEvent.VK_TAB:
1762 // Special case: distinguish TAB vs BTAB
1763 if (shift) {
1764 keypress = kbShiftTab;
1765 } else {
1766 keypress = kbTab;
1767 }
1768 break;
1769 case KeyEvent.VK_ENTER:
1770 keypress = new TKeypress(true, TKeypress.ENTER, ' ',
1771 alt, ctrl, shift);
1772 break;
1773 case KeyEvent.VK_ESCAPE:
1774 keypress = new TKeypress(true, TKeypress.ESC, ' ',
1775 alt, ctrl, shift);
1776 break;
1777 case KeyEvent.VK_BACK_SPACE:
1778 keypress = kbBackspace;
1779 break;
1780 default:
1781 // Unsupported, ignore
1782 return;
1783 }
1784 }
1785
1786 if (keypress == null) {
1787 switch (ch) {
1788 case 0x08:
1789 // Disambiguate ^H from Backspace.
1790 if (KeyEvent.getKeyText(key.getKeyCode()).equals("H")) {
1791 // This is ^H.
1792 keypress = kbBackspace;
1793 } else {
1794 // We are emulating Xterm here, where the backspace key
1795 // on the keyboard returns ^?.
1796 keypress = kbBackspaceDel;
1797 }
1798 break;
1799 case 0x0A:
1800 keypress = kbEnter;
1801 break;
1802 case 0x1B:
1803 keypress = kbEsc;
1804 break;
1805 case 0x0D:
1806 keypress = kbEnter;
1807 break;
1808 case 0x09:
1809 if (shift) {
1810 keypress = kbShiftTab;
1811 } else {
1812 keypress = kbTab;
1813 }
1814 break;
1815 case 0x7F:
1816 keypress = kbDel;
1817 break;
1818 default:
1819 if (!alt && ctrl && !shift) {
1820 ch = KeyEvent.getKeyText(key.getKeyCode()).charAt(0);
1821 }
1822 // Not a special key, put it together
1823 keypress = new TKeypress(false, 0, ch, alt, ctrl, shift);
1824 }
1825 }
1826
1827 // Save it and we are done.
1828 synchronized (eventQueue) {
1829 eventQueue.add(new TKeypressEvent(keypress));
1830 resetBlinkTimer();
1831 }
1832 if (listener != null) {
1833 synchronized (listener) {
1834 listener.notifyAll();
1835 }
1836 }
1837 }
1838
1839 // ------------------------------------------------------------------------
1840 // WindowListener ---------------------------------------------------------
1841 // ------------------------------------------------------------------------
1842
1843 /**
1844 * Pass window events into the event queue.
1845 *
1846 * @param event window event received
1847 */
1848 public void windowActivated(final WindowEvent event) {
1849 // Force a total repaint
1850 synchronized (this) {
1851 clearPhysical();
1852 }
1853 }
1854
1855 /**
1856 * Pass window events into the event queue.
1857 *
1858 * @param event window event received
1859 */
1860 public void windowClosed(final WindowEvent event) {
1861 // Ignore
1862 }
1863
1864 /**
1865 * Pass window events into the event queue.
1866 *
1867 * @param event window event received
1868 */
1869 public void windowClosing(final WindowEvent event) {
1870 // Drop a cmBackendDisconnect and walk away
1871 synchronized (eventQueue) {
1872 eventQueue.add(new TCommandEvent(cmBackendDisconnect));
1873 resetBlinkTimer();
1874 }
1875 if (listener != null) {
1876 synchronized (listener) {
1877 listener.notifyAll();
1878 }
1879 }
1880 }
1881
1882 /**
1883 * Pass window events into the event queue.
1884 *
1885 * @param event window event received
1886 */
1887 public void windowDeactivated(final WindowEvent event) {
1888 // Ignore
1889 }
1890
1891 /**
1892 * Pass window events into the event queue.
1893 *
1894 * @param event window event received
1895 */
1896 public void windowDeiconified(final WindowEvent event) {
1897 // Ignore
1898 }
1899
1900 /**
1901 * Pass window events into the event queue.
1902 *
1903 * @param event window event received
1904 */
1905 public void windowIconified(final WindowEvent event) {
1906 // Ignore
1907 }
1908
1909 /**
1910 * Pass window events into the event queue.
1911 *
1912 * @param event window event received
1913 */
1914 public void windowOpened(final WindowEvent event) {
1915 // Ignore
1916 }
1917
1918 // ------------------------------------------------------------------------
1919 // ComponentListener ------------------------------------------------------
1920 // ------------------------------------------------------------------------
1921
1922 /**
1923 * Pass component events into the event queue.
1924 *
1925 * @param event component event received
1926 */
1927 public void componentHidden(final ComponentEvent event) {
1928 // Ignore
1929 }
1930
1931 /**
1932 * Pass component events into the event queue.
1933 *
1934 * @param event component event received
1935 */
1936 public void componentShown(final ComponentEvent event) {
1937 // Ignore
1938 }
1939
1940 /**
1941 * Pass component events into the event queue.
1942 *
1943 * @param event component event received
1944 */
1945 public void componentMoved(final ComponentEvent event) {
1946 // Ignore
1947 }
1948
1949 /**
1950 * Pass component events into the event queue.
1951 *
1952 * @param event component event received
1953 */
1954 public void componentResized(final ComponentEvent event) {
1955 if (gotFontDimensions == false) {
1956 // We are still waiting to get font information. Don't pass a
1957 // resize event up.
1958 // System.err.println("size " + swing.getComponent().getSize());
1959 return;
1960 }
1961
1962 if (sessionInfo == null) {
1963 // This is the initial component resize in construction, bail
1964 // out.
1965 return;
1966 }
1967
1968 // Drop a new TResizeEvent into the queue
1969 sessionInfo.queryWindowSize();
1970 synchronized (eventQueue) {
1971 TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
1972 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
1973 eventQueue.add(windowResize);
1974 resetBlinkTimer();
1975 /*
1976 System.err.println("Add resize event: " + windowResize.getWidth() +
1977 " x " + windowResize.getHeight());
1978 */
1979 }
1980 if (listener != null) {
1981 synchronized (listener) {
1982 listener.notifyAll();
1983 }
1984 }
1985 }
1986
1987 // ------------------------------------------------------------------------
1988 // MouseMotionListener ----------------------------------------------------
1989 // ------------------------------------------------------------------------
1990
1991 /**
1992 * Pass mouse events into the event queue.
1993 *
1994 * @param mouse mouse event received
1995 */
1996 public void mouseDragged(final MouseEvent mouse) {
1997 int modifiers = mouse.getModifiersEx();
1998 boolean eventMouse1 = false;
1999 boolean eventMouse2 = false;
2000 boolean eventMouse3 = false;
2001 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2002 eventMouse1 = true;
2003 }
2004 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2005 eventMouse2 = true;
2006 }
2007 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2008 eventMouse3 = true;
2009 }
2010 mouse1 = eventMouse1;
2011 mouse2 = eventMouse2;
2012 mouse3 = eventMouse3;
2013 int x = textColumn(mouse.getX());
2014 int y = textRow(mouse.getY());
2015
2016 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
2017 x, y, x, y, mouse1, mouse2, mouse3, false, false);
2018
2019 synchronized (eventQueue) {
2020 eventQueue.add(mouseEvent);
2021 resetBlinkTimer();
2022 }
2023 if (listener != null) {
2024 synchronized (listener) {
2025 listener.notifyAll();
2026 }
2027 }
2028 }
2029
2030 /**
2031 * Pass mouse events into the event queue.
2032 *
2033 * @param mouse mouse event received
2034 */
2035 public void mouseMoved(final MouseEvent mouse) {
2036 int x = textColumn(mouse.getX());
2037 int y = textRow(mouse.getY());
2038 if ((x == oldMouseX) && (y == oldMouseY)) {
2039 // Bail out, we've moved some pixels but not a whole text cell.
2040 return;
2041 }
2042 oldMouseX = x;
2043 oldMouseY = y;
2044
2045 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
2046 x, y, x, y, mouse1, mouse2, mouse3, false, false);
2047
2048 synchronized (eventQueue) {
2049 eventQueue.add(mouseEvent);
2050 resetBlinkTimer();
2051 }
2052 if (listener != null) {
2053 synchronized (listener) {
2054 listener.notifyAll();
2055 }
2056 }
2057 }
2058
2059 // ------------------------------------------------------------------------
2060 // MouseListener ----------------------------------------------------------
2061 // ------------------------------------------------------------------------
2062
2063 /**
2064 * Pass mouse events into the event queue.
2065 *
2066 * @param mouse mouse event received
2067 */
2068 public void mouseClicked(final MouseEvent mouse) {
2069 // Ignore
2070 }
2071
2072 /**
2073 * Pass mouse events into the event queue.
2074 *
2075 * @param mouse mouse event received
2076 */
2077 public void mouseEntered(final MouseEvent mouse) {
2078 // Ignore
2079 }
2080
2081 /**
2082 * Pass mouse events into the event queue.
2083 *
2084 * @param mouse mouse event received
2085 */
2086 public void mouseExited(final MouseEvent mouse) {
2087 // Ignore
2088 }
2089
2090 /**
2091 * Pass mouse events into the event queue.
2092 *
2093 * @param mouse mouse event received
2094 */
2095 public void mousePressed(final MouseEvent mouse) {
2096 int modifiers = mouse.getModifiersEx();
2097 boolean eventMouse1 = false;
2098 boolean eventMouse2 = false;
2099 boolean eventMouse3 = false;
2100 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2101 eventMouse1 = true;
2102 }
2103 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2104 eventMouse2 = true;
2105 }
2106 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2107 eventMouse3 = true;
2108 }
2109 mouse1 = eventMouse1;
2110 mouse2 = eventMouse2;
2111 mouse3 = eventMouse3;
2112 int x = textColumn(mouse.getX());
2113 int y = textRow(mouse.getY());
2114
2115 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
2116 x, y, x, y, mouse1, mouse2, mouse3, false, false);
2117
2118 synchronized (eventQueue) {
2119 eventQueue.add(mouseEvent);
2120 resetBlinkTimer();
2121 }
2122 if (listener != null) {
2123 synchronized (listener) {
2124 listener.notifyAll();
2125 }
2126 }
2127 }
2128
2129 /**
2130 * Pass mouse events into the event queue.
2131 *
2132 * @param mouse mouse event received
2133 */
2134 public void mouseReleased(final MouseEvent mouse) {
2135 int modifiers = mouse.getModifiersEx();
2136 boolean eventMouse1 = false;
2137 boolean eventMouse2 = false;
2138 boolean eventMouse3 = false;
2139 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2140 eventMouse1 = true;
2141 }
2142 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2143 eventMouse2 = true;
2144 }
2145 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2146 eventMouse3 = true;
2147 }
2148 if (mouse1) {
2149 mouse1 = false;
2150 eventMouse1 = true;
2151 }
2152 if (mouse2) {
2153 mouse2 = false;
2154 eventMouse2 = true;
2155 }
2156 if (mouse3) {
2157 mouse3 = false;
2158 eventMouse3 = true;
2159 }
2160 int x = textColumn(mouse.getX());
2161 int y = textRow(mouse.getY());
2162
2163 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP,
2164 x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false);
2165
2166 synchronized (eventQueue) {
2167 eventQueue.add(mouseEvent);
2168 resetBlinkTimer();
2169 }
2170 if (listener != null) {
2171 synchronized (listener) {
2172 listener.notifyAll();
2173 }
2174 }
2175 }
2176
2177 // ------------------------------------------------------------------------
2178 // MouseWheelListener -----------------------------------------------------
2179 // ------------------------------------------------------------------------
2180
2181 /**
2182 * Pass mouse events into the event queue.
2183 *
2184 * @param mouse mouse event received
2185 */
2186 public void mouseWheelMoved(final MouseWheelEvent mouse) {
2187 int modifiers = mouse.getModifiersEx();
2188 boolean eventMouse1 = false;
2189 boolean eventMouse2 = false;
2190 boolean eventMouse3 = false;
2191 boolean mouseWheelUp = false;
2192 boolean mouseWheelDown = false;
2193 if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
2194 eventMouse1 = true;
2195 }
2196 if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
2197 eventMouse2 = true;
2198 }
2199 if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
2200 eventMouse3 = true;
2201 }
2202 mouse1 = eventMouse1;
2203 mouse2 = eventMouse2;
2204 mouse3 = eventMouse3;
2205 int x = textColumn(mouse.getX());
2206 int y = textRow(mouse.getY());
2207 if (mouse.getWheelRotation() > 0) {
2208 mouseWheelDown = true;
2209 }
2210 if (mouse.getWheelRotation() < 0) {
2211 mouseWheelUp = true;
2212 }
2213
2214 TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
2215 x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
2216
2217 synchronized (eventQueue) {
2218 eventQueue.add(mouseEvent);
2219 resetBlinkTimer();
2220 }
2221 if (listener != null) {
2222 synchronized (listener) {
2223 listener.notifyAll();
2224 }
2225 }
2226 }
2227
2228 }