mirror of
https://gitlab.com/AutumnMeowMeow/jexer
synced 2024-09-19 11:50:19 -06:00
ANSI/ASCII picture viewer
This commit is contained in:
parent
8306a113e9
commit
6422872706
8 changed files with 965 additions and 59 deletions
|
@ -1166,6 +1166,10 @@ public class TApplication implements Runnable {
|
|||
openImage();
|
||||
return true;
|
||||
}
|
||||
if (menu.getId() == TMenu.MID_VIEW_ANSI) {
|
||||
openAnsiFile();
|
||||
return true;
|
||||
}
|
||||
if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) {
|
||||
new TScreenOptionsWindow(this);
|
||||
return true;
|
||||
|
@ -2083,6 +2087,21 @@ public class TApplication implements Runnable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Tool | Open ANSI menu item.
|
||||
*/
|
||||
private void openAnsiFile() {
|
||||
try {
|
||||
String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN);
|
||||
if (filename != null) {
|
||||
new TTextPictureWindow(this, filename);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Show this exception to the user.
|
||||
new TExceptionDialog(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if application is still running.
|
||||
*
|
||||
|
@ -3635,6 +3654,7 @@ public class TApplication implements Runnable {
|
|||
TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
|
||||
toolMenu.addDefaultItem(TMenu.MID_REPAINT);
|
||||
toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
|
||||
toolMenu.addDefaultItem(TMenu.MID_VIEW_ANSI);
|
||||
toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
|
||||
TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
|
||||
getString("toolMenuStatus"));
|
||||
|
|
|
@ -1390,47 +1390,6 @@ public class TTerminalWidget extends TScrollableWidget
|
|||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DisplayListener --------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called by emulator when fresh data has come in.
|
||||
*/
|
||||
public void displayChanged() {
|
||||
synchronized (emulator) {
|
||||
setDirty();
|
||||
}
|
||||
TApplication app = getApplication();
|
||||
if (app != null) {
|
||||
app.postEvent(new TMenuEvent(null, TMenu.MID_REPAINT));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call to obtain the display width.
|
||||
*
|
||||
* @return the number of columns in the display
|
||||
*/
|
||||
public int getDisplayWidth() {
|
||||
if (ptypipe) {
|
||||
return getWidth();
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call to obtain the display height.
|
||||
*
|
||||
* @return the number of rows in the display
|
||||
*/
|
||||
public int getDisplayHeight() {
|
||||
if (ptypipe) {
|
||||
return getHeight();
|
||||
}
|
||||
return 24;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exit value for the emulator.
|
||||
*
|
||||
|
@ -1514,6 +1473,47 @@ public class TTerminalWidget extends TScrollableWidget
|
|||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DisplayListener --------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called by emulator when fresh data has come in.
|
||||
*/
|
||||
public void displayChanged() {
|
||||
synchronized (emulator) {
|
||||
setDirty();
|
||||
}
|
||||
TApplication app = getApplication();
|
||||
if (app != null) {
|
||||
app.postEvent(new TMenuEvent(null, TMenu.MID_REPAINT));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call to obtain the display width.
|
||||
*
|
||||
* @return the number of columns in the display
|
||||
*/
|
||||
public int getDisplayWidth() {
|
||||
if (ptypipe) {
|
||||
return getWidth();
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call to obtain the display height.
|
||||
*
|
||||
* @return the number of rows in the display
|
||||
*/
|
||||
public int getDisplayHeight() {
|
||||
if (ptypipe) {
|
||||
return getHeight();
|
||||
}
|
||||
return 24;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// EditMenuUser -----------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
526
src/jexer/TTextPicture.java
Normal file
526
src/jexer/TTextPicture.java
Normal file
|
@ -0,0 +1,526 @@
|
|||
/*
|
||||
* Jexer - Java Text User Interface
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (C) 2021 Autumn Lamonte
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
|
||||
* @version 1
|
||||
*/
|
||||
package jexer;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
|
||||
import jexer.TScrollableWidget;
|
||||
import jexer.backend.ECMA48Terminal;
|
||||
import jexer.backend.GlyphMaker;
|
||||
import jexer.backend.SwingTerminal;
|
||||
import jexer.bits.Cell;
|
||||
import jexer.event.TKeypressEvent;
|
||||
import jexer.event.TMouseEvent;
|
||||
import jexer.event.TResizeEvent;
|
||||
import jexer.tterminal.DisplayLine;
|
||||
import jexer.tterminal.DisplayListener;
|
||||
import jexer.tterminal.ECMA48;
|
||||
import static jexer.TKeypress.*;
|
||||
|
||||
|
||||
/**
|
||||
* TTextPicture displays a color-and-text canvas, also called "ANSI Art" or
|
||||
* "ASCII Art".
|
||||
*/
|
||||
public class TTextPicture extends TScrollableWidget
|
||||
implements DisplayListener {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constants --------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Variables --------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The terminal containing the display.
|
||||
*/
|
||||
private ECMA48 terminal;
|
||||
|
||||
/**
|
||||
* If true, the terminal is not reading and is closed.
|
||||
*/
|
||||
private boolean terminalClosed = true;
|
||||
|
||||
/**
|
||||
* Double-height font.
|
||||
*/
|
||||
private GlyphMaker doubleFont;
|
||||
|
||||
/**
|
||||
* Last text width value.
|
||||
*/
|
||||
private int lastTextWidth = -1;
|
||||
|
||||
/**
|
||||
* Last text height value.
|
||||
*/
|
||||
private int lastTextHeight = -1;
|
||||
|
||||
/**
|
||||
* The blink state, used only by ECMA48 backend and when double-width
|
||||
* chars must be drawn.
|
||||
*/
|
||||
private boolean blinkState = true;
|
||||
|
||||
/**
|
||||
* Timer, used only by ECMA48 backend and when double-width chars must be
|
||||
* drawn.
|
||||
*/
|
||||
private TTimer blinkTimer = null;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constructors -----------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param parent parent widget
|
||||
* @param filename the file containing the picture data
|
||||
* @param x column relative to parent
|
||||
* @param y row relative to parent
|
||||
* @param width width of text area
|
||||
* @param height height of text area
|
||||
*/
|
||||
public TTextPicture(final TWidget parent, final String filename,
|
||||
final int x, final int y, final int width, final int height) {
|
||||
|
||||
// Set parent and window
|
||||
super(parent, x, y, width, height);
|
||||
|
||||
try {
|
||||
terminal = new ECMA48(ECMA48.DeviceType.XTERM,
|
||||
new FileInputStream(filename), new ByteArrayOutputStream(),
|
||||
this);
|
||||
|
||||
terminalClosed = false;
|
||||
} catch (FileNotFoundException e) {
|
||||
// SQUASH
|
||||
terminal = null;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// SQUASH
|
||||
terminal = null;
|
||||
}
|
||||
|
||||
// We will have scrollers for data fields and mouse event handling,
|
||||
// but do not want to draw it.
|
||||
vScroller = new TVScroller(null, getWidth(), 0, getHeight());
|
||||
vScroller.setVisible(false);
|
||||
setBottomValue(0);
|
||||
hScroller = new THScroller(null, 0, getHeight() - 1,
|
||||
Math.max(1, getWidth() - 1));
|
||||
hScroller.setVisible(false);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Event handlers ---------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle window/screen resize events.
|
||||
*
|
||||
* @param resize resize event
|
||||
*/
|
||||
@Override
|
||||
public void onResize(final TResizeEvent resize) {
|
||||
// Let TWidget set my size.
|
||||
super.onResize(resize);
|
||||
|
||||
if (terminal == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (terminal) {
|
||||
if (resize.getType() == TResizeEvent.Type.WIDGET) {
|
||||
// Resize the scroll bars
|
||||
reflowData();
|
||||
placeScrollbars();
|
||||
|
||||
// Get out of scrollback
|
||||
setVerticalValue(0);
|
||||
|
||||
terminal.setWidth(getWidth());
|
||||
terminal.setHeight(getHeight());
|
||||
}
|
||||
} // synchronized (emulator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keystrokes.
|
||||
*
|
||||
* @param keypress keystroke event
|
||||
*/
|
||||
@Override
|
||||
public void onKeypress(final TKeypressEvent keypress) {
|
||||
// Scrollback up/down/home/end
|
||||
if (keypress.equals(kbShiftHome)
|
||||
|| keypress.equals(kbCtrlHome)
|
||||
|| keypress.equals(kbAltHome)
|
||||
) {
|
||||
toTop();
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbShiftEnd)
|
||||
|| keypress.equals(kbCtrlEnd)
|
||||
|| keypress.equals(kbAltEnd)
|
||||
) {
|
||||
toBottom();
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbShiftPgUp)
|
||||
|| keypress.equals(kbCtrlPgUp)
|
||||
|| keypress.equals(kbAltPgUp)
|
||||
) {
|
||||
bigVerticalDecrement();
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbShiftPgDn)
|
||||
|| keypress.equals(kbCtrlPgDn)
|
||||
|| keypress.equals(kbAltPgDn)
|
||||
) {
|
||||
bigVerticalIncrement();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onKeypress(keypress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse press events.
|
||||
*
|
||||
* @param mouse mouse button press event
|
||||
*/
|
||||
@Override
|
||||
public void onMouseDown(final TMouseEvent mouse) {
|
||||
if (mouse.isMouseWheelUp()) {
|
||||
verticalDecrement();
|
||||
return;
|
||||
}
|
||||
if (mouse.isMouseWheelDown()) {
|
||||
verticalIncrement();
|
||||
return;
|
||||
}
|
||||
super.onMouseDown(mouse);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// TScrollableWidget ------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle widget close.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (terminal != null) {
|
||||
terminal.close();
|
||||
}
|
||||
if (blinkTimer != null) {
|
||||
TApplication app = getApplication();
|
||||
if (app != null) {
|
||||
app.removeTimer(blinkTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize scrollbars for a new width/height.
|
||||
*/
|
||||
@Override
|
||||
public void reflowData() {
|
||||
if (terminal == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertical scrollbar
|
||||
setTopValue(getHeight()
|
||||
- (terminal.getScrollbackBuffer().size()
|
||||
+ terminal.getDisplayBuffer().size()));
|
||||
setVerticalBigChange(getHeight());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DisplayListener --------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called by emulator when fresh data has come in.
|
||||
*/
|
||||
public void displayChanged() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call to obtain the display width.
|
||||
*
|
||||
* @return the number of columns in the display
|
||||
*/
|
||||
public int getDisplayWidth() {
|
||||
return getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call to obtain the display height.
|
||||
*
|
||||
* @return the number of rows in the display
|
||||
*/
|
||||
public int getDisplayHeight() {
|
||||
return getHeight();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// TTextPicture -----------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Draw the text box.
|
||||
*/
|
||||
@Override
|
||||
public void draw() {
|
||||
if (terminal == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if the shell has died.
|
||||
if (!terminalClosed && !terminal.isReading()) {
|
||||
try {
|
||||
terminal.close();
|
||||
terminalClosed = true;
|
||||
} catch (IllegalThreadStateException e) {
|
||||
// SQUASH
|
||||
}
|
||||
}
|
||||
|
||||
int width = 80;
|
||||
int left = 0;
|
||||
List<DisplayLine> display = null;
|
||||
|
||||
synchronized (terminal) {
|
||||
// Update the scroll bars
|
||||
reflowData();
|
||||
|
||||
display = terminal.getVisibleDisplay(getHeight(),
|
||||
-getVerticalValue());
|
||||
assert (display.size() == getHeight());
|
||||
width = terminal.getWidth();
|
||||
left = getHorizontalValue();
|
||||
} // synchronized (terminal)
|
||||
|
||||
int row = 0;
|
||||
for (DisplayLine line: display) {
|
||||
int widthMax = width;
|
||||
if (line.isDoubleWidth()) {
|
||||
widthMax /= 2;
|
||||
}
|
||||
if (widthMax > getWidth()) {
|
||||
widthMax = getWidth();
|
||||
}
|
||||
for (int i = 0; i < widthMax; i++) {
|
||||
Cell ch = line.charAt(i + left);
|
||||
|
||||
if (ch.isImage()) {
|
||||
putCharXY(i, row, ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
Cell newCell = new Cell(ch);
|
||||
boolean reverse = line.isReverseColor() ^ ch.isReverse();
|
||||
newCell.setReverse(false);
|
||||
if (reverse) {
|
||||
if (ch.getForeColorRGB() < 0) {
|
||||
newCell.setBackColor(ch.getForeColor());
|
||||
newCell.setBackColorRGB(-1);
|
||||
} else {
|
||||
newCell.setBackColorRGB(ch.getForeColorRGB());
|
||||
}
|
||||
if (ch.getBackColorRGB() < 0) {
|
||||
newCell.setForeColor(ch.getBackColor());
|
||||
newCell.setForeColorRGB(-1);
|
||||
} else {
|
||||
newCell.setForeColorRGB(ch.getBackColorRGB());
|
||||
}
|
||||
}
|
||||
if (line.isDoubleWidth()) {
|
||||
putDoubleWidthCharXY(line, (i * 2), row, newCell);
|
||||
} else {
|
||||
putCharXY(i, row, newCell);
|
||||
}
|
||||
}
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw glyphs for a double-width or double-height VT100 cell to two
|
||||
* screen cells.
|
||||
*
|
||||
* @param line the line this VT100 cell is in
|
||||
* @param x the X position to draw the left half to
|
||||
* @param y the Y position to draw to
|
||||
* @param cell the cell to draw
|
||||
*/
|
||||
private void putDoubleWidthCharXY(final DisplayLine line, final int x,
|
||||
final int y, final Cell cell) {
|
||||
|
||||
int textWidth = getScreen().getTextWidth();
|
||||
int textHeight = getScreen().getTextHeight();
|
||||
boolean cursorBlinkVisible = true;
|
||||
|
||||
if (getScreen() instanceof SwingTerminal) {
|
||||
SwingTerminal terminal = (SwingTerminal) getScreen();
|
||||
cursorBlinkVisible = terminal.getCursorBlinkVisible();
|
||||
} else if (getScreen() instanceof ECMA48Terminal) {
|
||||
ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
|
||||
|
||||
if (!terminal.hasSixel()
|
||||
&& !terminal.hasJexerImages()
|
||||
&& !terminal.hasIterm2Images()
|
||||
) {
|
||||
// The backend does not have images support, draw this as
|
||||
// text and bail out.
|
||||
putCharXY(x, y, cell);
|
||||
putCharXY(x + 1, y, ' ', cell);
|
||||
return;
|
||||
}
|
||||
cursorBlinkVisible = blinkState;
|
||||
} else {
|
||||
// We don't know how to dray glyphs to this screen, draw them as
|
||||
// text and bail out.
|
||||
putCharXY(x, y, cell);
|
||||
putCharXY(x + 1, y, ' ', cell);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
|
||||
// Screen size has changed, reset the font.
|
||||
setupFont(textHeight);
|
||||
lastTextWidth = textWidth;
|
||||
lastTextHeight = textHeight;
|
||||
}
|
||||
assert (doubleFont != null);
|
||||
|
||||
BufferedImage image;
|
||||
if (line.getDoubleHeight() == 1) {
|
||||
// Double-height top half: don't draw the underline.
|
||||
Cell newCell = new Cell(cell);
|
||||
newCell.setUnderline(false);
|
||||
image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
|
||||
cursorBlinkVisible);
|
||||
} else {
|
||||
image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2,
|
||||
cursorBlinkVisible);
|
||||
}
|
||||
|
||||
// Now that we have the double-wide glyph drawn, copy the right
|
||||
// pieces of it to the cells.
|
||||
Cell left = new Cell(cell);
|
||||
Cell right = new Cell(cell);
|
||||
right.setChar(' ');
|
||||
BufferedImage leftImage = null;
|
||||
BufferedImage rightImage = null;
|
||||
/*
|
||||
System.err.println("image " + image + " textWidth " + textWidth +
|
||||
" textHeight " + textHeight);
|
||||
*/
|
||||
|
||||
switch (line.getDoubleHeight()) {
|
||||
case 1:
|
||||
// Top half double height
|
||||
leftImage = image.getSubimage(0, 0, textWidth, textHeight);
|
||||
rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
|
||||
break;
|
||||
case 2:
|
||||
// Bottom half double height
|
||||
leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
|
||||
rightImage = image.getSubimage(textWidth, textHeight,
|
||||
textWidth, textHeight);
|
||||
break;
|
||||
default:
|
||||
// Either single height double-width, or error fallback
|
||||
BufferedImage wideImage = new BufferedImage(textWidth * 2,
|
||||
textHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D grWide = wideImage.createGraphics();
|
||||
grWide.drawImage(image, 0, 0, wideImage.getWidth(),
|
||||
wideImage.getHeight(), null);
|
||||
grWide.dispose();
|
||||
leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
|
||||
rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
|
||||
textHeight);
|
||||
break;
|
||||
}
|
||||
left.setImage(leftImage);
|
||||
right.setImage(rightImage);
|
||||
// Since we have image data, ditch the character here. Otherwise, a
|
||||
// drawBoxShadow() over the terminal window will show the characters
|
||||
// which looks wrong.
|
||||
left.setChar(' ');
|
||||
right.setChar(' ');
|
||||
putCharXY(x, y, left);
|
||||
putCharXY(x + 1, y, right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the double-width font.
|
||||
*
|
||||
* @param fontSize the size of font to request for the single-width font.
|
||||
* The double-width font will be 2x this value.
|
||||
*/
|
||||
private void setupFont(final int fontSize) {
|
||||
doubleFont = GlyphMaker.getInstance(fontSize * 2);
|
||||
|
||||
// Special case: the ECMA48 backend needs to have a timer to drive
|
||||
// its blink state.
|
||||
if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
|
||||
if (blinkTimer == null) {
|
||||
// Blink every 500 millis.
|
||||
long millis = 500;
|
||||
blinkTimer = getApplication().addTimer(millis, true,
|
||||
new TAction() {
|
||||
public void DO() {
|
||||
blinkState = !blinkState;
|
||||
TApplication app = getApplication();
|
||||
if (app != null) {
|
||||
app.doRepaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
273
src/jexer/TTextPictureWindow.java
Normal file
273
src/jexer/TTextPictureWindow.java
Normal file
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* Jexer - Java Text User Interface
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (C) 2021 Autumn Lamonte
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
|
||||
* @version 1
|
||||
*/
|
||||
package jexer;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ResourceBundle;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import jexer.event.TKeypressEvent;
|
||||
import jexer.event.TMouseEvent;
|
||||
import jexer.event.TResizeEvent;
|
||||
import static jexer.TKeypress.*;
|
||||
|
||||
/**
|
||||
* TTextPictureWindow shows an ASCII/ANSI art file with scrollbars.
|
||||
*/
|
||||
public class TTextPictureWindow extends TScrollableWindow {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constants --------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The number of lines to scroll on mouse wheel up/down.
|
||||
*/
|
||||
private static final int wheelScrollSize = 3;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Variables --------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hang onto the TTextPicture so I can resize it with the window.
|
||||
*/
|
||||
private TTextPicture pictureField;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constructors -----------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Public constructor opens a file.
|
||||
*
|
||||
* @param parent the main application
|
||||
* @param filename the file to open
|
||||
* @throws IOException if a java.io operation throws
|
||||
*/
|
||||
public TTextPictureWindow(final TApplication parent,
|
||||
final String filename) throws IOException {
|
||||
|
||||
this(parent, filename, 0, 0, 82, 27);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor opens a file.
|
||||
*
|
||||
* @param parent the main application
|
||||
* @param filename the file to open
|
||||
* @param x column relative to parent
|
||||
* @param y row relative to parent
|
||||
* @param width width of window
|
||||
* @param height height of window
|
||||
* @throws IOException if a java.io operation throws
|
||||
*/
|
||||
public TTextPictureWindow(final TApplication parent, final String filename,
|
||||
final int x, final int y, final int width,
|
||||
final int height) throws IOException {
|
||||
|
||||
super(parent, filename, x, y, width, height, RESIZABLE);
|
||||
|
||||
pictureField = new TTextPicture(this, filename, 0, 0,
|
||||
getWidth() - 2, getHeight() - 2);
|
||||
setTitle(filename);
|
||||
|
||||
setupAfterTextPicture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup other fields after the picture is created.
|
||||
*/
|
||||
private void setupAfterTextPicture() {
|
||||
if (pictureField.getHeight() < getHeight() - 2) {
|
||||
setHeight(pictureField.getHeight() + 2);
|
||||
}
|
||||
if (pictureField.getWidth() < getWidth() - 2) {
|
||||
setWidth(pictureField.getWidth() + 2);
|
||||
}
|
||||
|
||||
hScroller = new THScroller(this,
|
||||
Math.min(Math.max(0, getWidth() - 17), 17),
|
||||
getHeight() - 2,
|
||||
getWidth() - Math.min(Math.max(0, getWidth() - 17), 17) - 3);
|
||||
vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
|
||||
reflowData();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Event handlers ---------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle mouse press events.
|
||||
*
|
||||
* @param mouse mouse button press event
|
||||
*/
|
||||
@Override
|
||||
public void onMouseDown(final TMouseEvent mouse) {
|
||||
super.onMouseDown(mouse);
|
||||
setVerticalValue(pictureField.getVerticalValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse release events.
|
||||
*
|
||||
* @param mouse mouse button release event
|
||||
*/
|
||||
@Override
|
||||
public void onMouseUp(final TMouseEvent mouse) {
|
||||
super.onMouseUp(mouse);
|
||||
|
||||
if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
|
||||
// Clicked/dragged on vertical scrollbar
|
||||
pictureField.setVerticalValue(getVerticalValue());
|
||||
}
|
||||
if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
|
||||
// Clicked/dragged on horizontal scrollbar
|
||||
pictureField.setHorizontalValue(getHorizontalValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that subclasses can override to handle mouse movements.
|
||||
*
|
||||
* @param mouse mouse motion event
|
||||
*/
|
||||
@Override
|
||||
public void onMouseMotion(final TMouseEvent mouse) {
|
||||
super.onMouseMotion(mouse);
|
||||
|
||||
if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
|
||||
// Clicked/dragged on vertical scrollbar
|
||||
pictureField.setVerticalValue(getVerticalValue());
|
||||
}
|
||||
if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
|
||||
// Clicked/dragged on horizontal scrollbar
|
||||
pictureField.setHorizontalValue(getHorizontalValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window/screen resize events.
|
||||
*
|
||||
* @param event resize event
|
||||
*/
|
||||
@Override
|
||||
public void onResize(final TResizeEvent event) {
|
||||
if (event.getType() == TResizeEvent.Type.WIDGET) {
|
||||
// Resize the picture field
|
||||
TResizeEvent pictureSize = new TResizeEvent(event.getBackend(),
|
||||
TResizeEvent.Type.WIDGET, event.getWidth() - 2,
|
||||
event.getHeight() - 2);
|
||||
pictureField.onResize(pictureSize);
|
||||
|
||||
// Have TScrollableWindow handle the scrollbars
|
||||
super.onResize(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass to children instead
|
||||
for (TWidget widget: getChildren()) {
|
||||
widget.onResize(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keystrokes.
|
||||
*
|
||||
* @param keypress keystroke event
|
||||
*/
|
||||
@Override
|
||||
public void onKeypress(final TKeypressEvent keypress) {
|
||||
if (keypress.equals(kbUp)) {
|
||||
verticalDecrement();
|
||||
pictureField.setVerticalValue(getVerticalValue());
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbDown)) {
|
||||
verticalIncrement();
|
||||
pictureField.setVerticalValue(getVerticalValue());
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbPgUp)) {
|
||||
bigVerticalDecrement();
|
||||
pictureField.setVerticalValue(getVerticalValue());
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbPgDn)) {
|
||||
bigVerticalIncrement();
|
||||
pictureField.setVerticalValue(getVerticalValue());
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbRight)) {
|
||||
horizontalIncrement();
|
||||
pictureField.setHorizontalValue(getHorizontalValue());
|
||||
return;
|
||||
}
|
||||
if (keypress.equals(kbLeft)) {
|
||||
horizontalDecrement();
|
||||
pictureField.setHorizontalValue(getHorizontalValue());
|
||||
return;
|
||||
}
|
||||
|
||||
// We did not take it, let the TTextPicture instance see it.
|
||||
super.onKeypress(keypress);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// TScrollableWindow ------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Draw the window.
|
||||
*/
|
||||
@Override
|
||||
public void draw() {
|
||||
reflowData();
|
||||
super.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize scrollbars for a new width/height.
|
||||
*/
|
||||
@Override
|
||||
public void reflowData() {
|
||||
pictureField.reflowData();
|
||||
setTopValue(pictureField.getTopValue());
|
||||
setBottomValue(pictureField.getBottomValue());
|
||||
setVerticalBigChange(pictureField.getVerticalBigChange());
|
||||
setVerticalValue(pictureField.getVerticalValue());
|
||||
setHorizontalValue(pictureField.getHorizontalValue());
|
||||
|
||||
setRightValue(Math.min(80, 80 - pictureField.getWidth()));
|
||||
}
|
||||
|
||||
}
|
|
@ -58,6 +58,11 @@ public class TimeoutInputStream extends InputStream {
|
|||
*/
|
||||
private volatile boolean cancel = false;
|
||||
|
||||
/**
|
||||
* If true, EOF was encountered.
|
||||
*/
|
||||
private boolean eof = false;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constructors -----------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
@ -108,14 +113,26 @@ public class TimeoutInputStream extends InputStream {
|
|||
@Override
|
||||
public int read() throws IOException {
|
||||
|
||||
if (eof) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (timeoutMillis == 0) {
|
||||
// Block on the read().
|
||||
return stream.read();
|
||||
int rc = stream.read();
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (stream.available() > 0) {
|
||||
// A byte is available now, return it.
|
||||
return stream.read();
|
||||
int rc = stream.read();
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
// We will wait up to timeoutMillis to see if a byte is available.
|
||||
|
@ -143,7 +160,11 @@ public class TimeoutInputStream extends InputStream {
|
|||
|
||||
if (stream.available() > 0) {
|
||||
// A byte is available now, return it.
|
||||
return stream.read();
|
||||
int rc = stream.read();
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
throw new IOException("InputStream claimed a byte was available, but " +
|
||||
|
@ -161,16 +182,29 @@ public class TimeoutInputStream extends InputStream {
|
|||
*/
|
||||
@Override
|
||||
public int read(final byte[] b) throws IOException {
|
||||
|
||||
if (eof) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (timeoutMillis == 0) {
|
||||
// Block on the read().
|
||||
return stream.read(b);
|
||||
int rc = stream.read(b);
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int remaining = b.length;
|
||||
|
||||
if (stream.available() >= remaining) {
|
||||
// Enough bytes are available now, return them.
|
||||
return stream.read(b);
|
||||
int rc = stream.read(b);
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
while (remaining > 0) {
|
||||
|
@ -179,7 +213,7 @@ public class TimeoutInputStream extends InputStream {
|
|||
// available. If not, we throw ReadTimeoutException.
|
||||
long checkTime = System.currentTimeMillis();
|
||||
while (stream.available() == 0) {
|
||||
if (remaining > 0) {
|
||||
if ((remaining > 0) && (remaining != b.length)) {
|
||||
return (b.length - remaining);
|
||||
}
|
||||
|
||||
|
@ -211,6 +245,8 @@ public class TimeoutInputStream extends InputStream {
|
|||
}
|
||||
int rc = stream.read(b, b.length - remaining, n);
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
|
||||
// This shouldn't happen.
|
||||
throw new IOException("InputStream claimed bytes were " +
|
||||
"available, but read() returned -1. What is going " +
|
||||
|
@ -242,16 +278,28 @@ public class TimeoutInputStream extends InputStream {
|
|||
public int read(final byte[] b, final int off,
|
||||
final int len) throws IOException {
|
||||
|
||||
if (eof) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (timeoutMillis == 0) {
|
||||
// Block on the read().
|
||||
return stream.read(b, off, len);
|
||||
int rc = stream.read(b, off, len);
|
||||
if (rc == -1) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int remaining = len;
|
||||
|
||||
if (stream.available() >= remaining) {
|
||||
// Enough bytes are available now, return them.
|
||||
return stream.read(b, off, remaining);
|
||||
int rc = stream.read(b, off, remaining);
|
||||
if (rc <= 0) {
|
||||
eof = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
while (remaining > 0) {
|
||||
|
@ -260,7 +308,7 @@ public class TimeoutInputStream extends InputStream {
|
|||
// available. If not, we throw ReadTimeoutException.
|
||||
long checkTime = System.currentTimeMillis();
|
||||
while (stream.available() == 0) {
|
||||
if (remaining > 0) {
|
||||
if ((remaining > 0) && (remaining != len)) {
|
||||
return (len - remaining);
|
||||
}
|
||||
|
||||
|
@ -291,7 +339,9 @@ public class TimeoutInputStream extends InputStream {
|
|||
n = remaining;
|
||||
}
|
||||
int rc = stream.read(b, off + len - remaining, n);
|
||||
if (rc == -1) {
|
||||
if (rc <= 0) {
|
||||
eof = true;
|
||||
|
||||
// This shouldn't happen.
|
||||
throw new IOException("InputStream claimed bytes were " +
|
||||
"available, but read() returned -1. What is going " +
|
||||
|
@ -390,4 +440,13 @@ public class TimeoutInputStream extends InputStream {
|
|||
cancel = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying stream.
|
||||
*
|
||||
* @return the stream
|
||||
*/
|
||||
public InputStream getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@ public class TMenu extends TWindow {
|
|||
// Tools menu
|
||||
public static final int MID_REPAINT = 1;
|
||||
public static final int MID_VIEW_IMAGE = 2;
|
||||
public static final int MID_SCREEN_OPTIONS = 3;
|
||||
public static final int MID_VIEW_ANSI = 3;
|
||||
public static final int MID_SCREEN_OPTIONS = 4;
|
||||
|
||||
// File menu
|
||||
public static final int MID_NEW = 10;
|
||||
|
@ -631,6 +632,10 @@ public class TMenu extends TWindow {
|
|||
label = i18n.getString("menuViewImage");
|
||||
break;
|
||||
|
||||
case MID_VIEW_ANSI:
|
||||
label = i18n.getString("menuViewAnsiArt");
|
||||
break;
|
||||
|
||||
case MID_SCREEN_OPTIONS:
|
||||
label = i18n.getString("menuScreenOptions");
|
||||
break;
|
||||
|
|
|
@ -60,5 +60,6 @@ menuTableFileSaveText=Save As &Text...
|
|||
|
||||
menuRepaintDesktop=&Repaint desktop
|
||||
menuViewImage=&Open image...
|
||||
menuViewAnsiArt=&View ANSI/ASCII...
|
||||
menuScreenOptions=&Screen options...
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import java.io.CharArrayWriter;
|
|||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
@ -658,9 +659,10 @@ public class ECMA48 implements Runnable {
|
|||
|
||||
this.type = type;
|
||||
if (inputStream instanceof TimeoutInputStream) {
|
||||
this.inputStream = (TimeoutInputStream)inputStream;
|
||||
this.inputStream = (TimeoutInputStream) inputStream;
|
||||
} else {
|
||||
this.inputStream = new TimeoutInputStream(inputStream, 2000);
|
||||
this.inputStream = new TimeoutInputStream(inputStream,
|
||||
((inputStream instanceof FileInputStream) ? 0 : 2000));
|
||||
}
|
||||
if (type == DeviceType.XTERM) {
|
||||
this.input = new InputStreamReader(new BufferedInputStream(
|
||||
|
@ -743,7 +745,14 @@ public class ECMA48 implements Runnable {
|
|||
} catch (InterruptedException e) {
|
||||
// SQUASH
|
||||
}
|
||||
continue;
|
||||
|
||||
if (inputStream.getStream() instanceof FileInputStream) {
|
||||
// Special case: force a read of files in order
|
||||
// to see the EOF.
|
||||
} else {
|
||||
// Go back to waiting.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int rc = -1;
|
||||
|
@ -756,6 +765,7 @@ public class ECMA48 implements Runnable {
|
|||
readBuffer.length);
|
||||
}
|
||||
} catch (ReadTimeoutException e) {
|
||||
// System.err.println("ReadTimeoutException");
|
||||
rc = 0;
|
||||
}
|
||||
|
||||
|
@ -803,13 +813,14 @@ public class ECMA48 implements Runnable {
|
|||
}
|
||||
// System.err.println("end while loop"); System.err.flush();
|
||||
} catch (IOException e) {
|
||||
// System.err.println("IOException");
|
||||
done = true;
|
||||
|
||||
// This is an unusual case. We want to see the stack trace,
|
||||
// but it is related to the spawned process rather than the
|
||||
// actual UI. We will generate the stack trace, and consume
|
||||
// it as though it was emitted by the shell.
|
||||
CharArrayWriter writer= new CharArrayWriter();
|
||||
CharArrayWriter writer = new CharArrayWriter();
|
||||
// Send a ST and RIS to clear the emulator state.
|
||||
try {
|
||||
writer.write("\033\\\033c");
|
||||
|
@ -1243,16 +1254,27 @@ public class ECMA48 implements Runnable {
|
|||
if (scrollRegionTop >= scrollRegionBottom) {
|
||||
scrollRegionTop = 0;
|
||||
}
|
||||
currentState.cursorY += delta;
|
||||
savedState.cursorY += delta;
|
||||
if (currentState.cursorY < 0) {
|
||||
currentState.cursorY = 0;
|
||||
}
|
||||
if (currentState.cursorY >= height) {
|
||||
currentState.cursorY = height - 1;
|
||||
}
|
||||
if (savedState.cursorY < 0) {
|
||||
savedState.cursorY = 0;
|
||||
}
|
||||
if (savedState.cursorY >= height) {
|
||||
savedState.cursorY = height - 1;
|
||||
}
|
||||
while (display.size() < height) {
|
||||
DisplayLine line = new DisplayLine(currentState.attr);
|
||||
line.setReverseColor(reverseVideo);
|
||||
display.add(line);
|
||||
if (scrollback.size() == 0) {
|
||||
DisplayLine line = new DisplayLine(currentState.attr);
|
||||
line.setReverseColor(reverseVideo);
|
||||
scrollback.add(0, line);
|
||||
}
|
||||
display.add(0, scrollback.remove(scrollback.size() - 1));
|
||||
}
|
||||
while (display.size() > height) {
|
||||
appendScrollbackLine(display.remove(0));
|
||||
|
|
Loading…
Reference in a new issue