/* * 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.internal.spelling; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.text.ITextListener; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.TextEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.xmind.ui.texteditor.IControlContentAdapter2; import org.xmind.ui.texteditor.IMenuContributor; import org.xmind.ui.texteditor.ISpellingActivation; import org.xmind.ui.texteditor.ISpellingSupport; import org.xmind.ui.texteditor.StyledTextContentAdapter; import com.swabunga.spell.event.SpellCheckEvent; import com.swabunga.spell.event.SpellCheckListener; import com.swabunga.spell.event.SpellChecker; import com.swabunga.spell.event.StringWordTokenizer; public class SpellingHelper implements ISpellingActivation, Listener, ITextListener { private static final long CHECK_DELAY = 200; private class SuggestionAction extends Action { private SpellCheckEvent range; private String suggestion; public SuggestionAction(SpellCheckEvent range, String suggestion) { super(range.getInvalidWord() + " -> " + suggestion); //$NON-NLS-1$ this.range = range; this.suggestion = suggestion; } public void run() { if (!isActive()) return; String old = contentAdapter.getControlContents(control); int oldLength = old.length(); int start = range.getWordContextPosition(); String invalidWord = range.getInvalidWord(); int invalidLength = invalidWord.length(); if (start < oldLength && start + invalidLength <= oldLength) { contentAdapter.replaceControlContents(control, start, invalidLength, suggestion); check(Display.getCurrent()); } } } private class NewWordAction extends Action { private SpellCheckEvent range; public NewWordAction(SpellCheckEvent range) { super(Messages.addToDictionary); this.range = range; } public void run() { addToDict(Display.getCurrent(), range); } } private static class NoSuggestionAction extends Action { public NoSuggestionAction() { super(Messages.noSpellSuggestion); setEnabled(false); } } private class SpellingMenuContributor implements IMenuContributor { private SpellCheckEvent getCurrentRange() { int pos = contentAdapter.getCursorPosition(control); for (Entry<Integer, SpellCheckEvent> en : ranges.entrySet()) { SpellCheckEvent range = en.getValue(); int start = en.getKey().intValue(); int length = range.getInvalidWord().length(); if (start <= pos && pos <= start + length) { return range; } } return null; } public void fillMenu(IMenuManager menu) { SpellCheckEvent range = getCurrentRange(); if (range != null) { List list = range.getSuggestions(); if (list.isEmpty()) { menu.add(new NoSuggestionAction()); } else { for (Object o : list) { String suggestion = o.toString(); menu.add(new SuggestionAction(range, suggestion)); } } menu.add(new Separator()); menu.add(new NewWordAction(range)); } } } private class CheckJob extends Job implements SpellCheckListener { private Display display; private long start = -1; private boolean rescheduling = false; public CheckJob() { super(Messages.spellCheckProgress_Text); setSystem(true); } public synchronized void check(Display display) { if (display == null || display.isDisposed() || !isActive()) return; if (start == -1) { // not scheduled this.display = display; schedule(); } else if (start == -2) { // working rescheduling = true; } else { // scheduling start = System.currentTimeMillis(); } } public void dispose() { rescheduling = false; cancel(); } protected IStatus run(IProgressMonitor monitor) { try { return doRun(monitor); } finally { start = -1; if (rescheduling) { rescheduling = false; schedule(); } } } protected IStatus doRun(final IProgressMonitor monitor) { start = System.currentTimeMillis(); if (monitor.isCanceled() || display.isDisposed() || !isActive()) return Status.CANCEL_STATUS; final String[] context = new String[1]; display.syncExec(new Runnable() { public void run() { if (monitor.isCanceled() || display.isDisposed() || !isActive()) return; context[0] = contentAdapter.getControlContents(control); } }); if (monitor.isCanceled() || display.isDisposed() || !isActive()) return Status.CANCEL_STATUS; if (context[0] == null || "".equals(context[0])) {//$NON-NLS-1$ if (!ranges.isEmpty()) { ranges.clear(); redraw(display); } return Status.OK_STATUS; } while (System.currentTimeMillis() < start + CHECK_DELAY) { if (monitor.isCanceled() || display.isDisposed() || !isActive()) return Status.CANCEL_STATUS; try { Thread.sleep(1); } catch (InterruptedException e) { return Status.CANCEL_STATUS; } } start = -2; if (monitor.isCanceled() || display.isDisposed() || !isActive()) return Status.CANCEL_STATUS; display.syncExec(new Runnable() { public void run() { if (monitor.isCanceled() || display.isDisposed() || !isActive()) return; context[0] = contentAdapter.getControlContents(control); } }); if (monitor.isCanceled() || display.isDisposed() || !isActive()) return Status.CANCEL_STATUS; if (context[0] != null) { ranges.clear(); if (!"".equals(context[0])) { //$NON-NLS-1$ SpellChecker theSpellChecker = spellChecker; theSpellChecker.addSpellCheckListener(this); theSpellChecker .checkSpelling(new StringWordTokenizer(context[0])); theSpellChecker.removeSpellCheckListener(this); } if (monitor.isCanceled() || display.isDisposed() || !isActive()) return Status.CANCEL_STATUS; redraw(display); } return Status.OK_STATUS; } public void spellingError(SpellCheckEvent event) { int start = event.getWordContextPosition(); ranges.put(Integer.valueOf(start), event); } } private static class Line { int x1, x2, y; } private ISpellingSupport support; private ITextViewer viewer; private Control control; private IControlContentAdapter2 contentAdapter; private SpellChecker spellChecker; private Map<Integer, SpellCheckEvent> ranges = new HashMap<Integer, SpellCheckEvent>(); private SpellingMenuContributor contributor; private CheckJob job = null; private boolean disposed = false; private static Map<Integer, Line> lineCache = new HashMap<Integer, Line>(); private ISpellCheckerVisitor visitor = null; public SpellingHelper(ISpellingSupport support, ITextViewer viewer) { this.support = support; this.viewer = viewer; this.control = viewer.getTextWidget(); this.contentAdapter = new StyledTextContentAdapter(); init(viewer); } public SpellingHelper(ISpellingSupport support, Control control, IControlContentAdapter2 adapter) { this.support = support; this.viewer = null; this.control = control; this.contentAdapter = adapter; init(control); } private void init(ITextViewer viewer) { viewer.addTextListener(this); init(); } private void init(Control control) { control.addListener(SWT.Modify, this); init(); } /** * */ private void init() { control.addListener(SWT.Paint, this); control.addListener(SWT.Dispose, this); final Display display = Display.getCurrent(); visitor = new ISpellCheckerVisitor() { public void handleWith(SpellChecker spellChecker) { if (control == null || control.isDisposed() || disposed) return; SpellingHelper.this.spellChecker = spellChecker; check(display); } }; SpellCheckerAgent.visitSpellChecker(visitor); SpellCheckerAgent.addListener(visitor); } /** * @param range */ private void addToDict(final Display display, final SpellCheckEvent range) { SpellCheckerAgent.visitSpellChecker(new ISpellCheckerVisitor() { //This listener is just to get value from a long time delay. public void handleWith(SpellChecker spellChecker) { if (!isActive()) return; spellChecker.addToDictionary(range.getInvalidWord()); check(display); } }); } /** * @param gc */ private void paintSpellError(GC gc) { if (ranges.isEmpty()) return; int lineStyle = gc.getLineStyle(); int lineWidth = gc.getLineWidth(); Color lineColor = gc.getForeground(); gc.setLineWidth(2); gc.setLineStyle(SWT.LINE_DOT); gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_RED)); int charCount = contentAdapter.getControlContents(control).length(); Rectangle clipping = gc.getClipping(); lineCache.clear(); for (Object obj : ranges.values().toArray()) { SpellCheckEvent range = (SpellCheckEvent) obj; int start = range.getWordContextPosition(); if (start < 0 || start >= charCount) continue; int length = Math.min(range.getInvalidWord().length(), charCount - start); if (length <= 0) continue; drawLines(gc, start, start + length - 1, clipping); } gc.setLineWidth(lineWidth); gc.setLineStyle(lineStyle); gc.setForeground(lineColor); } private void drawLines(GC gc, int start, int end, Rectangle clipping) { Line startLine = getLine(gc, start); Line endLine = getLine(gc, end); if (startLine.y == endLine.y) { gc.drawLine(startLine.x1, startLine.y, endLine.x2, endLine.y); } else if (start < end) { int mid = (start + end) / 2; drawLines(gc, start, mid, clipping); if (mid < end) { drawLines(gc, mid + 1, end, clipping); } } } private Line getLine(GC gc, int offset) { Line p = lineCache.get(Integer.valueOf(offset)); if (p == null) { p = new Line(); Point loc = contentAdapter.getLocationAtOffset(control, offset + 1); int h = contentAdapter.getLineHeightAtOffset(control, offset + 1); p.y = loc.y + h - 1; p.x2 = loc.x; p.x1 = p.x2 - gc.stringExtent( contentAdapter.getControlContents(control, offset, 1)).x; lineCache.put(Integer.valueOf(offset), p); } return p; } /** * */ private void check(Display display) { if (!isActive()) return; if (job == null) job = new CheckJob(); job.check(display); } public void handleEvent(Event event) { if (control.isDisposed()) return; int type = event.type; switch (type) { case SWT.Modify: handleTextModified(event); break; case SWT.Paint: paintSpellError(event.gc); break; case SWT.Dispose: handleWidgetDispose(); break; } } public ISpellingSupport getSpellingSupport() { return support; } public boolean isActive() { return !disposed && spellChecker != null && control != null && !control.isDisposed(); } @SuppressWarnings("unchecked") public Object getAdapter(Class adapter) { if (adapter == IMenuContributor.class) { if (contributor == null) contributor = new SpellingMenuContributor(); return contributor; } return null; } private void handleTextModified(Event event) { check(event.display); } private void handleWidgetDispose() { deactivate(); } private void deactivate() { SpellCheckerAgent.removeListener(visitor); if (viewer != null) { viewer.removeTextListener(this); viewer = null; } if (control != null && !control.isDisposed()) { control.removeListener(SWT.Modify, this); control.removeListener(SWT.Paint, this); control.removeListener(SWT.Dispose, this); redraw(control.getDisplay()); control = null; } if (job != null) { job.dispose(); job = null; } ranges.clear(); } void dispose() { deactivate(); disposed = true; } private void redraw(Display display) { display.asyncExec(new Runnable() { public void run() { if (control != null && !control.isDisposed()) { control.redraw(); } } }); } public void textChanged(TextEvent event) { check(Display.getCurrent()); } }