org.languagetool.gui.Main.java Source code

Java tutorial

Introduction

Here is the source code for org.languagetool.gui.Main.java

Source

/* LanguageTool, a natural language style checker 
 * Copyright (C) 2005 Daniel Naber (http://www.danielnaber.de)
 * 
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package org.languagetool.gui;

import java.awt.AWTException;
import java.awt.CheckboxMenuItem;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MenuItem;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.TextAction;
import javax.swing.text.Utilities;

import org.apache.commons.collections4.queue.CircularFifoQueue;
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.languagetool.AnalyzedSentence;
import org.languagetool.AnalyzedToken;
import org.languagetool.AnalyzedTokenReadings;
import org.languagetool.JLanguageTool;
import org.languagetool.Language;
import org.languagetool.rules.Rule;
import org.languagetool.server.HTTPServer;
import org.languagetool.server.HTTPServerConfig;
import org.languagetool.server.PortBindingException;
import org.languagetool.tools.JnaTools;
import org.languagetool.tools.StringTools;

/**
 * A simple GUI to check texts with.
 *
 * @author Daniel Naber
 */
public final class Main {

    static final String EXTERNAL_LANGUAGE_SUFFIX = " (ext.)";

    private static final String HTML_FONT_START = "<font face='Arial,Helvetica'>";
    private static final String HTML_FONT_END = "</font>";

    private static final String TRAY_ICON = "/TrayIcon.png";
    private static final String TRAY_SERVER_ICON = "/TrayIconWithServer.png";
    private static final String TRAY_SMALL_ICON = "/TrayIconSmall.png";
    private static final String TRAY_SMALL_SERVER_ICON = "/TrayIconSmallWithServer.png";
    private static final String TRAY_TOOLTIP = "LanguageTool";
    private static final String TAG_COLOR = "#888888";
    private static final String GUI_STATE = "gui.state";

    private static final int WINDOW_WIDTH = 600;
    private static final int WINDOW_HEIGHT = 550;
    private static final int MAX_RECENT_FILES = 8;

    private final ResourceBundle messages;
    private final List<Language> externalLanguages = new ArrayList<>();

    private JFrame frame;
    private JDialog taggerDialog;
    private JTextPane taggerArea;
    private JTextArea textArea;
    private TextLineNumber textLineNumber;
    private JScrollPane numberedTextAreaPane;
    private JTextPane resultArea;
    private LanguageComboBox languageBox;
    private CheckboxMenuItem enableHttpServerItem;
    private HTTPServer httpServer;

    private TrayIcon trayIcon;
    private boolean closeHidesToTray;
    private boolean isInTray;
    private boolean taggerShowsDisambigLog = false;

    private LanguageToolSupport ltSupport;
    private OpenAction openAction;
    private SaveAction saveAction;
    private SaveAsAction saveAsAction;
    private AutoCheckAction autoCheckAction;
    private ShowResultAction showResultAction;

    private CheckAction checkAction;
    private File currentFile;
    private ByteOrderMark bom;
    private UndoRedoSupport undoRedo;
    private final JLabel statusLabel = new JLabel(" ", null, SwingConstants.RIGHT);
    private FontChooser fontChooserDialog;
    private final CircularFifoQueue<String> recentFiles = new CircularFifoQueue<>(MAX_RECENT_FILES);
    private JMenu recentFilesMenu;
    private final LocalStorage localStorage;
    private final Map<Language, ConfigurationDialog> configDialogs = new HashMap<>();
    private JSplitPane splitPane;
    private final JPanel mainPanel = new JPanel();

    private Main(LocalStorage localStorage) {
        this.localStorage = localStorage;
        messages = JLanguageTool.getMessageBundle();
    }

    private void loadFile() {
        File file = Tools.openFileDialog(frame, new PlainTextFileFilter());
        if (file == null) { // user clicked cancel
            return;
        }
        loadFile(file);
    }

    private void loadFile(File file) {
        try (FileInputStream inputStream = new FileInputStream(file)) {
            BOMInputStream bomIn = new BOMInputStream(inputStream, false, ByteOrderMark.UTF_8,
                    ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
            String charsetName;
            if (bomIn.hasBOM()) {
                bom = bomIn.getBOM();
                charsetName = bom.getCharsetName();
            } else {
                // No BOM found
                bom = null;
                charsetName = null;
            }
            String fileContents = StringTools.readStream(bomIn, charsetName);
            textArea.setText(fileContents);
            currentFile = file;
            updateTitle();
            if (recentFiles.contains(file.getAbsolutePath())) {
                recentFiles.remove(file.getAbsolutePath());
            }
            recentFiles.add(file.getAbsolutePath());
            localStorage.saveProperty("recentFiles", recentFiles);
            updateRecentFilesMenu();
        } catch (IOException e) {
            Tools.showError(e);
        }
    }

    private void saveFile(boolean newFile) {
        if (currentFile == null || newFile) {
            JFileChooser jfc = new JFileChooser();
            jfc.setFileFilter(new PlainTextFileFilter());
            jfc.showSaveDialog(frame);

            File file = jfc.getSelectedFile();
            if (file == null) { // user clicked cancel
                return;
            }
            currentFile = file;
            bom = null;
            updateTitle();
        }
        try {
            if (bom != null) {
                FileUtils.writeByteArrayToFile(currentFile, bom.getBytes());
                FileUtils.write(currentFile, textArea.getText(), bom.getCharsetName(), true);
            } else {
                FileUtils.write(currentFile, textArea.getText(), Charset.defaultCharset());
            }
        } catch (IOException ex) {
            Tools.showError(ex);
        }
    }

    private void addLanguage() throws InstantiationException, IllegalAccessException {
        LanguageManagerDialog dialog = new LanguageManagerDialog(frame, externalLanguages);
        dialog.show();
        List<Language> newExtLanguages = dialog.getLanguages();
        externalLanguages.clear();
        externalLanguages.addAll(newExtLanguages);
        languageBox.setModel(
                LanguageComboBoxModel.create(messages, EXTERNAL_LANGUAGE_SUFFIX, false, externalLanguages, null));
        languageBox.selectLanguage(ltSupport.getLanguage());
    }

    private void showOptions() {
        JLanguageTool langTool = ltSupport.getLanguageTool();
        List<Rule> rules = langTool.getAllRules();
        ConfigurationDialog configDialog = getCurrentConfigDialog();
        configDialog.show(rules); // this blocks until OK/Cancel is clicked in the dialog
        Configuration config = ltSupport.getConfig();
        try { //save config - needed for the server
            config.saveConfiguration(langTool.getLanguage());
        } catch (IOException e) {
            Tools.showError(e);
        }
        ltSupport.reloadConfig();
        // Stop server, start new server if requested:
        stopServer();
        maybeStartServer();
    }

    private void showSelectFontDialog() {
        Configuration config = ltSupport.getConfig();
        if (fontChooserDialog == null) {
            fontChooserDialog = new FontChooser(frame, true);
            Tools.centerDialog(fontChooserDialog);
            fontChooserDialog.addPropertyChangeListener(textLineNumber);
        }
        fontChooserDialog.setSelectedFont(this.textArea.getFont());
        fontChooserDialog.setVisible(true);
        if (fontChooserDialog.getSelectedFont() != null) {
            this.textArea.setFont(fontChooserDialog.getSelectedFont());
            config.setFontName(fontChooserDialog.getSelectedFont().getFamily());
            config.setFontStyle(fontChooserDialog.getSelectedFont().getStyle());
            config.setFontSize(fontChooserDialog.getSelectedFont().getSize());
            try {
                config.saveConfiguration(ltSupport.getLanguage());
            } catch (IOException e) {
                Tools.showError(e);
            }
        }
    }

    private Component getFrame() {
        return frame;
    }

    private void updateTitle() {
        StringBuilder sb = new StringBuilder();
        if (currentFile != null) {
            sb.append(currentFile.getName());
            if (bom != null) {
                sb.append(" (").append(bom.getCharsetName()).append(')');
            }
            sb.append(" - ");
        }
        sb.append("LanguageTool ").append(JLanguageTool.VERSION);
        frame.setTitle(sb.toString());
    }

    private void createGUI() {
        loadRecentFiles();
        frame = new JFrame("LanguageTool " + JLanguageTool.VERSION);

        setLookAndFeel();
        openAction = new OpenAction();
        saveAction = new SaveAction();
        saveAsAction = new SaveAsAction();
        checkAction = new CheckAction();
        autoCheckAction = new AutoCheckAction(true);
        showResultAction = new ShowResultAction(true);

        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new CloseListener());
        URL iconUrl = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(TRAY_ICON);
        frame.setIconImage(new ImageIcon(iconUrl).getImage());

        textArea = new JTextArea();
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        textArea.addKeyListener(new ControlReturnTextCheckingListener());

        textLineNumber = new TextLineNumber(textArea, 2);
        numberedTextAreaPane = new JScrollPane(textArea);
        numberedTextAreaPane.setRowHeaderView(textLineNumber);

        resultArea = new JTextPane();
        undoRedo = new UndoRedoSupport(this.textArea, messages);
        frame.setJMenuBar(createMenuBar());

        GridBagConstraints buttonCons = new GridBagConstraints();

        JPanel insidePanel = new JPanel();
        insidePanel.setOpaque(false);
        insidePanel.setLayout(new GridBagLayout());

        buttonCons.gridx = 0;
        buttonCons.gridy = 0;
        buttonCons.anchor = GridBagConstraints.LINE_START;
        insidePanel.add(new JLabel(messages.getString("textLanguage") + " "), buttonCons);

        //create a ComboBox with flags, do not include hidden languages
        languageBox = LanguageComboBox.create(messages, EXTERNAL_LANGUAGE_SUFFIX, true, false);
        buttonCons.gridx = 1;
        buttonCons.gridy = 0;
        buttonCons.anchor = GridBagConstraints.LINE_START;
        insidePanel.add(languageBox, buttonCons);

        JCheckBox autoDetectBox = new JCheckBox(messages.getString("atd"));
        buttonCons.gridx = 2;
        buttonCons.gridy = 0;
        buttonCons.gridwidth = GridBagConstraints.REMAINDER;
        buttonCons.anchor = GridBagConstraints.LINE_START;
        insidePanel.add(autoDetectBox, buttonCons);

        buttonCons.gridx = 0;
        buttonCons.gridy = 1;
        buttonCons.gridwidth = GridBagConstraints.REMAINDER;
        buttonCons.fill = GridBagConstraints.HORIZONTAL;
        buttonCons.anchor = GridBagConstraints.LINE_END;
        buttonCons.weightx = 1.0;
        insidePanel.add(statusLabel, buttonCons);

        Container contentPane = frame.getContentPane();
        GridBagLayout gridLayout = new GridBagLayout();
        contentPane.setLayout(gridLayout);
        GridBagConstraints cons = new GridBagConstraints();

        cons.gridx = 0;
        cons.gridy = 1;
        cons.fill = GridBagConstraints.HORIZONTAL;
        cons.anchor = GridBagConstraints.FIRST_LINE_START;
        JToolBar toolbar = new JToolBar("Toolbar", JToolBar.HORIZONTAL);
        toolbar.setFloatable(false);
        contentPane.add(toolbar, cons);

        JButton openButton = new JButton(openAction);
        openButton.setHideActionText(true);
        openButton.setFocusable(false);
        toolbar.add(openButton);

        JButton saveButton = new JButton(saveAction);
        saveButton.setHideActionText(true);
        saveButton.setFocusable(false);
        toolbar.add(saveButton);

        JButton saveAsButton = new JButton(saveAsAction);
        saveAsButton.setHideActionText(true);
        saveAsButton.setFocusable(false);
        toolbar.add(saveAsButton);

        JButton spellButton = new JButton(this.checkAction);
        spellButton.setHideActionText(true);
        spellButton.setFocusable(false);
        toolbar.add(spellButton);

        JToggleButton autoSpellButton = new JToggleButton(autoCheckAction);
        autoSpellButton.setHideActionText(true);
        autoSpellButton.setFocusable(false);
        toolbar.add(autoSpellButton);

        JButton clearTextButton = new JButton(new ClearTextAction());
        clearTextButton.setHideActionText(true);
        clearTextButton.setFocusable(false);
        toolbar.add(clearTextButton);

        cons.insets = new Insets(5, 5, 5, 5);
        cons.fill = GridBagConstraints.BOTH;
        cons.weightx = 10.0f;
        cons.weighty = 10.0f;
        cons.gridx = 0;
        cons.gridy = 2;
        cons.weighty = 5.0f;
        splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, numberedTextAreaPane, new JScrollPane(resultArea));
        mainPanel.setLayout(new GridLayout(0, 1));
        contentPane.add(mainPanel, cons);
        mainPanel.add(splitPane);

        cons.fill = GridBagConstraints.HORIZONTAL;
        cons.gridx = 0;
        cons.gridy = 3;
        cons.weightx = 1.0f;
        cons.weighty = 0.0f;
        cons.insets = new Insets(4, 12, 4, 12);
        contentPane.add(insidePanel, cons);

        ltSupport = new LanguageToolSupport(this.frame, this.textArea, this.undoRedo);
        ResultAreaHelper.install(messages, ltSupport, resultArea);
        languageBox.selectLanguage(ltSupport.getLanguage());
        languageBox.setEnabled(!ltSupport.getConfig().getAutoDetect());
        autoDetectBox.setSelected(ltSupport.getConfig().getAutoDetect());
        taggerShowsDisambigLog = ltSupport.getConfig().getTaggerShowsDisambigLog();

        languageBox.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED) {
                    // we cannot re-use the existing LT object anymore
                    frame.applyComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
                    Language lang = languageBox.getSelectedLanguage();
                    ComponentOrientation componentOrientation = ComponentOrientation
                            .getOrientation(lang.getLocale());
                    textArea.applyComponentOrientation(componentOrientation);
                    resultArea.applyComponentOrientation(componentOrientation);
                    ltSupport.setLanguage(lang);
                }
            }
        });
        autoDetectBox.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                boolean selected = e.getStateChange() == ItemEvent.SELECTED;
                languageBox.setEnabled(!selected);
                ltSupport.getConfig().setAutoDetect(selected);
                if (selected) {
                    Language detected = ltSupport.autoDetectLanguage(textArea.getText());
                    languageBox.selectLanguage(detected);
                }
            }
        });
        ltSupport.addLanguageToolListener(new LanguageToolListener() {
            @Override
            public void languageToolEventOccurred(LanguageToolEvent event) {
                if (event.getType() == LanguageToolEvent.Type.CHECKING_STARTED) {
                    String msg = org.languagetool.tools.Tools.i18n(messages, "checkStart");
                    statusLabel.setText(msg);
                    if (event.getCaller() == getFrame()) {
                        setWaitCursor();
                        checkAction.setEnabled(false);
                    }
                } else if (event.getType() == LanguageToolEvent.Type.CHECKING_FINISHED) {
                    if (event.getCaller() == getFrame()) {
                        checkAction.setEnabled(true);
                        unsetWaitCursor();
                    }
                    String msg = org.languagetool.tools.Tools.i18n(messages, "checkDone",
                            event.getSource().getMatches().size(), event.getElapsedTime());
                    statusLabel.setText(msg);
                } else if (event.getType() == LanguageToolEvent.Type.LANGUAGE_CHANGED) {
                    languageBox.selectLanguage(ltSupport.getLanguage());
                } else if (event.getType() == LanguageToolEvent.Type.RULE_ENABLED) {
                    //this will trigger a check and the result will be updated by
                    //the CHECKING_FINISHED event
                } else if (event.getType() == LanguageToolEvent.Type.RULE_DISABLED) {
                    String msg = org.languagetool.tools.Tools.i18n(messages, "checkDoneNoTime",
                            event.getSource().getMatches().size());
                    statusLabel.setText(msg);
                }
            }
        });
        frame.applyComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
        Language lang = ltSupport.getLanguage();
        ComponentOrientation componentOrientation = ComponentOrientation.getOrientation(lang.getLocale());
        textArea.applyComponentOrientation(componentOrientation);
        resultArea.applyComponentOrientation(componentOrientation);

        ResourceBundle textLanguageMessageBundle = JLanguageTool.getMessageBundle(ltSupport.getLanguage());
        textArea.setText(textLanguageMessageBundle.getString("guiDemoText"));

        Configuration config = ltSupport.getConfig();
        if (config.getFontName() != null || config.getFontStyle() != Configuration.FONT_STYLE_INVALID
                || config.getFontSize() != Configuration.FONT_SIZE_INVALID) {
            String fontName = config.getFontName();
            if (fontName == null) {
                fontName = textArea.getFont().getFamily();
            }
            int fontSize = config.getFontSize();
            if (fontSize == Configuration.FONT_SIZE_INVALID) {
                fontSize = textArea.getFont().getSize();
            }
            Font font = new Font(fontName, config.getFontStyle(), fontSize);
            textArea.setFont(font);
        }

        frame.pack();
        frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
        frame.setLocationByPlatform(true);
        splitPane.setDividerLocation(200);
        MainWindowStateBean state = localStorage.loadProperty("gui.state", MainWindowStateBean.class);
        if (state != null) {
            if (state.getBounds() != null) {
                frame.setBounds(state.getBounds());
                ResizeComponentListener.setBoundsProperty(frame, state.getBounds());
            }
            if (state.getDividerLocation() != null) {
                splitPane.setDividerLocation(state.getDividerLocation());
            }
            if (state.getState() != null) {
                frame.setExtendedState(state.getState());
            }
        }
        ResizeComponentListener.attachToWindow(frame);
        maybeStartServer();
    }

    private String getLabel(String key) {
        return Tools.getLabel(messages.getString(key));
    }

    private int getMnemonic(String key) {
        return Tools.getMnemonic(messages.getString(key));
    }

    private KeyStroke getMenuKeyStroke(int keyEvent) {
        return KeyStroke.getKeyStroke(keyEvent, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
    }

    private JMenuBar createMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        JMenu fileMenu = new JMenu(getLabel("guiMenuFile"));
        fileMenu.setMnemonic(getMnemonic("guiMenuFile"));
        JMenu editMenu = new JMenu(getLabel("guiMenuEdit"));
        editMenu.setMnemonic(getMnemonic("guiMenuEdit"));
        JMenu grammarMenu = new JMenu(getLabel("guiMenuGrammar"));
        grammarMenu.setMnemonic(getMnemonic("guiMenuGrammar"));
        JMenu helpMenu = new JMenu(getLabel("guiMenuHelp"));
        helpMenu.setMnemonic(getMnemonic("guiMenuHelp"));

        fileMenu.add(openAction);
        fileMenu.add(saveAction);
        fileMenu.add(saveAsAction);
        recentFilesMenu = new JMenu(getLabel("guiMenuRecentFiles"));
        recentFilesMenu.setMnemonic(getMnemonic("guiMenuRecentFiles"));
        fileMenu.add(recentFilesMenu);
        updateRecentFilesMenu();
        fileMenu.addSeparator();
        fileMenu.add(new HideAction());
        fileMenu.addSeparator();
        fileMenu.add(new QuitAction());

        grammarMenu.add(checkAction);
        JCheckBoxMenuItem item = new JCheckBoxMenuItem(autoCheckAction);
        grammarMenu.add(item);
        JCheckBoxMenuItem showResult = new JCheckBoxMenuItem(showResultAction);
        grammarMenu.add(showResult);
        grammarMenu.add(new CheckClipboardAction());
        grammarMenu.add(new TagTextAction());
        grammarMenu.add(new AddRulesAction());
        grammarMenu.add(new OptionsAction());
        grammarMenu.add(new SelectFontAction());
        JMenu lafMenu = new JMenu(messages.getString("guiLookAndFeelMenu"));
        UIManager.LookAndFeelInfo[] lafInfo = UIManager.getInstalledLookAndFeels();
        ButtonGroup buttonGroup = new ButtonGroup();
        for (UIManager.LookAndFeelInfo laf : lafInfo) {
            if (!"Nimbus".equals(laf.getName())) {
                continue;
            }
            addLookAndFeelMenuItem(lafMenu, laf, buttonGroup);
        }
        for (UIManager.LookAndFeelInfo laf : lafInfo) {
            if ("Nimbus".equals(laf.getName())) {
                continue;
            }
            addLookAndFeelMenuItem(lafMenu, laf, buttonGroup);
        }
        grammarMenu.add(lafMenu);

        helpMenu.add(new AboutAction());

        undoRedo.undoAction.putValue(Action.NAME, getLabel("guiMenuUndo"));
        undoRedo.undoAction.putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuUndo"));
        undoRedo.redoAction.putValue(Action.NAME, getLabel("guiMenuRedo"));
        undoRedo.redoAction.putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuRedo"));

        editMenu.add(undoRedo.undoAction);
        editMenu.add(undoRedo.redoAction);
        editMenu.addSeparator();

        Action cutAction = new DefaultEditorKit.CutAction();
        cutAction.putValue(Action.SMALL_ICON, getImageIcon("sc_cut.png"));
        cutAction.putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_cut.png"));
        cutAction.putValue(Action.NAME, getLabel("guiMenuCut"));
        cutAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_T);
        editMenu.add(cutAction);

        Action copyAction = new DefaultEditorKit.CopyAction();
        copyAction.putValue(Action.SMALL_ICON, getImageIcon("sc_copy.png"));
        copyAction.putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_copy.png"));
        copyAction.putValue(Action.NAME, getLabel("guiMenuCopy"));
        copyAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_C);
        editMenu.add(copyAction);

        Action pasteAction = new DefaultEditorKit.PasteAction();
        pasteAction.putValue(Action.SMALL_ICON, getImageIcon("sc_paste.png"));
        pasteAction.putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_paste.png"));
        pasteAction.putValue(Action.NAME, getLabel("guiMenuPaste"));
        pasteAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_P);
        editMenu.add(pasteAction);

        editMenu.addSeparator();

        editMenu.add(new SelectAllAction());

        menuBar.add(fileMenu);
        menuBar.add(editMenu);
        menuBar.add(grammarMenu);
        menuBar.add(helpMenu);
        return menuBar;
    }

    private void updateRecentFilesMenu() {
        recentFilesMenu.removeAll();
        String[] files = recentFiles.toArray(new String[recentFiles.size()]);
        ArrayUtils.reverse(files);
        for (String filename : files) {
            recentFilesMenu.add(new RecentFileAction(new File(filename)));
        }
    }

    private void loadRecentFiles() {
        CircularFifoQueue<String> l = localStorage.loadProperty("recentFiles", CircularFifoQueue.class);
        if (l != null) {
            for (String name : l) {
                File f = new File(name);
                if (f.exists() && f.isFile()) {
                    recentFiles.add(name);
                }
            }
        }
    }

    private void addLookAndFeelMenuItem(JMenu lafMenu, UIManager.LookAndFeelInfo laf, ButtonGroup buttonGroup) {
        JRadioButtonMenuItem lfItem = new JRadioButtonMenuItem(new SelectLFAction(laf));
        lafMenu.add(lfItem);
        buttonGroup.add(lfItem);
        if (laf.getClassName().equals(UIManager.getLookAndFeel().getClass().getName())) {
            buttonGroup.setSelected(lfItem.getModel(), true);
        }
    }

    private void setLookAndFeel() {
        String lookAndFeelName = null;
        String className = null;
        try {
            Configuration config = new Configuration(new File(System.getProperty("user.home")),
                    LanguageToolSupport.CONFIG_FILE, null);
            if (config.getLookAndFeelName() != null) {
                lookAndFeelName = config.getLookAndFeelName();
            }
        } catch (IOException ex) {
            // ignore
        }
        if (lookAndFeelName == null) {
            lookAndFeelName = "Nimbus";
            // to use the system's look and feel:
            //lookAndFeelName = UIManager.getSystemLookAndFeelClassName();
        }
        for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
            if (lookAndFeelName.equals(info.getName())) {
                className = info.getClassName();
                break;
            }
        }
        if (className != null) {
            try {
                UIManager.setLookAndFeel(className);
            } catch (Exception ignored) {
                // Well, what can we do...
            }
        }
    }

    private PopupMenu makePopupMenu() {
        PopupMenu popup = new PopupMenu();
        ActionListener rmbListener = new TrayActionRMBListener();
        // Enable or disable embedded HTTP server:
        enableHttpServerItem = new CheckboxMenuItem(Tools.getLabel(messages.getString("tray_menu_enable_server")));
        enableHttpServerItem.setState(httpServer != null && httpServer.isRunning());
        enableHttpServerItem.addItemListener(new TrayActionItemListener());
        popup.add(enableHttpServerItem);
        // Check clipboard text:
        MenuItem checkClipboardItem = new MenuItem(Tools.getLabel(messages.getString("guiMenuCheckClipboard")));
        checkClipboardItem.addActionListener(rmbListener);
        popup.add(checkClipboardItem);
        // Open main window:
        MenuItem restoreItem = new MenuItem(Tools.getLabel(messages.getString("guiMenuShowMainWindow")));
        restoreItem.addActionListener(rmbListener);
        popup.add(restoreItem);
        // Exit:
        MenuItem exitItem = new MenuItem(Tools.getLabel(messages.getString("guiMenuQuit")));
        exitItem.addActionListener(rmbListener);
        popup.add(exitItem);
        return popup;
    }

    private void checkClipboardText() {
        String s = getClipboardText();
        textArea.setText(s);
    }

    private void hideToTray() {
        if (!isInTray) {
            SystemTray tray = SystemTray.getSystemTray();
            String iconPath = tray.getTrayIconSize().height > 16 ? TRAY_ICON : TRAY_SMALL_ICON;
            URL iconUrl = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(iconPath);
            Image img = Toolkit.getDefaultToolkit().getImage(iconUrl);
            PopupMenu popup = makePopupMenu();
            try {
                trayIcon = new TrayIcon(img, TRAY_TOOLTIP, popup);
                trayIcon.addMouseListener(new TrayActionListener());
                setTrayIcon();
                tray.add(trayIcon);
            } catch (AWTException e1) {
                Tools.showError(e1);
            }
        }
        isInTray = true;
        frame.setVisible(false);
    }

    private void tagText() {
        if (StringTools.isEmpty(textArea.getText().trim())) {
            textArea.setText(messages.getString("enterText2"));
            return;
        }
        setWaitCursor();
        new Thread() {
            @Override
            public void run() {
                try {
                    tagTextAndDisplayResults();
                } finally {
                    SwingUtilities.invokeLater(() -> unsetWaitCursor());
                }
            }
        }.start();
    }

    private void quitOrHide() {
        if (closeHidesToTray) {
            hideToTray();
        } else {
            quit();
        }
    }

    private void quit() {
        stopServer();
        try {
            Configuration config = ltSupport.getConfig();
            config.setLanguage(ltSupport.getLanguage());
            config.saveConfiguration(ltSupport.getLanguage());
        } catch (IOException e) {
            Tools.showError(e);
        }

        MainWindowStateBean bean = new MainWindowStateBean();
        bean.setBounds(ResizeComponentListener.getBoundsProperty(frame));
        bean.setState(frame.getExtendedState());
        Component comp = mainPanel.getComponent(0);
        if (comp instanceof JSplitPane) {
            bean.setDividerLocation(((JSplitPane) comp).getDividerLocation());
        } else {
            MainWindowStateBean old = localStorage.loadProperty(GUI_STATE, MainWindowStateBean.class);
            bean.setDividerLocation(old.getDividerLocation());
        }
        localStorage.saveProperty(GUI_STATE, bean);

        frame.setVisible(false);
        JLanguageTool.removeTemporaryFiles();
        System.exit(0);
    }

    private void setTrayIcon() {
        if (trayIcon != null) {
            SystemTray tray = SystemTray.getSystemTray();
            boolean httpServerRunning = httpServer != null && httpServer.isRunning();
            boolean smallTray = tray.getTrayIconSize().height <= 16;
            String iconPath;
            if (httpServerRunning) {
                trayIcon.setToolTip(messages.getString("tray_tooltip_server_running"));
                iconPath = smallTray ? TRAY_SMALL_SERVER_ICON : TRAY_SERVER_ICON;
            } else {
                trayIcon.setToolTip(TRAY_TOOLTIP);
                iconPath = smallTray ? TRAY_SMALL_ICON : TRAY_ICON;
            }
            URL iconUrl = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(iconPath);
            Image img = Toolkit.getDefaultToolkit().getImage(iconUrl);
            trayIcon.setImage(img);
        }
    }

    private void showGUI() {
        frame.setVisible(true);
    }

    private void restoreFromTray() {
        frame.setVisible(true);
    }

    // show GUI and check the text from clipboard/selection:
    private void restoreFromTrayAndCheck() {
        String s = getClipboardText();
        restoreFromTray();
        textArea.setText(s);
    }

    private String getClipboardText() {
        // get text from clipboard or selection:
        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemSelection();
        if (clipboard == null) { // on Windows
            clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
        }
        String s;
        Transferable data = clipboard.getContents(this);
        try {
            if (data != null && data.isDataFlavorSupported(DataFlavor.getTextPlainUnicodeFlavor())) {
                DataFlavor df = DataFlavor.getTextPlainUnicodeFlavor();
                try (Reader sr = df.getReaderForText(data)) {
                    s = StringTools.readerToString(sr);
                }
            } else {
                s = "";
            }
        } catch (Exception ex) {
            s = data.toString();
        }
        return s;
    }

    private boolean maybeStartServer() {
        Configuration config = ltSupport.getConfig();
        if (config.getRunServer()) {
            try {
                HTTPServerConfig serverConfig = new HTTPServerConfig(config.getServerPort(), false);
                serverConfig.setAllowOriginUrl("*"); // needed for Firefox so this server can be used from the add-on
                if (config.getNgramDirectory() != null) {
                    serverConfig.setLanguageModelDirectory(config.getNgramDirectory().getAbsolutePath());
                }
                httpServer = new HTTPServer(serverConfig, true);
                httpServer.run();
                if (enableHttpServerItem != null) {
                    enableHttpServerItem.setState(httpServer.isRunning());
                    setTrayIcon();
                }
            } catch (PortBindingException e) {
                JOptionPane.showMessageDialog(null, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
        return httpServer != null && httpServer.isRunning();
    }

    private void stopServer() {
        if (httpServer != null) {
            httpServer.stop();
            if (enableHttpServerItem != null) {
                enableHttpServerItem.setState(httpServer.isRunning());
                setTrayIcon();
            }
            httpServer = null;
        }
    }

    private void checkTextAndDisplayResults() {
        if (StringTools.isEmpty(textArea.getText().trim())) {
            textArea.setText(messages.getString("enterText2"));
            return;
        }
        ltSupport.checkImmediately(getFrame());
    }

    private String getStackTraceAsHtml(Exception e) {
        return "<br><br><b><font color=\"red\">"
                + org.languagetool.tools.Tools.getFullStackTrace(e).replace("\n", "<br/>") + "</font></b><br>";
    }

    private void setWaitCursor() {
        frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        // For some reason we also have to set the cursor here so it also shows
        // when user starts checking text with Ctrl+Return:
        textArea.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        resultArea.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    }

    private void unsetWaitCursor() {
        frame.setCursor(Cursor.getDefaultCursor());
        textArea.setCursor(Cursor.getDefaultCursor());
        resultArea.setCursor(Cursor.getDefaultCursor());
    }

    private boolean appendTagsWithDisambigLog(StringBuilder sb, AnalyzedSentence sentence, boolean odd) {

        for (AnalyzedTokenReadings t : sentence.getTokens()) {
            if (t.isWhitespace() && !t.isSentenceStart()) {
                continue;
            }
            odd = !odd;
            sb.append("<tr>");
            sb.append("<td bgcolor=\"");
            if (odd) {
                sb.append("#ffffff");
            } else {
                sb.append("#f1f1f1");
            }
            sb.append("\">");
            if (!t.isWhitespace()) {
                sb.append(t.getToken());
                sb.append("<font color='");
                sb.append(TAG_COLOR);
                sb.append("'>[");
            }
            Iterator<AnalyzedToken> iterator = t.iterator();
            while (iterator.hasNext()) {
                AnalyzedToken token = iterator.next();
                String posTag = token.getPOSTag();
                if (t.isSentenceStart()) {
                    sb.append(StringTools.escapeHTML("<S>"));
                } else if (JLanguageTool.SENTENCE_END_TAGNAME.equals(posTag)) {
                    sb.append(StringTools.escapeHTML("</S>"));
                } else if (JLanguageTool.PARAGRAPH_END_TAGNAME.equals(posTag)) {
                    sb.append(StringTools.escapeHTML("<P/>"));
                } else {
                    if (!t.isWhitespace()) {
                        sb.append(token);
                        if (iterator.hasNext()) {
                            sb.append(", ");
                        }
                    }
                }
            }
            if (!t.isWhitespace()) {
                if (t.getChunkTags().size() > 0) {
                    sb.append(',');
                    sb.append(StringUtils.join(t.getChunkTags(), "|"));
                }
                if (t.isImmunized()) {
                    sb.append("{!}");
                }
                sb.append("]</font>");
            } else {
                sb.append(' ');
            }
            sb.append("</td>");
            sb.append("<td bgcolor=\"");
            if (odd) {
                sb.append("#ffffff");
            } else {
                sb.append("#f1f1f1");
            }
            sb.append("\">");
            if (!"".equals(t.getHistoricalAnnotations())) {
                sb.append(StringTools.escapeHTML(t.getHistoricalAnnotations()).trim().replace("\n", "<br>"));
            }
            sb.append("</td>");
            sb.append("</tr>");
        }
        return odd;
    }

    private void tagTextAndDisplayResults() {
        JLanguageTool langTool = ltSupport.getLanguageTool();
        // tag text
        List<String> sentences = langTool.sentenceTokenize(textArea.getText());
        StringBuilder sb = new StringBuilder();
        if (taggerShowsDisambigLog) {
            sb.append("<table>");
            sb.append("<tr>");
            sb.append("<td><b>");
            sb.append(messages.getString("token"));
            sb.append("</b></td>");
            sb.append("<td><b>");
            sb.append(messages.getString("disambiguatorLog"));
            sb.append("</b></td>");
            sb.append("</tr>");
            boolean odd = true;
            try {
                for (String sent : sentences) {
                    AnalyzedSentence analyzed = langTool.getAnalyzedSentence(sent);
                    odd = appendTagsWithDisambigLog(sb, analyzed, odd);
                }
            } catch (Exception e) {
                sb.append(getStackTraceAsHtml(e));
            }
            sb.append("</table>");
        } else {
            try {
                for (String sent : sentences) {
                    AnalyzedSentence analyzed = langTool.getAnalyzedSentence(sent);
                    String analyzedString = StringTools.escapeHTML(analyzed.toString(","))
                            .replace("&lt;S&gt;", "&lt;S&gt;<br>").replace("[", "<font color='" + TAG_COLOR + "'>[")
                            .replace("]", "]</font><br>");
                    sb.append(analyzedString).append('\n');
                }
            } catch (Exception e) {
                sb.append(getStackTraceAsHtml(e));
            }
        }
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                if (taggerDialog == null) {
                    taggerDialog = new JDialog(frame);
                    taggerDialog.setTitle(messages.getString("taggerWindowTitle"));
                    taggerDialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
                    taggerDialog.setResizable(true);
                    taggerDialog.setSize(640, 480);
                    taggerDialog.setLocationRelativeTo(frame);
                    KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
                    ActionListener actionListener = actionEvent -> taggerDialog.setVisible(false);
                    taggerDialog.getRootPane().registerKeyboardAction(actionListener, stroke,
                            JComponent.WHEN_IN_FOCUSED_WINDOW);
                    JPanel panel = new JPanel(new GridBagLayout());
                    taggerDialog.add(panel);
                    taggerArea = new JTextPane();
                    taggerArea.setContentType("text/html");
                    taggerArea.setEditable(false);
                    GridBagConstraints c = new GridBagConstraints();
                    c.gridx = 0;
                    c.gridwidth = 2;
                    c.gridy = 0;
                    c.weightx = 1.0;
                    c.weighty = 1.0;
                    c.insets = new Insets(8, 8, 4, 8);
                    c.fill = GridBagConstraints.BOTH;
                    panel.add(new JScrollPane(taggerArea), c);
                    c.gridwidth = 1;
                    c.gridx = 0;
                    c.gridy = 1;
                    c.weightx = 0.0;
                    c.weighty = 0.0;
                    c.insets = new Insets(4, 8, 8, 8);
                    c.fill = GridBagConstraints.NONE;
                    c.anchor = GridBagConstraints.EAST;
                    JCheckBox showDisAmbig = new JCheckBox(messages.getString("ShowDisambiguatorLog"));
                    showDisAmbig.setSelected(taggerShowsDisambigLog);
                    showDisAmbig.addItemListener((ItemEvent e) -> {
                        taggerShowsDisambigLog = e.getStateChange() == ItemEvent.SELECTED;
                        ltSupport.getConfig().setTaggerShowsDisambigLog(taggerShowsDisambigLog);
                    });
                    panel.add(showDisAmbig, c);
                    c.gridx = 1;
                    JButton closeButton = new JButton(messages.getString("guiCloseButton"));
                    closeButton.addActionListener(actionListener);
                    panel.add(closeButton, c);
                }
                // orientation each time should be set as language may is changed
                taggerDialog.applyComponentOrientation(
                        ComponentOrientation.getOrientation(languageBox.getSelectedLanguage().getLocale()));

                taggerDialog.setVisible(true);
                taggerArea.setText(HTML_FONT_START + sb + HTML_FONT_END);
            }
        });
    }

    private void setResultAreaVisible(boolean enable) {
        if (enable) {
            this.mainPanel.removeAll();
            mainPanel.add(this.splitPane);
            splitPane.setTopComponent(numberedTextAreaPane);
            splitPane.setDividerLocation(200);
            MainWindowStateBean state = localStorage.loadProperty(GUI_STATE, MainWindowStateBean.class);
            if (state != null && state.getDividerLocation() != null) {
                splitPane.setDividerLocation(state.getDividerLocation());
            }
            ResultAreaHelper.enable(resultArea);
        } else {
            MainWindowStateBean bean = new MainWindowStateBean();
            bean.setDividerLocation(splitPane.getDividerLocation());
            localStorage.saveProperty(GUI_STATE, bean);
            this.mainPanel.removeAll();
            splitPane.setTopComponent(null);
            mainPanel.add(numberedTextAreaPane);
            ResultAreaHelper.disable(resultArea);
        }
        mainPanel.validate();
    }

    private void setTrayMode(boolean trayMode) {
        this.closeHidesToTray = trayMode;
    }

    public static void main(String[] args) {
        if (System.getSecurityManager() == null) {
            JnaTools.setBugWorkaroundProperty();
        }
        LocalStorage localStorage = new LocalStorage();
        LocaleBean bean = localStorage.loadProperty("gui.locale", LocaleBean.class);
        if (bean != null) {
            Locale.setDefault(bean.asLocale());
        }
        Main prg = new Main(localStorage);
        if (args.length == 1 && (args[0].equals("-t") || args[0].equals("--tray"))) {
            // dock to systray on startup
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        prg.createGUI();
                        prg.setTrayMode(true);
                        prg.hideToTray();
                    } catch (Exception e) {
                        Tools.showError(e);
                        System.exit(1);
                    }
                }
            });
        } else if (args.length == 1 && (args[0].equals("-h") || args[0].equals("--help"))) {
            printUsage();
        } else if (args.length == 0 || args.length == 1) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        prg.createGUI();
                        prg.showGUI();
                        if (args.length == 1) {
                            prg.loadFile(new File(args[0]));
                        }
                    } catch (Exception e) {
                        Tools.showError(e);
                    }
                }
            });
        } else {
            printUsage();
            System.exit(1);
        }
    }

    private static void printUsage() {
        System.out.println("Usage: java org.languagetool.gui.Main [-t|--tray]");
        System.out.println("    or java org.languagetool.gui.Main [file]");
        System.out.println("Parameters:");
        System.out.println("    -t, --tray: dock LanguageTool to system tray on startup");
        System.out.println("    file:       a plain text file to load on startup");
    }

    private class ControlReturnTextCheckingListener extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_ENTER
                    && (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) == KeyEvent.CTRL_DOWN_MASK) {
                checkTextAndDisplayResults();
            }
        }
    }

    //
    // The System Tray stuff
    //

    class TrayActionItemListener implements ItemListener {
        @Override
        public void itemStateChanged(ItemEvent e) {
            try {
                Configuration config = ltSupport.getConfig();
                if (e.getStateChange() == ItemEvent.SELECTED) {
                    config.setRunServer(true);
                    boolean serverStarted = maybeStartServer();
                    enableHttpServerItem.setState(serverStarted);
                    config.setRunServer(serverStarted);
                    config.saveConfiguration(ltSupport.getLanguage());
                } else if (e.getStateChange() == ItemEvent.DESELECTED) {
                    config.setRunServer(false);
                    config.saveConfiguration(ltSupport.getLanguage());
                    stopServer();
                }
            } catch (IOException ex) {
                Tools.showError(ex);
            }
        }
    }

    class TrayActionRMBListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            if (isCommand(e, "guiMenuCheckClipboard")) {
                restoreFromTrayAndCheck();
            } else if (isCommand(e, "guiMenuShowMainWindow")) {
                restoreFromTray();
            } else if (isCommand(e, "guiMenuQuit")) {
                quit();
            } else {
                JOptionPane.showMessageDialog(null, "Unknown action: " + e.getActionCommand(), "Error",
                        JOptionPane.ERROR_MESSAGE);
            }
        }

        private boolean isCommand(ActionEvent e, String label) {
            return e.getActionCommand().equalsIgnoreCase(Tools.getLabel(messages.getString(label)));
        }

    }

    class TrayActionListener extends MouseAdapter {

        @Override
        public void mouseClicked(@SuppressWarnings("unused") MouseEvent e) {
            if (frame.isVisible() && frame.isActive()) {
                frame.setVisible(false);
            } else if (frame.isVisible() && !frame.isActive()) {
                frame.toFront();
                restoreFromTrayAndCheck();
            } else {
                restoreFromTrayAndCheck();
            }
        }

    }

    class CloseListener extends WindowAdapter {

        @Override
        public void windowClosing(@SuppressWarnings("unused") WindowEvent e) {
            quitOrHide();
        }

    }

    static class PlainTextFileFilter extends FileFilter {

        @Override
        public boolean accept(File f) {
            boolean isTextFile = f.getName().toLowerCase().endsWith(".txt");
            return isTextFile || f.isDirectory();
        }

        @Override
        public String getDescription() {
            return "*.txt";
        }

    }

    class OpenAction extends AbstractAction {

        OpenAction() {
            super(getLabel("guiMenuOpen"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("guiMenuOpenShortDesc"));
            putValue(Action.LONG_DESCRIPTION, messages.getString("guiMenuOpenLongDesc"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuOpen"));
            putValue(Action.ACCELERATOR_KEY, getMenuKeyStroke(KeyEvent.VK_O));
            putValue(Action.SMALL_ICON, getImageIcon("sc_open.png"));
            putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_open.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            loadFile();
        }
    }

    class SaveAction extends AbstractAction {

        SaveAction() {
            super(getLabel("guiMenuSave"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("guiMenuSaveShortDesc"));
            putValue(Action.LONG_DESCRIPTION, messages.getString("guiMenuSaveLongDesc"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuSave"));
            putValue(Action.ACCELERATOR_KEY,
                    KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
            putValue(Action.SMALL_ICON, getImageIcon("sc_save.png"));
            putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_save.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            saveFile(false);
        }
    }

    class SaveAsAction extends AbstractAction {

        SaveAsAction() {
            super(getLabel("guiMenuSaveAs"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("guiMenuSaveAsShortDesc"));
            putValue(Action.LONG_DESCRIPTION, messages.getString("guiMenuSaveAsLongDesc"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuSaveAs"));
            putValue(Action.SMALL_ICON, getImageIcon("sc_saveas.png"));
            putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_saveas.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            saveFile(true);
        }
    }

    class CheckClipboardAction extends AbstractAction {

        CheckClipboardAction() {
            super(getLabel("guiMenuCheckClipboard"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuCheckClipboard"));
            putValue(Action.ACCELERATOR_KEY, getMenuKeyStroke(KeyEvent.VK_Y));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            checkClipboardText();
        }
    }

    class TagTextAction extends AbstractAction {

        TagTextAction() {
            super(getLabel("guiTagText"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiTagText"));
            putValue(Action.ACCELERATOR_KEY, getMenuKeyStroke(KeyEvent.VK_T));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            tagText();
        }
    }

    class AddRulesAction extends AbstractAction {

        AddRulesAction() {
            super(getLabel("guiMenuAddRules"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuAddRules"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                addLanguage();
            } catch (Exception ex) {
                Tools.showError(ex);
            }
        }
    }

    class OptionsAction extends AbstractAction {

        OptionsAction() {
            super(getLabel("guiMenuOptions"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuOptions"));
            putValue(Action.ACCELERATOR_KEY, getMenuKeyStroke(KeyEvent.VK_S));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            showOptions();
        }
    }

    class SelectFontAction extends AbstractAction {

        SelectFontAction() {
            super(getLabel("guiSelectFont"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            showSelectFontDialog();
        }
    }

    class SelectLFAction extends AbstractAction {

        private final UIManager.LookAndFeelInfo lf;

        SelectLFAction(UIManager.LookAndFeelInfo lf) {
            super(lf.getName());
            this.lf = lf;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                UIManager.setLookAndFeel(lf.getClassName());
                SwingUtilities.updateComponentTreeUI(frame);
                frame.pack();
                ltSupport.getConfig().setLookAndFeelName(lf.getName());
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                    | UnsupportedLookAndFeelException ex) {
                Tools.showError(ex);
            }
        }
    }

    class HideAction extends AbstractAction {

        HideAction() {
            super(getLabel("guiMenuHide"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuHide"));
            putValue(Action.ACCELERATOR_KEY, getMenuKeyStroke(KeyEvent.VK_D));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            hideToTray();
        }
    }

    class QuitAction extends AbstractAction {

        QuitAction() {
            super(getLabel("guiMenuQuit"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuQuit"));
            putValue(Action.ACCELERATOR_KEY, getMenuKeyStroke(KeyEvent.VK_Q));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            quit();
        }
    }

    class AboutAction extends AbstractAction {

        AboutAction() {
            super(getLabel("guiMenuAbout"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiMenuAbout"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            AboutDialog about = new AboutDialog(messages, getFrame());
            about.show();
        }
    }

    class CheckAction extends AbstractAction {

        CheckAction() {
            super(getLabel("checkText"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("checkTextShortDesc"));
            putValue(Action.LONG_DESCRIPTION, messages.getString("checkTextLongDesc"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("checkText"));
            putValue(Action.SMALL_ICON, getImageIcon("sc_spelldialog.png"));
            putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_spelldialog.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            checkTextAndDisplayResults();
        }
    }

    class AutoCheckAction extends AbstractAction {

        private boolean enable;

        AutoCheckAction(boolean initial) {
            super(getLabel("autoCheckText"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("autoCheckTextShortDesc"));
            putValue(Action.LONG_DESCRIPTION, messages.getString("autoCheckTextLongDesc"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("autoCheckText"));
            putValue(Action.SMALL_ICON, getImageIcon("sc_spellonline.png"));
            putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_spellonline.png"));
            enable = initial;
            putValue(Action.SELECTED_KEY, enable);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            enable = !enable;
            putValue(Action.SELECTED_KEY, enable);
            ltSupport.setBackgroundCheckEnabled(enable);
        }
    }

    class ClearTextAction extends AbstractAction {

        ClearTextAction() {
            super(getLabel("clearText"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("clearText"));
            putValue(Action.SMALL_ICON, getImageIcon("sc_closedoc.png"));
            putValue(Action.LARGE_ICON_KEY, getImageIcon("lc_closedoc.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            ltSupport.getTextComponent().setText("");
        }
    }

    private class SelectAllAction extends TextAction {

        private SelectAllAction() {
            super(getLabel("guiMenuSelectAll"));
            putValue(Action.MNEMONIC_KEY, KeyEvent.VK_A);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JTextComponent component = getFocusedComponent();
            component.selectAll();
        }
    }

    class RecentFileAction extends AbstractAction {

        private final File file;

        RecentFileAction(File file) {
            super(file.getName());
            this.file = file;
            putValue(Action.SHORT_DESCRIPTION, file.getAbsolutePath());
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (file.exists()) {
                loadFile(file);
            } else {
                JOptionPane.showMessageDialog(frame, messages.getString("guiFileNotFound"),
                        messages.getString("dialogTitleError"), JOptionPane.ERROR_MESSAGE);
                recentFiles.remove(file.getAbsolutePath());
                updateRecentFilesMenu();
            }
        }
    }

    class ShowResultAction extends AbstractAction {

        private boolean enable;

        ShowResultAction(boolean initial) {
            super(getLabel("guiShowResultArea"));
            putValue(Action.SHORT_DESCRIPTION, messages.getString("guiShowResultArea"));
            putValue(Action.LONG_DESCRIPTION, messages.getString("guiShowResultArea"));
            putValue(Action.MNEMONIC_KEY, getMnemonic("guiShowResultArea"));
            enable = initial;
            putValue(Action.SELECTED_KEY, enable);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            enable = !enable;
            putValue(Action.SELECTED_KEY, enable);
            setResultAreaVisible(enable);
        }
    }

    private ImageIcon getImageIcon(String filename) {
        Image image = Toolkit.getDefaultToolkit()
                .getImage(JLanguageTool.getDataBroker().getFromResourceDirAsUrl(filename));
        return new ImageIcon(image);
    }

    private ConfigurationDialog getCurrentConfigDialog() {
        Language language = ltSupport.getLanguage();
        ConfigurationDialog configDialog;
        if (configDialogs.containsKey(language)) {
            configDialog = configDialogs.get(language);
        } else {
            configDialog = new ConfigurationDialog(frame, false, ltSupport.getConfig());
            configDialog.addExtraPanel(new GuiLangConfigPanel(messages, localStorage));
            configDialogs.put(language, configDialog);
        }
        return configDialog;
    }

    /**
     *  This class will display line numbers for a related text component. The text
     *  component must use the same line height for each line. TextLineNumber
     *  supports wrapped lines and will highlight the line number of the current
     *  line in the text component.
     *
     *  This class was designed to be used as a component added to the row header
     *  of a JScrollPane.
     *
     *  Credits: The implementation of this class is based on
     *  "Text Component Line Number" Posted by Rob Camick on May 23, 2009 at
     *  https://tips4java.wordpress.com/2009/05/23/text-component-line-number/
     *
     *  Usage / License according to https://tips4java.wordpress.com/about/ :
     *  You are free to use and/or modify and/or distribute any or all code posted
     *  on the Java Tips Weblog without restriction. A credit in the code comments
     *  would be nice, but not in any way mandatory.
     */
    public class TextLineNumber extends JPanel
            implements CaretListener, DocumentListener, ComponentListener, PropertyChangeListener {
        private static final long serialVersionUID = 1L;
        public final static float LEFT = 0.0f;
        public final static float CENTER = 0.5f;
        public final static float RIGHT = 1.0f;

        private final Border OUTER = new MatteBorder(0, 0, 0, 1, Color.GRAY);

        //  Text component this TextTextLineNumber component is in sync with
        private JTextComponent component;

        //  Properties that can be changed
        private int borderGap;
        private Color currentLineForeground;
        private float digitAlignment;
        private int minimumDisplayDigits;

        //  Keep history information to reduce the number of times the component
        //  needs to be repainted
        private int lastDigits;
        private int lastLine;

        /**
         *  Create a line number component for a text component.
         *
         *  @param component  the related text component
         *  @param minimumDisplayDigits  the number of digits used to calculate
         *                               the minimum width of the component
         */
        public TextLineNumber(JTextComponent component, int minimumDisplayDigits) {
            this.component = component;

            setFont(component.getFont());
            setBorderGap(5);
            setCurrentLineForeground(Color.RED);
            setDigitAlignment(RIGHT);
            setMinimumDisplayDigits(minimumDisplayDigits);

            component.getDocument().addDocumentListener(this);
            component.addCaretListener(this);
            component.addPropertyChangeListener("font", this);
            component.addComponentListener(this);
        }

        /**
         *  Gets the border gap
         *  @return the border gap in pixels
         */
        public int getBorderGap() {
            return borderGap;
        }

        /**
         *  The border gap is used in calculating the left and right insets of the
         *  border. Default value is 5.
         *  @param borderGap  the gap in pixels
         */
        public void setBorderGap(int borderGap) {
            this.borderGap = borderGap;
            Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
            setBorder(new CompoundBorder(OUTER, inner));
            lastDigits = 0;
            setPreferredWidth();
        }

        /**
         *  Gets the current line rendering Color
         *
         *  @return the Color used to render the current line number
         */
        public Color getCurrentLineForeground() {
            return currentLineForeground == null ? getForeground() : currentLineForeground;
        }

        /**
         *  The Color used to render the current line digits. Default is Coolor.RED.
         *
         *  @param currentLineForeground  the Color used to render the current line
         */
        public void setCurrentLineForeground(Color currentLineForeground) {
            this.currentLineForeground = currentLineForeground;
        }

        /**
         *  Gets the digit alignment
         *
         *  @return the alignment of the painted digits
         */
        public float getDigitAlignment() {
            return digitAlignment;
        }

        /**
         *  Specify the horizontal alignment of the digits within the component.
         *  Common values would be:
         *  <ul>
         *  <li>TextLineNumber.LEFT
         *  <li>TextLineNumber.CENTER
         *  <li>TextLineNumber.RIGHT (default)
         *  </ul>
         */
        public void setDigitAlignment(float digitAlignment) {
            this.digitAlignment = digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment;
        }

        /**
         *  Gets the minimum display digits
         *
         *  @return the minimum display digits
         */
        public int getMinimumDisplayDigits() {
            return minimumDisplayDigits;
        }

        /**
         *  Specify the minimum number of digits used to calculate the preferred
         *  width of the component.
         *
         *  @param minimumDisplayDigits  the number digits used in the preferred
         *                               width calculation
         */
        public void setMinimumDisplayDigits(int minimumDisplayDigits) {
            this.minimumDisplayDigits = minimumDisplayDigits;
            setPreferredWidth();
        }

        /**
         *  Calculate the width needed to display the maximum line number
         */
        private void setPreferredWidth() {
            Element root = component.getDocument().getDefaultRootElement();
            int lines = root.getElementCount();
            int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);

            FontMetrics fontMetrics = getFontMetrics(getFont());
            int width = fontMetrics.charWidth('0') * digits;
            Insets insets = getInsets();
            int preferredWidth = insets.left + insets.right + width;

            Dimension d = getPreferredSize();
            d.setSize(preferredWidth, component.getHeight());
            setPreferredSize(d);
            setSize(d);
        }

        /**
         *  Draw the line numbers
         */
        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);

            //  Determine the width of the space available to draw the line number
            FontMetrics fontMetrics = component.getFontMetrics(component.getFont());
            Insets insets = getInsets();
            int availableWidth = getSize().width - insets.left - insets.right;

            //  Determine the rows to draw within the clipped bounds.
            Rectangle clip = g.getClipBounds();
            int rowStartOffset = component.viewToModel(new Point(0, clip.y));
            int endOffset = component.viewToModel(new Point(0, clip.y + clip.height));

            while (rowStartOffset <= endOffset) {
                try {
                    if (isCurrentLine(rowStartOffset)) {
                        g.setColor(getCurrentLineForeground());
                    } else {
                        g.setColor(getForeground());
                    }
                    //  Get the line number as a string and then determine the
                    //  "X" and "Y" offsets for drawing the string.

                    String lineNumber = getTextLineNumber(rowStartOffset);
                    if (!lineNumber.isEmpty()) {
                        int stringWidth = fontMetrics.stringWidth(lineNumber);
                        int x = getOffsetX(availableWidth, stringWidth) + insets.left;
                        int y = getOffsetY(rowStartOffset, fontMetrics);
                        g.drawString(lineNumber, x, y);
                    }
                    //  Move to the next row
                    rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1;
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                }
            }
        }

        /*
         *  We need to know if the caret is currently positioned on the line we
         *  are about to paint so the line number can be highlighted.
         */
        private boolean isCurrentLine(int rowStartOffset) {
            int caretPosition = component.getCaretPosition();
            Element root = component.getDocument().getDefaultRootElement();

            return root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition);
        }

        /*
         *  Get the line number to be drawn. The empty string will be returned
         *  when a line of text has wrapped.
         */
        protected String getTextLineNumber(int rowStartOffset) {
            Element root = component.getDocument().getDefaultRootElement();
            int index = root.getElementIndex(rowStartOffset);
            Element line = root.getElement(index);

            if (line.getStartOffset() == rowStartOffset) {
                return String.valueOf(index + 1);
            } else {
                return "";
            }
        }

        /*
         *  Determine the X offset to properly align the line number when drawn
         */
        private int getOffsetX(int availableWidth, int stringWidth) {
            return (int) ((availableWidth - stringWidth) * digitAlignment);
        }

        /*
         *  Determine the Y offset for the current row
         */
        private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics) throws BadLocationException {
            //  Get the bounding rectangle of the row
            Rectangle r = component.modelToView(rowStartOffset);
            int y = r.y + r.height;

            //  The text needs to be positioned above the bottom of the bounding
            //  rectangle based on the descent of the font(s) contained on the row.

            return y - fontMetrics.getDescent();
        }

        @Override
        public void caretUpdate(CaretEvent e) {
            //  Get the line the caret is positioned on
            int caretPosition = component.getCaretPosition();
            Element root = component.getDocument().getDefaultRootElement();
            int currentLine = root.getElementIndex(caretPosition);

            //  Need to repaint so the correct line number can be highlighted
            if (lastLine != currentLine) {
                setPreferredWidth();
                documentChanged();
                lastLine = currentLine;
            }
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
            //setPreferredWidth();
            //documentChanged();
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            setPreferredWidth();
            documentChanged();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            //setPreferredWidth();
            documentChanged();
        }

        /*
         *  A document change may affect the number of displayed lines of text.
         *  Therefore the lines numbers will also change.
         */
        private void documentChanged() {
            //  View of the component has not been updated at the time
            //  the DocumentEvent is fired
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    repaint();
                }
            });
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getNewValue() instanceof Font) {
                Font newFont = (Font) evt.getNewValue();
                setFont(newFont);
                lastDigits = 0;
                setPreferredWidth();
                documentChanged();
            }
        }

        @Override
        public void componentResized(ComponentEvent e) {
            setPreferredWidth();
            documentChanged();
        }

        @Override
        public void componentMoved(ComponentEvent e) {
            // nothing to do here because moving the component does not change line numbers
        }

        @Override
        public void componentShown(ComponentEvent e) {
            setPreferredWidth();
            documentChanged();
        }

        @Override
        public void componentHidden(ComponentEvent e) {
            // nothing to do here because moving the component does not change line numbers
        }
    }
}