/* ****************************************************************************** * Copyright (c) 2006-2012 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ package org.xmind.ui.richtext; import static org.xmind.ui.richtext.ImagePlaceHolder.PLACE_HOLDER; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.TextViewer; import org.eclipse.jface.util.Util; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.TextStyle; import org.eclipse.swt.widgets.Display; import org.xmind.ui.resources.FontUtils; /** * @author Frank Shaka */ public class RichTextUtils { private static class SystemColorFactory { private static Color getColor(final int which) { Display display = Display.getCurrent(); if (display != null) return display.getSystemColor(which); display = Display.getDefault(); final Color result[] = new Color[1]; display.syncExec(new Runnable() { public void run() { synchronized (result) { result[0] = Display.getCurrent().getSystemColor(which); } } }); synchronized (result) { return result[0]; } } } static final String LineDelimiter = System.getProperty("line.separator"); //$NON-NLS-1$ public static Font DEFAULT_FONT = JFaceResources.getDefaultFont(); public static FontData DEFAULT_FONT_DATA = DEFAULT_FONT.getFontData()[0]; public static Color DEFAULT_FOREGROUND = SystemColorFactory .getColor(SWT.COLOR_BLACK); public static Color DEFAULT_BACKGROUND = SystemColorFactory .getColor(SWT.COLOR_WHITE); public static final char INDENT_CHAR = '\t'; public static final String EMPTY = ""; //$NON-NLS-1$ public static final StyleRange DEFAULT_STYLE; static { DEFAULT_STYLE = new StyleRange(0, 0, DEFAULT_FOREGROUND, DEFAULT_BACKGROUND, 0); DEFAULT_STYLE.font = DEFAULT_FONT; DEFAULT_STYLE.borderColor = DEFAULT_FOREGROUND; DEFAULT_STYLE.borderStyle = SWT.NONE; DEFAULT_STYLE.strikeout = false; DEFAULT_STYLE.strikeoutColor = DEFAULT_FOREGROUND; DEFAULT_STYLE.underline = false; DEFAULT_STYLE.underlineColor = DEFAULT_FOREGROUND; DEFAULT_STYLE.underlineStyle = SWT.UNDERLINE_SINGLE; } public static final LineStyle DEFAULT_LINE_STYLE = new LineStyle(); private RichTextUtils() { } public static boolean isItalic(StyleRange style) { return hasFontStyle(style, SWT.ITALIC); } public static boolean setItalic(StyleRange style, boolean italic) { return changeFontStyle(style, SWT.ITALIC, italic); } public static boolean isBold(StyleRange style) { return hasFontStyle(style, SWT.BOLD); } public static boolean setBold(StyleRange style, boolean bold) { return changeFontStyle(style, SWT.BOLD, bold); } public static boolean hasFontStyle(StyleRange style, int singleStyle) { return (getFontStyle(style) & singleStyle) != 0; } public static boolean changeFontStyle(StyleRange style, int singleStyle, boolean value) { int fontStyle = getFontStyle(style); int changedFontStyle = getChangedFontStyle(fontStyle, singleStyle, value); if (fontStyle == changedFontStyle) return false; setFontStyle(style, changedFontStyle); return true; } public static int getChangedFontStyle(int fontStyle, int singleStyle, boolean value) { if (value) { fontStyle |= singleStyle; } else { fontStyle &= ~singleStyle; } return fontStyle; } public static int getFontStyle(StyleRange style) { return style.font == null ? style.fontStyle : style.font.getFontData()[0].getStyle(); } public static void setFontStyle(StyleRange style, int fontStyle) { style.fontStyle = fontStyle; if (style.font != null) { style.font = FontUtils.getStyled(style.font, fontStyle); // if ((fontStyle & SWT.BOLD) != 0) { // style.font = FontUtils.getBold(style.font); // } // if ((fontStyle & SWT.ITALIC) != 0) { // style.font = FontUtils.getItalic(style.font); // } } } public static int getFontStyle(boolean bold, boolean italic) { int fontStyle = SWT.NORMAL; if (bold) fontStyle |= SWT.BOLD; if (italic) fontStyle |= SWT.ITALIC; return fontStyle; } public static String getFontFace(StyleRange style) { return getFont(style).getFontData()[0].getName(); } public static int getFontSize(StyleRange style) { return getFont(style).getFontData()[0].getHeight(); } public static Font getFont(StyleRange style) { return style.font == null ? DEFAULT_FONT : style.font; } public static Color getForeground(StyleRange style) { return style.foreground == null ? DEFAULT_FOREGROUND : style.foreground; } public static Color getBackground(StyleRange style) { return style.background == null ? DEFAULT_BACKGROUND : style.background; } public static boolean setFont(StyleRange style, Font font) { if (font == style.font || (font != null && equals(font, style.font))) return false; style.font = font; return true; } public static boolean setFontFace(StyleRange style, String name) { if (name == null) { name = DEFAULT_FONT_DATA.getName(); } if (getFontFace(style).equals(name)) { return false; } style.font = FontUtils.getNewName(getFont(style), name); return true; } public static boolean setFontSize(StyleRange style, int size) { if (size <= 0) { size = DEFAULT_FONT_DATA.getHeight(); } if (getFontSize(style) == size) return false; style.font = FontUtils.getNewHeight(getFont(style), size); return true; } public static boolean setForeground(StyleRange style, Color color) { if (color == style.foreground || (color != null && color.equals(style.foreground))) return false; style.foreground = color; return true; } public static boolean setBackground(StyleRange style, Color color) { if (color == style.background || (color != null && color.equals(style.background))) return false; style.background = color; return true; } public static List<Hyperlink> getHyperlinksInRange(Hyperlink[] hyperlinks, int start, int end) { List<Hyperlink> results = new ArrayList<Hyperlink>(hyperlinks.length); for (Hyperlink hyperlink : hyperlinks) { if (hyperlink.start <= end && hyperlink.end() >= start) { results.add(hyperlink); } } return results; } public static LineStyle updateLineStylePositions(int startLine, int oldLineCount, int newLineCount, List<LineStyle> lineStyles, IDocument document) { int oldEndLine = startLine + oldLineCount; int deltaLines = newLineCount - oldLineCount; LineStyle firstInRange = null; int firstInRangeIndex = 0; for (int i = 0; i < lineStyles.size();) { LineStyle current = lineStyles.get(i); int currentLineIndex = current.lineIndex; if (currentLineIndex == 0) { String content = document.get(); if ("".equals(content)) //$NON-NLS-1$ current.bulletStyle = LineStyle.NONE_STYLE; } if (currentLineIndex < startLine) { i++; continue; } if (currentLineIndex >= oldEndLine) { current.lineIndex += deltaLines; i++; continue; } if (currentLineIndex == startLine) { firstInRange = current; firstInRangeIndex = i; i++; } else { lineStyles.remove(i); } } int newEndLine = startLine + newLineCount; if (firstInRange != null) { int styleIndex = firstInRangeIndex + 1; for (int lineIndex = firstInRange.lineIndex + 1; lineIndex < newEndLine; lineIndex++) { LineStyle lineStyle = (LineStyle) firstInRange.clone(); lineStyle.lineIndex = lineIndex; lineStyle.indent = calcLineIndentCount(document, lineIndex); lineStyles.add(styleIndex, lineStyle); styleIndex++; } } else { int styleIndex = firstInRangeIndex; for (int lineIndex = startLine; lineIndex < newEndLine; lineIndex++) { int indent = calcLineIndentCount(document, lineIndex); if (indent > 0) { LineStyle lineStyle = (LineStyle) RichTextUtils.DEFAULT_LINE_STYLE .clone(); lineStyle.indent = indent; lineStyles.add(styleIndex, lineStyle); styleIndex++; } } } return firstInRange; } public static void updateImagePositions(int start, int oldLength, int newLength, List<ImagePlaceHolder> images) { int oldEnd = start + oldLength; int deltaOffset = newLength - oldLength; int imagePlaceHolderLength = PLACE_HOLDER.length(); for (int i = 0; i < images.size();) { ImagePlaceHolder current = images.get(i); int currentStart = current.offset; int currentEnd = currentStart + imagePlaceHolderLength; // before range if (currentEnd <= start) { // simply skip i++; continue; } // after range if (currentStart >= oldEnd) { // push back to fit the new range current.offset += deltaOffset; // skip i++; continue; } // remove all images within the old range images.remove(i); } } /** * @param start * --the position of Cursor * @param oldLength * --the selection text's length * @param newLength * --later input the text's length * @param hyperlinks */ public static void updateHyperlinksPositions(int start, int oldLength, int newLength, List<Hyperlink> hyperlinks) { int oldEnd = start + oldLength; int deltaOffset = newLength - oldLength; for (int i = 0; i < hyperlinks.size();) { Hyperlink currentHyper = hyperlinks.get(i); int currentStart = currentHyper.start; int currentEnd = currentHyper.end(); //in front of range if (oldEnd <= currentStart) { currentHyper.start += deltaOffset; i++; continue; } //behind of range if (start >= currentEnd) { i++; continue; } //the overlap at the front half of range if (oldEnd <= currentEnd && oldEnd > currentStart) { if (start >= currentStart) { currentHyper.length += deltaOffset; if (currentHyper.length == 0) { hyperlinks.remove(i); continue; } } else { currentHyper.length = currentHyper.end() - oldEnd; currentHyper.start = oldEnd + deltaOffset; } i++; continue; } // the overlap at the behind half of range if (start >= currentStart && start < currentEnd) { if (oldEnd <= currentEnd) { currentHyper.length += deltaOffset; if (currentHyper.length == 0) { hyperlinks.remove(i); continue; } } else { currentHyper.length = start - currentStart; } i++; continue; } hyperlinks.remove(i); } } public static void replaceStyleRanges(int start, int oldLength, int newLength, List<StyleRange> styles, StyleRange replacement) { int oldEnd = start + oldLength; int deltaOffset = newLength - oldLength; StyleRange last = null; StyleRange lastStyleBeforeRange = null; int lastIndexBeforeRange = -1; for (int styleIndex = 0; styleIndex < styles.size();) { StyleRange current = styles.get(styleIndex); int currentStart = current.start; int currentLength = current.length; int currentEnd = currentStart + currentLength; // before the old range if (currentEnd <= start) { last = current; lastStyleBeforeRange = current; lastIndexBeforeRange = styleIndex; // simply skip to next style styleIndex++; continue; } // after the old range if (currentStart >= oldEnd) { // push back to fit the new range current.start += deltaOffset; // merge with the last style if possible if (merge(current, last)) { styles.remove(styleIndex); } else { styleIndex++; } continue; } // some part before the old range if (currentStart < start) { // make a copy of that part to protect it from modification StyleRange beforePart = (StyleRange) current.clone(); beforePart.length = start - currentStart; styles.add(styleIndex, beforePart); // refresh the current style range currentStart = start; current.start = currentStart; current.length = currentEnd - currentStart; last = beforePart; lastStyleBeforeRange = beforePart; lastIndexBeforeRange = styleIndex; styleIndex++; } // some part after the old range if (currentEnd > oldEnd) { // make a copy of that part to protect it from modification StyleRange afterPart = (StyleRange) current.clone(); afterPart.start = oldEnd; afterPart.length = currentEnd - oldEnd; // refresh the current style range styles.add(styleIndex + 1, afterPart); currentEnd = oldEnd; current.length = currentEnd - currentStart; } // remove all style ranges within the old range styles.remove(styleIndex); } StyleRange newStyle = (StyleRange) replacement.clone(); newStyle.start = start; newStyle.length = newLength; if (!merge(newStyle, lastStyleBeforeRange)) { styles.add(lastIndexBeforeRange + 1, newStyle); } } public static boolean modifyTextStyles(int start, int length, List<StyleRange> styles, IStyleRangeModifier modifier) { if (length == 0 || modifier == null) return false; int end = start + length; boolean changed = false; StyleRange last = null; int unhandledStart = start; int unhandledEnd = end; int unhandledIndex = 0; for (int styleIndex = 0; styleIndex < styles.size();) { StyleRange current = styles.get(styleIndex); int currentStart = current.start; int currentEnd = currentStart + current.length; // before selection range if (currentEnd <= start) { // simply skip to next style last = current; styleIndex++; unhandledIndex = styleIndex; continue; } // after selection range if (currentStart >= end) { // merge with the last style if possible if (merge(current, last)) { styles.remove(styleIndex); changed = true; } // loop should end as the following style ranges // are all beyond the selection range and should // remain unmodified break; } // some part before selection range if (currentStart < start) { unhandledStart = currentEnd; // make a copy of that part to protect it from modification StyleRange beforePart = (StyleRange) current.clone(); beforePart.length = start - currentStart; styles.add(styleIndex, beforePart); changed = true; last = beforePart; // refresh the current style range currentStart = start; current.start = currentStart; current.length = currentEnd - currentStart; styleIndex++; } // some part after selection range if (currentEnd > end) { unhandledEnd = currentStart; // make a copy of that part to protect it from modification StyleRange afterPart = (StyleRange) current.clone(); afterPart.start = end; afterPart.length = currentEnd - end; styles.add(styleIndex + 1, afterPart); changed = true; // refresh the current style range currentEnd = end; current.length = currentEnd - currentStart; } // check if there's still unhandled regions before this range if (currentStart >= unhandledStart) { // modify and add the unhandled region before this range if (currentStart > unhandledStart) { StyleRange newStyle = (StyleRange) RichTextUtils.DEFAULT_STYLE .clone(); newStyle.start = unhandledStart; newStyle.length = currentStart - unhandledStart; modifier.modify(newStyle); // merge with the last style if possible if (!merge(newStyle, last)) { styles.add(styleIndex, newStyle); last = newStyle; styleIndex++; } changed = true; } unhandledStart = currentEnd; } // modify current style changed |= modifier.modify(current); // merge with the last style if possible if (merge(current, last)) { styles.remove(styleIndex); changed = true; } else { last = current; styleIndex++; } unhandledIndex = styleIndex; } // check if there's still unhandled regions before this range if (unhandledEnd > unhandledStart) { // modify and add the unhandled region before this range StyleRange newStyle = (StyleRange) RichTextUtils.DEFAULT_STYLE .clone(); newStyle.start = unhandledStart; newStyle.length = unhandledEnd - unhandledStart; modifier.modify(newStyle); // merge with the last style if possible if (!merge(newStyle, last)) { styles.add(unhandledIndex, newStyle); changed = true; } } return changed; } public static boolean merge(StyleRange current, StyleRange last) { if (current == null) return false; if (current.length == 0) return true; if (last != null && last.start + last.length == current.start) { if (isSimilar(current, last)) { last.length = last.length + current.length; return true; } return false; } return isSimilar(current, RichTextUtils.DEFAULT_STYLE); } public static boolean modifyLineStyles(int startLine, int lineCount, List<LineStyle> lineStyles, ILineStyleModifier modifier) { int endLine = startLine + lineCount; int unhandledLine = startLine; int unhandledIndex = 0; boolean changed = false; for (int i = 0; i < lineStyles.size();) { LineStyle current = lineStyles.get(i); int currentLineIndex = current.lineIndex; if (currentLineIndex < startLine) { i++; unhandledIndex = i; continue; } if (currentLineIndex >= endLine) { break; } if (currentLineIndex > unhandledLine) { LineStyle lineStyle = new LineStyle(unhandledLine); boolean modified = modifier.modify(lineStyle); changed |= modified; if (modified && !lineStyle.isUnstyled()) { lineStyles.add(i, lineStyle); i++; } } else { boolean modified = modifier.modify(current); changed |= modified; if (modified && current.isUnstyled()) { lineStyles.remove(i); } else { i++; } } unhandledIndex = i; unhandledLine++; } for (int i = unhandledLine; i < endLine; i++) { LineStyle lineStyle = new LineStyle(i); boolean modified = modifier.modify(lineStyle); changed |= modified; if (modified && !lineStyle.isUnstyled()) { lineStyles.add(unhandledIndex, lineStyle); } unhandledIndex++; } return changed; } public static int replaceDocumentIndent(IDocument document, int line, int newIndent) throws BadLocationException { int realDeltaIndent = 0; IRegion region = document.getLineInformation(line); int lineOffset = region.getOffset(); int lineLength = region.getLength(); String lineContent = document.get(lineOffset, lineLength); int oldIndent = calcIndentCount(lineContent); if (oldIndent != newIndent) { char[] chars = new char[newIndent]; Arrays.fill(chars, INDENT_CHAR); String value = String.valueOf(chars); document.replace(lineOffset, oldIndent, value); realDeltaIndent = newIndent - oldIndent; } return realDeltaIndent; } public static int modifyDocumentIndent(TextViewer viewer, IDocument document, int line, int deltaIndent) throws BadLocationException { int realDeltaIndent = 0; if (deltaIndent == 0) return realDeltaIndent; IRegion region = document.getLineInformation(line); int lineOffset = region.getOffset(); // StyledText styledText = viewer.getTextWidget(); // styledText.setLineBullet(lineOffset, 1, null); if (deltaIndent > 0) { char[] chars = new char[deltaIndent]; Arrays.fill(chars, INDENT_CHAR); String value = String.valueOf(chars); document.replace(lineOffset, 0, value); realDeltaIndent = deltaIndent; // Bullet bullet = styledText.getLineBullet(lineOffset); // StyleRange style = new StyleRange(); // style.metrics = new GlyphMetrics(0, 0, 80); // Bullet bullet = new Bullet(ST.BULLET_NUMBER | ST.BULLET_TEXT, style); // bullet.text = "."; // styledText.setLineBullet(lineOffset, 1, bullet); // document.replace(0, 0, value); } else if (deltaIndent < 0) { int lineLength = region.getLength(); String lineContent = document.get(lineOffset, lineLength); int oldIndent = calcIndentCount(lineContent); int deleteCount = Math.min(oldIndent, Math.abs(deltaIndent)); if (deleteCount > 0) { document.replace(lineOffset, deleteCount, EMPTY); realDeltaIndent = -deleteCount; } } return realDeltaIndent; } public static int calcLineCount(String text) { int numDelimiter = 0; int length = text.length(); for (int i = 0; i < length;) { if (match(text, i, LineDelimiter)) { numDelimiter++; i += LineDelimiter.length(); } else { i++; } } return numDelimiter + 1; } private static boolean match(String source, int start, String target) { int length = target.length(); if (start + length > source.length()) return false; for (int i = 0; i < length; i++) { char c1 = source.charAt(start + i); char c2 = target.charAt(i); if (c1 != c2) return false; } return true; } public static int calcLineIndentCount(IDocument document, int line) { try { IRegion region = document.getLineInformation(line); int lineOffset = region.getOffset(); int lineLength = region.getLength(); String lineContent = document.get(lineOffset, lineLength); return calcIndentCount(lineContent); } catch (BadLocationException e) { e.printStackTrace(); return 0; } } public static int calcIndentCount(String content) { int indent = 0; for (int i = 0; i < content.length(); i++) { char c = content.charAt(i); if (c != '\t') { return indent; } indent++; } return indent; } public static boolean isSimilar(StyleRange sr1, StyleRange sr2) { if (sr1 == sr2) return true; if (sr1 == null || sr2 == null) return false; if (!equals(sr1, sr2)) return false; if (sr1.fontStyle == sr2.fontStyle) return true; return false; } public static boolean equals(TextStyle style1, TextStyle style2) { if (style1 == style2) return true; if (style1 == null || style2 == null) return false; if (style1.foreground != null) { if (!style1.foreground.equals(style2.foreground)) return false; } else if (style2.foreground != null) return false; if (style1.background != null) { if (!style1.background.equals(style2.background)) return false; } else if (style2.background != null) return false; if (style1.font != null) { if (!equals(style1.font, style2.font)) return false; } else if (style2.font != null) return false; if (style1.metrics != null || style2.metrics != null) return false; if (style1.underline != style2.underline) return false; if (style1.strikeout != style2.strikeout) return false; if (style1.rise != style2.rise) return false; return true; } public static boolean equals(Font f1, Font f2) { if (f1 == f2) return true; if (f1 == null && f2 != null) return false; if (f2 == null && f1 != null) return false; if (!Util.isMac()) return f1.equals(f2); if (f1.isDisposed() || f2.isDisposed()) return false; FontData fd1 = f1.getFontData()[0]; FontData fd2 = f2.getFontData()[0]; return fd1.equals(fd2); } // public static List<LineStyle> reduceLineStyles(int startLine, // List<LineStyle> lineStyles, IRichDocument document) { // if (lineStyles == null || lineStyles.isEmpty()) // return null; // LineStyle lineStyle = lineStyles.get(startLine - 1); // lineStyle.bulletStyle = LineStyle.NONE_STYLE; // lineStyles.remove(startLine); // return lineStyles; // } public static void replaceHyperlinkHref(IRichDocument doc, Hyperlink hyperlink, String newHref) { Hyperlink newHyperlink = (Hyperlink) hyperlink.clone(); newHyperlink.href = newHref; Hyperlink[] oldHyperlinks = doc.getHyperlinks(); List<Hyperlink> newHyperlinks = new ArrayList<Hyperlink>( oldHyperlinks.length); for (Hyperlink oldHyperlink : oldHyperlinks) { if (oldHyperlink.start == hyperlink.start && oldHyperlink.length == hyperlink.length) { newHyperlinks.add(newHyperlink); } else { newHyperlinks.add(oldHyperlink); } } doc.setHyperlinks( newHyperlinks.toArray(new Hyperlink[newHyperlinks.size()])); } }