/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.tools;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
/**
* This class is capable of batch line inserts which only trigger one GUI refresh and only uses a
* single lock. This gives a significant performance boost compared to the
* {@link DefaultStyledDocument} that refreshes the GUI each time a {@link String} is inserted via
* {@link StyledDocument#insertString(int, String, AttributeSet)}.
* <p>
* Usage: Call {@link #appendLineForBatch(String, AttributeSet)} as many times as desired, then call
* {@link #executeBatch(int)} or {@link #executeBatchAppend()} to update the document with all
* changes at once. This will only trigger one GUI update.
* </p>
* <p>
* This document is thread safe in the same way as the regular {@link StyledDocument}.
* </p>
*
* @author Marco Boeck
*
*/
public class ExtendedStyledDocument extends DefaultStyledDocument {
private static final long serialVersionUID = 1L;
/** name of the default font family for batch strings */
private static final String DEFAULT_FONT_FAMILY = "SansSerif";
/** this list contains the elements that should be inserted with the next batch update */
private final LinkedList<ElementSpec> listToInsert;
/** this list contains the length of each line in the batch */
private final LinkedList<Integer> lineLength;
/** the max number of rows per batch. If exceeded, starts discarding oldest entries for new ones */
private int maxRows;
private final Object LOCK = new Object();
/**
* Creates an empty styled document which supports batch insertion.
*
* @param maxRows
* if set to > 0, will limit the max batch row amount. Oldest entries will be
* discarded for new entries.
*/
public ExtendedStyledDocument(int maxRows) {
this.listToInsert = new LinkedList<>();
this.lineLength = new LinkedList<>();
this.maxRows = maxRows;
}
/**
* Stores the given {@link String} line for the next batch update. If the number of elements
* awaiting batch update are >= maxRows, will discard the oldest element. Call
* {@link #executeBatch(int)} or {@link #executeBatchAppend()} to execute the batch update.
* <p>
* <strong>Attention:</strong> Every {@link String} is considered as one line so a line
* separator will be added into the document after it.
* </p>
* <p>
* This method is thread safe.
* </p>
*
* @param str
* the {@link String} to add to the document.
* @param a
* the style formatting settings
*/
public void appendLineForBatch(String str, SimpleAttributeSet a) {
if (str == null || str.isEmpty()) {
throw new IllegalArgumentException("str must not be null or empty!");
}
if (!str.endsWith(System.lineSeparator())) {
str += System.lineSeparator();
}
char[] txt = str.toCharArray();
a = a != null ? (SimpleAttributeSet) a.copyAttributes() : new SimpleAttributeSet();
// set font family if not set
if (a.getAttribute(StyleConstants.FontFamily) == null) {
StyleConstants.setFontFamily(a, DEFAULT_FONT_FAMILY);
}
synchronized (LOCK) {
// make sure batch size does not exceed maxRows *3 (*3 because we add the str and 2 line
// separator tags)
if (maxRows > 0) {
while (listToInsert.size() >= maxRows * 3) {
// remove element itself and both line separator elements)
// we start at the beginning because we discard oldest first
listToInsert.removeFirst();
listToInsert.removeFirst();
listToInsert.removeFirst();
lineLength.removeFirst();
}
}
// close previous paragraph tag, start new one, add text
// yes the order is correct; no you cannot change to start/text/end
// if you do, linebreaks get messed up
listToInsert.add(new ElementSpec(new SimpleAttributeSet(), ElementSpec.EndTagType));
listToInsert.add(new ElementSpec(new SimpleAttributeSet(), ElementSpec.StartTagType));
listToInsert.add(new ElementSpec(a, ElementSpec.ContentType, txt, 0, txt.length));
// store length of each row we add
lineLength.add(txt.length);
}
}
/**
* Executes a batch update. This takes all previously stored {@link String}s and appends them to
* the document together. If no {@link String}s have been appended since the last batch update,
* does nothing.
* <p>
* This method is thread safe.
* </p>
*
* @return a list with the length of each added row. Note that this might return one more row
* than added which is of length 0
* @throws BadLocationException
*/
public List<Integer> executeBatchAppend() throws BadLocationException {
return executeBatch(getLength());
}
/**
* Executes a batch update. This takes all previously stored {@link String}s and adds them to
* the document together at the specified offset. If no {@link String}s have been appended since
* the last batch update, does nothing.
* <p>
* This method is thread safe.
* </p>
*
* @param offset
* the offset from the beginning of the document where the batch should be added
* @return a list with the length of each added row. Note that this might return one more row
* than added which is of length 0
* @throws BadLocationException
*/
public List<Integer> executeBatch(int offset) throws BadLocationException {
ElementSpec[] data = null;
List<Integer> toReturn = new LinkedList<>();
synchronized (LOCK) {
if (listToInsert.isEmpty()) {
// nothing to do
return Collections.emptyList();
}
data = new ElementSpec[listToInsert.size()];
listToInsert.toArray(data);
listToInsert.clear();
toReturn.addAll(lineLength);
lineLength.clear();
}
super.insert(offset, data);
return toReturn;
}
/**
* Returns the size of the current batch update, i.e. how many batch elements have been added
* since the last batch update.
* <p>
* This method is thread safe.
* </p>
*
* @return the current number of added batch updates
*/
public int getBatchSize() {
int size = 0;
synchronized (LOCK) {
// we take the linelength list because the other one contains start/end tags
size = lineLength.size();
}
return size;
}
/**
* Discards the accumulated strings for a batch update. Does not change the {@link Document}.
* After this method has been called, {@link #isBatchUpdateReady()} will return
* <code>false</code>.
* <p>
* This method is thread safe.
* </p>
*/
public void clearBatch() {
synchronized (LOCK) {
listToInsert.clear();
lineLength.clear();
}
}
/**
* Sets the maximum amount of lines allowed in a batch before oldest ones get discarded for new
* ones.
*
* @param maxRows
* if set to > 0, limits the number of batch items
*/
public void setMaxRows(int maxRows) {
this.maxRows = maxRows;
}
}