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}