2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
32 import java
.awt
.FontMetrics
;
33 import java
.awt
.Graphics2D
;
34 import java
.awt
.image
.BufferedImage
;
36 import java
.io
.InputStream
;
37 import java
.io
.IOException
;
38 import java
.lang
.reflect
.Field
;
39 import java
.text
.MessageFormat
;
40 import java
.util
.ArrayList
;
41 import java
.util
.HashMap
;
42 import java
.util
.List
;
44 import java
.util
.ResourceBundle
;
46 import jexer
.backend
.ECMA48Terminal
;
47 import jexer
.backend
.GlyphMaker
;
48 import jexer
.backend
.MultiScreen
;
49 import jexer
.backend
.SwingTerminal
;
50 import jexer
.bits
.Cell
;
51 import jexer
.bits
.CellAttributes
;
52 import jexer
.event
.TCommandEvent
;
53 import jexer
.event
.TKeypressEvent
;
54 import jexer
.event
.TMenuEvent
;
55 import jexer
.event
.TMouseEvent
;
56 import jexer
.event
.TResizeEvent
;
57 import jexer
.menu
.TMenu
;
58 import jexer
.tterminal
.DisplayLine
;
59 import jexer
.tterminal
.DisplayListener
;
60 import jexer
.tterminal
.ECMA48
;
61 import static jexer
.TCommand
.*;
62 import static jexer
.TKeypress
.*;
65 * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
67 public class TTerminalWidget
extends TScrollableWidget
68 implements DisplayListener
, EditMenuUser
{
73 private static final ResourceBundle i18n
= ResourceBundle
.getBundle(TTerminalWidget
.class.getName());
75 // ------------------------------------------------------------------------
76 // Variables --------------------------------------------------------------
77 // ------------------------------------------------------------------------
82 private ECMA48 emulator
;
85 * The Process created by the shell spawning constructor.
87 private Process shell
;
90 * If true, we are using the ptypipe utility to support dynamic window
91 * resizing. ptypipe is available at
92 * https://gitlab.com/klamonte/ptypipe .
94 private boolean ptypipe
= false;
99 private GlyphMaker doubleFont
;
102 * Last text width value.
104 private int lastTextWidth
= -1;
107 * Last text height value.
109 private int lastTextHeight
= -1;
112 * The blink state, used only by ECMA48 backend and when double-width
113 * chars must be drawn.
115 private boolean blinkState
= true;
118 * Timer flag, used only by ECMA48 backend and when double-width chars
121 private boolean haveTimer
= false;
124 * The last seen visible display.
126 private List
<DisplayLine
> display
;
129 * If true, the display has changed and needs updating.
131 private volatile boolean dirty
= true;
134 * Time that the display was last updated.
136 private long lastUpdateTime
= 0;
139 * If true, hide the mouse after typing a keystroke.
141 private boolean hideMouseWhenTyping
= true;
144 * If true, the mouse should not be displayed because a keystroke was
147 private boolean typingHidMouse
= false;
150 * The return value from the emulator.
152 private int exitValue
= -1;
155 * Title to expose to a window.
157 private String title
= "";
160 * Action to perform when the terminal exits.
162 private TAction closeAction
= null;
164 // ------------------------------------------------------------------------
165 // Constructors -----------------------------------------------------------
166 // ------------------------------------------------------------------------
169 * Public constructor spawns a custom command line.
171 * @param parent parent widget
172 * @param x column relative to parent
173 * @param y row relative to parent
174 * @param commandLine the command line to execute
176 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
177 final String commandLine
) {
179 this(parent
, x
, y
, commandLine
.split("\\s+"));
183 * Public constructor spawns a custom command line.
185 * @param parent parent widget
186 * @param x column relative to parent
187 * @param y row relative to parent
188 * @param command the command line to execute
190 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
191 final String
[] command
) {
193 this(parent
, x
, y
, command
, null);
197 * Public constructor spawns a custom command line.
199 * @param parent parent widget
200 * @param x column relative to parent
201 * @param y row relative to parent
202 * @param command the command line to execute
203 * @param closeAction action to perform when the shell exits
205 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
206 final String
[] command
, final TAction closeAction
) {
208 this(parent
, x
, y
, 80, 24, command
, closeAction
);
212 * Public constructor spawns a custom command line.
214 * @param parent parent widget
215 * @param x column relative to parent
216 * @param y row relative to parent
217 * @param width width of widget
218 * @param height height of widget
219 * @param command the command line to execute
220 * @param closeAction action to perform when the shell exits
222 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
223 final int width
, final int height
, final String
[] command
,
224 final TAction closeAction
) {
226 super(parent
, x
, y
, width
, height
);
228 this.closeAction
= closeAction
;
230 String
[] fullCommand
;
232 // Spawn a shell and pass its I/O to the other constructor.
233 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
234 && (System
.getProperty("jexer.TTerminal.ptypipe").
238 fullCommand
= new String
[command
.length
+ 1];
239 fullCommand
[0] = "ptypipe";
240 System
.arraycopy(command
, 0, fullCommand
, 1, command
.length
);
241 } else if (System
.getProperty("os.name").startsWith("Windows")) {
242 fullCommand
= new String
[3];
243 fullCommand
[0] = "cmd";
244 fullCommand
[1] = "/c";
245 fullCommand
[2] = stringArrayToString(command
);
246 } else if (System
.getProperty("os.name").startsWith("Mac")) {
247 fullCommand
= new String
[6];
248 fullCommand
[0] = "script";
249 fullCommand
[1] = "-q";
250 fullCommand
[2] = "-F";
251 fullCommand
[3] = "/dev/null";
252 fullCommand
[4] = "-c";
253 fullCommand
[5] = stringArrayToString(command
);
255 // Default: behave like Linux
256 if (System
.getProperty("jexer.TTerminal.setsid",
257 "true").equals("false")
259 fullCommand
= new String
[5];
260 fullCommand
[0] = "script";
261 fullCommand
[1] = "-fqe";
262 fullCommand
[2] = "/dev/null";
263 fullCommand
[3] = "-c";
264 fullCommand
[4] = stringArrayToString(command
);
266 fullCommand
= new String
[6];
267 fullCommand
[0] = "setsid";
268 fullCommand
[1] = "script";
269 fullCommand
[2] = "-fqe";
270 fullCommand
[3] = "/dev/null";
271 fullCommand
[4] = "-c";
272 fullCommand
[5] = stringArrayToString(command
);
275 spawnShell(fullCommand
);
279 * Public constructor spawns a shell.
281 * @param parent parent widget
282 * @param x column relative to parent
283 * @param y row relative to parent
285 public TTerminalWidget(final TWidget parent
, final int x
, final int y
) {
286 this(parent
, x
, y
, (TAction
) null);
290 * Public constructor spawns a shell.
292 * @param parent parent widget
293 * @param x column relative to parent
294 * @param y row relative to parent
295 * @param closeAction action to perform when the shell exits
297 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
298 final TAction closeAction
) {
300 this(parent
, x
, y
, 80, 24, closeAction
);
304 * Public constructor spawns a shell.
306 * @param parent parent widget
307 * @param x column relative to parent
308 * @param y row relative to parent
309 * @param width width of widget
310 * @param height height of widget
311 * @param closeAction action to perform when the shell exits
313 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
314 final int width
, final int height
, final TAction closeAction
) {
316 super(parent
, x
, y
, width
, height
);
318 this.closeAction
= closeAction
;
320 if (System
.getProperty("jexer.TTerminal.shell") != null) {
321 String shell
= System
.getProperty("jexer.TTerminal.shell");
322 if (shell
.trim().startsWith("ptypipe")) {
325 spawnShell(shell
.split("\\s+"));
329 String cmdShellWindows
= "cmd.exe";
331 // You cannot run a login shell in a bare Process interactively, due
332 // to libc's behavior of buffering when stdin/stdout aren't a tty.
333 // Use 'script' instead to run a shell in a pty. And because BSD and
334 // GNU differ on the '-f' vs '-F' flags, we need two different
336 String cmdShellGNU
= "script -fqe /dev/null";
337 String cmdShellGNUSetsid
= "setsid script -fqe /dev/null";
338 String cmdShellBSD
= "script -q -F /dev/null";
340 // ptypipe is another solution that permits dynamic window resizing.
341 String cmdShellPtypipe
= "ptypipe /bin/bash --login";
343 // Spawn a shell and pass its I/O to the other constructor.
344 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
345 && (System
.getProperty("jexer.TTerminal.ptypipe").
349 spawnShell(cmdShellPtypipe
.split("\\s+"));
350 } else if (System
.getProperty("os.name").startsWith("Windows")) {
351 spawnShell(cmdShellWindows
.split("\\s+"));
352 } else if (System
.getProperty("os.name").startsWith("Mac")) {
353 spawnShell(cmdShellBSD
.split("\\s+"));
354 } else if (System
.getProperty("os.name").startsWith("Linux")) {
355 if (System
.getProperty("jexer.TTerminal.setsid",
356 "true").equals("false")
358 spawnShell(cmdShellGNU
.split("\\s+"));
360 spawnShell(cmdShellGNUSetsid
.split("\\s+"));
363 // When all else fails, assume GNU.
364 spawnShell(cmdShellGNU
.split("\\s+"));
368 // ------------------------------------------------------------------------
369 // Event handlers ---------------------------------------------------------
370 // ------------------------------------------------------------------------
373 * Handle window/screen resize events.
375 * @param resize resize event
378 public void onResize(final TResizeEvent resize
) {
379 // Let TWidget set my size.
380 super.onResize(resize
);
382 if (emulator
== null) {
386 // Synchronize against the emulator so we don't stomp on its reader
388 synchronized (emulator
) {
390 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
391 // Resize the scroll bars
395 // Get out of scrollback
399 emulator
.setWidth(getWidth());
400 emulator
.setHeight(getHeight());
402 emulator
.writeRemote("\033[8;" + getHeight() + ";" +
406 // Pass the correct text cell width/height to the emulator
407 if (getScreen() != null) {
408 emulator
.setTextWidth(getScreen().getTextWidth());
409 emulator
.setTextHeight(getScreen().getTextHeight());
414 } // synchronized (emulator)
420 * @param keypress keystroke event
423 public void onKeypress(final TKeypressEvent keypress
) {
424 if (hideMouseWhenTyping
) {
425 typingHidMouse
= true;
428 // Scrollback up/down
429 if (keypress
.equals(kbShiftPgUp
)
430 || keypress
.equals(kbCtrlPgUp
)
431 || keypress
.equals(kbAltPgUp
)
433 bigVerticalDecrement();
437 if (keypress
.equals(kbShiftPgDn
)
438 || keypress
.equals(kbCtrlPgDn
)
439 || keypress
.equals(kbAltPgDn
)
441 bigVerticalIncrement();
446 if ((emulator
!= null) && (emulator
.isReading())) {
447 // Get out of scrollback
449 emulator
.addUserEvent(keypress
);
451 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
452 // this is kBEnter then also send kbCtrlJ.
453 if (keypress
.equals(kbEnter
)) {
454 if (System
.getProperty("os.name").startsWith("Windows")
455 && (System
.getProperty("jexer.TTerminal.cmdHack",
456 "true").equals("true"))
458 emulator
.addUserEvent(new TKeypressEvent(kbCtrlJ
));
466 // Process is closed, honor "normal" TUI keystrokes
467 super.onKeypress(keypress
);
471 * Handle mouse press events.
473 * @param mouse mouse button press event
476 public void onMouseDown(final TMouseEvent mouse
) {
477 if (hideMouseWhenTyping
) {
478 typingHidMouse
= false;
481 if (emulator
!= null) {
482 // If the emulator is tracking mouse buttons, it needs to see
484 if (emulator
.getMouseProtocol() == ECMA48
.MouseProtocol
.OFF
) {
485 if (mouse
.isMouseWheelUp()) {
490 if (mouse
.isMouseWheelDown()) {
496 if (mouseOnEmulator(mouse
)) {
497 emulator
.addUserEvent(mouse
);
503 // Emulator didn't consume it, pass it on
504 super.onMouseDown(mouse
);
508 * Handle mouse release events.
510 * @param mouse mouse button release event
513 public void onMouseUp(final TMouseEvent mouse
) {
514 if (hideMouseWhenTyping
) {
515 typingHidMouse
= false;
518 if ((emulator
!= null) && (mouseOnEmulator(mouse
))) {
519 emulator
.addUserEvent(mouse
);
524 // Emulator didn't consume it, pass it on
525 super.onMouseUp(mouse
);
529 * Handle mouse motion events.
531 * @param mouse mouse motion event
534 public void onMouseMotion(final TMouseEvent mouse
) {
535 if (hideMouseWhenTyping
) {
536 typingHidMouse
= false;
539 if ((emulator
!= null) && (mouseOnEmulator(mouse
))) {
540 emulator
.addUserEvent(mouse
);
545 // Emulator didn't consume it, pass it on
546 super.onMouseMotion(mouse
);
550 * Handle posted command events.
552 * @param command command event
555 public void onCommand(final TCommandEvent command
) {
556 if (emulator
== null) {
560 if (command
.equals(cmPaste
)) {
561 // Paste text from clipboard.
562 String text
= getClipboard().pasteText();
564 for (int i
= 0; i
< text
.length(); ) {
565 int ch
= text
.codePointAt(i
);
566 emulator
.addUserEvent(new TKeypressEvent(false, 0, ch
,
567 false, false, false));
568 i
+= Character
.charCount(ch
);
575 // ------------------------------------------------------------------------
576 // TScrollableWidget ------------------------------------------------------
577 // ------------------------------------------------------------------------
580 * Draw the display buffer.
584 if (emulator
== null) {
588 int width
= getDisplayWidth();
590 boolean syncEmulator
= false;
591 if (System
.currentTimeMillis() - lastUpdateTime
>= 50) {
592 // Too much time has passed, draw it all.
594 } else if (emulator
.isReading() && (dirty
== false)) {
595 // Wait until the emulator has brought more data in.
596 syncEmulator
= false;
597 } else if (!emulator
.isReading() && (dirty
== true)) {
598 // The emulator won't receive more data, update the display.
602 if ((syncEmulator
== true)
605 // We want to minimize the amount of time we have the emulator
606 // locked. Grab a copy of its display.
607 synchronized (emulator
) {
608 // Update the scroll bars
612 // We lost the connection, onShellExit() called an action
613 // that ultimately removed this widget from the UI
614 // hierarchy, so no one cares if we update the display.
619 if ((display
== null) || emulator
.isReading()) {
620 display
= emulator
.getVisibleDisplay(getHeight(),
621 -getVerticalValue());
622 assert (display
.size() == getHeight());
624 width
= emulator
.getWidth();
629 // Now draw the emulator screen
631 for (DisplayLine line
: display
) {
632 int widthMax
= width
;
633 if (line
.isDoubleWidth()) {
636 if (widthMax
> getWidth()) {
637 widthMax
= getWidth();
639 for (int i
= 0; i
< widthMax
; i
++) {
640 Cell ch
= line
.charAt(i
);
643 putCharXY(i
, row
, ch
);
647 Cell newCell
= new Cell(ch
);
648 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
649 newCell
.setReverse(false);
651 if (ch
.getForeColorRGB() < 0) {
652 newCell
.setBackColor(ch
.getForeColor());
653 newCell
.setBackColorRGB(-1);
655 newCell
.setBackColorRGB(ch
.getForeColorRGB());
657 if (ch
.getBackColorRGB() < 0) {
658 newCell
.setForeColor(ch
.getBackColor());
659 newCell
.setForeColorRGB(-1);
661 newCell
.setForeColorRGB(ch
.getBackColorRGB());
664 if (line
.isDoubleWidth()) {
665 putDoubleWidthCharXY(line
, (i
* 2), row
, newCell
);
667 putCharXY(i
, row
, newCell
);
675 * Set current value of the vertical scroll.
677 * @param value the new scroll value
680 public void setVerticalValue(final int value
) {
681 super.setVerticalValue(value
);
686 * Perform a small step change up.
689 public void verticalDecrement() {
690 super.verticalDecrement();
695 * Perform a small step change down.
698 public void verticalIncrement() {
699 super.verticalIncrement();
704 * Perform a big step change up.
706 public void bigVerticalDecrement() {
707 super.bigVerticalDecrement();
712 * Perform a big step change down.
714 public void bigVerticalIncrement() {
715 super.bigVerticalIncrement();
720 * Go to the top edge of the vertical scroller.
722 public void toTop() {
728 * Go to the bottom edge of the vertical scroller.
730 public void toBottom() {
736 * Handle widget close.
739 public void close() {
740 if (emulator
!= null) {
744 terminateShellChildProcess();
751 * Resize scrollbars for a new width/height.
754 public void reflowData() {
755 if (emulator
== null) {
759 // Synchronize against the emulator so we don't stomp on its reader
761 synchronized (emulator
) {
763 // Pull cursor information
766 // Vertical scrollbar
767 setTopValue(getHeight()
768 - (emulator
.getScrollbackBuffer().size()
769 + emulator
.getDisplayBuffer().size()));
770 setVerticalBigChange(getHeight());
772 } // synchronized (emulator)
775 // ------------------------------------------------------------------------
776 // TTerminalWidget --------------------------------------------------------
777 // ------------------------------------------------------------------------
780 * Get the desired window title.
784 public String
getTitle() {
789 * Returns true if this widget does not want the application-wide mouse
790 * cursor drawn over it.
792 * @return true if this widget does not want the application-wide mouse
793 * cursor drawn over it
795 public boolean hasHiddenMouse() {
796 if (emulator
== null) {
799 return (emulator
.hasHiddenMousePointer() || typingHidMouse
);
803 * See if the terminal is still running.
805 * @return if true, we are still connected to / reading from the remote
808 public boolean isReading() {
809 if (emulator
== null) {
812 return emulator
.isReading();
816 * Convert a string array to a whitespace-separated string.
818 * @param array the string array
819 * @return a single string
821 private String
stringArrayToString(final String
[] array
) {
822 StringBuilder sb
= new StringBuilder(array
[0].length());
823 for (int i
= 0; i
< array
.length
; i
++) {
825 if (i
< array
.length
- 1) {
829 return sb
.toString();
835 * @param command the command line to execute
837 private void spawnShell(final String
[] command
) {
840 System.err.printf("spawnShell(): '%s'\n",
841 stringArrayToString(command));
844 // We will have vScroller for its data fields and mouse event
845 // handling, but do not want to draw it.
846 vScroller
= new TVScroller(null, getWidth(), 0, getHeight());
847 vScroller
.setVisible(false);
850 title
= i18n
.getString("windowTitle");
853 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
856 ProcessBuilder pb
= new ProcessBuilder(command
);
857 Map
<String
, String
> env
= pb
.environment();
858 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
859 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
860 env
.put("COLUMNS", "80");
861 env
.put("LINES", "24");
862 pb
.redirectErrorStream(true);
864 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
865 shell
.getOutputStream(), this);
866 } catch (IOException e
) {
867 messageBox(i18n
.getString("errorLaunchingShellTitle"),
868 MessageFormat
.format(i18n
.getString("errorLaunchingShellText"),
872 // Setup the scroll bars
873 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
876 // Hide mouse when typing option
877 if (System
.getProperty("jexer.TTerminal.hideMouseWhenTyping",
878 "true").equals("false")) {
880 hideMouseWhenTyping
= false;
885 * Terminate the child of the 'script' process used on POSIX. This may
888 private void terminateShellChildProcess() {
890 if (shell
.getClass().getName().equals("java.lang.UNIXProcess")) {
891 /* get the PID on unix/linux systems */
893 Field field
= shell
.getClass().getDeclaredField("pid");
894 field
.setAccessible(true);
895 pid
= field
.getInt(shell
);
896 } catch (Throwable e
) {
897 // SQUASH, this didn't work. Just bail out quietly.
902 // shell.destroy() works successfully at killing this side of
903 // 'script'. But we need to make sure the other side (child
904 // process) is also killed.
905 String
[] cmdKillIt
= {
906 "pkill", "-P", Integer
.toString(pid
)
909 Runtime
.getRuntime().exec(cmdKillIt
);
910 } catch (Throwable e
) {
911 // SQUASH, this didn't work. Just bail out quietly.
918 * Hook for subclasses to be notified of the shell termination.
920 public void onShellExit() {
921 TApplication app
= getApplication();
923 if (closeAction
!= null) {
924 // We have to put this action inside invokeLater() because it
925 // could be executed during draw() when syncing with ECMA48.
926 app
.invokeLater(new Runnable() {
928 closeAction
.DO(TTerminalWidget
.this);
937 * Copy out variables from the emulator that TTerminal has to expose on
940 private void readEmulatorState() {
941 if (emulator
== null) {
945 // Synchronize against the emulator so we don't stomp on its reader
947 synchronized (emulator
) {
949 setCursorX(emulator
.getCursorX());
950 setCursorY(emulator
.getCursorY()
951 + (getHeight() - emulator
.getHeight())
952 - getVerticalValue());
953 setCursorVisible(emulator
.isCursorVisible());
954 if (getCursorX() > getWidth()) {
955 setCursorVisible(false);
957 if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
958 setCursorVisible(false);
960 if (emulator
.getScreenTitle().length() > 0) {
961 // Only update the title if the shell is still alive
963 title
= emulator
.getScreenTitle();
967 // Check to see if the shell has died.
968 if (!emulator
.isReading() && (shell
!= null)) {
970 int rc
= shell
.exitValue();
971 // The emulator exited on its own, all is fine
972 title
= MessageFormat
.format(i18n
.
973 getString("windowTitleCompleted"), title
, rc
);
978 } catch (IllegalThreadStateException e
) {
979 // The emulator thread has exited, but the shell Process
980 // hasn't figured that out yet. Do nothing, we will see
981 // this in a future tick.
983 } else if (emulator
.isReading() && (shell
!= null)) {
984 // The shell might be dead, let's check
986 int rc
= shell
.exitValue();
987 // If we got here, the shell died.
988 title
= MessageFormat
.format(i18n
.
989 getString("windowTitleCompleted"), title
, rc
);
994 } catch (IllegalThreadStateException e
) {
995 // The shell is still running, do nothing.
999 } // synchronized (emulator)
1003 * Check if a mouse press/release/motion event coordinate is over the
1006 * @param mouse a mouse-based event
1007 * @return whether or not the mouse is on the emulator
1009 private boolean mouseOnEmulator(final TMouseEvent mouse
) {
1010 if (emulator
== null) {
1014 if (!emulator
.isReading()) {
1018 if ((mouse
.getX() >= 0)
1019 && (mouse
.getX() < getWidth() - 1)
1020 && (mouse
.getY() >= 0)
1021 && (mouse
.getY() < getHeight())
1029 * Draw glyphs for a double-width or double-height VT100 cell to two
1032 * @param line the line this VT100 cell is in
1033 * @param x the X position to draw the left half to
1034 * @param y the Y position to draw to
1035 * @param cell the cell to draw
1037 private void putDoubleWidthCharXY(final DisplayLine line
, final int x
,
1038 final int y
, final Cell cell
) {
1040 int textWidth
= getScreen().getTextWidth();
1041 int textHeight
= getScreen().getTextHeight();
1042 boolean cursorBlinkVisible
= true;
1044 if (getScreen() instanceof SwingTerminal
) {
1045 SwingTerminal terminal
= (SwingTerminal
) getScreen();
1046 cursorBlinkVisible
= terminal
.getCursorBlinkVisible();
1047 } else if (getScreen() instanceof ECMA48Terminal
) {
1048 ECMA48Terminal terminal
= (ECMA48Terminal
) getScreen();
1050 if (!terminal
.hasSixel()) {
1051 // The backend does not have sixel support, draw this as text
1053 putCharXY(x
, y
, cell
);
1054 putCharXY(x
+ 1, y
, ' ', cell
);
1057 cursorBlinkVisible
= blinkState
;
1059 // We don't know how to dray glyphs to this screen, draw them as
1060 // text and bail out.
1061 putCharXY(x
, y
, cell
);
1062 putCharXY(x
+ 1, y
, ' ', cell
);
1066 if ((textWidth
!= lastTextWidth
) || (textHeight
!= lastTextHeight
)) {
1067 // Screen size has changed, reset the font.
1068 setupFont(textHeight
);
1069 lastTextWidth
= textWidth
;
1070 lastTextHeight
= textHeight
;
1072 assert (doubleFont
!= null);
1074 BufferedImage image
;
1075 if (line
.getDoubleHeight() == 1) {
1076 // Double-height top half: don't draw the underline.
1077 Cell newCell
= new Cell(cell
);
1078 newCell
.setUnderline(false);
1079 image
= doubleFont
.getImage(newCell
, textWidth
* 2, textHeight
* 2,
1080 cursorBlinkVisible
);
1082 image
= doubleFont
.getImage(cell
, textWidth
* 2, textHeight
* 2,
1083 cursorBlinkVisible
);
1086 // Now that we have the double-wide glyph drawn, copy the right
1087 // pieces of it to the cells.
1088 Cell left
= new Cell(cell
);
1089 Cell right
= new Cell(cell
);
1091 BufferedImage leftImage
= null;
1092 BufferedImage rightImage
= null;
1094 System.err.println("image " + image + " textWidth " + textWidth +
1095 " textHeight " + textHeight);
1098 switch (line
.getDoubleHeight()) {
1100 // Top half double height
1101 leftImage
= image
.getSubimage(0, 0, textWidth
, textHeight
);
1102 rightImage
= image
.getSubimage(textWidth
, 0, textWidth
, textHeight
);
1105 // Bottom half double height
1106 leftImage
= image
.getSubimage(0, textHeight
, textWidth
, textHeight
);
1107 rightImage
= image
.getSubimage(textWidth
, textHeight
,
1108 textWidth
, textHeight
);
1111 // Either single height double-width, or error fallback
1112 BufferedImage wideImage
= new BufferedImage(textWidth
* 2,
1113 textHeight
, BufferedImage
.TYPE_INT_ARGB
);
1114 Graphics2D grWide
= wideImage
.createGraphics();
1115 grWide
.drawImage(image
, 0, 0, wideImage
.getWidth(),
1116 wideImage
.getHeight(), null);
1118 leftImage
= wideImage
.getSubimage(0, 0, textWidth
, textHeight
);
1119 rightImage
= wideImage
.getSubimage(textWidth
, 0, textWidth
,
1123 left
.setImage(leftImage
);
1124 right
.setImage(rightImage
);
1125 // Since we have image data, ditch the character here. Otherwise, a
1126 // drawBoxShadow() over the terminal window will show the characters
1127 // which looks wrong.
1130 putCharXY(x
, y
, left
);
1131 putCharXY(x
+ 1, y
, right
);
1135 * Set up the double-width font.
1137 * @param fontSize the size of font to request for the single-width font.
1138 * The double-width font will be 2x this value.
1140 private void setupFont(final int fontSize
) {
1141 doubleFont
= GlyphMaker
.getInstance(fontSize
* 2);
1143 // Special case: the ECMA48 backend needs to have a timer to drive
1145 if (getScreen() instanceof jexer
.backend
.ECMA48Terminal
) {
1147 // Blink every 500 millis.
1149 getApplication().addTimer(millis
, true,
1152 blinkState
= !blinkState
;
1153 getApplication().doRepaint();
1162 // ------------------------------------------------------------------------
1163 // DisplayListener --------------------------------------------------------
1164 // ------------------------------------------------------------------------
1167 * Called by emulator when fresh data has come in.
1169 public void displayChanged() {
1170 if (emulator
!= null) {
1171 // Force sync here: EMCA48.run() thread might be setting
1172 // dirty=true while TTerminalWdiget.draw() is setting
1173 // dirty=false. If these writes start interleaving, the display
1174 // stops getting updated.
1175 synchronized (emulator
) {
1181 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
1185 * Function to call to obtain the display width.
1187 * @return the number of columns in the display
1189 public int getDisplayWidth() {
1197 * Function to call to obtain the display height.
1199 * @return the number of rows in the display
1201 public int getDisplayHeight() {
1208 // ------------------------------------------------------------------------
1209 // EditMenuUser -----------------------------------------------------------
1210 // ------------------------------------------------------------------------
1213 * Check if the cut menu item should be enabled.
1215 * @return true if the cut menu item should be enabled
1217 public boolean isEditMenuCut() {
1222 * Check if the copy menu item should be enabled.
1224 * @return true if the copy menu item should be enabled
1226 public boolean isEditMenuCopy() {
1231 * Check if the paste menu item should be enabled.
1233 * @return true if the paste menu item should be enabled
1235 public boolean isEditMenuPaste() {
1240 * Check if the clear menu item should be enabled.
1242 * @return true if the clear menu item should be enabled
1244 public boolean isEditMenuClear() {