| 1 | package com.googlecode.lanterna.screen; |
| 2 | |
| 3 | import com.googlecode.lanterna.*; |
| 4 | import com.googlecode.lanterna.graphics.TextGraphics; |
| 5 | import com.googlecode.lanterna.input.KeyStroke; |
| 6 | import com.googlecode.lanterna.input.KeyType; |
| 7 | |
| 8 | import java.io.IOException; |
| 9 | |
| 10 | /** |
| 11 | * VirtualScreen wraps a normal screen and presents it as a screen that has a configurable minimum size; if the real |
| 12 | * screen is smaller than this size, the presented screen will add scrolling to get around it. To anyone using this |
| 13 | * class, it will appear and behave just as a normal screen. Scrolling is done by using CTRL + arrow keys. |
| 14 | * <p> |
| 15 | * 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 |
| 16 | * small the user makes the terminal. This should make programming GUIs easier. |
| 17 | * @author Martin |
| 18 | */ |
| 19 | public class VirtualScreen extends AbstractScreen { |
| 20 | private final Screen realScreen; |
| 21 | private final FrameRenderer frameRenderer; |
| 22 | private TerminalSize minimumSize; |
| 23 | private TerminalPosition viewportTopLeft; |
| 24 | private TerminalSize viewportSize; |
| 25 | |
| 26 | /** |
| 27 | * Creates a new VirtualScreen that wraps a supplied Screen. The screen passed in here should be the real screen |
| 28 | * that is created on top of the real {@code Terminal}, it will have the correct size and content for what's |
| 29 | * actually displayed to the user, but this class will present everything as one view with a fixed minimum size, |
| 30 | * no matter what size the real terminal has. |
| 31 | * <p> |
| 32 | * The initial minimum size will be the current size of the screen. |
| 33 | * @param screen Real screen that will be used when drawing the whole or partial virtual screen |
| 34 | */ |
| 35 | public VirtualScreen(Screen screen) { |
| 36 | super(screen.getTerminalSize()); |
| 37 | this.frameRenderer = new DefaultFrameRenderer(); |
| 38 | this.realScreen = screen; |
| 39 | this.minimumSize = screen.getTerminalSize(); |
| 40 | this.viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER; |
| 41 | this.viewportSize = minimumSize; |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * Sets the minimum size we want the virtual screen to have. If the user resizes the real terminal to something |
| 46 | * smaller than this, the virtual screen will refuse to make it smaller and add scrollbars to the view. |
| 47 | * @param minimumSize Minimum size we want the screen to have |
| 48 | */ |
| 49 | public void setMinimumSize(TerminalSize minimumSize) { |
| 50 | this.minimumSize = minimumSize; |
| 51 | TerminalSize virtualSize = minimumSize.max(realScreen.getTerminalSize()); |
| 52 | if(!minimumSize.equals(virtualSize)) { |
| 53 | addResizeRequest(virtualSize); |
| 54 | super.doResizeIfNecessary(); |
| 55 | } |
| 56 | calculateViewport(realScreen.getTerminalSize()); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Returns the minimum size this virtual screen can have. If the real terminal is made smaller than this, the |
| 61 | * virtual screen will draw scrollbars and implement scrolling |
| 62 | * @return Minimum size configured for this virtual screen |
| 63 | */ |
| 64 | public TerminalSize getMinimumSize() { |
| 65 | return minimumSize; |
| 66 | } |
| 67 | |
| 68 | @Override |
| 69 | public void startScreen() throws IOException { |
| 70 | realScreen.startScreen(); |
| 71 | } |
| 72 | |
| 73 | @Override |
| 74 | public void stopScreen() throws IOException { |
| 75 | realScreen.stopScreen(); |
| 76 | } |
| 77 | |
| 78 | @Override |
| 79 | public TextCharacter getFrontCharacter(TerminalPosition position) { |
| 80 | return null; |
| 81 | } |
| 82 | |
| 83 | @Override |
| 84 | public void setCursorPosition(TerminalPosition position) { |
| 85 | super.setCursorPosition(position); |
| 86 | if(position == null) { |
| 87 | realScreen.setCursorPosition(null); |
| 88 | return; |
| 89 | } |
| 90 | position = position.withRelativeColumn(-viewportTopLeft.getColumn()).withRelativeRow(-viewportTopLeft.getRow()); |
| 91 | if(position.getColumn() >= 0 && position.getColumn() < viewportSize.getColumns() && |
| 92 | position.getRow() >= 0 && position.getRow() < viewportSize.getRows()) { |
| 93 | realScreen.setCursorPosition(position); |
| 94 | } |
| 95 | else { |
| 96 | realScreen.setCursorPosition(null); |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | @Override |
| 101 | public synchronized TerminalSize doResizeIfNecessary() { |
| 102 | TerminalSize underlyingSize = realScreen.doResizeIfNecessary(); |
| 103 | if(underlyingSize == null) { |
| 104 | return null; |
| 105 | } |
| 106 | |
| 107 | TerminalSize newVirtualSize = calculateViewport(underlyingSize); |
| 108 | if(!getTerminalSize().equals(newVirtualSize)) { |
| 109 | addResizeRequest(newVirtualSize); |
| 110 | return super.doResizeIfNecessary(); |
| 111 | } |
| 112 | return newVirtualSize; |
| 113 | } |
| 114 | |
| 115 | private TerminalSize calculateViewport(TerminalSize realTerminalSize) { |
| 116 | TerminalSize newVirtualSize = minimumSize.max(realTerminalSize); |
| 117 | if(newVirtualSize.equals(realTerminalSize)) { |
| 118 | viewportSize = realTerminalSize; |
| 119 | viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER; |
| 120 | } |
| 121 | else { |
| 122 | TerminalSize newViewportSize = frameRenderer.getViewportSize(realTerminalSize, newVirtualSize); |
| 123 | if(newViewportSize.getRows() > viewportSize.getRows()) { |
| 124 | viewportTopLeft = viewportTopLeft.withRow(Math.max(0, viewportTopLeft.getRow() - (newViewportSize.getRows() - viewportSize.getRows()))); |
| 125 | } |
| 126 | if(newViewportSize.getColumns() > viewportSize.getColumns()) { |
| 127 | viewportTopLeft = viewportTopLeft.withColumn(Math.max(0, viewportTopLeft.getColumn() - (newViewportSize.getColumns() - viewportSize.getColumns()))); |
| 128 | } |
| 129 | viewportSize = newViewportSize; |
| 130 | } |
| 131 | return newVirtualSize; |
| 132 | } |
| 133 | |
| 134 | @Override |
| 135 | public void refresh(RefreshType refreshType) throws IOException { |
| 136 | setCursorPosition(getCursorPosition()); //Make sure the cursor is at the correct position |
| 137 | if(!viewportSize.equals(realScreen.getTerminalSize())) { |
| 138 | frameRenderer.drawFrame( |
| 139 | realScreen.newTextGraphics(), |
| 140 | realScreen.getTerminalSize(), |
| 141 | getTerminalSize(), |
| 142 | viewportTopLeft); |
| 143 | } |
| 144 | |
| 145 | //Copy the rows |
| 146 | TerminalPosition viewportOffset = frameRenderer.getViewportOffset(); |
| 147 | if(realScreen instanceof AbstractScreen) { |
| 148 | AbstractScreen asAbstractScreen = (AbstractScreen)realScreen; |
| 149 | getBackBuffer().copyTo( |
| 150 | asAbstractScreen.getBackBuffer(), |
| 151 | viewportTopLeft.getRow(), |
| 152 | viewportSize.getRows(), |
| 153 | viewportTopLeft.getColumn(), |
| 154 | viewportSize.getColumns(), |
| 155 | viewportOffset.getRow(), |
| 156 | viewportOffset.getColumn()); |
| 157 | } |
| 158 | else { |
| 159 | for(int y = 0; y < viewportSize.getRows(); y++) { |
| 160 | for(int x = 0; x < viewportSize.getColumns(); x++) { |
| 161 | realScreen.setCharacter( |
| 162 | x + viewportOffset.getColumn(), |
| 163 | y + viewportOffset.getRow(), |
| 164 | getBackBuffer().getCharacterAt( |
| 165 | x + viewportTopLeft.getColumn(), |
| 166 | y + viewportTopLeft.getRow())); |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | realScreen.refresh(refreshType); |
| 171 | } |
| 172 | |
| 173 | @Override |
| 174 | public KeyStroke pollInput() throws IOException { |
| 175 | return filter(realScreen.pollInput()); |
| 176 | } |
| 177 | |
| 178 | @Override |
| 179 | public KeyStroke readInput() throws IOException { |
| 180 | return filter(realScreen.readInput()); |
| 181 | } |
| 182 | |
| 183 | private KeyStroke filter(KeyStroke keyStroke) throws IOException { |
| 184 | if(keyStroke == null) { |
| 185 | return null; |
| 186 | } |
| 187 | else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowLeft) { |
| 188 | if(viewportTopLeft.getColumn() > 0) { |
| 189 | viewportTopLeft = viewportTopLeft.withRelativeColumn(-1); |
| 190 | refresh(); |
| 191 | return null; |
| 192 | } |
| 193 | } |
| 194 | else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowRight) { |
| 195 | if(viewportTopLeft.getColumn() + viewportSize.getColumns() < getTerminalSize().getColumns()) { |
| 196 | viewportTopLeft = viewportTopLeft.withRelativeColumn(1); |
| 197 | refresh(); |
| 198 | return null; |
| 199 | } |
| 200 | } |
| 201 | else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowUp) { |
| 202 | if(viewportTopLeft.getRow() > 0) { |
| 203 | viewportTopLeft = viewportTopLeft.withRelativeRow(-1); |
| 204 | realScreen.scrollLines(0,viewportSize.getRows()-1,-1); |
| 205 | refresh(); |
| 206 | return null; |
| 207 | } |
| 208 | } |
| 209 | else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowDown) { |
| 210 | if(viewportTopLeft.getRow() + viewportSize.getRows() < getTerminalSize().getRows()) { |
| 211 | viewportTopLeft = viewportTopLeft.withRelativeRow(1); |
| 212 | realScreen.scrollLines(0,viewportSize.getRows()-1,1); |
| 213 | refresh(); |
| 214 | return null; |
| 215 | } |
| 216 | } |
| 217 | return keyStroke; |
| 218 | } |
| 219 | |
| 220 | @Override |
| 221 | public void scrollLines(int firstLine, int lastLine, int distance) { |
| 222 | // do base class stuff (scroll own back buffer) |
| 223 | super.scrollLines(firstLine, lastLine, distance); |
| 224 | // vertical range visible in realScreen: |
| 225 | int vpFirst = viewportTopLeft.getRow(), |
| 226 | vpRows = viewportSize.getRows(); |
| 227 | // adapt to realScreen range: |
| 228 | firstLine = Math.max(0, firstLine - vpFirst); |
| 229 | lastLine = Math.min(vpRows - 1, lastLine - vpFirst); |
| 230 | // if resulting range non-empty: scroll that range in realScreen: |
| 231 | if (firstLine <= lastLine) { |
| 232 | realScreen.scrollLines(firstLine, lastLine, distance); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Interface for rendering the virtual screen's frame when the real terminal is too small for the virtual screen |
| 238 | */ |
| 239 | public interface FrameRenderer { |
| 240 | /** |
| 241 | * Given the size of the real terminal and the current size of the virtual screen, how large should the viewport |
| 242 | * where the screen content is drawn be? |
| 243 | * @param realSize Size of the real terminal |
| 244 | * @param virtualSize Size of the virtual screen |
| 245 | * @return Size of the viewport, according to this FrameRenderer |
| 246 | */ |
| 247 | TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize); |
| 248 | |
| 249 | /** |
| 250 | * Where in the virtual screen should the top-left position of the viewport be? To draw the viewport from the |
| 251 | * top-left position of the screen, return 0x0 (or TerminalPosition.TOP_LEFT_CORNER) here. |
| 252 | * @return Position of the top-left corner of the viewport inside the screen |
| 253 | */ |
| 254 | TerminalPosition getViewportOffset(); |
| 255 | |
| 256 | /** |
| 257 | * Drawn the 'frame', meaning anything that is outside the viewport (title, scrollbar, etc) |
| 258 | * @param graphics Graphics to use to text drawing operations |
| 259 | * @param realSize Size of the real terminal |
| 260 | * @param virtualSize Size of the virtual screen |
| 261 | * @param virtualScrollPosition If the virtual screen is larger than the real terminal, this is the current |
| 262 | * scroll offset the VirtualScreen is using |
| 263 | */ |
| 264 | void drawFrame( |
| 265 | TextGraphics graphics, |
| 266 | TerminalSize realSize, |
| 267 | TerminalSize virtualSize, |
| 268 | TerminalPosition virtualScrollPosition); |
| 269 | } |
| 270 | |
| 271 | private static class DefaultFrameRenderer implements FrameRenderer { |
| 272 | @Override |
| 273 | public TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize) { |
| 274 | if(realSize.getColumns() > 1 && realSize.getRows() > 2) { |
| 275 | return realSize.withRelativeColumns(-1).withRelativeRows(-2); |
| 276 | } |
| 277 | else { |
| 278 | return realSize; |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | @Override |
| 283 | public TerminalPosition getViewportOffset() { |
| 284 | return TerminalPosition.TOP_LEFT_CORNER; |
| 285 | } |
| 286 | |
| 287 | @Override |
| 288 | public void drawFrame( |
| 289 | TextGraphics graphics, |
| 290 | TerminalSize realSize, |
| 291 | TerminalSize virtualSize, |
| 292 | TerminalPosition virtualScrollPosition) { |
| 293 | |
| 294 | if(realSize.getColumns() == 1 || realSize.getRows() <= 2) { |
| 295 | return; |
| 296 | } |
| 297 | TerminalSize viewportSize = getViewportSize(realSize, virtualSize); |
| 298 | |
| 299 | graphics.setForegroundColor(TextColor.ANSI.WHITE); |
| 300 | graphics.setBackgroundColor(TextColor.ANSI.BLACK); |
| 301 | graphics.fill(' '); |
| 302 | graphics.putString(0, graphics.getSize().getRows() - 1, "Terminal too small, use ALT+arrows to scroll"); |
| 303 | |
| 304 | int horizontalSize = (int)(((double)(viewportSize.getColumns()) / (double)virtualSize.getColumns()) * (viewportSize.getColumns())); |
| 305 | int scrollable = viewportSize.getColumns() - horizontalSize - 1; |
| 306 | int horizontalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getColumn() / (double)(virtualSize.getColumns() - viewportSize.getColumns()))); |
| 307 | graphics.drawLine( |
| 308 | new TerminalPosition(horizontalPosition, graphics.getSize().getRows() - 2), |
| 309 | new TerminalPosition(horizontalPosition + horizontalSize, graphics.getSize().getRows() - 2), |
| 310 | Symbols.BLOCK_MIDDLE); |
| 311 | |
| 312 | int verticalSize = (int)(((double)(viewportSize.getRows()) / (double)virtualSize.getRows()) * (viewportSize.getRows())); |
| 313 | scrollable = viewportSize.getRows() - verticalSize - 1; |
| 314 | int verticalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getRow() / (double)(virtualSize.getRows() - viewportSize.getRows()))); |
| 315 | graphics.drawLine( |
| 316 | new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition), |
| 317 | new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition + verticalSize), |
| 318 | Symbols.BLOCK_MIDDLE); |
| 319 | } |
| 320 | } |
| 321 | } |