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    }