/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.plugin.spellcheck; import java.awt.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import net.java.sip.communicator.util.*; /** * Notifies subclasses when words are changed and lets them decide if text * should be underlined with a red squiggle. Text appended to the end isn't * formatted until the word's completed. * * @author Damian Johnson */ abstract class DocUnderliner implements DocumentListener { private static final Logger logger = Logger.getLogger(DocUnderliner.class); private static final Color UNDERLINE_COLOR = new Color(255, 100, 100); private static final DefaultHighlighter.DefaultHighlightPainter UNDERLINER; private final Highlighter docHighlighter; private final CaretListener endChecker; private boolean isEnabled = true; static { UNDERLINER = new DefaultHighlighter.DefaultHighlightPainter(UNDERLINE_COLOR) { @Override public Shape paintLayer(Graphics g, int offs0, int offs1, Shape area, JTextComponent comp, View view) { Color color = getColor(); if (color == null) g.setColor(comp.getSelectionColor()); else g.setColor(color); if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset()) { // contained in view, can just use bounds drawWavyLine(g, area.getBounds()); return area; } else { // should only render part of View try { Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward, area); drawWavyLine(g, shape.getBounds()); return shape.getBounds(); } catch (BadLocationException exc) { String msg = "Bad bounds (programmer error in spell checker)"; logger.error(msg, exc); return area; // can't render } } } private void drawWavyLine(Graphics g, Rectangle bounds) { int y = (int) (bounds.getY() + bounds.getHeight()); int x1 = (int) bounds.getX(); int x2 = (int) (bounds.getX() + bounds.getWidth()); boolean upperCurve = true; for (int i = x1; i < x2 - 2; i += 3) { if (upperCurve) g.drawArc(i, y - 2, 3, 3, 0, 180); else g.drawArc(i, y - 2, 3, 3, 180, 180); upperCurve = !upperCurve; } } }; } { this.endChecker = new CaretListener() { private boolean atEnd = false; public void caretUpdate(CaretEvent event) { if (event.getSource() instanceof JTextComponent) { JTextComponent comp = (JTextComponent) event.getSource(); Document doc = comp.getDocument(); boolean currentlyAtEnd = event.getDot() == doc.getLength(); if (isEnabled && this.atEnd && !currentlyAtEnd) { String text = comp.getText(); Word changed = Word.getWord(text, text.length() - 1, false); format(changed); promptRepaint(); } this.atEnd = currentlyAtEnd; } } }; } /** * Queries to see if a word should be underlined. This is called on every * internal change and whenever a word's completed so it should be a * lightweight process. * * @param word word to be checked * @return true if the word should be underlined, false otherwise */ abstract boolean getFormatting(String word); /** * Provides the index of the character the cursor is in front of. * * @return index of caret */ abstract int getCaretPosition(); /** * Prompts the text field to repaint. */ abstract void promptRepaint(); public static void main(String[] args) { // Basic demo that underlines words containing "foo" JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final JEditorPane editorPane = new JEditorPane(); editorPane.setPreferredSize(new Dimension(400, 500)); final DocUnderliner formatter = new DocUnderliner(editorPane.getHighlighter()) { @Override boolean getFormatting(String word) { return word.contains("foo"); } @Override int getCaretPosition() { return editorPane.getCaretPosition(); } @Override void promptRepaint() { editorPane.repaint(); } }; editorPane.getDocument().addDocumentListener(formatter); editorPane.addCaretListener(formatter.getEndChecker()); frame.add(editorPane); frame.pack(); frame.setVisible(true); } DocUnderliner(Highlighter docHighlighter) { this.docHighlighter = docHighlighter; } public void insertUpdate(DocumentEvent event) { if (!this.isEnabled) return; try { Document doc = event.getDocument(); String text = doc.getText(0, doc.getLength()); if (event.getLength() == 1) { char changeChar = text.charAt(event.getOffset()); if (getCaretPosition() == text.length() - 1) { if (!Character.isLetter(changeChar)) { // finished last word Word last = Word.getWord(text, text.length() - 1, true); format(last); } else { // new character at end (ensure it isn't initially // underlined) clearUnderlining(event.getOffset(), event.getOffset() + 1); } } else { if (Character.isLetter(changeChar)) { // change within word Word changed; int previousIndex = Math.max(0, event.getOffset() - 1); if (Character.isLetter(text.charAt(previousIndex))) changed = Word.getWord(text, event.getOffset(), true); else changed = Word.getWord(text, event.getOffset(), false); format(changed); } else { // dividing a word - need to check both sides Word firstWord = Word.getWord(text, event.getOffset(), true); Word secondWord = Word.getWord(text, event.getOffset() + 1, false); format(firstWord); format(secondWord); } } } else { // pasting in a chunk of text (checks all words in modified // range) Word changed = Word.getWord(text, event.getOffset(), true); int wordStart = changed.getStart(); while (wordStart < event.getOffset() + event.getLength()) { format(changed); int end = Math.min(changed.getStart() + changed.getText().length() + 1, text.length()); changed = Word.getWord(text, end, false); wordStart = end; } } } catch (BadLocationException exc) { String msg = "Bad bounds (programmer error in spell checker)"; logger.error(msg, exc); } catch (Throwable exc) { logger.error("Error words processing", exc); } promptRepaint(); } public void removeUpdate(DocumentEvent event) { if (!this.isEnabled) return; try { Document doc = event.getDocument(); String text = doc.getText(0, doc.getLength()); if (text.length() != 0) { Word changed; if (event.getOffset() == 0 || !Character.isLetter(text.charAt(event.getOffset() - 1))) { changed = Word.getWord(text, event.getOffset(), false); } else { changed = Word.getWord(text, event.getOffset() - 1, true); } format(changed); } promptRepaint(); } catch (BadLocationException exc) { String msg = "Bad bounds (programmer error in spell checker)"; logger.error(msg, exc); } catch (Throwable exc) { logger.error("Error words processing", exc); } } public void changedUpdate(DocumentEvent e) { } /** * Provides a listener that prompts the last word to be checked when the * cursor moves away from it. * * @return listener for caret position that formats last word when * appropriate */ public CaretListener getEndChecker() { return this.endChecker; } /** * Formats the word with the appropriate underlining (or lack thereof). * * @param word word to be formatted */ public void format(Word word) { if (!this.isEnabled) return; String text = word.getText(); if (text.length() > 0) { clearUnderlining(word.getStart(), word.getStart() + text.length()); if (getFormatting(text)) underlineRange(word.getStart(), word.getStart() + text.length()); } } /** * Sets a range in the editor to be underlined. * * @param start start of range to be underlined * @param end end of range to be underlined */ private void underlineRange(int start, int end) { if (end > start) { try { if (this.isEnabled) this.docHighlighter.addHighlight(start, end, UNDERLINER); } catch (BadLocationException exc) { String msg = "Bad bounds (programmer error in spell checker)"; logger.error(msg, exc); } } } /** * Clears any underlining that spans to include the given range. Since * formatting is defined by ranges this will likely clear more than the * defined range. * * @param start start of range in which to clear underlining * @param end end of range in which to clear underlining */ private void clearUnderlining(int start, int end) { if (end > start) { // removes highlighting if visible if (this.isEnabled) { for (Highlighter.Highlight highlight : this.docHighlighter .getHighlights()) { if ((highlight.getStartOffset() <= start && highlight .getEndOffset() > start) || (highlight.getStartOffset() < end && highlight .getEndOffset() >= end)) { this.docHighlighter.removeHighlight(highlight); } } } } } public void setEnabled(boolean enable, String message) { if (this.isEnabled != enable) { this.isEnabled = enable; if (this.isEnabled) reset(message); else this.docHighlighter.removeAllHighlights(); promptRepaint(); } } /** * Clears underlining and re-evaluates message's contents * * @param message textual contents of document */ public void reset(String message) { if (!this.isEnabled) return; // clears previous underlined sections this.docHighlighter.removeAllHighlights(); // runs over message if (message.length() > 0) { Word changed = Word.getWord(message, 0, true); int wordStart = changed.getStart(); while (wordStart < message.length()) { format(changed); int end = Math.min(changed.getStart() + changed.getText().length() + 1, message.length()); changed = Word.getWord(message, end, false); wordStart = end; } } promptRepaint(); } }