X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Ftterminal%2FECMA48.java;h=5ff3518f3e3fb29785d55580e9377ba2855bb1fe;hb=34bb6e525628111b87a36556bbdd2719c6d7925f;hp=c277795d69450b2db44f79d77a1caa8062c11275;hpb=955c55b766a8aebb528d5af5f7582a857c72e2f5;p=fanfix.git diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index c277795..5ff3518 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -30,7 +30,9 @@ package jexer.tterminal; import java.awt.Graphics2D; import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; import java.io.CharArrayWriter; import java.io.InputStream; import java.io.InputStreamReader; @@ -45,6 +47,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import javax.imageio.ImageIO; import jexer.TKeypress; import jexer.backend.GlyphMaker; @@ -256,7 +259,7 @@ public class ECMA48 implements Runnable { /** * The type of emulator to be. */ - private DeviceType type = DeviceType.VT102; + private final DeviceType type; /** * The scrollback buffer characters + attributes. @@ -323,29 +326,29 @@ public class ECMA48 implements Runnable { * Physical display width. We start at 80x24, but the user can resize us * bigger/smaller. */ - private int width; + private int width = 80; /** * Physical display height. We start at 80x24, but the user can resize * us bigger/smaller. */ - private int height; + private int height = 24; /** * Top margin of the scrolling region. */ - private int scrollRegionTop; + private int scrollRegionTop = 0; /** * Bottom margin of the scrolling region. */ - private int scrollRegionBottom; + private int scrollRegionBottom = height - 1; /** * Right margin column number. This can be selected by the remote side * to be 80/132 (rightMargin values 79/131), or it can be (width - 1). */ - private int rightMargin; + private int rightMargin = 79; /** * Last character printed. @@ -357,7 +360,7 @@ public class ECMA48 implements Runnable { * 132), but the line does NOT wrap until another character is written to * column 1 of the next line, after which the cursor moves to column 2. */ - private boolean wrapLineFlag; + private boolean wrapLineFlag = false; /** * VT220 single shift flag. @@ -394,7 +397,7 @@ public class ECMA48 implements Runnable { /** * Non-csi collect buffer. */ - private StringBuilder collectBuffer; + private StringBuilder collectBuffer = new StringBuilder(128); /** * When true, use the G1 character set. @@ -469,7 +472,12 @@ public class ECMA48 implements Runnable { /** * Sixel collection buffer. */ - private StringBuilder sixelParseBuffer; + private StringBuilder sixelParseBuffer = new StringBuilder(2048); + + /** + * Sixel shared palette. + */ + private HashMap sixelPalette; /** * The width of a character cell in pixels. @@ -650,7 +658,8 @@ public class ECMA48 implements Runnable { this.inputStream = new TimeoutInputStream(inputStream, 2000); } if (type == DeviceType.XTERM) { - this.input = new InputStreamReader(this.inputStream, "UTF-8"); + this.input = new InputStreamReader(new BufferedInputStream( + this.inputStream, 1024 * 128), "UTF-8"); this.output = new OutputStreamWriter(new BufferedOutputStream(outputStream), "UTF-8"); this.outputStream = null; @@ -664,6 +673,8 @@ public class ECMA48 implements Runnable { for (int i = 0; i < height; i++) { display.add(new DisplayLine(currentState.attr)); } + assert (currentState.cursorY < height); + assert (currentState.cursorX < width); // Spin up the input reader readerThread = new Thread(this); @@ -755,11 +766,28 @@ public class ECMA48 implements Runnable { int ch = Character.codePointAt(readBufferUTF8, i); i += Character.charCount(ch); - consume(ch); + + // Special case for VT10x: 7-bit characters + // only. + if ((type == DeviceType.VT100) + || (type == DeviceType.VT102) + ) { + consume(ch & 0x7F); + } else { + consume(ch); + } } } else { for (int i = 0; i < rc; i++) { - consume(readBuffer[i]); + // Special case for VT10x: 7-bit characters + // only. + if ((type == DeviceType.VT100) + || (type == DeviceType.VT102) + ) { + consume(readBuffer[i] & 0x7F); + } else { + consume(readBuffer[i]); + } } } } @@ -869,12 +897,14 @@ public class ECMA48 implements Runnable { case VT220: case XTERM: - // "I am a VT220" - 7 bit version + // "I am a VT220" - 7 bit version, with sixel and Jexer image + // support. if (!s8c1t) { - return "\033[?62;1;6;9;4c"; + return "\033[?62;1;6;9;4;22;444c"; } - // "I am a VT220" - 8 bit version - return "\u009b?62;1;6;9;4c"; + // "I am a VT220" - 8 bit version, with sixel and Jexer image + // support. + return "\u009b?62;1;6;9;4;22;444c"; default: throw new IllegalArgumentException("Invalid device type: " + type); } @@ -1078,6 +1108,64 @@ public class ECMA48 implements Runnable { return display; } + /** + * Get the visible display + scrollback buffer, offset by a specified + * number of rows from the bottom. + * + * @param visibleHeight the total height of the display to show + * @param scrollBottom the number of rows from the bottom to scroll back + * @return a copy of the display + scrollback buffers + */ + public final List getVisibleDisplay(final int visibleHeight, + final int scrollBottom) { + + assert (visibleHeight >= 0); + assert (scrollBottom >= 0); + + int visibleBottom = scrollback.size() + display.size() - scrollBottom; + + List preceedingBlankLines = new ArrayList(); + int visibleTop = visibleBottom - visibleHeight; + if (visibleTop < 0) { + for (int i = visibleTop; i < 0; i++) { + preceedingBlankLines.add(getBlankDisplayLine()); + } + visibleTop = 0; + } + assert (visibleTop >= 0); + + List displayLines = new ArrayList(); + displayLines.addAll(scrollback); + displayLines.addAll(display); + + List visibleLines = new ArrayList(); + visibleLines.addAll(preceedingBlankLines); + visibleLines.addAll(displayLines.subList(visibleTop, visibleBottom)); + + // Fill in the blank lines on bottom + int bottomBlankLines = visibleHeight - visibleLines.size(); + assert (bottomBlankLines >= 0); + for (int i = 0; i < bottomBlankLines; i++) { + visibleLines.add(getBlankDisplayLine()); + } + + return copyBuffer(visibleLines); + } + + /** + * Copy a display buffer. + * + * @param buffer the buffer to copy + * @return a deep copy of the buffer's data + */ + private List copyBuffer(final List buffer) { + ArrayList result = new ArrayList(buffer.size()); + for (DisplayLine line: buffer) { + result.add(new DisplayLine(line)); + } + return result; + } + /** * Get the display width. * @@ -1121,8 +1209,8 @@ public class ECMA48 implements Runnable { int delta = height - this.height; this.height = height; scrollRegionBottom += delta; - if (scrollRegionBottom < 0) { - scrollRegionBottom = height; + if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) { + scrollRegionBottom = height - 1; } if (scrollRegionTop >= scrollRegionBottom) { scrollRegionTop = 0; @@ -1177,7 +1265,7 @@ public class ECMA48 implements Runnable { */ private void toGround() { csiParams.clear(); - collectBuffer = new StringBuilder(8); + collectBuffer.setLength(0); scanState = ScanState.GROUND; } @@ -1292,8 +1380,13 @@ public class ECMA48 implements Runnable { currentState = new SaveableState(); savedState = new SaveableState(); scanState = ScanState.GROUND; - width = 80; - height = 24; + if (displayListener != null) { + width = displayListener.getDisplayWidth(); + height = displayListener.getDisplayHeight(); + } else { + width = 80; + height = 24; + } scrollRegionTop = 0; scrollRegionBottom = height - 1; rightMargin = width - 1; @@ -1301,11 +1394,6 @@ public class ECMA48 implements Runnable { arrowKeyMode = ArrowKeyMode.ANSI; keypadMode = KeypadMode.Numeric; wrapLineFlag = false; - if (displayListener != null) { - width = displayListener.getDisplayWidth(); - height = displayListener.getDisplayHeight(); - rightMargin = width - 1; - } // Flags shiftOut = false; @@ -1388,7 +1476,6 @@ public class ECMA48 implements Runnable { * Handle a linefeed. */ private void linefeed() { - if (currentState.cursorY < scrollRegionBottom) { // Increment screen y currentState.cursorY++; @@ -1748,6 +1835,9 @@ public class ECMA48 implements Runnable { // Local echo for everything else printCharacter(keypress.getChar()); } + if (displayListener != null) { + displayListener.displayChanged(); + } } if ((newLineMode == true) && (keypress.equals(kbEnter))) { @@ -2816,6 +2906,7 @@ public class ECMA48 implements Runnable { */ private void setToggle(final boolean value) { boolean decPrivateModeFlag = false; + for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; @@ -3082,6 +3173,21 @@ public class ECMA48 implements Runnable { break; + case 80: + if (type == DeviceType.XTERM) { + if (decPrivateModeFlag == true) { + if (value == true) { + // Enable sixel scrolling (default). + // TODO + } else { + // Disable sixel scrolling. + // TODO + } + } + } + + break; + case 1000: if ((type == DeviceType.XTERM) && (decPrivateModeFlag == true) @@ -3147,6 +3253,22 @@ public class ECMA48 implements Runnable { } break; + case 1070: + if (type == DeviceType.XTERM) { + if (decPrivateModeFlag == true) { + if (value == true) { + // Use private color registers for each sixel + // graphic (default). + sixelPalette = null; + } else { + // Use shared color registers for each sixel + // graphic. + sixelPalette = new HashMap(); + } + } + } + break; + default: break; @@ -4279,6 +4401,9 @@ public class ECMA48 implements Runnable { // DECSTBM int top = getCsiParam(0, 1, 1, height) - 1; int bottom = getCsiParam(1, height, 1, height) - 1; + if (bottom > height - 1) { + bottom = height - 1; + } if (top > bottom) { top = bottom; @@ -4665,6 +4790,45 @@ public class ECMA48 implements Runnable { } } } + + if (p[0].equals("10")) { + if (p[1].equals("?")) { + // Respond with foreground color. + java.awt.Color color = jexer.backend.SwingTerminal.attrToForegroundColor(currentState.attr); + + writeRemote(String.format( + "\033]10;rgb:%04x/%04x/%04x\033\\", + color.getRed() << 8, + color.getGreen() << 8, + color.getBlue() << 8)); + } + } + + if (p[0].equals("11")) { + if (p[1].equals("?")) { + // Respond with background color. + java.awt.Color color = jexer.backend.SwingTerminal.attrToBackgroundColor(currentState.attr); + + writeRemote(String.format( + "\033]11;rgb:%04x/%04x/%04x\033\\", + color.getRed() << 8, + color.getGreen() << 8, + color.getBlue() << 8)); + } + } + + if (p[0].equals("444")) { + if (p[1].equals("0") && (p.length == 6)) { + // Jexer image - RGB + parseJexerImageRGB(p[2], p[3], p[4], p[5]); + } else if (p[1].equals("1") && (p.length == 4)) { + // Jexer image - PNG + parseJexerImageFile(1, p[2], p[3]); + } else if (p[1].equals("2") && (p.length == 4)) { + // Jexer image - JPG + parseJexerImageFile(2, p[2], p[3]); + } + } } // Go to SCAN_GROUND state @@ -4745,21 +4909,39 @@ public class ECMA48 implements Runnable { } } + /** + * Respond to xterm sixel query. + */ + private void xtermSixelQuery() { + int item = getCsiParam(0, 0); + int action = getCsiParam(1, 0); + int value = getCsiParam(2, 0); + + switch (item) { + case 1: + if (action == 1) { + // Report number of color registers. + writeRemote(String.format("\033[?%d;%d;%dS", item, 0, 1024)); + return; + } + break; + default: + break; + } + // We will not support this option. + writeRemote(String.format("\033[?%d;%dS", item, action)); + } + /** * Run this input character through the ECMA48 state machine. * * @param ch character from the remote side */ - private void consume(int ch) { + private void consume(final int ch) { // DEBUG // System.err.printf("%c STATE = %s\n", ch, scanState); - // Special case for VT10x: 7-bit characters only - if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) { - ch = (ch & 0x7F); - } - // Special "anywhere" states // 18, 1A --> execute, then switch to SCAN_GROUND @@ -5908,7 +6090,18 @@ public class ECMA48 implements Runnable { case 'S': // Scroll up X lines (default 1) if (type == DeviceType.XTERM) { - su(); + boolean xtermPrivateModeFlag = false; + for (int i = 0; i < collectBuffer.length(); i++) { + if (collectBuffer.charAt(i) == '?') { + xtermPrivateModeFlag = true; + break; + } + } + if (xtermPrivateModeFlag) { + xtermSixelQuery(); + } else { + su(); + } } break; case 'T': @@ -6182,7 +6375,18 @@ public class ECMA48 implements Runnable { case 'S': // Scroll up X lines (default 1) if (type == DeviceType.XTERM) { - su(); + boolean xtermPrivateModeFlag = false; + for (int i = 0; i < collectBuffer.length(); i++) { + if (collectBuffer.charAt(i) == '?') { + xtermPrivateModeFlag = true; + break; + } + } + if (xtermPrivateModeFlag) { + xtermSixelQuery(); + } else { + su(); + } } break; case 'T': @@ -6510,7 +6714,7 @@ public class ECMA48 implements Runnable { // 0x71 goes to DCS_SIXEL if (ch == 0x71) { - sixelParseBuffer = new StringBuilder(); + sixelParseBuffer.setLength(0); scanState = ScanState.DCS_SIXEL; } else if ((ch >= 0x40) && (ch <= 0x7E)) { // 0x40-7E goes to DCS_PASSTHROUGH @@ -6595,7 +6799,7 @@ public class ECMA48 implements Runnable { // 0x71 goes to DCS_SIXEL if (ch == 0x71) { - sixelParseBuffer = new StringBuilder(); + sixelParseBuffer.setLength(0); scanState = ScanState.DCS_SIXEL; } else if ((ch >= 0x40) && (ch <= 0x7E)) { // 0x40-7E goes to DCS_PASSTHROUGH @@ -6622,17 +6826,20 @@ public class ECMA48 implements Runnable { } // 00-17, 19, 1C-1F, 20-7E --> put - // TODO if (ch <= 0x17) { + // We ignore all DCS except sixel. return; } if (ch == 0x19) { + // We ignore all DCS except sixel. return; } if ((ch >= 0x1C) && (ch <= 0x1F)) { + // We ignore all DCS except sixel. return; } if ((ch >= 0x20) && (ch <= 0x7E)) { + // We ignore all DCS except sixel. return; } @@ -6655,11 +6862,13 @@ public class ECMA48 implements Runnable { if (ch == 0x9C) { parseSixel(); toGround(); + return; } // 0x1B 0x5C goes to GROUND if (ch == 0x1B) { collect((char) ch); + return; } if (ch == 0x5C) { if ((collectBuffer.length() > 0) @@ -6667,29 +6876,20 @@ public class ECMA48 implements Runnable { ) { parseSixel(); toGround(); + return; } } // 00-17, 19, 1C-1F, 20-7E --> put - if (ch <= 0x17) { - sixelParseBuffer.append((char) ch); - return; - } - if (ch == 0x19) { - sixelParseBuffer.append((char) ch); - return; - } - if ((ch >= 0x1C) && (ch <= 0x1F)) { - sixelParseBuffer.append((char) ch); - return; - } - if ((ch >= 0x20) && (ch <= 0x7E)) { + if ((ch <= 0x17) + || (ch == 0x19) + || ((ch >= 0x1C) && (ch <= 0x1F)) + || ((ch >= 0x20) && (ch <= 0x7E)) + ) { sixelParseBuffer.append((char) ch); - return; } // 7F --> ignore - return; case SOSPMAPC_STRING: @@ -6787,9 +6987,43 @@ public class ECMA48 implements Runnable { return mouseProtocol; } - // ------------------------------------------------------------------------ - // Sixel support ---------------------------------------------------------- - // ------------------------------------------------------------------------ + /** + * Draw the left and right cells of a two-cell-wide (full-width) glyph. + * + * @param leftX the x position to draw the left half to + * @param leftY the y position to draw the left half to + * @param rightX the x position to draw the right half to + * @param rightY the y position to draw the right half to + * @param ch the character to draw + */ + private void drawHalves(final int leftX, final int leftY, + final int rightX, final int rightY, final int ch) { + + // System.err.println("drawHalves(): " + Integer.toHexString(ch)); + + if (lastTextHeight != textHeight) { + glyphMaker = GlyphMaker.getInstance(textHeight); + lastTextHeight = textHeight; + } + + Cell cell = new Cell(ch, currentState.attr); + BufferedImage image = glyphMaker.getImage(cell, textWidth * 2, + textHeight); + BufferedImage leftImage = image.getSubimage(0, 0, textWidth, + textHeight); + BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth, + textHeight); + + Cell left = new Cell(cell); + left.setImage(leftImage); + left.setWidth(Cell.Width.LEFT); + display.get(leftY).replace(leftX, left); + + Cell right = new Cell(cell); + right.setImage(rightImage); + right.setWidth(Cell.Width.RIGHT); + display.get(rightY).replace(rightX, right); + } /** * Set the width of a character cell in pixels. @@ -6818,9 +7052,9 @@ public class ECMA48 implements Runnable { /* System.err.println("parseSixel(): '" + sixelParseBuffer.toString() + "'"); - */ + */ - Sixel sixel = new Sixel(sixelParseBuffer.toString()); + Sixel sixel = new Sixel(sixelParseBuffer.toString(), sixelPalette); BufferedImage image = sixel.getImage(); // System.err.println("parseSixel(): image " + image); @@ -6829,6 +7063,167 @@ public class ECMA48 implements Runnable { // Sixel data was malformed in some way, bail out. return; } + if ((image.getWidth() < 1) + || (image.getWidth() > 10000) + || (image.getHeight() < 1) + || (image.getHeight() > 10000) + ) { + return; + } + + imageToCells(image, true); + } + + /** + * Parse a "Jexer" RGB image string into a bitmap image, and overlay that + * image onto the text cells. + * + * @param pw width token + * @param ph height token + * @param ps scroll token + * @param data pixel data + */ + private void parseJexerImageRGB(final String pw, final String ph, + final String ps, final String data) { + + int imageWidth = 0; + int imageHeight = 0; + boolean scroll = false; + try { + imageWidth = Integer.parseInt(pw); + imageHeight = Integer.parseInt(ph); + } catch (NumberFormatException e) { + // SQUASH + return; + } + if ((imageWidth < 1) + || (imageWidth > 10000) + || (imageHeight < 1) + || (imageHeight > 10000) + ) { + return; + } + if (ps.equals("1")) { + scroll = true; + } else if (ps.equals("0")) { + scroll = false; + } else { + return; + } + + byte [] bytes = StringUtils.fromBase64(data.getBytes()); + if (bytes.length != (imageWidth * imageHeight * 3)) { + return; + } + + BufferedImage image = new BufferedImage(imageWidth, imageHeight, + BufferedImage.TYPE_INT_ARGB); + + for (int x = 0; x < imageWidth; x++) { + for (int y = 0; y < imageHeight; y++) { + int red = bytes[(y * imageWidth * 3) + (x * 3) ]; + if (red < 0) { + red += 256; + } + int green = bytes[(y * imageWidth * 3) + (x * 3) + 1]; + if (green < 0) { + green += 256; + } + int blue = bytes[(y * imageWidth * 3) + (x * 3) + 2]; + if (blue < 0) { + blue += 256; + } + int rgb = 0xFF000000 | (red << 16) | (green << 8) | blue; + image.setRGB(x, y, rgb); + } + } + + imageToCells(image, scroll); + } + + /** + * Parse a "Jexer" PNG or JPG image string into a bitmap image, and + * overlay that image onto the text cells. + * + * @param type 1 for PNG, 2 for JPG + * @param ps scroll token + * @param data pixel data + */ + private void parseJexerImageFile(final int type, final String ps, + final String data) { + + int imageWidth = 0; + int imageHeight = 0; + boolean scroll = false; + BufferedImage image = null; + try { + byte [] bytes = StringUtils.fromBase64(data.getBytes()); + + switch (type) { + case 1: + if ((bytes[0] != (byte) 0x89) + || (bytes[1] != 'P') + || (bytes[2] != 'N') + || (bytes[3] != 'G') + || (bytes[4] != (byte) 0x0D) + || (bytes[5] != (byte) 0x0A) + || (bytes[6] != (byte) 0x1A) + || (bytes[7] != (byte) 0x0A) + ) { + // File does not have PNG header, bail out. + return; + } + break; + + case 2: + if ((bytes[0] != (byte) 0XFF) + || (bytes[1] != (byte) 0xD8) + || (bytes[2] != (byte) 0xFF) + ) { + // File does not have JPG header, bail out. + return; + } + break; + + default: + // Unsupported type, bail out. + return; + } + + image = ImageIO.read(new ByteArrayInputStream(bytes)); + } catch (IOException e) { + // SQUASH + return; + } + assert (image != null); + imageWidth = image.getWidth(); + imageHeight = image.getHeight(); + if ((imageWidth < 1) + || (imageWidth > 10000) + || (imageHeight < 1) + || (imageHeight > 10000) + ) { + return; + } + if (ps.equals("1")) { + scroll = true; + } else if (ps.equals("0")) { + scroll = false; + } else { + return; + } + + imageToCells(image, scroll); + } + + /** + * Break up an image into the cells at the current cursor. + * + * @param image the image to display + * @param scroll if true, scroll the image and move the cursor + */ + private void imageToCells(final BufferedImage image, final boolean scroll) { + assert (image != null); /* * Procedure: @@ -6885,64 +7280,42 @@ public class ECMA48 implements Runnable { } int x0 = currentState.cursorX; + int y0 = currentState.cursorY; for (int y = 0; y < cellRows; y++) { for (int x = 0; x < cellColumns; x++) { - printCharacter(' '); - cursorLeft(1, false); - if ((x == cellColumns - 1) || (y == cellRows - 1)) { - // TODO: render text of current cell first, then image - // over it. For now, just copy the cell. - DisplayLine line = display.get(currentState.cursorY); - line.replace(currentState.cursorX, cells[x][y]); - } else { - // Copy the image cell into the display. - DisplayLine line = display.get(currentState.cursorY); - line.replace(currentState.cursorX, cells[x][y]); + assert (currentState.cursorX <= rightMargin); + + // TODO: Render text of current cell first, then image over + // it (accounting for blank pixels). For now, just copy the + // cell. + DisplayLine line = display.get(currentState.cursorY); + line.replace(currentState.cursorX, cells[x][y]); + + // If at the end of the visible screen, stop. + if (currentState.cursorX == rightMargin) { + break; } - cursorRight(1, false); + // Room for more image on the visible screen. + currentState.cursorX++; } - linefeed(); + if (currentState.cursorY < scrollRegionBottom - 1) { + // Not at the bottom, down a line. + linefeed(); + } else if (scroll == true) { + // At the bottom, scroll as needed. + linefeed(); + } else { + // At the bottom, no more scrolling, done. + break; + } + cursorPosition(currentState.cursorY, x0); } - } - - /** - * Draw the left and right cells of a two-cell-wide (full-width) glyph. - * - * @param leftX the x position to draw the left half to - * @param leftY the y position to draw the left half to - * @param rightX the x position to draw the right half to - * @param rightY the y position to draw the right half to - * @param ch the character to draw - */ - private void drawHalves(final int leftX, final int leftY, - final int rightX, final int rightY, final int ch) { - - // System.err.println("drawHalves(): " + Integer.toHexString(ch)); - - if (lastTextHeight != textHeight) { - glyphMaker = GlyphMaker.getInstance(textHeight); - lastTextHeight = textHeight; + if (scroll == false) { + cursorPosition(y0, x0); } - Cell cell = new Cell(ch, currentState.attr); - BufferedImage image = glyphMaker.getImage(cell, textWidth * 2, - textHeight); - BufferedImage leftImage = image.getSubimage(0, 0, textWidth, - textHeight); - BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth, - textHeight); - - Cell left = new Cell(cell); - left.setImage(leftImage); - left.setWidth(Cell.Width.LEFT); - display.get(leftY).replace(leftX, left); - - Cell right = new Cell(cell); - right.setImage(rightImage); - right.setWidth(Cell.Width.RIGHT); - display.get(rightY).replace(rightX, right); } }