001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.ComponentOrientation;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.beans.PropertyChangeEvent;
011import java.beans.PropertyChangeListener;
012import java.util.Arrays;
013import java.util.HashSet;
014import java.util.List;
015import java.util.Locale;
016import java.util.Set;
017import java.util.regex.Matcher;
018import java.util.regex.Pattern;
019
020import javax.swing.AbstractAction;
021import javax.swing.Action;
022import javax.swing.ImageIcon;
023import javax.swing.KeyStroke;
024
025import org.openstreetmap.josm.data.preferences.ListProperty;
026import org.openstreetmap.josm.gui.MainApplication;
027import org.openstreetmap.josm.spi.preferences.Config;
028import org.openstreetmap.josm.tools.ImageProvider;
029import org.openstreetmap.josm.tools.PlatformManager;
030
031/**
032 * An action that toggles text orientation.
033 * @since 18221
034 */
035public class OrientationAction extends AbstractAction implements PropertyChangeListener {
036    /** Default for {@link #RTL_LANGUAGES} */
037    private static final List<String> DEFAULT_RTL_LANGUAGES = Arrays.asList("ar", "he", "fa", "iw", "ur");
038
039    /** Default for {@link #LOCALIZED_KEYS} */
040    private static final List<String> DEFAULT_LOCALIZED_KEYS = Arrays.asList(
041        "(\\p{Alnum}+_)?name", "addr", "description", "fixme", "note", "source", "strapline", "operator");
042
043    /**
044     * Language codes of languages that are right-to-left
045     *
046     * @see #getValueOrientation
047     */
048    public static final ListProperty RTL_LANGUAGES = new ListProperty("properties.rtl-languages", DEFAULT_RTL_LANGUAGES);
049    /**
050     * Keys whose values are localized
051     *
052     * Regex fractions are allowed. The items will be merged into a regular expression.
053     *
054     * @see #getValueOrientation
055     */
056    public static final ListProperty LOCALIZED_KEYS = new ListProperty("properties.localized-keys", DEFAULT_LOCALIZED_KEYS);
057
058    private static final Pattern LANG_PATTERN = Pattern.compile(":([a-z]{2,3})$");
059    private static final String NEW_STATE = "newState";
060
061    private Component component;
062    private ImageIcon iconRTL;
063    private ImageIcon iconLTR;
064    protected static final Set<String> RTLLanguages = new HashSet<>(RTL_LANGUAGES.get());
065    protected static final Pattern localizedKeys = compileLocalizedKeys();
066
067    /**
068     * Constructs a new {@code OrientationAction}.
069     *
070     * @param component The component to toggle
071     */
072    public OrientationAction(Component component) {
073        super(null);
074        this.component = component;
075        setEnabled(true);
076        if (Config.getPref().getBoolean("text.popupmenu.useicons", true)) {
077            iconLTR = new ImageProvider("dialogs/next").setSize(ImageProvider.ImageSizes.SMALLICON).get();
078            iconRTL = new ImageProvider("dialogs/previous").setSize(ImageProvider.ImageSizes.SMALLICON).get();
079        }
080        component.addPropertyChangeListener(this);
081        putValue(Action.ACCELERATOR_KEY, getShortcutKey());
082        updateState();
083    }
084
085    @Override
086    public void actionPerformed(ActionEvent e) {
087        firePropertyChange("orientationAction", null, getValue(NEW_STATE));
088    }
089
090    /**
091     * Updates the text and the icon.
092     */
093    public void updateState() {
094        if (component.getComponentOrientation().isLeftToRight()) {
095            putValue(Action.NAME, tr("Right to Left"));
096            putValue(Action.SMALL_ICON, iconRTL);
097            putValue(Action.SHORT_DESCRIPTION, tr("Switch the text orientation to Right-to-Left."));
098            putValue(NEW_STATE, ComponentOrientation.RIGHT_TO_LEFT);
099        } else {
100            putValue(Action.NAME, tr("Left to Right"));
101            putValue(Action.SMALL_ICON, iconLTR);
102            putValue(Action.SHORT_DESCRIPTION, tr("Switch the text orientation to Left-to-Right."));
103            putValue(NEW_STATE, ComponentOrientation.LEFT_TO_RIGHT);
104        }
105    }
106
107    @Override
108    public void propertyChange(PropertyChangeEvent evt) {
109        if ("componentOrientation".equals(evt.getPropertyName())) {
110            updateState();
111        }
112    }
113
114    /**
115     * Returns the shortcut key to assign to this action.
116     *
117     * @return the shortcut key
118     */
119    public static KeyStroke getShortcutKey() {
120        return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx());
121    }
122
123    /**
124     * Returns the default component orientation by the user's locale
125     *
126     * @return the default component orientation
127     */
128    public static ComponentOrientation getDefaultComponentOrientation() {
129        Component main = MainApplication.getMainFrame();
130        // is null while testing
131        return main != null ? main.getComponentOrientation() : ComponentOrientation.LEFT_TO_RIGHT;
132    }
133
134    /**
135     * Returns the text orientation of the value for the given key.
136     *
137     * This is intended for Preset Dialog comboboxes. The choices in the dropdown list are
138     * typically translated. Ideally the user needs not see the English value.
139     *
140     * The algorithm is as follows:
141     * <ul>
142     * <li>If the key has an explicit language suffix, return the text orientation for that
143     * language.
144     * <li>Else return the text orientation of the user's locale.
145     * </ul>
146     *
147     * You can configure which languages are RTL with the list property: {@code rtl-languages}.
148     *
149     * @param key the key
150     * @return the text orientation of the value
151     */
152    public static ComponentOrientation getValueOrientation(String key) {
153        if (key == null || key.isEmpty())
154            return ComponentOrientation.LEFT_TO_RIGHT;
155
156        // if the key has an explicit language suffix, use it
157        Matcher m = LANG_PATTERN.matcher(key);
158        if (m.find()) {
159            if (RTLLanguages.contains(m.group(1))) {
160                return ComponentOrientation.RIGHT_TO_LEFT;
161            }
162            return ComponentOrientation.LEFT_TO_RIGHT;
163        }
164        // return the user's locale
165        return ComponentOrientation.getOrientation(Locale.getDefault());
166    }
167
168    /**
169     * Returns the text orientation of the value for the given key.
170     *
171     * This expansion of {@link #getValueOrientation} is intended for Preset Dialog textfields and
172     * for the Add Tag and Edit Tag dialog comboboxes.
173     *
174     * The algorithm is as follows:
175     * <ul>
176     * <li>If the key has an explicit language suffix, return the text orientation for that
177     * language.
178     * <li>If the key is usually localized, return the text orientation of the user's locale.
179     * <li>Else return left to right.
180     * </ul>
181     *
182     * You can configure which keys are localized with the list property: {@code localized-keys}.
183     * You can configure which languages are RTL with the list property: {@code rtl-languages}.
184     *
185     * @param key the key
186     * @return the text orientation of the value
187     */
188    public static ComponentOrientation getNamelikeOrientation(String key) {
189        if (key == null || key.isEmpty())
190            return ComponentOrientation.LEFT_TO_RIGHT;
191
192        // if the key has an explicit language suffix, use it
193        Matcher m = LANG_PATTERN.matcher(key);
194        if (m.find()) {
195            if (RTLLanguages.contains(m.group(1))) {
196                return ComponentOrientation.RIGHT_TO_LEFT;
197            }
198            return ComponentOrientation.LEFT_TO_RIGHT;
199        }
200        // if the key is usually localized, use the user's locale
201        m = localizedKeys.matcher(key);
202        if (m.find()) {
203            return ComponentOrientation.getOrientation(Locale.getDefault());
204        }
205        // all other keys are LTR
206        return ComponentOrientation.LEFT_TO_RIGHT;
207    }
208
209    private static Pattern compileLocalizedKeys() {
210        return Pattern.compile("^(" + String.join("|", LOCALIZED_KEYS.get()) + ")$");
211    }
212}