/* 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 org.languagetool.AnalyzedSentence; import org.languagetool.JLanguageTool; import org.languagetool.Language; import org.languagetool.language.RuleFilenameException; import org.languagetool.rules.Rule; import org.languagetool.rules.RuleMatch; import org.languagetool.server.HTTPServer; import org.languagetool.server.HTTPServerConfig; import org.languagetool.server.PortBindingException; import org.languagetool.tools.LanguageIdentifierTools; import org.languagetool.tools.StringTools; import javax.swing.*; import javax.swing.filechooser.FileFilter; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.event.*; import java.io.*; import java.net.URL; import java.util.*; import java.util.List; /** * A simple GUI to check texts with. * * @author Daniel Naber */ public final class Main implements ActionListener { static final String EXTERNAL_LANGUAGE_SUFFIX = " (ext.)"; static final String HTML_FONT_START = "<font face='Arial,Helvetica'>"; static final String HTML_FONT_END = "</font>"; static final String HTML_GREY_FONT_START = "<font face='Arial,Helvetica' color='#666666'>"; 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 CONFIG_FILE = ".languagetool.cfg"; private static final int WINDOW_WIDTH = 600; private static final int WINDOW_HEIGHT = 550; private final ResourceBundle messages; private List<RuleMatch> ruleMatches; private Configuration config; private JLanguageTool langTool; private JFrame frame; private JTextArea textArea; private ResultArea resultArea; private JButton checkTextButton; private LanguageComboBox languageBox; private LanguageDetectionCheckbox autoDetectBox; private Cursor prevCursor; private CheckboxMenuItem enableHttpServerItem; private HTTPServer httpServer; private final Map<Language, ConfigurationDialog> configDialogs = new HashMap<Language, ConfigurationDialog>(); private TrayIcon trayIcon; private boolean closeHidesToTray; private boolean isInTray; private boolean isAlreadyChecking; private Main() throws IOException { LanguageIdentifierTools.addLtProfiles(); config = new Configuration(new File(System.getProperty("user.home")), CONFIG_FILE, null); messages = JLanguageTool.getMessageBundle(); maybeStartServer(); } @Override public void actionPerformed(final ActionEvent e) { try { if (e.getActionCommand().equals(StringTools.getLabel(messages.getString("checkText")))) { checkTextAndDisplayResults(); } else { throw new IllegalArgumentException("Unknown action " + e); } } catch (Exception exc) { Tools.showError(exc); } } void loadFile() { final File file = Tools.openFileDialog(frame, new PlainTextFileFilter()); if (file == null) { // user clicked cancel return; } try { final FileInputStream inputStream = new FileInputStream(file); try { final String fileContents = StringTools.readFile(inputStream); textArea.setText(fileContents); } finally { inputStream.close(); } checkTextAndDisplayResults(); } catch (IOException e) { Tools.showError(e); } } void addLanguage() { final LanguageManagerDialog lmd = new LanguageManagerDialog(frame, Language.getExternalLanguages()); lmd.show(); try { Language.reInit(lmd.getLanguages()); } catch (RuleFilenameException e) { Tools.showErrorMessage(e); } languageBox.populateLanguageBox(); } void showOptions() { final Language currentLanguage = getCurrentLanguage(); final JLanguageTool langTool = getCurrentLanguageTool(currentLanguage); final List<Rule> rules = langTool.getAllRules(); final ConfigurationDialog configDialog = getCurrentConfigDialog(currentLanguage); configDialog.show(rules); // this blocks until OK/Cancel is clicked in the dialog config.setDisabledRuleIds(configDialog.getDisabledRuleIds()); config.setEnabledRuleIds(configDialog.getEnabledRuleIds()); config.setDisabledCategoryNames(configDialog.getDisabledCategoryNames()); config.setMotherTongue(configDialog.getMotherTongue()); config.setRunServer(configDialog.getRunServer()); config.setUseGUIConfig(configDialog.getUseGUIConfig()); config.setServerPort(configDialog.getServerPort()); try { //save config - needed for the server config.saveConfiguration(langTool.getLanguage()); } catch (IOException e) { Tools.showError(e); } // Stop server, start new server if requested: stopServer(); maybeStartServer(); } private void createGUI() { frame = new JFrame("LanguageTool " + JLanguageTool.VERSION); setLookAndFeel(); frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); frame.addWindowListener(new CloseListener()); final URL iconUrl = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(TRAY_ICON); frame.setIconImage(new ImageIcon(iconUrl).getImage()); frame.setJMenuBar(new MainMenuBar(this, messages)); textArea = new JTextArea(messages.getString("guiDemoText")); // TODO: wrong line number is displayed for lines that are wrapped automatically: textArea.setLineWrap(true); textArea.setWrapStyleWord(true); textArea.addKeyListener(new ControlReturnTextCheckingListener()); resultArea = new ResultArea(messages, textArea, config); checkTextButton = new JButton(StringTools.getLabel(messages.getString("checkText"))); checkTextButton.setMnemonic(StringTools.getMnemonic(messages.getString("checkText"))); checkTextButton.addActionListener(this); final JPanel panel = new JPanel(); panel.setOpaque(false); // to get rid of the gray background panel.setLayout(new GridBagLayout()); final GridBagConstraints buttonCons = new GridBagConstraints(); final JPanel insidePanel = new JPanel(); insidePanel.setOpaque(false); insidePanel.setLayout(new GridBagLayout()); buttonCons.gridx = 0; buttonCons.gridy = 0; buttonCons.anchor = GridBagConstraints.WEST; insidePanel.add(new JLabel(" " + messages.getString("textLanguage") + " "), buttonCons); languageBox = new LanguageComboBox(messages); languageBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { langTool = null; // we cannot re-use the existing LT object anymore } }); buttonCons.gridx = 1; buttonCons.gridy = 0; insidePanel.add(languageBox, buttonCons); buttonCons.gridx = 0; buttonCons.gridy = 0; panel.add(insidePanel); buttonCons.gridx = 2; buttonCons.gridy = 0; insidePanel.add(checkTextButton, buttonCons); autoDetectBox = new LanguageDetectionCheckbox(messages, languageBox, config); languageBox.setEnabled(!autoDetectBox.isSelected()); buttonCons.gridx = 1; buttonCons.gridy = 1; buttonCons.gridwidth = 2; buttonCons.anchor = GridBagConstraints.WEST; insidePanel.add(autoDetectBox, buttonCons); final Container contentPane = frame.getContentPane(); final GridBagLayout gridLayout = new GridBagLayout(); contentPane.setLayout(gridLayout); final GridBagConstraints cons = new GridBagConstraints(); 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 = 1; cons.weighty = 5.0f; final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(textArea), new JScrollPane(resultArea)); splitPane.setDividerLocation(200); contentPane.add(splitPane, cons); cons.fill = GridBagConstraints.NONE; cons.gridx = 0; cons.gridy = 2; cons.weighty = 0.0f; cons.insets = new Insets(1, 10, 10, 1); cons.gridy = 3; contentPane.add(panel, cons); warmUpChecker(); frame.pack(); frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT); } private void warmUpChecker() { // Warm-up: we have a lot of lazy init in LT, which causes the first check to // be very slow (several seconds) for languages with a lot of data and a lot of // rules. We just assume that the default language is the language that the user // often uses and init the LT object for that now, not just when it's first used. // This makes the first check feel much faster: getCurrentLanguageTool(languageBox.getDefaultLanguage()); } private void setLookAndFeel() { try { for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { if ("Nimbus".equals(info.getName())) { UIManager.setLookAndFeel(info.getClassName()); break; } } } catch (Exception ignored) { // Well, what can we do... } } private PopupMenu makePopupMenu() { final PopupMenu popup = new PopupMenu(); final ActionListener rmbListener = new TrayActionRMBListener(); // Enable or disable embedded HTTP server: enableHttpServerItem = new CheckboxMenuItem(StringTools.getLabel(messages.getString("tray_menu_enable_server"))); enableHttpServerItem.setState(httpServer != null && httpServer.isRunning()); enableHttpServerItem.addItemListener(new TrayActionItemListener()); popup.add(enableHttpServerItem); // Check clipboard text: final MenuItem checkClipboardItem = new MenuItem(StringTools.getLabel(messages.getString("guiMenuCheckClipboard"))); checkClipboardItem.addActionListener(rmbListener); popup.add(checkClipboardItem); // Open main window: final MenuItem restoreItem = new MenuItem(StringTools.getLabel(messages.getString("guiMenuShowMainWindow"))); restoreItem.addActionListener(rmbListener); popup.add(restoreItem); // Exit: final MenuItem exitItem = new MenuItem(StringTools.getLabel(messages.getString("guiMenuQuit"))); exitItem.addActionListener(rmbListener); popup.add(exitItem); return popup; } void checkClipboardText() { final String s = getClipboardText(); textArea.setText(s); checkTextAndDisplayResults(); } void hideToTray() { if (!isInTray) { final SystemTray tray = SystemTray.getSystemTray(); final String iconPath = tray.getTrayIconSize().height > 16 ? TRAY_ICON : TRAY_SMALL_ICON; final URL iconUrl = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(iconPath); final Image img = Toolkit.getDefaultToolkit().getImage(iconUrl); final 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); } void tagText() { new Thread() { @Override public void run() { setWaitCursor(); try { final JLanguageTool langTool = getCurrentLanguageTool(getCurrentLanguage()); tagTextAndDisplayResults(langTool); } finally { unsetWaitCursor(); } } }.start(); } void quitOrHide() { if (closeHidesToTray) { hideToTray(); } else { quit(); } } void quit() { stopServer(); try { config.saveConfiguration(getCurrentLanguage()); } catch (IOException e) { Tools.showError(e); } frame.setVisible(false); JLanguageTool.removeTemporaryFiles(); System.exit(0); } private void setTrayIcon() { if (trayIcon != null) { final SystemTray tray = SystemTray.getSystemTray(); final boolean httpServerRunning = httpServer != null && httpServer.isRunning(); final boolean smallTray = tray.getTrayIconSize().height <= 16; final 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; } final URL iconUrl = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(iconPath); final 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() { final String s = getClipboardText(); restoreFromTray(); textArea.setText(s); checkTextAndDisplayResults(); } 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; final Transferable data = clipboard.getContents(this); try { if (data != null && data.isDataFlavorSupported(DataFlavor.getTextPlainUnicodeFlavor())) { final DataFlavor df = DataFlavor.getTextPlainUnicodeFlavor(); final Reader sr = df.getReaderForText(data); s = StringTools.readerToString(sr); } else { s = ""; } } catch (Exception ex) { if (data != null) { s = data.toString(); } else { s = ""; } } return s; } private boolean maybeStartServer() { if (config.getRunServer()) { try { final HTTPServerConfig serverConfig = new HTTPServerConfig(config.getServerPort(), false); 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 Language getCurrentLanguage() { if (autoDetectBox.isSelected()) { return autoDetectBox.autoDetectLanguage(textArea.getText()); } else { return ((I18nLanguage) languageBox.getSelectedItem()).getLanguage(); } } private ConfigurationDialog getCurrentConfigDialog(Language language) { final ConfigurationDialog configDialog; if (configDialogs.containsKey(language)) { configDialog = configDialogs.get(language); } else { configDialog = new ConfigurationDialog(frame, false); configDialog.setMotherTongue(config.getMotherTongue()); configDialog.setDisabledRules(config.getDisabledRuleIds()); configDialog.setEnabledRules(config.getEnabledRuleIds()); configDialog.setDisabledCategories(config.getDisabledCategoryNames()); configDialog.setRunServer(config.getRunServer()); configDialog.setServerPort(config.getServerPort()); configDialog.setUseGUIConfig(config.getUseGUIConfig()); configDialogs.put(language, configDialog); } return configDialog; } private JLanguageTool getCurrentLanguageTool(Language currentLanguage) { if (langTool == null) { try { config = new Configuration(new File(System.getProperty("user.home")), CONFIG_FILE, currentLanguage); resultArea.setConfiguration(config); final ConfigurationDialog configDialog = getCurrentConfigDialog(currentLanguage); langTool = new JLanguageTool(currentLanguage, configDialog.getMotherTongue()); langTool.activateDefaultPatternRules(); langTool.activateDefaultFalseFriendRules(); resultArea.setLanguageTool(langTool); final Set<String> disabledRules = configDialog.getDisabledRuleIds(); if (disabledRules != null) { for (final String ruleId : disabledRules) { langTool.disableRule(ruleId); } } final Set<String> disabledCategories = configDialog.getDisabledCategoryNames(); if (disabledCategories != null) { for (final String categoryName : disabledCategories) { langTool.disableCategory(categoryName); } } final Set<String> enabledRules = configDialog.getEnabledRuleIds(); if (enabledRules != null) { for (String ruleName : enabledRules) { langTool.enableDefaultOffRule(ruleName); langTool.enableRule(ruleName); } } } catch (Exception e) { throw new RuntimeException(e); } } return langTool; } private void checkTextAndDisplayResults() { final Language lang = getCurrentLanguage(); if (StringTools.isEmpty(textArea.getText().trim())) { textArea.setText(messages.getString("enterText2")); } else { final String langName; if (lang.isExternal()) { langName = lang.getTranslatedName(messages) + EXTERNAL_LANGUAGE_SUFFIX; } else { langName = lang.getTranslatedName(messages); } new Thread() { @Override public void run() { if (!isAlreadyChecking) { isAlreadyChecking = true; setWaitCursor(); checkTextButton.setEnabled(false); try { final long startTime = System.currentTimeMillis(); final String startCheckText = HTML_GREY_FONT_START + Tools.makeTexti18n(messages, "startChecking", langName) + "..." + HTML_FONT_END; resultArea.setText(startCheckText); resultArea.repaint(); try { final JLanguageTool langTool = getCurrentLanguageTool(lang); ruleMatches = langTool.check(textArea.getText()); resultArea.setStartText(startCheckText); resultArea.setInputText(textArea.getText()); resultArea.setRuleMatches(ruleMatches); resultArea.setRunTime(System.currentTimeMillis() - startTime); resultArea.setLanguageTool(langTool); resultArea.displayResult(); } catch (Exception e) { final String error = getStackTraceAsHtml(e); resultArea.displayText(error); } } finally { checkTextButton.setEnabled(true); unsetWaitCursor(); isAlreadyChecking = false; } } } }.start(); } } 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() { prevCursor = resultArea.getCursor(); frame.setCursor(new Cursor(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(new Cursor(Cursor.WAIT_CURSOR)); resultArea.setCursor(new Cursor(Cursor.WAIT_CURSOR)); } private void unsetWaitCursor() { frame.setCursor(prevCursor); textArea.setCursor(prevCursor); resultArea.setCursor(prevCursor); } private void tagTextAndDisplayResults(final JLanguageTool langTool) { if (StringTools.isEmpty(textArea.getText().trim())) { textArea.setText(messages.getString("enterText2")); } else { // tag text final List<String> sentences = langTool.sentenceTokenize(textArea.getText()); final StringBuilder sb = new StringBuilder(); try { for (String sent : sentences) { final AnalyzedSentence analyzedText = langTool.getAnalyzedSentence(sent); final String analyzedTextString = StringTools.escapeHTML(analyzedText.toString(", ")). replace("[", "<font color='#888888'>[").replace("]", "]</font>"); sb.append(analyzedTextString).append("\n"); } } catch (Exception e) { sb.append(getStackTraceAsHtml(e)); } resultArea.setText(HTML_FONT_START + sb.toString() + HTML_FONT_END); } } private void setTrayMode(boolean trayMode) { this.closeHidesToTray = trayMode; } public static void main(final String[] args) { try { final Main prg = new Main(); if (args.length == 1 && (args[0].equals("-t") || args[0].equals("--tray"))) { // dock to systray on startup javax.swing.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) { System.out.println("Usage: java org.languagetool.gui.Main [-t|--tray]"); System.out.println(" -t, --tray: dock LanguageTool to system tray on startup"); prg.stopServer(); } else { javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { prg.createGUI(); prg.showGUI(); } catch (Exception e) { Tools.showError(e); } } }); } } catch (Exception e) { Tools.showError(e); } } private class ControlReturnTextCheckingListener implements KeyListener { @Override public void keyTyped(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) {} @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { if ((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 { final Language language = getCurrentLanguage(); final ConfigurationDialog configDialog = configDialogs.get(language); if (e.getStateChange() == ItemEvent.SELECTED) { config.setRunServer(true); final boolean serverStarted = maybeStartServer(); enableHttpServerItem.setState(serverStarted); config.setRunServer(serverStarted); config.saveConfiguration(language); if (configDialog != null) { configDialog.setRunServer(true); } } else if (e.getStateChange() == ItemEvent.DESELECTED) { config.setRunServer(false); config.saveConfiguration(language); if (configDialog != null) { configDialog.setRunServer(false); } 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(StringTools.getLabel(messages.getString(label))); } } class TrayActionListener implements MouseListener { @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(); } } @Override public void mouseEntered(@SuppressWarnings("unused") MouseEvent e) {} @Override public void mouseExited(@SuppressWarnings("unused")MouseEvent e) {} @Override public void mousePressed(@SuppressWarnings("unused")MouseEvent e) {} @Override public void mouseReleased(@SuppressWarnings("unused")MouseEvent e) {} } class CloseListener implements WindowListener { @Override public void windowClosing(@SuppressWarnings("unused")WindowEvent e) { quitOrHide(); } @Override public void windowActivated(@SuppressWarnings("unused")WindowEvent e) {} @Override public void windowClosed(@SuppressWarnings("unused")WindowEvent e) {} @Override public void windowDeactivated(@SuppressWarnings("unused")WindowEvent e) {} @Override public void windowDeiconified(@SuppressWarnings("unused")WindowEvent e) {} @Override public void windowIconified(@SuppressWarnings("unused")WindowEvent e) {} @Override public void windowOpened(@SuppressWarnings("unused")WindowEvent e) {} } static class PlainTextFileFilter extends FileFilter { @Override public boolean accept(final File f) { final boolean isTextFile = f.getName().toLowerCase().endsWith(".txt"); return isTextFile || f.isDirectory(); } @Override public String getDescription() { return "*.txt"; } } }