/**
* Warlock, the open-source cross-platform game client
*
* Copyright 2008, Warlock LLC, and individual contributors as indicated
* by the @authors tag.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package cc.warlock.rcp.ui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import cc.warlock.core.client.IWarlockClient;
import cc.warlock.core.client.IWarlockStyle;
import cc.warlock.core.client.WarlockString;
import cc.warlock.core.client.WarlockStringMarker;
import cc.warlock.core.client.internal.WarlockStyle;
import cc.warlock.core.client.settings.IHighlightString;
import cc.warlock.rcp.ui.style.StyleProviders;
import cc.warlock.rcp.util.SoundPlayer;
/**
* This is an extension of the StyledText widget which has special support for
* embedding of arbitrary Controls/Links
* @author Marshall
*/
public class WarlockText {
protected IWarlockClient client;
private StyledText textWidget;
private Cursor handCursor, defaultCursor;
private int lineLimit = 5000;
private int doScrollDirection = SWT.DOWN;
private Menu contextMenu;
private boolean ignoreEmptyLines = true;
private LinkedList<WarlockStringMarker> markers = new LinkedList<WarlockStringMarker>();
public WarlockText(Composite parent) {
textWidget = new StyledText(parent, SWT.V_SCROLL);
textWidget.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, true));
textWidget.setEditable(false);
textWidget.setWordWrap(true);
textWidget.setIndent(1);
ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
Display display = parent.getDisplay();
handCursor = new Cursor(display, SWT.CURSOR_HAND);
defaultCursor = parent.getCursor();
contextMenu = new Menu(textWidget);
MenuItem itemCopy = new MenuItem(contextMenu, SWT.PUSH);
itemCopy.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent arg0) {
textWidget.copy();
}
});
itemCopy.setText("Copy");
itemCopy.setImage(images.getImage(ISharedImages.IMG_TOOL_COPY));
MenuItem itemClear = new MenuItem(contextMenu, SWT.PUSH);
itemClear.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent arg0) {
textWidget.setText("");
}
});
itemClear.setText("Clear");
itemClear.setImage(images.getImage(ISharedImages.IMG_TOOL_DELETE));
textWidget.setMenu(contextMenu);
textWidget.addMouseMoveListener(new MouseMoveListener() {
public void mouseMove(MouseEvent e) {
try {
if (!textWidget.isDisposed() && textWidget.isVisible())
{
Point point = new Point(e.x, e.y);
int offset = textWidget.getOffsetAtLocation(point);
StyleRange range = textWidget.getStyleRangeAtOffset(offset);
if (range != null && range instanceof StyleRangeWithData)
{
StyleRangeWithData range2 = (StyleRangeWithData) range;
if (range2.action != null)
{
textWidget.setCursor(handCursor);
return;
}
}
textWidget.setCursor(defaultCursor);
}
} catch (IllegalArgumentException ex) {
// swallow -- this happens if the mouse cursor moves to an
// area not covered by the imaginary rectangle surround the
// current text
textWidget.setCursor(defaultCursor);
}
}
});
textWidget.addMouseListener(new MouseListener () {
public void mouseDoubleClick(MouseEvent e) {}
public void mouseDown(MouseEvent e) {}
public void mouseUp(MouseEvent e) {
try {
Point point = new Point(e.x, e.y);
int offset = textWidget.getOffsetAtLocation(point);
StyleRange range = textWidget.getStyleRangeAtOffset(offset);
if (range != null && range instanceof StyleRangeWithData)
{
StyleRangeWithData range2 = (StyleRangeWithData) range;
if (range2.action != null)
{
range2.action.run();
}
}
} catch (IllegalArgumentException ex) {
// swallow -- see note above
}
}
});
}
public void selectAll() {
textWidget.selectAll();
}
public void copy() {
textWidget.copy();
}
public void pageUp() {
if (isAtBottom()) {
textWidget.setCaretOffset(textWidget.getCharCount());
}
textWidget.invokeAction(ST.PAGE_UP);
}
public void pageDown() {
textWidget.invokeAction(ST.PAGE_DOWN);
}
public void setBackground(Color color) {
textWidget.setBackground(color);
}
public void setForeground(Color color) {
textWidget.setForeground(color);
}
public void setFont(Font font) {
textWidget.setFont(font);
}
public void clearText() {
textWidget.setText("");
markers.clear();
}
public void setLineLimit(int limit) {
lineLimit = limit;
}
public void appendRaw(String string) {
boolean atBottom = isAtBottom();
textWidget.append(string);
if(atBottom)
scrollToEnd();
}
private Pattern newlinePattern = Pattern.compile("\r?\n");
private void removeEmptyLines(int offset) {
int line = textWidget.getLineAtOffset(offset);
int start = textWidget.getOffsetAtLine(line);
int end = textWidget.getCharCount();
if(start >= end)
return;
String str = textWidget.getTextRange(start, end - start);
Matcher m = newlinePattern.matcher(str);
int lineStart = 0;
while(m.find(lineStart)) {
if(lineStart == m.start()) {
int matchPos = start + m.start();
int matchLen = m.end() - m.start();
// Add the newline marker. We give it an initial length of 1
// so it gets added correctly into the tree of markers
WarlockStringMarker marker = new WarlockStringMarker(
new WarlockStyle("newline"), matchPos, matchPos + matchLen);
this.addInternalMarker(marker, markers);
// then remove the newline from the text
textWidget.replaceTextRange(matchPos, matchLen, "");
// and shrink down the newline marker because the actual newline is no longer there.
marker.setEnd(matchPos);
WarlockStringMarker.updateMarkers(-matchLen, marker, markers);
// Recursive call. if this could be a tail call, that would be awesome.
removeEmptyLines(start);
break;
} else {
lineStart = m.end();
}
}
}
private void restoreNewlines(int offset, Collection<WarlockStringMarker> markerList) {
for(Iterator<WarlockStringMarker> iter = markerList.iterator();
iter.hasNext(); )
{
WarlockStringMarker marker = iter.next();
Collection<WarlockStringMarker> subList = marker.getSubMarkers();
if(subList != null)
restoreNewlines(offset, subList);
// check to make sure we're a newline in the appropriate area
if(marker.getStart() < offset)
continue;
String name = marker.getName();
if(name == null || !name.equals("newline"))
continue;
// check if we're an empty line
if(marker.getStart() == 0 || textWidget.getTextRange(marker.getStart() - 1, 1).equals("\n"))
continue;
// we're not an empty line, put us back into action
textWidget.replaceTextRange(marker.getStart(), 0, "\n");
// TODO: this should actually just affect markers after us... oh well.
WarlockStringMarker.updateMarkers(1, marker, markers);
iter.remove();
}
}
private Collection<StyleRange> mergeStyleRangeLists(Collection<StyleRange> list1, Collection<StyleRange> list2) {
LinkedList<StyleRange> resultList = new LinkedList<StyleRange>();
for(StyleRange style : list1) {
resultList.add(style);
}
mergeLoop: for(StyleRange mergingStyle : list2) {
if(mergingStyle == null)
continue;
for(ListIterator<StyleRange> iter = resultList.listIterator();
iter.hasNext(); )
{
StyleRange style = iter.next();
if(style == null)
continue;
// if the mergeStyle came before the current style, add the
// mergeStyle before it and go to the next mergeStyle
if(style.start >= mergingStyle.start + mergingStyle.length) {
iter.previous();
iter.add(mergingStyle);
continue mergeLoop;
}
// If the mergeStyle came after the current style, continue on
if(mergingStyle.start >= style.start + style.length)
continue;
iter.remove();
int subStart;
if(style.start < mergingStyle.start) {
// Add the style before they overlap
StyleRange newStyle = (StyleRange)style.clone();
newStyle.length = mergingStyle.start - style.start;
iter.add(newStyle);
subStart = mergingStyle.start;
} else if(mergingStyle.start < style.start) {
// Add the style before they overlap
StyleRange newStyle = (StyleRange)mergingStyle.clone();
newStyle.length = style.start - mergingStyle.start;
iter.add(newStyle);
subStart = style.start;
} else {
subStart = style.start;
}
int subEnd;
if(style.start + style.length < mergingStyle.start + mergingStyle.length) {
subEnd = style.start + style.length;
} else {
subEnd = mergingStyle.start + mergingStyle.length;
}
StyleRange newStyle = this.mergeStyleRanges(style, mergingStyle);
newStyle.start = subStart;
newStyle.length = subEnd - subStart;
iter.add(newStyle);
if(style.start + style.length < mergingStyle.start + mergingStyle.length) {
int length = mergingStyle.start + mergingStyle.length - subEnd;
mergingStyle.start = subEnd;
mergingStyle.length = length;
continue;
} else if(mergingStyle.start + mergingStyle.length < style.start + style.length) {
StyleRange endStyle = (StyleRange)style.clone();
endStyle.start = subEnd;
endStyle.length = style.start + style.length - subEnd;
iter.add(endStyle);
}
// else both styles end at the same time
// We matched a style and inserted the new style, so we're done
continue mergeLoop;
}
resultList.add(mergingStyle);
}
return resultList;
}
private void showStyles(Collection<StyleRange> styles, int start, int end) {
try {
Collection<StyleRange> finalList = mergeStyleRangeLists(styles, getHighlights(start, end));
for(StyleRange style : finalList) {
textWidget.setStyleRange(style);
}
} catch(Exception e) {
e.printStackTrace();
}
}
private Collection<StyleRange> getHighlights(int start, int end) {
ArrayList<StyleRange> highlightList = new ArrayList<StyleRange>();
if(client == null)
return highlightList;
String text = textWidget.getTextRange(start, end - start);
for (IHighlightString highlight : client.getClientSettings().getAllHighlightStrings())
{
Pattern p;
try {
p = highlight.getPattern();
} catch(PatternSyntaxException e) {
continue;
}
if(p == null)
continue;
Matcher matcher = p.matcher(text);
while (matcher.find())
{
MatchResult result = matcher.toMatchResult();
IWarlockStyle style = highlight.getStyle();
int highlightStart = result.start() + start;
int highlightLength = result.end() - result.start();
if(style.isFullLine()) {
int lineNum = textWidget.getLineAtOffset(highlightStart);
highlightStart = textWidget.getOffsetAtLine(lineNum);
if(lineNum + 1 >= textWidget.getLineCount())
highlightLength = end - highlightStart;
else
highlightLength = textWidget.getOffsetAtLine(lineNum + 1) - highlightStart;
}
StyleRangeWithData styleRange = warlockStyleToStyleRange(style,
highlightStart, highlightLength);
if(styleRange == null)
continue;
highlightList.add(styleRange);
try{
if (style.getSound() != null && !style.getSound().equals("")){
//System.out.println("Playing sound " + style.getSound());
SoundPlayer.play(style.getSound());
//InputStream soundStream = new FileInputStream(style.getSound());
//RCPUtil.playSound(soundStream);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
return highlightList;
}
private void getMarkerStyles(WarlockStringMarker marker,
StyleRange baseStyle, Collection<StyleRange> resultStyles) {
int pos = marker.getStart();
for(WarlockStringMarker subMarker : marker.getSubMarkers()) {
int nextPos = subMarker.getStart();
StyleRange styleRange = mergeStyleRanges(baseStyle,
warlockStyleToStyleRange(marker.getStyle(), pos, nextPos - pos));
if(nextPos > pos)
resultStyles.add(styleRange);
getMarkerStyles(subMarker, styleRange, resultStyles);
pos = subMarker.getEnd();
}
if(marker.getEnd() > pos) {
StyleRange styleRange = mergeStyleRanges(baseStyle,
warlockStyleToStyleRange(marker.getStyle(), pos, marker.getEnd() - pos));
resultStyles.add(styleRange);
}
}
public void append(WarlockString wstring) {
boolean atBottom = isAtBottom();
int offset = textWidget.getCharCount();
textWidget.append(wstring.toString());
/* Break up the ranges and merge overlapping styles because SWT only
* allows 1 style per section
*/
LinkedList<StyleRange> finishedStyles = new LinkedList<StyleRange>();
for(WarlockStringMarker strMarker : wstring.getStyles()) {
WarlockStringMarker marker = strMarker.copy(offset);
addComponentMarker(marker, marker);
getMarkerStyles(marker, new StyleRangeWithData(), finishedStyles);
}
showStyles(finishedStyles, offset, textWidget.getCharCount());
postTextChange(atBottom, offset);
}
private void addComponentMarker(WarlockStringMarker marker, WarlockStringMarker topLevel) {
if(marker.getComponentName() != null) {
this.addMarker(marker);
IWarlockStyle baseStyle = topLevel.getBaseStyle(marker);
if(baseStyle != null)
marker.setStyle(baseStyle);
} else {
for(WarlockStringMarker subMarker : marker.getSubMarkers()) {
addComponentMarker(subMarker, topLevel);
}
}
}
private StyleRangeWithData warlockStyleToStyleRange(IWarlockStyle style, int start, int length) {
IStyleProvider styleProvider = StyleProviders.getStyleProvider(client);
if(styleProvider == null)
return null;
StyleRangeWithData styleRange = styleProvider.getStyleRange(style);
if(styleRange == null)
return null;
styleRange.start = start;
styleRange.length = length;
if(style.isFullLine())
textWidget.setLineBackground(textWidget.getLineAtOffset(styleRange.start), 1, styleRange.background);
if(style.getAction() != null)
styleRange.action = style.getAction();
if(style.getName() != null)
styleRange.data.put("name", style.getName());
return styleRange;
}
private StyleRange mergeStyleRanges(StyleRange style1, StyleRange style2) {
if(style1 == null)
return style2;
if(style2 == null)
return style1;
StyleRange newStyle;
// start with a cloned style1, unless style2 has data, but style1 doesn't
if(style2 instanceof StyleRangeWithData && !(style1 instanceof StyleRangeWithData))
newStyle = new StyleRangeWithData(style1);
else
newStyle = (StyleRange)style1.clone();
newStyle.start = style2.start;
newStyle.length = style2.length;
if(style2.font != null)
newStyle.font = style2.font;
if(style2.background != null)
newStyle.background = style2.background;
if(style2.foreground != null)
newStyle.foreground = style2.foreground;
if(style2.fontStyle != SWT.NORMAL)
newStyle.fontStyle = style2.fontStyle;
if(style2.strikeout) newStyle.strikeout = true;
if(style2.underline) newStyle.underline = true;
if(style2 instanceof StyleRangeWithData) {
StyleRangeWithData _newStyle = (StyleRangeWithData)newStyle;
StyleRangeWithData _style2 = (StyleRangeWithData)style2;
_newStyle.data.putAll(_style2.data);
if(_style2.action != null)
_newStyle.action = _style2.action;
if(_style2.tooltip != null)
_newStyle.tooltip = _style2.tooltip;
}
return newStyle;
}
public boolean isAtBottom() {
return textWidget.getLinePixel(textWidget.getLineCount()) <= textWidget.getClientArea().height;
}
private void postTextChange(boolean atBottom, int offset) {
if(ignoreEmptyLines) {
removeEmptyLines(offset);
restoreNewlines(offset, markers);
}
constrainLineLimit(atBottom);
if(atBottom)
scrollToEnd();
}
public void scrollToEnd() {
if(doScrollDirection == SWT.DOWN)
textWidget.setTopPixel(textWidget.getTopPixel()
+ textWidget.getLinePixel(textWidget.getLineCount()));
}
// this function removes the first "delta" amount of characters
private void updateMarkers(int delta) {
for(Iterator<WarlockStringMarker> iter = markers.iterator();
iter.hasNext(); )
{
WarlockStringMarker marker = iter.next();
// If the marker is moved off the beginning, remove it
if(marker.getEnd() + delta < 0) {
iter.remove();
continue;
}
// move us accordingly
marker.move(delta);
}
}
public void addInternalMarker(WarlockStringMarker marker,
LinkedList<WarlockStringMarker> markerList) {
ListIterator<WarlockStringMarker> iter = markerList.listIterator();
try {
while(true) {
if(!iter.hasNext()) {
iter.add(marker);
break;
}
WarlockStringMarker cur = iter.next();
if(cur.getEnd() > marker.getStart()) {
if(marker.getEnd() > cur.getStart()) {
addInternalMarker(marker, cur.getSubMarkers());
return;
}
iter.previous();
iter.add(marker);
break;
}
}
} catch(Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
public void addMarker(WarlockStringMarker marker) {
ListIterator<WarlockStringMarker> iter = markers.listIterator();
try {
while(true) {
if(!iter.hasNext()) {
iter.add(marker);
break;
}
WarlockStringMarker cur = iter.next();
if(cur.getEnd() > marker.getStart()) {
if(marker.getEnd() > cur.getStart()) {
throw new Exception("Bad marker!");
}
iter.previous();
iter.add(marker);
break;
}
}
} catch(Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
public void replaceMarker(String name, WarlockString text) {
WarlockStringMarker marker = null;
IWarlockStyle baseStyle = null;
for(WarlockStringMarker subMarker : markers) {
marker = getMarkerByComponent(name, subMarker);
if(marker != null) {
baseStyle = subMarker.getBaseStyle(marker);
break;
}
}
if(marker == null)
return;
int start = marker.getStart();
int length = marker.getEnd() - start;
boolean atBottom = isAtBottom();
textWidget.replaceTextRange(start, length, text.toString());
marker.clear();
int newLength = text.length();
marker.setEnd(start + newLength);
WarlockStringMarker.updateMarkers(newLength - length, marker, markers);
// Add the new styles to the existing marker
for(WarlockStringMarker newMarker : text.getStyles()) {
marker.addMarker(newMarker.copy(start));
}
/* Break up the ranges and merge overlapping styles because SWT only
* allows 1 style per section
*/
WarlockStringMarker markerWithStyle = marker.clone();
markerWithStyle.setStyle(baseStyle);
LinkedList<StyleRange> newStyles = new LinkedList<StyleRange>();
getMarkerStyles(markerWithStyle, new StyleRangeWithData(), newStyles);
showStyles(newStyles, marker.getStart(), marker.getEnd());
postTextChange(atBottom, start);
}
private WarlockStringMarker getMarkerByComponent(String componentName,
WarlockStringMarker marker) {
String myName = marker.getComponentName();
if(myName != null && myName.equals(componentName))
return marker;
for(WarlockStringMarker subMarker : marker.getSubMarkers()) {
WarlockStringMarker result = getMarkerByComponent(componentName, subMarker);
if(result != null)
return result;
}
return null;
}
private void constrainLineLimit(boolean atBottom) {
// 'status' is a pointer that allows us to change the object in our parent..
// in this method... it is intentional.
if (lineLimit > 0) {
int lines = textWidget.getLineCount();
if (lines > lineLimit) {
int linesToRemove = lines - lineLimit;
int charsToRemove = textWidget.getOffsetAtLine(linesToRemove);
if(atBottom) {
textWidget.replaceTextRange(0, charsToRemove, "");
updateMarkers(-charsToRemove);
} else {
int pixelsToRemove = textWidget.getLinePixel(linesToRemove);
textWidget.replaceTextRange(0, charsToRemove, "");
updateMarkers(-charsToRemove);
if(pixelsToRemove < 0)
textWidget.setTopPixel(-pixelsToRemove);
}
}
}
}
public void setScrollDirection(int dir) {
if (dir == SWT.DOWN || dir == SWT.UP)
doScrollDirection = dir;
// TODO: Else throw an error
}
public void setIgnoreEmptyLines(boolean ignoreLines) {
this.ignoreEmptyLines = ignoreLines;
}
public StyledText getTextWidget() {
return textWidget;
}
}