Bug 4840 fixed: The more there was text in the console, the slower was the display
[scilab.git] / scilab / modules / console / src / java / org / scilab / modules / console / SciOutputView.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2007-2008 - INRIA - Vincent COUVERT
4  *
5  * This file must be used under the terms of the CeCILL.
6  * This source file is licensed as described in the file COPYING, which
7  * you should have received as part of this distribution.  The terms
8  * are also available at
9  * http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt
10  *
11  */
12
13 package org.scilab.modules.console;
14
15 import java.awt.Color;
16 import java.awt.Dimension;
17 import java.awt.Toolkit;
18 import java.awt.datatransfer.Clipboard;
19 import java.awt.datatransfer.DataFlavor;
20 import java.awt.datatransfer.Transferable;
21 import java.awt.datatransfer.UnsupportedFlavorException;
22 import java.awt.dnd.DnDConstants;
23 import java.awt.dnd.DropTarget;
24 import java.awt.event.FocusEvent;
25 import java.awt.event.FocusListener;
26 import java.awt.event.MouseEvent;
27 import java.io.IOException;
28 import java.io.Writer;
29 import java.util.LinkedList;
30 import java.util.concurrent.ArrayBlockingQueue;
31 import java.util.concurrent.BlockingQueue;
32
33 import javax.swing.BorderFactory;
34 import javax.swing.JPanel;
35 import javax.swing.JScrollPane;
36 import javax.swing.JEditorPane;
37 import javax.swing.JTextPane;
38 import javax.swing.SwingUtilities;
39 import javax.swing.UIManager;
40 import javax.swing.text.BadLocationException;
41 import javax.swing.text.DefaultCaret;
42 import javax.swing.text.DefaultStyledDocument;
43 import javax.swing.text.JTextComponent;
44 import javax.swing.text.PlainDocument;
45 import javax.swing.text.StyleContext;
46 import javax.swing.text.StyledDocument;
47
48 import org.scilab.modules.commons.gui.ScilabCaret;
49
50 import com.artenum.rosetta.interfaces.ui.OutputView;
51 import com.artenum.rosetta.util.BufferedWriter;
52 import com.artenum.rosetta.util.StringConstants;
53
54 /**
55  * Scilab Console UI which contains the previous commands and their outputs
56  *
57  * @author Vincent COUVERT
58  */
59 public class SciOutputView extends JEditorPane implements OutputView {
60     private static final long serialVersionUID = 1L;
61
62     private static final int TOP_BORDER = 0;
63
64     private static final int BOTTOM_BORDER = 0;
65
66     private static final int LEFT_BORDER = 0;
67
68     private static final int RIGHT_BORDER = 0;
69
70     private static final int BUFFER_SIZE = 10;
71
72     private String activeStyle;
73
74     private String lastAppendedStyle;
75
76     private BlockingQueue<StringBuffer> bufferQueue;
77
78     private LinkedList<String> styleQueue;
79
80     private StringBuffer currentWorkingBuffer;
81
82     private SciConsole console;
83
84     private Thread thread;
85
86     private int insertPosition;
87
88     private int maxNumberOfLines;
89     private int numberOfLines;
90
91     /**
92      * Constructor
93      */
94     public SciOutputView() {
95         super();
96
97         /* A PlainDocument contains only "box" for lines not for all characters (as in a StyledDocument)
98            so there are less boxes to explore in a PlainDocument... */
99         setDocument(new PlainDocument());
100         setMaxSize(10000);
101         setBorder(BorderFactory.createEmptyBorder(TOP_BORDER, LEFT_BORDER, BOTTOM_BORDER, RIGHT_BORDER));
102
103         // Enabled Drag&Drop with this component
104         this.setDragEnabled(true);
105         this.setDoubleBuffered(true);
106
107         activeStyle = StyleContext.DEFAULT_STYLE;
108         bufferQueue = new ArrayBlockingQueue<StringBuffer>(BUFFER_SIZE);
109         styleQueue = new LinkedList<String>();
110         setFocusable(false);
111
112         /**
113          * Default caret for output view (to handle paste actions using middle button)
114          * @author Vincent COUVERT
115          */
116         final class FixedCaret extends ScilabCaret {
117
118             /**
119              * Constructor
120              */
121             private FixedCaret() {
122                 super(SciOutputView.this);
123             }
124
125             /**
126              * Manages mouse clicks
127              * @param e the event
128              * @see javax.swing.text.DefaultCaret#mouseClicked(java.awt.event.MouseEvent)
129              */
130             public void mouseClicked(MouseEvent e) {
131                 if (SwingUtilities.isMiddleMouseButton(e) && e.getClickCount() == 1) {
132                     /*** PASTE USING MIDDLE BUTTON ***/
133                     JTextComponent c = (JTextComponent) e.getSource();
134                     if (c != null) {
135                         Toolkit tk = c.getToolkit();
136                         Clipboard buffer = tk.getSystemSelection();
137                         if (buffer != null) {
138                             Transferable trans = buffer.getContents(null);
139                             if (trans.isDataFlavorSupported(DataFlavor.stringFlavor)) {
140                                 try {
141                                     String pastedText = (String) trans.getTransferData(DataFlavor.stringFlavor);
142                                     ((JTextPane) getConsole().getConfiguration().getInputCommandView()).replaceSelection(pastedText);
143                                 } catch (UnsupportedFlavorException e1) {
144                                     e1.printStackTrace();
145                                 } catch (IOException e1) {
146                                     e1.printStackTrace();
147                                 }
148                             }
149                         }
150                     }
151                 } else if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 1) {
152                     /*** SEND THE FOCUS TO THE INPUT COMMAND VIEW ***/
153                     ((JTextPane) getConsole().getConfiguration().getInputCommandView()).requestFocus();
154                     ((JTextPane) getConsole().getConfiguration().getInputCommandView()).getCaret().setVisible(true);
155                 } else {
156                     /*** DELEGATE TO THE SYSTEM ***/
157                     super.mouseClicked(e);
158                 }
159             }
160         }
161
162         // Set the caret
163         setCaret(new FixedCaret());
164         // Selection is forced to be visible because the component is not editable
165         getCaret().setSelectionVisible(true);
166     }
167
168     /**
169      * @param styledDocument
170      */
171     public void setStyledDocument(StyledDocument styledDocument) { }
172
173     /**
174      * Display a buffer entry in the console
175      * @param buff the string  to write
176      * @param style the style to use to format the string
177      */
178     private void displayLineBuffer(String buff, String style) {
179         int sDocLength = getDocument().getLength();
180
181         if (buff.equals("\r")) {
182             /* If \r sent by mprintf then display nothing but prepare next display */
183             /* Insertion will be done just after last NEW_LINE */
184             try {
185                 String outputTxt = getDocument().getText(0, sDocLength);
186                 insertPosition = outputTxt.lastIndexOf(StringConstants.NEW_LINE) + 1;
187             } catch (BadLocationException e) {
188                 e.printStackTrace();
189             }
190             return;
191         } else {
192             /* Change position for insertion if a previous \r still influence display */
193             if ((insertPosition != 0) && (insertPosition < sDocLength)) {
194                 sDocLength = insertPosition;
195                 try {
196                     /* Remove chars to be replaced */
197                     if (insertPosition + buff.length() <= getDocument().getLength()) {
198                         getDocument().remove(insertPosition, buff.length());
199                     } else {
200                         /* Remove end of line */
201                         getDocument().remove(insertPosition, getDocument().getLength() - insertPosition);
202                     }
203                 } catch (BadLocationException e) {
204                     e.printStackTrace();
205                 }
206             } else {
207                 /* Reinit insertPosition: 0 is equivalent to insertPosition value ignored */
208                 insertPosition = 0;
209             }
210         }
211
212         try {
213             getDocument().insertString(sDocLength, buff, null);
214             /* Move insertPosition to the end of last inserted data */
215             if (insertPosition != 0) {
216                 insertPosition += buff.length();
217             }
218         } catch (BadLocationException e) {
219             // TODO Auto-generated catch block
220             e.printStackTrace();
221         }
222
223         int count = getDocument().getDefaultRootElement().getElementCount();
224         if (count > 1.5 * maxNumberOfLines) {
225             /* A removal is costly: array copy and with a gap buffer that leads to two array copies (when remove is followed by an insert).
226                So the idea is to minimize the number of removal: a removal only when 0.5*maxNumberOfLines useless lines are entered.
227             */
228             try {
229                 getDocument().remove(0, getDocument().getDefaultRootElement().getElement(count - maxNumberOfLines - 1).getEndOffset());
230             } catch (BadLocationException e) {
231                 e.printStackTrace();
232             }
233         }
234
235         /* Special case for Scilab when clc or tohome have been used */
236         String[] lines = buff.split(StringConstants.NEW_LINE);
237
238         /* Change the size of the input command view if necessary */
239         /* - if the console size has been forced to a value */
240         /* - if a carriage return has been appended */
241         if (console != null && console.getInputCommandViewSizeForced() && lines.length > 0) {
242             JEditorPane outputView = ((JEditorPane) console.getConfiguration().getOutputView());
243
244             // Get JScrollPane viewport size to adapt input command
245             // view size
246             JScrollPane jSP = console.getJScrollPane();
247             Dimension jSPExtSize = jSP.getViewport().getExtentSize();
248
249             /* Height of a text line in the ouput view */
250             int charHeight = outputView.getFontMetrics(outputView.getFont()).getHeight();
251             JPanel promptView = ((JPanel) console.getConfiguration().getPromptView());
252
253             /* Input command view dimensions */
254             JTextPane inputCmdView = ((JTextPane) console.getConfiguration().getInputCommandView());
255             int height = inputCmdView.getPreferredSize().height;
256             int width = inputCmdView.getPreferredSize().width - jSPExtSize.width;
257
258             int promptViewHeight = promptView.getPreferredSize().height;
259
260             /* New dimension for the input command view */
261             /*
262              * -1 because last EOL removed in
263              * SwingScilabConsole.readline
264              */
265             int newHeight = height - (lines.length - 1) * charHeight;
266             Dimension newDim = null;
267
268             if (newHeight > promptViewHeight) {
269                 /*
270                  * If the input command view is bigger than the
271                  * promptUI
272                  */
273                 /*
274                  * It's height is descreased according to line
275                  * number of lines added to output view
276                  */
277                 newDim = new Dimension(width, newHeight);
278             } else {
279                 /*
280                  * If the input command view is smaller than the
281                  * promptUI
282                  */
283                 /* It's height adapted to the promptUI height */
284                 newDim = new Dimension(width, promptViewHeight);
285                 console.setInputCommandViewSizeForced(false);
286             }
287             /* Change the input command view size */
288             ((JTextPane) console.getConfiguration().getInputCommandView()).setPreferredSize(newDim);
289             ((JTextPane) console.getConfiguration().getInputCommandView()).invalidate();
290             ((JTextPane) console.getConfiguration().getInputCommandView()).doLayout();
291         }
292         /* Update scroll only if console has been set */
293         /* TODO : Must not do this each time... consume pretty much computing resources */
294         if (console != null) {
295             console.updateScrollPosition();
296         }
297     }
298
299     /**
300      * Adds text to the output view and change the size of others components if
301      * necessary
302      *
303      * @param content
304      *            text to add
305      */
306     public void append(String content) {
307         //append(content, activeStyle);
308         displayLineBuffer(content, activeStyle);
309     }
310
311     /**
312      * Adds text to the output view and change the size of others components if
313      * necessary
314      *
315      * @param content
316      *            text to add
317      * @param styleName
318      *            style to set for content
319      */
320     public void append(String content, String styleName) {
321         if (styleName.equals(lastAppendedStyle) && bufferQueue.size() > 1) {
322             currentWorkingBuffer.append(content);
323         } else {
324             lastAppendedStyle = styleName;
325             styleQueue.add(lastAppendedStyle);
326             try {
327                 currentWorkingBuffer = new StringBuffer(content);
328                 bufferQueue.put(currentWorkingBuffer);
329             } catch (InterruptedException e) {
330                 e.printStackTrace();
331             }
332         }
333         if (!thread.isAlive()) {
334             thread.run();
335         }
336     }
337
338     /**
339      * Gets the error writer
340      *
341      * @return the error writer
342      * @see com.artenum.rosetta.interfaces.ui.OutputView#getErrorWriter()
343      */
344     public Writer getErrorWriter() {
345         return new BufferedWriter(StyleContext.DEFAULT_STYLE, bufferQueue, styleQueue);
346     }
347
348     /**
349      * Gets the writer
350      *
351      * @return the writer
352      * @see com.artenum.rosetta.interfaces.ui.OutputView#getWriter()
353      */
354     public Writer getWriter() {
355         return new BufferedWriter(StyleContext.DEFAULT_STYLE, bufferQueue, styleQueue);
356     }
357
358     /**
359      * Resets the output view (remove text)
360      *
361      * @see com.artenum.rosetta.interfaces.ui.OutputView#reset()
362      */
363     public void reset() {
364         setText("");
365         setCaretPosition(0);
366     }
367
368     /**
369      * Move the caret to the beginning of the styled document
370      *
371      * @see com.artenum.rosetta.interfaces.ui.OutputView#setCaretPositionToBeginning()
372      */
373     public void setCaretPositionToBeginning() {
374         insertPosition = 0;
375         setCaretPosition(0);
376     }
377
378     /**
379      * Move the caret to the end of the styled document
380      *
381      * @see com.artenum.rosetta.interfaces.ui.OutputView#setCaretPositionToEnd()
382      */
383     public void setCaretPositionToEnd() {
384         insertPosition = 0;
385         setCaretPosition(getDocument().getLength());
386     }
387
388     /**
389      * Set the style for current text
390      *
391      * @param styleName
392      *            the style to set
393      * @see com.artenum.rosetta.interfaces.ui.OutputView#setStyleName(java.lang.String)
394      */
395     public void setStyleName(String styleName) {
396         activeStyle = styleName;
397     }
398
399     /**
400      * Sets the console object containing this output view
401      *
402      * @param c
403      *            the console associated
404      */
405     public void setConsole(SciConsole c) {
406         console = c;
407
408         // Drag n' Drop handling
409         this.setDropTarget(new DropTarget(this,
410                                           DnDConstants.ACTION_COPY_OR_MOVE, new SciDropTargetListener(console)));
411
412         // Commented because now done by the caret class
413         //FocusMouseListener focusGrabber = new FocusMouseListener(console);
414         //this.addMouseListener(focusGrabber);
415     }
416
417     /**
418      * Gets the console object containing this output view
419      *
420      * @return the console associated
421      */
422     public SciConsole getConsole() {
423         return console;
424     }
425
426     /**
427      * Get the current thread used to display
428      * @return the thread
429      */
430     public Thread getThread() {
431         return thread;
432     }
433
434     /**
435      * Set the maximum number of lines to keep before deleting the older one
436      * @param number the maximum
437      */
438     public void setMaxSize(int number) {
439         maxNumberOfLines = Math.max(1, number);
440     }
441
442 }