001 /* 002 * Copyright (C) 2012 eXo Platform SAS. 003 * 004 * This is free software; you can redistribute it and/or modify it 005 * under the terms of the GNU Lesser General Public License as 006 * published by the Free Software Foundation; either version 2.1 of 007 * the License, or (at your option) any later version. 008 * 009 * This software is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this software; if not, write to the Free 016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 018 */ 019 package org.crsh.text; 020 021 import org.crsh.util.Pair; 022 import org.crsh.util.Utils; 023 024 import java.io.IOException; 025 import java.util.ArrayList; 026 027 /** 028 * A virtual screen that can be scrolled. This class is thread safe, as it can be used concurrently by two 029 * threads, for example one thread can provide new elements while another thread is repainting the buffer 030 * to the screen, both threads can either modify the underlying data structure. Paint could also be called concurrently 031 * by two threads, one that just provided a new element and wants to repaint the structure and another that changes 032 * the current cursor and asks for a repaint too. 033 * 034 * @author Julien Viet 035 */ 036 public class VirtualScreen implements ScreenContext { 037 038 /** The cached width and height for the current refresh. */ 039 private int width, height; 040 041 /** . */ 042 private final ArrayList<Foo> buffer; 043 044 /** The current style for last chunk in the buffer. */ 045 private Style style; 046 047 /** The absolute offset, index and row. */ 048 private int offset, index, row; 049 050 /** The cursor coordinate. */ 051 private int cursorX, cursorY; 052 053 // Invariant: 054 // currentIndex always points at the end of a valid offset 055 // except when the buffer is empty, in this situation we have 056 // (currentOffset = 0, currentIndex = 0) 057 // othewise we always have 058 // (currentOffset = 0, currentIndex = 1) for {"a"} and not (currentOffset = 1, currentIndex = 0) 059 060 /** The cursor offset in the {@link #buffer}. */ 061 private int cursorOffset; 062 063 /** The cursor index in the chunk at the current {@link #cursorOffset}. */ 064 private int cursorIndex; 065 066 /** . */ 067 private Style cursorStyle; 068 069 /** . */ 070 private final ScreenContext out; 071 072 /** Do we need to clear screen. */ 073 private int status; 074 075 private static final int 076 REFRESH = 0, // Need a full refresh 077 PAINTING = 1, // Screen is partially painted 078 PAINTED = 3; // Screen is fully painted 079 080 private static class Foo { 081 final CharSequence text; 082 final Style style; 083 private Foo(CharSequence text, Style style) { 084 this.text = text; 085 this.style = style; 086 } 087 } 088 089 public VirtualScreen(ScreenContext out) { 090 this.out = out; 091 this.width = Utils.notNegative(out.getWidth()); 092 this.height = Utils.notNegative(out.getHeight()); 093 this.cursorX = 0; 094 this.cursorY = 0; 095 this.cursorOffset = 0; 096 this.cursorIndex = 0; 097 this.offset = 0; 098 this.index = 0; 099 this.row = 0; 100 this.buffer = new ArrayList<Foo>(); 101 this.style = Style.style(); 102 this.status = REFRESH; 103 this.cursorStyle = null; // on purpose 104 } 105 106 public int getWidth() { 107 return out.getWidth(); 108 } 109 110 public int getHeight() { 111 return out.getHeight(); 112 } 113 114 @Override 115 public Screenable append(CharSequence s) throws IOException { 116 buffer.add(new Foo(s, style)); 117 return this; 118 } 119 120 @Override 121 public Screenable append(char c) throws IOException { 122 return append(Character.toString(c)); 123 } 124 125 @Override 126 public Screenable append(CharSequence csq, int start, int end) throws IOException { 127 return append(csq.subSequence(start, end)); 128 } 129 130 @Override 131 public Screenable append(Style style) throws IOException { 132 this.style = style.merge(style); 133 return this; 134 } 135 136 @Override 137 public Screenable cls() throws IOException { 138 buffer.clear(); 139 cursorX = 0; 140 cursorY = 0; 141 cursorOffset = 0; 142 cursorIndex = 0; 143 offset = 0; 144 index = 0; 145 row = 0; 146 status = REFRESH; 147 return this; 148 } 149 150 /** 151 * Pain the underlying screen context. 152 * 153 * @return this screen buffer 154 * @throws IOException any io exception 155 */ 156 public synchronized VirtualScreen paint() throws IOException { 157 if (status == REFRESH) { 158 out.cls(); 159 out.append(Style.reset); 160 cursorStyle = Style.reset; 161 status = PAINTING; 162 } 163 if (buffer.size() > 0) { 164 // We ensure there is a least one chunk in the buffer, otherwise it will throw a NullPointerException 165 int prev = cursorIndex; 166 while (cursorX < width && cursorY < height) { 167 if (cursorIndex >= buffer.get(cursorOffset).text.length()) { 168 if (prev < cursorIndex) { 169 if (!buffer.get(cursorOffset).style.equals(cursorStyle)) { 170 out.append(buffer.get(cursorOffset).style); 171 cursorStyle = cursorStyle.merge(buffer.get(cursorOffset).style); 172 } 173 out.append(buffer.get(cursorOffset).text, prev, cursorIndex); 174 } 175 if (cursorOffset + 1 >= buffer.size()) { 176 return this; 177 } else { 178 prev = 0; 179 cursorIndex = 0; 180 cursorOffset++; 181 } 182 } else { 183 char c = buffer.get(cursorOffset).text.charAt(cursorIndex); 184 if (c == '\n') { 185 cursorX = 0; 186 cursorY++; 187 if (cursorY < height) { 188 cursorIndex++; 189 } 190 } else if (c >= 32) { 191 cursorX++; 192 cursorIndex++; // Not sure that should be done all the time -> maybe bug with edge case 193 if (cursorX == width) { 194 cursorX = 0; 195 cursorY++; 196 } 197 } else { 198 cursorIndex++; 199 } 200 } 201 } 202 if (prev < cursorIndex) { 203 if (!buffer.get(cursorOffset).style.equals(cursorStyle)) { 204 out.append(buffer.get(cursorOffset).style); 205 cursorStyle = cursorStyle.merge(buffer.get(cursorOffset).style); 206 } 207 out.append(buffer.get(cursorOffset).text.subSequence(prev, cursorIndex)); 208 } 209 status = PAINTED; 210 } 211 return this; 212 } 213 214 public synchronized boolean previousRow() throws IOException { 215 // Current strategy is to increment updates, a bit dumb, but fast (in memory) and works 216 // correctly 217 if (row > 0) { 218 int previousOffset = 0; 219 int previousIndex = 0; 220 int previousRow = 0; 221 while (previousRow < row - 1) { 222 Pair<Integer, Integer> next = nextRow(previousOffset, previousIndex, width); 223 if (next != null) { 224 previousOffset = next.getFirst(); 225 previousIndex = next.getSecond(); 226 previousRow++; 227 } else { 228 break; 229 } 230 } 231 status = REFRESH; 232 cursorX = cursorY = 0; 233 cursorOffset = offset = previousOffset; 234 cursorIndex = index = previousIndex; 235 row = previousRow; 236 return true; 237 } else { 238 return false; 239 } 240 } 241 242 /** 243 * @return true if the buffer is painted 244 */ 245 public synchronized boolean isPainted() { 246 return status == PAINTED; 247 } 248 249 /** 250 * @return true if the buffer is stale and needs a full repaint 251 */ 252 public synchronized boolean isRefresh() { 253 return status == REFRESH; 254 } 255 256 /** 257 * @return true if the buffer is waiting for input to become painted 258 */ 259 public synchronized boolean isPainting() { 260 return status == PAINTING; 261 } 262 263 public synchronized boolean nextRow() throws IOException { 264 return scroll(1) == 1; 265 } 266 267 public synchronized int nextPage() throws IOException { 268 return scroll(height); 269 } 270 271 private int scroll(int amount) throws IOException { 272 if (amount < 0) { 273 throw new UnsupportedOperationException("Not implemented for negative operations"); 274 } else if (amount == 0) { 275 // Nothing to do 276 return 0; 277 } else { 278 // This mean we already painted the screen and therefore maybe we can scroll 279 if (isPainted()) { 280 int count = 0; 281 int _offset = cursorOffset; 282 int _index = cursorIndex; 283 while (count < amount) { 284 Pair<Integer, Integer> next = nextRow(_offset, _index, width); 285 if (next != null) { 286 _offset = next.getFirst(); 287 _index = next.getSecond(); 288 count++; 289 } else { 290 // Perhaps we can scroll one more line 291 if (nextRow(_offset, _index, 1) != null) { 292 count++; 293 } 294 break; 295 } 296 } 297 if (count > 0) { 298 _offset = offset; 299 _index = index; 300 for (int i = 0;i < count;i++) { 301 Pair<Integer, Integer> next = nextRow(_offset, _index, width); 302 _offset = next.getFirst(); 303 _index = next.getSecond(); 304 } 305 status = REFRESH; 306 cursorX = cursorY = 0; 307 cursorOffset = offset = _offset; 308 cursorIndex = index = _index; 309 row += count; 310 } 311 return count; 312 } else { 313 return 0; 314 } 315 } 316 } 317 318 private Pair<Integer, Integer> nextRow(int offset, int index, int width) { 319 int count = 0; 320 while (true) { 321 if (index >= buffer.get(offset).text.length()) { 322 if (offset + 1 >= buffer.size()) { 323 return null; 324 } else { 325 index = 0; 326 offset++; 327 } 328 } else { 329 char c = buffer.get(offset).text.charAt(index++); 330 if (c == '\n') { 331 return new Pair<Integer, Integer>(offset, index); 332 } else if (c >= 32) { 333 if (++count == width) { 334 return new Pair<Integer, Integer>(offset, index); 335 } 336 } 337 } 338 } 339 } 340 341 public synchronized boolean update() throws IOException { 342 int nextWidth = out.getWidth(); 343 int nextHeight = out.getHeight(); 344 if (width != nextWidth || height != nextHeight) { 345 width = nextWidth; 346 height = nextHeight; 347 if (buffer.size() > 0) { 348 cursorIndex = index; 349 cursorOffset = offset; 350 cursorX = 0; 351 cursorY = 0; 352 status = REFRESH; 353 return true; 354 } else { 355 return false; 356 } 357 } else { 358 return false; 359 } 360 } 361 362 @Override 363 public synchronized void flush() throws IOException { 364 // I think flush should not always be propagated, specially when we consider that the screen context 365 // is already filled 366 out.flush(); 367 } 368 }