package com.googlecode.lanterna.screen; import com.googlecode.lanterna.*; import com.googlecode.lanterna.graphics.TextGraphics; import com.googlecode.lanterna.input.KeyStroke; import com.googlecode.lanterna.input.KeyType; import java.io.IOException; /** * VirtualScreen wraps a normal screen and presents it as a screen that has a configurable minimum size; if the real * screen is smaller than this size, the presented screen will add scrolling to get around it. To anyone using this * class, it will appear and behave just as a normal screen. Scrolling is done by using CTRL + arrow keys. *

* The use case for this class is to allow you to set a minimum size that you can count on be honored, no matter how * small the user makes the terminal. This should make programming GUIs easier. * @author Martin */ public class VirtualScreen extends AbstractScreen { private final Screen realScreen; private final FrameRenderer frameRenderer; private TerminalSize minimumSize; private TerminalPosition viewportTopLeft; private TerminalSize viewportSize; /** * Creates a new VirtualScreen that wraps a supplied Screen. The screen passed in here should be the real screen * that is created on top of the real {@code Terminal}, it will have the correct size and content for what's * actually displayed to the user, but this class will present everything as one view with a fixed minimum size, * no matter what size the real terminal has. *

* The initial minimum size will be the current size of the screen. * @param screen Real screen that will be used when drawing the whole or partial virtual screen */ public VirtualScreen(Screen screen) { super(screen.getTerminalSize()); this.frameRenderer = new DefaultFrameRenderer(); this.realScreen = screen; this.minimumSize = screen.getTerminalSize(); this.viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER; this.viewportSize = minimumSize; } /** * Sets the minimum size we want the virtual screen to have. If the user resizes the real terminal to something * smaller than this, the virtual screen will refuse to make it smaller and add scrollbars to the view. * @param minimumSize Minimum size we want the screen to have */ public void setMinimumSize(TerminalSize minimumSize) { this.minimumSize = minimumSize; TerminalSize virtualSize = minimumSize.max(realScreen.getTerminalSize()); if(!minimumSize.equals(virtualSize)) { addResizeRequest(virtualSize); super.doResizeIfNecessary(); } calculateViewport(realScreen.getTerminalSize()); } /** * Returns the minimum size this virtual screen can have. If the real terminal is made smaller than this, the * virtual screen will draw scrollbars and implement scrolling * @return Minimum size configured for this virtual screen */ public TerminalSize getMinimumSize() { return minimumSize; } @Override public void startScreen() throws IOException { realScreen.startScreen(); } @Override public void stopScreen() throws IOException { realScreen.stopScreen(); } @Override public TextCharacter getFrontCharacter(TerminalPosition position) { return null; } @Override public void setCursorPosition(TerminalPosition position) { super.setCursorPosition(position); if(position == null) { realScreen.setCursorPosition(null); return; } position = position.withRelativeColumn(-viewportTopLeft.getColumn()).withRelativeRow(-viewportTopLeft.getRow()); if(position.getColumn() >= 0 && position.getColumn() < viewportSize.getColumns() && position.getRow() >= 0 && position.getRow() < viewportSize.getRows()) { realScreen.setCursorPosition(position); } else { realScreen.setCursorPosition(null); } } @Override public synchronized TerminalSize doResizeIfNecessary() { TerminalSize underlyingSize = realScreen.doResizeIfNecessary(); if(underlyingSize == null) { return null; } TerminalSize newVirtualSize = calculateViewport(underlyingSize); if(!getTerminalSize().equals(newVirtualSize)) { addResizeRequest(newVirtualSize); return super.doResizeIfNecessary(); } return newVirtualSize; } private TerminalSize calculateViewport(TerminalSize realTerminalSize) { TerminalSize newVirtualSize = minimumSize.max(realTerminalSize); if(newVirtualSize.equals(realTerminalSize)) { viewportSize = realTerminalSize; viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER; } else { TerminalSize newViewportSize = frameRenderer.getViewportSize(realTerminalSize, newVirtualSize); if(newViewportSize.getRows() > viewportSize.getRows()) { viewportTopLeft = viewportTopLeft.withRow(Math.max(0, viewportTopLeft.getRow() - (newViewportSize.getRows() - viewportSize.getRows()))); } if(newViewportSize.getColumns() > viewportSize.getColumns()) { viewportTopLeft = viewportTopLeft.withColumn(Math.max(0, viewportTopLeft.getColumn() - (newViewportSize.getColumns() - viewportSize.getColumns()))); } viewportSize = newViewportSize; } return newVirtualSize; } @Override public void refresh(RefreshType refreshType) throws IOException { setCursorPosition(getCursorPosition()); //Make sure the cursor is at the correct position if(!viewportSize.equals(realScreen.getTerminalSize())) { frameRenderer.drawFrame( realScreen.newTextGraphics(), realScreen.getTerminalSize(), getTerminalSize(), viewportTopLeft); } //Copy the rows TerminalPosition viewportOffset = frameRenderer.getViewportOffset(); if(realScreen instanceof AbstractScreen) { AbstractScreen asAbstractScreen = (AbstractScreen)realScreen; getBackBuffer().copyTo( asAbstractScreen.getBackBuffer(), viewportTopLeft.getRow(), viewportSize.getRows(), viewportTopLeft.getColumn(), viewportSize.getColumns(), viewportOffset.getRow(), viewportOffset.getColumn()); } else { for(int y = 0; y < viewportSize.getRows(); y++) { for(int x = 0; x < viewportSize.getColumns(); x++) { realScreen.setCharacter( x + viewportOffset.getColumn(), y + viewportOffset.getRow(), getBackBuffer().getCharacterAt( x + viewportTopLeft.getColumn(), y + viewportTopLeft.getRow())); } } } realScreen.refresh(refreshType); } @Override public KeyStroke pollInput() throws IOException { return filter(realScreen.pollInput()); } @Override public KeyStroke readInput() throws IOException { return filter(realScreen.readInput()); } private KeyStroke filter(KeyStroke keyStroke) throws IOException { if(keyStroke == null) { return null; } else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowLeft) { if(viewportTopLeft.getColumn() > 0) { viewportTopLeft = viewportTopLeft.withRelativeColumn(-1); refresh(); return null; } } else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowRight) { if(viewportTopLeft.getColumn() + viewportSize.getColumns() < getTerminalSize().getColumns()) { viewportTopLeft = viewportTopLeft.withRelativeColumn(1); refresh(); return null; } } else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowUp) { if(viewportTopLeft.getRow() > 0) { viewportTopLeft = viewportTopLeft.withRelativeRow(-1); realScreen.scrollLines(0,viewportSize.getRows()-1,-1); refresh(); return null; } } else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowDown) { if(viewportTopLeft.getRow() + viewportSize.getRows() < getTerminalSize().getRows()) { viewportTopLeft = viewportTopLeft.withRelativeRow(1); realScreen.scrollLines(0,viewportSize.getRows()-1,1); refresh(); return null; } } return keyStroke; } @Override public void scrollLines(int firstLine, int lastLine, int distance) { // do base class stuff (scroll own back buffer) super.scrollLines(firstLine, lastLine, distance); // vertical range visible in realScreen: int vpFirst = viewportTopLeft.getRow(), vpRows = viewportSize.getRows(); // adapt to realScreen range: firstLine = Math.max(0, firstLine - vpFirst); lastLine = Math.min(vpRows - 1, lastLine - vpFirst); // if resulting range non-empty: scroll that range in realScreen: if (firstLine <= lastLine) { realScreen.scrollLines(firstLine, lastLine, distance); } } /** * Interface for rendering the virtual screen's frame when the real terminal is too small for the virtual screen */ public interface FrameRenderer { /** * Given the size of the real terminal and the current size of the virtual screen, how large should the viewport * where the screen content is drawn be? * @param realSize Size of the real terminal * @param virtualSize Size of the virtual screen * @return Size of the viewport, according to this FrameRenderer */ TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize); /** * Where in the virtual screen should the top-left position of the viewport be? To draw the viewport from the * top-left position of the screen, return 0x0 (or TerminalPosition.TOP_LEFT_CORNER) here. * @return Position of the top-left corner of the viewport inside the screen */ TerminalPosition getViewportOffset(); /** * Drawn the 'frame', meaning anything that is outside the viewport (title, scrollbar, etc) * @param graphics Graphics to use to text drawing operations * @param realSize Size of the real terminal * @param virtualSize Size of the virtual screen * @param virtualScrollPosition If the virtual screen is larger than the real terminal, this is the current * scroll offset the VirtualScreen is using */ void drawFrame( TextGraphics graphics, TerminalSize realSize, TerminalSize virtualSize, TerminalPosition virtualScrollPosition); } private static class DefaultFrameRenderer implements FrameRenderer { @Override public TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize) { if(realSize.getColumns() > 1 && realSize.getRows() > 2) { return realSize.withRelativeColumns(-1).withRelativeRows(-2); } else { return realSize; } } @Override public TerminalPosition getViewportOffset() { return TerminalPosition.TOP_LEFT_CORNER; } @Override public void drawFrame( TextGraphics graphics, TerminalSize realSize, TerminalSize virtualSize, TerminalPosition virtualScrollPosition) { if(realSize.getColumns() == 1 || realSize.getRows() <= 2) { return; } TerminalSize viewportSize = getViewportSize(realSize, virtualSize); graphics.setForegroundColor(TextColor.ANSI.WHITE); graphics.setBackgroundColor(TextColor.ANSI.BLACK); graphics.fill(' '); graphics.putString(0, graphics.getSize().getRows() - 1, "Terminal too small, use ALT+arrows to scroll"); int horizontalSize = (int)(((double)(viewportSize.getColumns()) / (double)virtualSize.getColumns()) * (viewportSize.getColumns())); int scrollable = viewportSize.getColumns() - horizontalSize - 1; int horizontalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getColumn() / (double)(virtualSize.getColumns() - viewportSize.getColumns()))); graphics.drawLine( new TerminalPosition(horizontalPosition, graphics.getSize().getRows() - 2), new TerminalPosition(horizontalPosition + horizontalSize, graphics.getSize().getRows() - 2), Symbols.BLOCK_MIDDLE); int verticalSize = (int)(((double)(viewportSize.getRows()) / (double)virtualSize.getRows()) * (viewportSize.getRows())); scrollable = viewportSize.getRows() - verticalSize - 1; int verticalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getRow() / (double)(virtualSize.getRows() - viewportSize.getRows()))); graphics.drawLine( new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition), new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition + verticalSize), Symbols.BLOCK_MIDDLE); } } }