/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2016 Lev Abashkin, Aaron Madlon-Kay Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OmegaT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.gui.preferences.view; import static org.omegat.languagetools.LanguageToolNativeBridge.getLTLanguage; import java.awt.BorderLayout; import java.awt.Component; import java.awt.ComponentOrientation; import java.awt.Dimension; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreePath; import javax.xml.parsers.ParserConfigurationException; import org.languagetool.AnalyzedSentence; import org.languagetool.JLanguageTool; import org.languagetool.rules.Category; import org.languagetool.rules.CategoryId; import org.languagetool.rules.Rule; import org.languagetool.rules.RuleMatch; import org.languagetool.tools.Tools; import org.omegat.core.Core; import org.omegat.gui.preferences.BasePreferencesController; import org.omegat.languagetools.LanguageToolPrefs; import org.omegat.languagetools.LanguageToolWrapper; import org.omegat.languagetools.LanguageToolWrapper.BridgeType; import org.omegat.util.Log; import org.omegat.util.OStrings; import org.omegat.util.StringUtil; import org.xml.sax.SAXException; /** * @author Panagiotis Minos * @author Lev Abashkin * @author Aaron Madlon-Kay */ public class LanguageToolConfigurationController extends BasePreferencesController { private final static String NEW_RULE_PATTERN = "^[A-Za-z_.]+$"; private BridgeType selectedBridgeType; private Set<String> disabledCategories, disabledRuleIds, enabledRuleIds; private String targetLanguageCode; private LanguageToolConfigurationPanel panel; @Override public JComponent getGui() { if (panel == null) { initGui(); initFromPrefs(); } return panel; } @Override public String toString() { return OStrings.getString("PREFS_TITLE_LANGUAGETOOL"); } private void initGui() { panel = new LanguageToolConfigurationPanel(); org.openide.awt.Mnemonics.setLocalizedText(panel.bridgeNativeRadioButton, StringUtil.format(OStrings.getString("GUI_LANGUAGETOOL_NATIVE_BRIDGE"), JLanguageTool.VERSION)); panel.bridgeNativeRadioButton.addActionListener(e -> handleBridgeTypeChange(BridgeType.NATIVE)); panel.bridgeLocalRadioButton.addActionListener(e -> handleBridgeTypeChange(BridgeType.LOCAL_INSTALLATION)); panel.bridgeRemoteRadioButton.addActionListener(e -> handleBridgeTypeChange(BridgeType.REMOTE_URL)); panel.directoryChooseButton.addActionListener(e -> chooseLocalInstallation()); panel.addRuleButton.addActionListener(e -> addRule()); panel.deleteRuleButton.addActionListener(e -> deleteRules()); if (Core.getProject().isProjectLoaded()) { targetLanguageCode = Core.getProject().getProjectProperties().getTargetLanguage() .getLanguageCode(); initWithLangs(); } else { initWithoutLangs(); } } private void initWithoutLangs() { disableRulesUI(OStrings.getString("GUI_LANGUAGETOOL_RULES_UNAVAILABLE_NO_PROJECT")); } private void chooseLocalInstallation() { JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); FileFilter filter = new FileNameExtensionFilter(OStrings.getString("GUI_LANGUAGETOOL_JAR_FILE_FILTER"), "jar"); fileChooser.setFileFilter(filter); fileChooser.setDialogTitle(OStrings.getString("GUI_LANGUAGETOOL_FILE_CHOOSER_TITLE")); int result = fileChooser.showOpenDialog(panel); if (result == JFileChooser.APPROVE_OPTION) { File file = fileChooser.getSelectedFile(); panel.localServerJarPathTextField.setText(file.getAbsolutePath()); } } private void addRule() { String newRuleId = (String) JOptionPane.showInputDialog(panel, OStrings.getString("GUI_LANGUAGETOOL_ADD_RULE_MESSAGE"), OStrings.getString("GUI_LANGUAGETOOL_ADD_RULE_DIALOG_TITLE"), JOptionPane.PLAIN_MESSAGE, null, null, null); if (newRuleId == null) { return; // Nothing to do } if (!Pattern.matches(NEW_RULE_PATTERN, newRuleId)) { JOptionPane.showMessageDialog(panel, OStrings.getString("GUI_LANGUAGETOOL_BAD_RULE_ID")); return; } DefaultTreeModel model = (DefaultTreeModel) panel.rulesTree.getModel(); DefaultMutableTreeNode root = (DefaultMutableTreeNode) panel.rulesTree.getModel().getRoot(); DefaultMutableTreeNode externalCategoryNode = null; // Check if external category node already exists (should be last // child) CategoryNode lastCategoryNode = (CategoryNode) root.getLastChild(); if (lastCategoryNode.getCategory().getId().toString().equals(ExternalRule.CATEGORY_ID)) { // Check if rule already exists for (int i = 0; i < lastCategoryNode.getChildCount(); i++) { if (((RuleNode) lastCategoryNode.getChildAt(i)).getRule().getId().equals(newRuleId)) { JOptionPane.showMessageDialog(panel, OStrings.getString("GUI_LANGUAGETOOL_RULE_ALREADY_EXISTS")); return; } } externalCategoryNode = lastCategoryNode; } else { externalCategoryNode = new CategoryNode(ExternalRule.DEFAULT_CATEGORY, true); model.insertNodeInto(externalCategoryNode, root, root.getChildCount()); } DefaultMutableTreeNode newRuleNode = new RuleNode(new ExternalRule(newRuleId), true); model.insertNodeInto(newRuleNode, externalCategoryNode, externalCategoryNode.getChildCount()); if (!((CategoryNode) externalCategoryNode).isEnabled()) { ((CategoryNode) externalCategoryNode).setEnabled(true); model.nodeChanged(externalCategoryNode); } panel.rulesTree.scrollPathToVisible(new TreePath(newRuleNode.getPath())); } private void deleteRules() { TreePath[] currentSelections = panel.rulesTree.getSelectionPaths(); if (currentSelections != null) { DefaultTreeModel model = (DefaultTreeModel) panel.rulesTree.getModel(); Stream.of(currentSelections).map(TreePath::getLastPathComponent) .filter(LanguageToolConfigurationController::isExternalRuleNode).forEach(node -> { MutableTreeNode currentNode = (MutableTreeNode) node; MutableTreeNode parent = (MutableTreeNode) currentNode.getParent(); model.removeNodeFromParent(currentNode); if (parent.getChildCount() == 0) { model.removeNodeFromParent(parent); } }); } } private void disableRulesUI(String message) { panel.rulesMessagePanel.setVisible(true); panel.rulesMessageLabel.setText(message); panel.rulesMessageLabel.setEnabled(false); panel.rulesScrollPane.setVisible(false); panel.rulesButtonsPanel.setVisible(false); } private void initWithLangs() { panel.rulesMessagePanel.setVisible(false); panel.rulesTree.addTreeSelectionListener(e -> updateButtonState()); panel.rulesTree.setCellRenderer(new CheckBoxTreeCellRenderer()); TreeListener.install(panel.rulesTree); } private void loadRules(org.omegat.util.Language sourceLang, org.omegat.util.Language targetLang) { // Load rule tree org.languagetool.Language targetLtLang = getLTLanguage(targetLang); org.languagetool.Language sourceLtLang = getLTLanguage(sourceLang); if (targetLtLang == null) { disableRulesUI(OStrings.getString("GUI_LANGUAGETOOL_RULES_UNAVAILABLE")); return; } JLanguageTool ltInstance; try { ltInstance = new JLanguageTool(targetLtLang); } catch (Throwable e) { // Disable tree and return if instance couldn't be gotten disableRulesUI(OStrings.getString("GUI_LANGUAGETOOL_RULES_UNAVAILABLE_ERROR")); Log.log(e); return; } List<Rule> rules = ltInstance.getAllRules(); if (sourceLtLang != null) { try { rules.addAll(Tools.getBitextRules(sourceLtLang, targetLtLang)); } catch (IOException | ParserConfigurationException | SAXException e) { Log.log(e); } } // Collect internal rule IDs List<String> internalRuleIds = rules.stream().map(Rule::getId).collect(Collectors.toList()); // Remove rule IDs of rules disabled by default from set of disabled // rules rules.stream().filter(Rule::isDefaultOff).map(Rule::getId).forEach(disabledRuleIds::remove); // Create ExternalRule instances for rules not found in built-in LT // and add them to our rules list List<String> externalRuleIds = new ArrayList<>(disabledRuleIds); externalRuleIds.addAll(enabledRuleIds); externalRuleIds.removeAll(internalRuleIds); externalRuleIds.stream().distinct().map(ExternalRule::new).forEach(rules::add); DefaultMutableTreeNode rootNode = createTree(rules); panel.rulesTree.setModel(getTreeModel(rootNode)); panel.rulesTree .applyComponentOrientation(ComponentOrientation.getOrientation(targetLtLang.getLocale())); updateButtonState(); } private DefaultMutableTreeNode createTree(List<Rule> rules) { DefaultMutableTreeNode root = new DefaultMutableTreeNode("Rules"); String lastRuleId = null; Map<String, DefaultMutableTreeNode> parents = new TreeMap<>(); for (Rule rule : rules) { if (!parents.containsKey(rule.getCategory().getId().toString())) { boolean enabled = true; if (disabledCategories != null && disabledCategories.contains(rule.getCategory().getId().toString())) { enabled = false; } if (rule.getCategory().isDefaultOff()) { enabled = false; } DefaultMutableTreeNode categoryNode = new CategoryNode(rule.getCategory(), enabled); root.add(categoryNode); parents.put(rule.getCategory().getId().toString(), categoryNode); } if (!rule.getId().equals(lastRuleId)) { RuleNode ruleNode = new RuleNode(rule, getEnabledState(rule)); CategoryNode parent = (CategoryNode) parents.get(rule.getCategory().getId().toString()); parent.add(ruleNode); // Ensure that a category appears enabled if any of its children // are enabled. We don't persist enabled categories, only // disabled ones, so this is purely cosmetic. if (ruleNode.isEnabled()) { parent.setEnabled(true); } } lastRuleId = rule.getId(); } return root; } void updateButtonState() { TreePath[] selected = panel.rulesTree.getSelectionPaths(); boolean deletable = selected != null && Stream.of(selected).map(TreePath::getLastPathComponent) .allMatch(LanguageToolConfigurationController::isExternalRuleNode); panel.deleteRuleButton.setEnabled(deletable); } static boolean isExternalRuleNode(Object node) { return node instanceof RuleNode && ((RuleNode) node).getRule() instanceof ExternalRule; } private boolean getEnabledState(Rule rule) { boolean ret = true; if (disabledRuleIds.contains(rule.getId())) { ret = false; } if (disabledCategories.contains(rule.getCategory().getId().toString())) { ret = false; } if ((rule.isDefaultOff() || rule.getCategory().isDefaultOff()) && !enabledRuleIds.contains(rule.getId())) { ret = false; } if (rule.isDefaultOff() && rule.getCategory().isDefaultOff() && enabledRuleIds.contains(rule.getId())) { disabledCategories.remove(rule.getCategory().getId().toString()); } return ret; } private DefaultTreeModel getTreeModel(DefaultMutableTreeNode rootNode) { DefaultTreeModel treeModel = new DefaultTreeModel(rootNode); treeModel.addTreeModelListener(new TreeModelListener() { @Override public void treeNodesChanged(TreeModelEvent e) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getTreePath().getLastPathComponent(); int index = e.getChildIndices()[0]; node = (DefaultMutableTreeNode) node.getChildAt(index); if (node instanceof RuleNode) { RuleNode o = (RuleNode) node; if (o.getRule().isDefaultOff() || o.getRule().getCategory().isDefaultOff()) { if (o.isEnabled()) { enabledRuleIds.add(o.getRule().getId()); } else { enabledRuleIds.remove(o.getRule().getId()); } } else { if (o.isEnabled()) { disabledRuleIds.remove(o.getRule().getId()); } else { disabledRuleIds.add(o.getRule().getId()); } // External rules are on by default. // Always keep their IDs in one of the sets. if (o.getRule().getCategory().getId().toString().equals(ExternalRule.CATEGORY_ID)) { if (o.isEnabled()) { enabledRuleIds.add(o.getRule().getId()); } else { enabledRuleIds.remove(o.getRule().getId()); } } } // Clean up disabled rules already disabled by category. // Do not remove IDs of external rules. if (disabledCategories.contains(o.getRule().getCategory().getId().toString()) && !o.getRule().getCategory().getId().toString().equals(ExternalRule.CATEGORY_ID)) { disabledRuleIds.remove(o.getRule().getId()); } } if (node instanceof CategoryNode) { CategoryNode o = (CategoryNode) node; if (o.isEnabled()) { disabledCategories.remove(o.getCategory().getId().toString()); } else { disabledCategories.add(o.getCategory().getId().toString()); } } } @Override public void treeNodesInserted(TreeModelEvent e) { DefaultMutableTreeNode newNode = (DefaultMutableTreeNode) e.getChildren()[0]; if (newNode instanceof RuleNode) { enabledRuleIds.add(((RuleNode) newNode).getRule().getId()); } if (newNode instanceof CategoryNode) { } } @Override public void treeNodesRemoved(TreeModelEvent e) { DefaultMutableTreeNode removedNode = (DefaultMutableTreeNode) e.getChildren()[0]; if (removedNode instanceof RuleNode) { if (((RuleNode) removedNode).isEnabled()) { enabledRuleIds.remove(((RuleNode) removedNode).getRule().getId()); } else { disabledRuleIds.remove(((RuleNode) removedNode).getRule().getId()); } } if (removedNode instanceof CategoryNode) { disabledCategories.remove(((CategoryNode) removedNode).getCategory().getId().toString()); } } @Override public void treeStructureChanged(TreeModelEvent e) { } }); return treeModel; } private void handleBridgeTypeChange(BridgeType type) { selectedBridgeType = type; switch (type) { case NATIVE: panel.localServerJarPathTextField.setEnabled(false); panel.directoryChooseButton.setEnabled(false); panel.urlTextField.setEnabled(false); break; case REMOTE_URL: panel.localServerJarPathTextField.setEnabled(false); panel.directoryChooseButton.setEnabled(false); panel.urlTextField.setEnabled(true); break; case LOCAL_INSTALLATION: panel.localServerJarPathTextField.setEnabled(true); panel.directoryChooseButton.setEnabled(true); panel.urlTextField.setEnabled(false); break; } } @Override protected void initFromPrefs() { BridgeType type = LanguageToolPrefs.getBridgeType(); switch (type) { case NATIVE: panel.bridgeNativeRadioButton.setSelected(true); break; case REMOTE_URL: panel.bridgeRemoteRadioButton.setSelected(true); break; case LOCAL_INSTALLATION: panel.bridgeLocalRadioButton.setSelected(true); break; } panel.urlTextField.setText(LanguageToolPrefs.getRemoteUrl()); panel.localServerJarPathTextField.setText(LanguageToolPrefs.getLocalServerJarPath()); if (targetLanguageCode != null) { disabledCategories = LanguageToolPrefs.getDisabledCategories(targetLanguageCode); disabledRuleIds = LanguageToolPrefs.getDisabledRules(targetLanguageCode); enabledRuleIds = LanguageToolPrefs.getEnabledRules(targetLanguageCode); loadRules(Core.getProject().getProjectProperties().getSourceLanguage(), Core.getProject().getProjectProperties().getTargetLanguage()); } handleBridgeTypeChange(type); } @Override public void restoreDefaults() { panel.bridgeNativeRadioButton.setSelected(true); panel.urlTextField.setText(""); panel.localServerJarPathTextField.setText(""); if (targetLanguageCode != null) { disabledCategories = LanguageToolPrefs.getDefaultDisabledCategories(); disabledRuleIds = LanguageToolPrefs.getDefaultDisabledRules(); enabledRuleIds = Collections.emptySet(); loadRules(Core.getProject().getProjectProperties().getSourceLanguage(), Core.getProject().getProjectProperties().getTargetLanguage()); } handleBridgeTypeChange(LanguageToolPrefs.DEFAULT_BRIDGE_TYPE); } @Override public void persist() { LanguageToolPrefs.setBridgeType(selectedBridgeType); LanguageToolPrefs.setRemoteUrl(panel.urlTextField.getText()); LanguageToolPrefs.setLocalServerJarPath(panel.localServerJarPathTextField.getText()); if (targetLanguageCode != null) { LanguageToolPrefs.setDisabledCategories(disabledCategories, targetLanguageCode); LanguageToolPrefs.setDisabledRules(disabledRuleIds, targetLanguageCode); LanguageToolPrefs.setEnabledRules(enabledRuleIds, targetLanguageCode); LanguageToolWrapper.setBridgeFromCurrentProject(); } SwingUtilities.invokeLater(() -> { if (Core.getProject().isProjectLoaded()) { Core.getEditor().refreshView(true); } }); } /** * Helper class to work with manually defined rules unknown to built-in * LanguageTool * * @author Lev Abashkin */ static class ExternalRule extends Rule { public final static String CATEGORY_ID = "EXTERNAL"; public final static Category DEFAULT_CATEGORY = new Category(new CategoryId(CATEGORY_ID), OStrings.getString("GUI_LANGUAGETOOL_EXTERNAL_CATEGORY_NAME")); private final String id; ExternalRule(String id) { this.id = id; setCategory(DEFAULT_CATEGORY); setDefaultOn(); } @Override public String getId() { return id; } @Override public String getDescription() { return id; } @Override public RuleMatch[] match(AnalyzedSentence as) throws IOException { throw new UnsupportedOperationException(); } @Override public void reset() { throw new UnsupportedOperationException(); } } /** * @author Panagiotis Minos */ static class TreeListener implements KeyListener, MouseListener { static void install(JTree tree) { TreeListener listener = new TreeListener(tree); tree.addMouseListener(listener); tree.addKeyListener(listener); } private static final Dimension CHECKBOX_DIMENSION = new JCheckBox().getPreferredSize(); private final JTree tree; private TreeListener(JTree tree) { this.tree = tree; } @Override public void keyTyped(KeyEvent e) { } @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { TreePath[] paths = tree.getSelectionPaths(); if (paths != null) { for (TreePath path : paths) { handle(path); } } } } @Override public void keyReleased(KeyEvent e) { } @Override public void mouseClicked(MouseEvent e) { TreePath path = tree.getPathForLocation(e.getX(), e.getY()); if ((path != null) && (path.getPathCount() > 0)) { if (isValidNode(path.getLastPathComponent())) { if (isOverCheckBox(e.getX(), e.getY(), path)) { handle(path); } } } } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } private void handle(TreePath path) { if ((path != null) && (path.getPathCount() > 0)) { if (path.getLastPathComponent() instanceof CategoryNode) { DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); CategoryNode node = (CategoryNode) path.getLastPathComponent(); node.setEnabled(!node.isEnabled()); model.nodeChanged(node); for (int i = 0; i < node.getChildCount(); i++) { RuleNode child = (RuleNode) node.getChildAt(i); if (child.isEnabled() != node.isEnabled()) { child.setEnabled(node.isEnabled()); model.nodeChanged(child); } } } if (path.getLastPathComponent() instanceof RuleNode) { DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); RuleNode node = (RuleNode) path.getLastPathComponent(); node.setEnabled(!node.isEnabled()); model.nodeChanged(node); if (node.isEnabled()) { CategoryNode parent = (CategoryNode) node.getParent(); parent.setEnabled(true); } model.nodeChanged(node.getParent()); } } } private boolean isOverCheckBox(int x, int y, TreePath path) { int offset = tree.getPathBounds(path).x + CHECKBOX_DIMENSION.width; return x <= offset; } private boolean isValidNode(Object c) { return ((c instanceof CategoryNode) || (c instanceof RuleNode)); } } /** * @author Panagiotis Minos */ @SuppressWarnings("serial") static class RuleNode extends DefaultMutableTreeNode { private final Rule rule; private boolean enabled; RuleNode(Rule rule, boolean enabled) { super(rule); this.rule = rule; this.enabled = enabled; } Rule getRule() { return rule; } boolean isEnabled() { return enabled; } void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public String toString() { return rule.getDescription(); } } /** * @author Panagiotis Minos */ @SuppressWarnings("serial") static class CategoryNode extends DefaultMutableTreeNode { private final Category category; private boolean enabled; CategoryNode(Category category, boolean enabled) { super(category); this.category = category; this.enabled = enabled; } Category getCategory() { return category; } boolean isEnabled() { return enabled; } void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public String toString() { int children = this.getChildCount(); int selected = 0; for (int i = 0; i < children; i++) { RuleNode child = (RuleNode) this.getChildAt(i); if (child.isEnabled()) { selected++; } } return String.format("%s (%d/%d)", category.getName(), selected, children); } } /** * @author Panagiotis Minos */ @SuppressWarnings("serial") static class CheckBoxTreeCellRenderer extends JPanel implements TreeCellRenderer { private final DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); private final JCheckBox checkBox = new JCheckBox(); private Component defaultComponent; CheckBoxTreeCellRenderer() { setLayout(new BorderLayout()); setOpaque(false); checkBox.setOpaque(false); renderer.setLeafIcon(null); renderer.setOpenIcon(null); renderer.setClosedIcon(null); add(checkBox, BorderLayout.WEST); } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component component = renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); if (value instanceof CategoryNode) { if (defaultComponent != null) { remove(defaultComponent); } defaultComponent = component; add(component, BorderLayout.CENTER); CategoryNode node = (CategoryNode) value; checkBox.setSelected(node.isEnabled()); return this; } if (value instanceof RuleNode) { if (defaultComponent != null) { remove(defaultComponent); } defaultComponent = component; add(component, BorderLayout.CENTER); RuleNode node = (RuleNode) value; checkBox.setSelected(node.isEnabled()); return this; } return component; } } }