/*
* JOrtho
*
* Copyright (C) 2005-2008 by i-net software
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA.
*
* Created on 05.12.2007
*/
package com.inet.jortho;
import java.awt.Dialog;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Locale;
import java.util.Properties;
import java.util.WeakHashMap;
import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.JMenu;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JToggleButton;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;
/**
* This class is the major class of the spell checker JOrtho (Java Orthography Checker).
* In the most cases this is the only class that you need to add spell checking to your application.
* First you need to do a one-time registration of your dictionaries. In standalone applications this can
* look like:
* <code><pre>
* SpellChecker.registerDictionaries( new URL("file", null, ""), "en,de", "de" );
* </pre></code>
* and in an applet this will look like:
* <code><pre>
* SpellChecker.registerDictionaries( getCodeBase(), "en,de", "en" );
* </pre></code>
* After this you can register your text component that should have the spell checker features
* (Highlighter, context menu, spell checking dialog).
* This looks like:<code><pre>
* JTextPane text = new JTextPane();
* SpellChecker.register( text );
* </pre></code>
* @author Volker Berlin
*/
public class SpellChecker {
private static class ActionToggleButtonModel extends JToggleButton.ToggleButtonModel {
/**
*
*/
private static final long serialVersionUID = 1L;
private final LanguageAction action;
ActionToggleButtonModel(final LanguageAction action) {
this.action = action;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSelected() {
return Boolean.TRUE.equals(action.getValue(SELECTED_KEY));
}
/**
* {@inheritDoc}
*/
@Override
public void setSelected(boolean b) {
// copy from super.setSelected
final ButtonGroup group = getGroup();
if (group != null) {
// use the group model instead
group.setSelected(this, b);
b = group.isSelected(this);
}
if (isSelected() == b) {
return;
}
action.setSelected(b);
// Send ChangeEvent
fireStateChanged();
// Send ItemEvent
fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, this,
this.isSelected() ? ItemEvent.SELECTED : ItemEvent.DESELECTED));
}
}
private static class DisableLanguageAction extends LanguageAction {
static DisableLanguageAction instance = new DisableLanguageAction();
/**
*
*/
private static final long serialVersionUID = 1L;
private DisableLanguageAction() {
super(Utils.getResource("disable"));
}
@Override
public void actionPerformed(final ActionEvent ev) {
if (!isEnabled()) {
//because multiple MenuItems share the same action that
//also the event occur multiple time
return;
}
setEnabled(false);
setSelected(true);
try {
currentDictionary = null;
final Locale oldLocale = currentLocale;
currentLocale = null;
SpellChecker.fireLanguageChanged(oldLocale);
}
finally {
setEnabled(true);
}
}
@Override
public int compareTo(final LanguageAction obj) {
return equals(obj) ? 0 : 1;
}
@Override
public boolean equals(final Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public void setSelected(final boolean b) {
super.setSelected(b);
}
}
/**
* Action for change the current dictionary language.
*/
private static class LanguageAction extends AbstractAction implements Comparable<LanguageAction> {
// the current active (selected) LanguageAction
private static LanguageAction currentAction;
/**
*
*/
private static final long serialVersionUID = 1L;
private final URL baseURL;
private final String extension;
private final Locale locale;
LanguageAction(final String name) {
super(name);
baseURL = null;
locale = null;
extension = null;
}
LanguageAction(final URL baseURL, final Locale locale, final String extension) {
super(locale.getDisplayLanguage());
this.baseURL = baseURL;
this.locale = locale;
this.extension = extension;
}
public void actionPerformed(final ActionEvent ev) {
if (!isEnabled()) {
//because multiple MenuItems share the same action that
//also the event occur multiple time
return;
}
setEnabled(false);
setSelected(true);
final Locale oldLocale = currentLocale;
currentDictionary = null;
currentLocale = null;
SpellChecker.fireLanguageChanged(oldLocale);
final Thread thread = new Thread(new Runnable() {
public void run() {
try {
final DictionaryFactory factory = new DictionaryFactory();
try {
factory.loadWordList(new URL(baseURL, "dictionary_" + locale + extension));
final UserDictionaryProvider provider = userDictionaryProvider;
if (provider != null) {
final String userWords = provider.getUserWords(locale);
if (userWords != null) {
factory.loadPlainWordList(new StringReader(userWords));
}
}
}
catch (final Exception ex) {
JOptionPane.showMessageDialog(null, ex.toString(), "Error", JOptionPane.ERROR_MESSAGE);
}
currentDictionary = factory.create();
try {
EventQueue.invokeAndWait(new Runnable() {
public void run() {
currentLocale = locale;
SpellChecker.fireLanguageChanged(null);
}
});
}
catch (Exception e) {
e.printStackTrace();
}
}
finally {
setEnabled(true);
}
}
});
thread.setPriority(Thread.NORM_PRIORITY);
thread.setDaemon(true);
thread.start();
}
/**
* Sort the displaynames in the order of the current language
*/
public int compareTo(final LanguageAction obj) {
return toString().compareTo(obj.toString());
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof LanguageAction) {
return locale.equals(((LanguageAction) obj).locale);
}
return false;
}
@Override
public int hashCode() {
return locale.hashCode();
}
/**
* Selects or deselects the menu item.
*
* @param b
* true selects the menu item, false deselects the menu item.
*/
public void setSelected(final boolean b) {
if (b) {
// because there are some problems with multiple ButtonGroups that we duplicate some of the logic here
if (currentAction != null && currentAction != this) {
currentAction.setSelected(false);
}
currentAction = this;
}
putValue(SELECTED_KEY, Boolean.valueOf(b));
}
public Locale getLocale() {
return locale;
}
}
private static String applicationName;
private static Dictionary currentDictionary;
private static Locale currentLocale;
private static final SpellCheckerOptions globalOptions = new SpellCheckerOptions();
private final static ArrayList<LanguageAction> languages = new ArrayList<LanguageAction>();
private final static java.util.Map<LanguageChangeListener, Object> listeners = Collections
.synchronizedMap(new WeakHashMap<LanguageChangeListener, Object>());
/**
* Duplicate of Action.SELECTED_KEY since 1.6
*/
static final String SELECTED_KEY = "SwingSelectedKey";
private static UserDictionaryProvider userDictionaryProvider;
/**
* Adds the LanguageChangeListener. You do not need to remove if the
* LanguageChangeListener is not needed anymore.
* @param listener listener to add
* @see LanguageChangeListener
*/
public static void addLanguageChangeLister(final LanguageChangeListener listener) {
listeners.put(listener, null);
}
/**
* Creates a menu item "Orthography" (or the equivalent depending on the user language) with a
* sub-menu that includes suggestions for a correct spelling.
* You can use this to add this menu item to your own popup.
* @return the new menu.
*/
public static JMenu createCheckerMenu() {
return SpellChecker.createCheckerMenu(null);
}
/**
* Creates a menu item "Orthography" (or the equivalent depending on the user language) with a
* sub-menu that includes suggestions for a correct spelling.
* You can use this to add this menu item to your own popup.
* @param options override the default options for this menu.
* @return the new menu.
*/
public static JMenu createCheckerMenu(final SpellCheckerOptions options) {
return new CheckerMenu(options);
}
/**
* Create a dynamic JPopupMenu with a list of suggestion. You can use the follow code sequence:<pre><code>
* JPopupMenu popup = SpellChecker.createCheckerPopup();
* text.addMouseListener( new PopupListener(popup) );
* </code></pre>
* @return the new JPopupMenu.
* @see #createCheckerMenu()
*/
public static JPopupMenu createCheckerPopup() {
return SpellChecker.createCheckerPopup(null);
}
/**
* Create a dynamic JPopupMenu with a list of suggestion. You can use the follow code sequence:<pre><code>
* JPopupMenu popup = SpellChecker.createCheckerPopup( null );
* text.addMouseListener( new PopupListener(popup) );
* </code></pre>
* @return the new JPopupMenu.
* @see #createCheckerMenu(SpellCheckerOptions)
*/
public static JPopupMenu createCheckerPopup(final SpellCheckerOptions options) {
return new CheckerPopup(options);
}
/**
* Creates a menu item "Languages" (or the equivalent depending on the user language) with a sub-menu
* that lists all available dictionary languages.
* You can use this to add this menu item to your own popup or to your menu bar.
* <code><pre>
* JPopupMenu popup = new JPopupMenu();
* popup.add( SpellChecker.createLanguagesMenu() );
* </pre></code>
* @return the new menu.
*/
public static JMenu createLanguagesMenu() {
final JMenu menu = new JMenu(Utils.getResource("languages"));
final ButtonGroup group = new ButtonGroup();
menu.setEnabled(languages.size() > 0);
for (final LanguageAction action : languages) {
final JRadioButtonMenuItem item = new JRadioButtonMenuItem(action);
//Hack that all items of the action have the same state.
//http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4133141
item.setModel(new ActionToggleButtonModel(action));
menu.add(item);
group.add(item);
}
if (languages.size() > 0) {
menu.addSeparator();
final JRadioButtonMenuItem item = new JRadioButtonMenuItem(DisableLanguageAction.instance);
item.setModel(new ActionToggleButtonModel(DisableLanguageAction.instance));
menu.add(item);
group.add(item);
}
return menu;
}
/**
* Enable or disable the auto spell checking feature (red zigzag line) for a text component.
* If you change the document then you need to reenable it.
*
* @param text
* the JTextComponent that should change
* @param enable
* true, enable the feature.
*/
public static void enableAutoSpell(final JTextComponent text, final boolean enable) {
SpellChecker.enableAutoSpell(text, enable, null);
}
/**
* Enable or disable the auto spell checking feature (red zigzag line) for a text component. If you change the
* document then you need to reenable it.
*
* @param text
* the JTextComponent that should change
* @param enable
* true, enable the feature.
* @param options
* override the default options for this menu.
*/
public static void enableAutoSpell(final JTextComponent text, final boolean enable,
final SpellCheckerOptions options) {
if (enable) {
new AutoSpellChecker(text, options);
}
else {
AutoSpellChecker.disable(text);
}
}
/**
* Enable or disable the popup menu with the menu item "Orthography" and "Languages".
* @param text the JTextComponent that should change
* @param enable true, enable the feature.
*/
public static void enablePopup(final JTextComponent text, final boolean enable) {
if (enable) {
final JPopupMenu menu = new JPopupMenu();
menu.add(SpellChecker.createCheckerMenu());
menu.add(SpellChecker.createLanguagesMenu());
text.addMouseListener(new PopupListener(menu));
}
else {
for (final MouseListener listener : text.getMouseListeners()) {
if (listener instanceof PopupListener) {
text.removeMouseListener(listener);
}
}
}
}
/**
* Enable or disable the F7 key. Pressing the F7 key will display the spell check dialog. This also
* register an Action with the name "spell-checking".
* @param text the JTextComponent that should change
* @param enable true, enable the feature.
*/
public static void enableShortKey(final JTextComponent text, final boolean enable) {
SpellChecker.enableShortKey(text, enable, null);
}
/**
* Enable or disable the F7 key. Pressing the F7 key will display the spell check dialog. This also
* register an Action with the name "spell-checking".
* @param text the JTextComponent that should change
* @param enable true, enable the feature.
* @param options override the default options for this menu.
*/
public static void enableShortKey(final JTextComponent text, final boolean enable, final SpellCheckerOptions options) {
if (enable) {
text.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0), "spell-checking");
text.getActionMap().put("spell-checking", new AbstractAction() {
/**
*
*/
private static final long serialVersionUID = 1L;
public void actionPerformed(final ActionEvent e) {
SpellChecker.showSpellCheckerDialog(text, options);
}
});
}
else {
text.getActionMap().remove("spell-checking");
}
}
/**
* Helper method to fire an Language change event.
*/
private static void fireLanguageChanged(final Locale oldLocale) {
final LanguageChangeEvent ev = new LanguageChangeEvent(currentLocale, oldLocale);
synchronized(listeners){
for (final LanguageChangeListener listener : listeners.keySet()) {
listener.languageChanged(ev);
}
}
}
/**
* Get the title of your application.
*/
public static String getApplicationName() {
return applicationName;
}
/**
* Get the current <code>Dictionary</code>. The current dictionary will be set if the user one select or on calling <code>registerDictionaries</code>.
* @return the current <code>Dictionary</code> or null if not set.
* @see #registerDictionaries(URL, String, String)
*/
public static Dictionary getCurrentDictionary() {
return currentDictionary;
}
/**
* Gets the current <code>Locale</code>. The current Locale will be set if the user selects
* one, or when calling <ode>registerDictionaries</code>.
* @return the current <code>Locale</code> or null if none is set.
* @see #registerDictionaries(URL, String, String)
*/
public static Locale getCurrentLocale() {
return currentLocale;
}
/**
* Get the default SpellCheckerOptions. This object is a singleton. That there is no get method.
* @return the default SpellCheckerOptions
*/
public static SpellCheckerOptions getOptions() {
return globalOptions;
}
/**
* Gets the currently set UserDictionaryProvider. If none has been set then null is returned.
*
* @see #setUserDictionaryProvider(UserDictionaryProvider)
*/
static UserDictionaryProvider getUserDictionaryProvider() {
return SpellChecker.userDictionaryProvider;
}
/**
* Activate the spell checker for the given <code>JTextComponent</code>. The call is equal to register( text,
* true, true ).
*
* @param text
* the JTextComponent
* @throws NullPointerException
* if text is null
*/
public static void register(final JTextComponent text) throws NullPointerException {
SpellChecker.register(text, true, true, true);
}
/**
* Activates the spell checker for the given <code>JTextComponent</code>. You do not need to unregister if the
* JTextComponent is not needed anymore.
*
* @param text
* the JTextComponent
* @param hasPopup
* if true, the JTextComponent is to have a popup menu with the menu item "Orthography" and "Languages".
* @param hasShortKey
* if true, pressing the F7 key will display the spell check dialog.
* @param hasAutoSpell
* if true, the JTextComponent has a auto spell checking.
* @throws NullPointerException
* if text is null
*/
public static void register(final JTextComponent text, final boolean hasPopup, final boolean hasShortKey,
final boolean hasAutoSpell) throws NullPointerException {
if (hasPopup) {
SpellChecker.enablePopup(text, true);
}
if (hasShortKey) {
SpellChecker.enableShortKey(text, true);
}
if (hasAutoSpell) {
SpellChecker.enableAutoSpell(text, true);
}
}
/**
* Registers the available dictionaries. The dictionaries' URLs must have the form "dictionary_xx.xxxxx" and must be
* relative to the baseURL. The available languages and extension of the dictionaries is load from a configuration file.
* The configuration file must also relative to the baseURL and must be named dictionaries.cnf, dictionaries.properties or
* dictionaries.txt. If the dictionary of the active Locale does not exist, the first dictionary is loaded. There is
* only one dictionary loaded in memory at a given time. The configuration file has a Java Properties format. Currently
* there are the follow options:
* <ul>
* <li>languages</li>
* <li>extension</li>
* </ul>
*
* @param baseURL
* the base URL where the dictionaries and configuration file can be found. If null then URL("file", null, "")
* is used.
* @param activeLocale
* the locale that should be loaded and made active. If null or empty then the default locale is used.
*/
public static void registerDictionaries(URL baseURL, final String activeLocale) {
if (baseURL == null) {
try {
baseURL = new URL("file", null, "");
}
catch (final MalformedURLException e) {
// should never occur because the URL is valid
e.printStackTrace();
}
}
InputStream input;
try {
input = new URL(baseURL, "dictionaries.cnf").openStream();
}
catch (final Exception e1) {
try {
input = new URL(baseURL, "dictionaries.properties").openStream();
}
catch (final Exception e2) {
try {
input = new URL(baseURL, "dictionaries.txt").openStream();
}
catch (final Exception e3) {
System.err.println("JOrtho configuration file not found!");
e1.printStackTrace();
e2.printStackTrace();
e3.printStackTrace();
return;
}
}
}
final Properties props = new Properties();
try {
props.load(input);
}
catch (final IOException e) {
e.printStackTrace();
return;
}
final String availableLocales = props.getProperty("languages");
final String extension = props.getProperty("extension", ".ortho");
SpellChecker.registerDictionaries(baseURL, availableLocales, activeLocale, extension);
}
/**
* Registers the available dictionaries. The dictionaries' URLs must have the form "dictionary_xx.ortho" and must be
* relative to the baseURL. If the dictionary of the active Locale does not exist, the first dictionary is loaded.
* There is only one dictionary loaded in memory at a given time.
*
* @param baseURL
* the base URL where the dictionaries can be found. If null then URL("file", null, "") is used.
* @param availableLocales
* a comma separated list of locales
* @param activeLocale
* the locale that should be loaded and made active. If null or empty then the default locale is used.
* @see #setUserDictionaryProvider(UserDictionaryProvider)
*/
public static void registerDictionaries(final URL baseURL, final String availableLocales, final String activeLocale) {
SpellChecker.registerDictionaries(baseURL, availableLocales, activeLocale, ".ortho");
}
/**
* Registers the available dictionaries. The dictionaries' URLs must have the form "dictionary_xx.xxxxx" and must be
* relative to the baseURL. The extension can be set via parameter.
* If the dictionary of the active Locale does not exist, the first dictionary is loaded.
* There is only one dictionary loaded in memory at a given time.
*
* @param baseURL
* the base URL where the dictionaries can be found. If null then URL("file", null, "") is used.
* @param availableLocales
* a comma separated list of locales
* @param activeLocale
* the locale that should be loaded and made active. If null or empty then the default locale is used.
* @param extension
* the file extension of the dictionaries. Some web server like the IIS6 does not support the default ".ortho".
* @see #setUserDictionaryProvider(UserDictionaryProvider)
*/
public static void registerDictionaries(URL baseURL, final String availableLocales, String activeLocale,
final String extension) {
if (baseURL == null) {
try {
baseURL = new URL("file", null, "");
}
catch (final MalformedURLException e) {
// should never occur because the URL is valid
e.printStackTrace();
}
}
for (String locale : availableLocales.split(",")) {
locale = locale.trim().toLowerCase();
if (locale.length() > 0) {
final LanguageAction action = new LanguageAction(baseURL, new Locale(locale), extension);
languages.remove(action);
languages.add(action);
}
}
//sort the display names in order of the current language
Collections.sort(languages);
setLanguage(activeLocale);
}
public static void setLanguage(String activeLocale) {
boolean activeSelected = false;
if (activeLocale != null) {
activeLocale = activeLocale.trim();
for(LanguageAction language:languages){
if (language.getLocale().getLanguage().equals(activeLocale)) {
language.actionPerformed(null);
activeSelected = true;
}
}
}
// if nothing selected then select the first entry
if (!activeSelected && languages.size() > 0) {
DisableLanguageAction.instance.actionPerformed(null);
}
}
public static String getLanguage(){
return currentLocale == null ? null : currentLocale.getLanguage();
}
/**
* Removes the LanguageChangeListener.
* @param listener listener to remove
*/
public static void removeLanguageChangeLister(final LanguageChangeListener listener) {
listeners.remove(listener);
}
/**
* Set the title of your application. This valuse is used as title for info boxes (JOptionPane).
* If not set then the translated "Spelling" is used.
*/
public static void setApplicationName(final String name) {
applicationName = name;
}
/**
* Sets the UserDictionaryProvider. This is needed if the user should be able to add their own words.
* This method must be called before {@link #registerDictionaries(URL, String, String)}.
*
* @param userDictionaryProvider the new UserDictionaryProvider or null
* @see #getUserDictionaryProvider()
* @see #registerDictionaries(URL, String, String)
*/
public static void setUserDictionaryProvider(final UserDictionaryProvider userDictionaryProvider) {
SpellChecker.userDictionaryProvider = userDictionaryProvider;
}
/**
* Show the Spell Checker dialog for the given JTextComponent. It will be do nothing if
* the JTextComponent is not editable or there are no dictionary loaded.
* The action for this method can you receive via:
* <code><pre>
* Action action = text.getActionMap().get("spell-checking");
* </pre></code>
* The action is only available if you have enable the short key (F7).
* @param text JTextComponent to check
* @param options override the default options for this menu.
*/
public static void showSpellCheckerDialog(final JTextComponent text, final SpellCheckerOptions options) {
if (!text.isEditable()) {
// only editable text component have spell checking
return;
}
final Dictionary dictionary = currentDictionary;
if (dictionary != null) {
final Window parent = SwingUtilities.getWindowAncestor(text);
SpellCheckerDialog dialog;
if (parent instanceof Frame) {
dialog = new SpellCheckerDialog((Frame) parent, true, options);
}
else {
dialog = new SpellCheckerDialog((Dialog) parent, true, options);
}
dialog.show(text, dictionary, currentLocale);
}
}
/**
* Removes all spell checker features from the JTextComponent. This does not need to be called
* if the text component is no longer needed.
* @param text the JTextComponent
*/
public static void unregister(final JTextComponent text) {
SpellChecker.enableShortKey(text, false);
SpellChecker.enablePopup(text, false);
SpellChecker.enableAutoSpell(text, false);
}
/**
* There is no instance needed of SpellChecker. All methods are static.
*/
private SpellChecker() {/*nothing*/
}
}