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 */
019package org.crsh.text;
020
021import org.crsh.util.Pair;
022import org.crsh.util.Utils;
023
024import java.io.IOException;
025import 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 */
036public 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}