/* * Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fhcrc.cpl.viewer; import org.apache.log4j.Logger; import javax.swing.plaf.FontUIResource; import javax.swing.*; import java.util.Locale; import java.util.Vector; import java.util.HashMap; import java.util.prefs.Preferences; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import org.fhcrc.cpl.toolbox.TextProvider; import org.fhcrc.cpl.toolbox.ApplicationContext; import org.fhcrc.cpl.viewer.gui.WorkbenchFrame; import org.swixml.SwingEngine; /** * Provides methods for defining aspects of behavior for different Locales, and for * switching between Locales */ public class Localizer { private static Logger _log = Logger.getLogger(Localizer.class); protected static HashMap _languageProperties = null; public static final String DEFAULT_LOCALE_STRING = "en"; public static final Locale DEFAULT_LOCALE = new Locale(DEFAULT_LOCALE_STRING); //Index of the language and font menus within the menubar. //It would be better if we could reference this by name somehow protected static int LANGUAGE_MENU_INDEX = 3; protected static int FONT_MENU_INDEX = 4; protected static Locale _locale = DEFAULT_LOCALE; protected static Font _font; //for storing language and font preference protected static final Preferences _prefs = Preferences.userNodeForPackage(Application.class); protected static final String _localePrefKey = "lastLocale"; protected static final String _fontPrefKeyPrefix = "lastFont_"; protected static Locale[] supportedLocales = {new Locale("en") , new Locale("zh") }; //constants for defining properties for each language protected static final String FONT_TEST_STRING="FONT_TEST_STRING"; protected static final String LABEL_SCALE="LABEL_SCALE"; protected static final String CONTAINER_WIDTH_SCALE="CONTAINER_WIDTH_SCALE"; protected static final String CONTAINER_HEIGHT_SCALE="CONTAINER_HEIGHT_SCALE"; protected static final String FONT_SIZE="FONT_SIZE"; protected static final String FONT_NAME="FONT_NAME"; protected static final String FONT_STYLE="FONT_STYLE"; protected static final String REQUIRES_FONT_CHOICE="REQUIRES_FONT_CHOICE"; public Localizer() { } /** * Initialize properties for each language */ protected static void initLanguageProperties() { _languageProperties = new HashMap(); HashMap enProperties = new HashMap(); enProperties.put(FONT_TEST_STRING,"a"); enProperties.put(LABEL_SCALE,"1"); enProperties.put(CONTAINER_WIDTH_SCALE,"1"); enProperties.put(CONTAINER_HEIGHT_SCALE,"1"); enProperties.put(FONT_SIZE,"12"); enProperties.put(FONT_NAME,"Dialog"); enProperties.put(FONT_STYLE,Integer.toString(Font.PLAIN)); enProperties.put(REQUIRES_FONT_CHOICE,"FALSE"); _languageProperties.put("en",enProperties); HashMap zhProperties = new HashMap(); zhProperties.put(FONT_TEST_STRING,"\u4e00"); zhProperties.put(LABEL_SCALE,"1"); zhProperties.put(CONTAINER_WIDTH_SCALE,"1.1"); zhProperties.put(CONTAINER_HEIGHT_SCALE,"1.1"); zhProperties.put(FONT_SIZE,"16"); zhProperties.put(FONT_NAME,"Dialog"); zhProperties.put(FONT_STYLE,Integer.toString(Font.BOLD)); zhProperties.put(REQUIRES_FONT_CHOICE,"TRUE"); _languageProperties.put("zh",zhProperties); } /** * * @return a HashMap full of HashMaps */ protected static HashMap getLanguageProperties() { if (_languageProperties == null) initLanguageProperties(); return _languageProperties; } /** * return a single language property * @param language * @param propertyName * @return */ protected static String getLanguageProperty(String language, String propertyName) { return (String) ((HashMap)(getLanguageProperties().get(language))).get(propertyName); } /** * return a single language property * @param propertyName * @return */ protected static String getLanguageProperty(String propertyName) { return getLanguageProperty(getLanguage(), propertyName); } /** * return a single languge property, as an int * @param language * @param propertyName * @return */ protected static int getLanguagePropertyInt(String language, String propertyName) { int result = 0; try { result = Integer.parseInt(getLanguageProperty(language,propertyName)); } catch(Exception e) {} return result; } protected static int getLanguagePropertyInt(String propertyName) { return getLanguagePropertyInt(getLanguage(), propertyName); } /** * return a single language property, as a double * @param language * @param propertyName * @return */ protected static double getLanguagePropertyDouble(String language, String propertyName) { double result = 0; try { result = Double.parseDouble(getLanguageProperty(language,propertyName)); } catch(Exception e) {} return result; } protected static double getLanguagePropertyDouble(String propertyName) { return getLanguagePropertyDouble(getLanguage(), propertyName); } /** * Create a locale from a string. Used for loading preferences * * @param localeString of the format "<language_code>" or "<language_code>_<country_code>" * @return the appropriate locale */ protected static Locale createLocale(String localeString) { String[] stringPieces = localeString.split("_"); if (stringPieces.length == 1) { return new Locale(stringPieces[0]); } return new Locale(stringPieces[0], stringPieces[1]); } /** * Turn a Locale into a string that we can store as a preference * @param locale * @return a string that we can store as a preference */ protected static String createLocaleString(Locale locale) { String result = locale.getLanguage(); if (locale.getCountry() != null && locale.getCountry().length() > 0) result = result + "_" + locale.getCountry(); return result; } /** * set the initial locale and font for the session, from the stored preference if one is available */ public static void setInitialLocaleProperties() { String localeString = _prefs.get(_localePrefKey, DEFAULT_LOCALE_STRING); changeLocale(createLocale(localeString)); String fontString = _prefs.get(getFontPrefKey(), null); if (fontString == null) { fontString = getWorkingFont().getName(); } changeFont(fontString); } /** * Set the locale for the application. Sets the default Java Locale, determines the * appropriate font, and updates all Swing components to use that font. Also updates * the menu item text in the Language menu * @param locale */ public static void changeLocale(Locale locale) { if (locale.equals(_locale)) return; //if no fonts supporting this language, give the user a warning, don't change if (!doesLanguageHaveFonts(locale.getLanguage())) { ApplicationContext.infoMessage(TextProvider.getText("NO_FONTS_FOR_LANGUAGE_CHOSEN")); return; } _locale = locale; _prefs.put(_localePrefKey, createLocaleString(locale)); Locale.setDefault(locale); TextProvider.reloadTextBundle(); String fontString = _prefs.get(getFontPrefKey(), null); changeFont(fontString); setSwingDefaultFont(_font); //redraw the entire application, losing any data Application application = (Application) ApplicationContext.getImpl(); if (application != null) { application.redrawWorkbench(); } /* it sure would be nice if we could update everything this way. However, this doesn't //give us a way to change all the text on the components to the right text for the new //Locale. WorkbenchFrame frame = (WorkbenchFrame) application.getFrame(); if (frame != null) { frame.getSwingEngine().setLocale(getLocale()); setFontForComponentTree(frame, font); recreateLanguageMenuItems(frame); //update all current UI components frame.update(frame.getGraphics()); } */ } /** * returns a swing engine with the appropriate locale set * @return a swing engine with the appropriate locale set */ public static SwingEngine getSwingEngine(Object client) { SwingEngine swingEngine = new SwingEngine(client); swingEngine.setLocale(Localizer.getLocale()); return swingEngine; } /** * Utility method to scale up the width of a dimension by a certain factor * @param dimension * @param widthFactor * @param heightFactor * @return */ protected static Dimension adjustDimension(Dimension dimension, double widthFactor, double heightFactor) { int newHeight = (int) (dimension.getHeight() * heightFactor); int newWidth = (int) (dimension.getWidth() * widthFactor); return new Dimension(newWidth,newHeight); } /** * Special method used to resize the main window * @param dimension * @return */ public static Dimension adjustContainerDimension(Dimension dimension) { return adjustDimension(dimension,getLanguagePropertyDouble(CONTAINER_WIDTH_SCALE), getLanguagePropertyDouble(CONTAINER_HEIGHT_SCALE)); } public static int adjustContainerWidth(int width) { return (int) ((double) width * getLanguagePropertyDouble(CONTAINER_WIDTH_SCALE)); } public static int adjustContainerHeight(int height) { return (int) ((double) height * getLanguagePropertyDouble(CONTAINER_HEIGHT_SCALE)); } /** * renders a swixml file * @param swixmlPath * @param client * @return the Container that holds everything created by the swixml file * @throws Exception */ public static Container renderSwixml(String swixmlPath, Object client) throws Exception { Container content = getSwingEngine(client).render(swixmlPath); localizeComponentTree(content); return content; } /** * Apply localization rules to the component, depending on its type * @param theComponent */ public static void localizeComponent(Component theComponent) { double labelScale = getLanguagePropertyDouble(LABEL_SCALE); double containerWidthScale = getLanguagePropertyDouble(CONTAINER_WIDTH_SCALE); double containerHeightScale = getLanguagePropertyDouble(CONTAINER_HEIGHT_SCALE); //if this language is at the same scale as English, do nothing if (labelScale == 1.0 && containerWidthScale == 1.0 && containerHeightScale == 1.0) return; //for labels if (theComponent instanceof JLabel) { theComponent.setPreferredSize(adjustDimension(theComponent.getPreferredSize(), labelScale, 1)); theComponent.setMinimumSize(adjustDimension(theComponent.getMinimumSize(), labelScale, 1)); } //for containers else if (theComponent instanceof JPanel || theComponent instanceof JSplitPane || //for some reason, vertical resizing of JTables causes their content rows not to render! // theComponent instanceof JTable || theComponent instanceof JScrollPane || theComponent instanceof JTabbedPane) { theComponent.setPreferredSize(adjustDimension(theComponent.getPreferredSize(), containerWidthScale, containerHeightScale)); theComponent.setMinimumSize(adjustDimension(theComponent.getMinimumSize(), containerWidthScale, containerHeightScale)); } } /** * "Localizes" a component tree by scaling up the dimensions of certain types of objects * in the tree in order to try to make things fit despite larger fonts and longer labels * @param parent */ public static void localizeComponentTree(Component parent) { if (parent == null) return; localizeComponent(parent); //recurse try { Component[] children = ((Container) parent).getComponents(); for (int i = 0; i < children.length; i++) { if (children[i] != null) localizeComponentTree(children[i]); } } catch (Exception e) { } } /** * Blow away the existing language menu items and recreate them. * This is necessary when switching languages, because the text * on the menu items needs to change * @param frame */ protected static void recreateLanguageMenuItems(JFrame frame) { JMenuBar menuBar = frame.getJMenuBar(); //it would be better if we could reference this by name JMenu languageMenu = menuBar.getMenu(LANGUAGE_MENU_INDEX); languageMenu.removeAll(); initializeLanguageMenu(languageMenu); } /** * add menu items for each available language * * @param languageMenu */ public static void initializeLanguageMenu(JMenu languageMenu) { //if only one language supported, disable the menu and hide it if (supportedLocales.length < 2) { languageMenu.setEnabled(false); languageMenu.setVisible(false); return; } for (int i = 0; i < supportedLocales.length; i++) { Locale locale = supportedLocales[i]; JMenuItem menuItem = new JMenuItem(locale.getDisplayName(_locale)); LanguageActionListener listener = new LanguageActionListener(locale); menuItem.addActionListener(listener); languageMenu.add(menuItem); } } /** * get the current locale * @return the current Locale */ public static Locale getLocale() { return _locale; } /** * return the current language * @return the current language */ public static String getLanguage() { return _locale.getLanguage(); } //Font-related methods /** * return a font preference key for the current language * @return */ protected static String getFontPrefKey() { return _fontPrefKeyPrefix + getLanguage(); } /** * Set the default font for all Swing components. This font will be used for all * components instantiated after this method is called * @param font */ public static void setSwingDefaultFont(Font font) { _font = font; FontUIResource f = new javax.swing.plaf.FontUIResource(font); java.util.Enumeration keys = UIManager.getDefaults().keys(); while (keys.hasMoreElements()) { Object key = keys.nextElement(); Object value = UIManager.get(key); if (value instanceof FontUIResource) { UIManager.put(key, f); } } } /** * Set the font for the application. Sets the default Java Locale, determines the * appropriate font, and updates all Swing components to use that font. * If, however, the font can't handle the language, find one that can * @param fontName */ public static void changeFont(String fontName) { if (fontName == null) fontName = getLanguageProperty(FONT_NAME); if ( _font != null && fontName.equals(_font.getName())) { return; } _font = createFont(fontName); //if this font can't handle the current language, don't use it if (!canHandleLanguage(getLanguage(),_font)) { _font = getWorkingFont(); String newFontName = _font.getName(); System.err.println("Chosen font " + fontName + " can't handle the current language. Switching to " + newFontName); fontName = newFontName; } _prefs.put(getFontPrefKey(), fontName); setSwingDefaultFont(_font); Application application = (Application) ApplicationContext.getImpl(); WorkbenchFrame frame = (WorkbenchFrame) application.getFrame(); if (frame != null) { getSwingEngine(frame).setLocale(getLocale()); setFontForComponentTree(frame, _font); //update all current UI components frame.update(frame.getGraphics()); } } /** * Find a working font for the Locale. This requires special handling for some languages * @return the appropriate font for the locale */ protected static Font getWorkingFont() { //this will work for languages where font doesn't matter so much Font result = createFont(getLanguageProperty(FONT_NAME)); if ("TRUE".equals(getLanguageProperty(REQUIRES_FONT_CHOICE))) { //use the previously chosen font if it exists and can handle the language //Otherwise, find one that can, if there's one installed. if (!canHandleLanguage(getLanguage(),result)) { //Default font can't handle the language. Looking for other fonts Font alternateFont = pickFirstFontForLanguage(); if (alternateFont != null) { result = alternateFont; _log.info("Found fonts for language " + getLanguage() + ". Using font " + alternateFont.getName()); } else ApplicationContext.infoMessage("No fonts found for language " + getLanguage() + ". Proceeding with default font."); } } return result; } /** * Change the font for a tree of components rooted at parent * Not useful, since we don't have a way of changing the text, too * @param parent * @param font */ protected static void setFontForComponentTree(Component parent, Font font) { parent.setFont(font); try { Component[] children = ((Container) parent).getComponents(); for (int i = 0; i < children.length; i++) { if (children[i] != null) setFontForComponentTree(children[i], font); } } catch (Exception e) { } } /** * Determines whether a given font can handle a given language * @param language * @param font * @return whether the font can handle the language */ protected static boolean canHandleLanguage(String language, Font font) { return (font.canDisplayUpTo(getLanguageProperty(language,FONT_TEST_STRING))) == -1; } /** * Get all font names that support this language * @param language * @return */ public static Vector getFontNamesForLanguage(String language) { Vector languageFonts = new Vector(); Font[] allfonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); int languageFontCount = 0; for (int j = 0; j < allfonts.length; j++) { if (canHandleLanguage(language,allfonts[j])) { languageFonts.add(allfonts[j].getName()); } } return languageFonts; } /** * Is there at least one supporting font for the language? * @param language * @return */ public static boolean doesLanguageHaveFonts(String language) { //always assume english has a supporting font if ("en".equals(language)) return true; return (getFontNamesForLanguage(language).size() > 0); } /** * Search through all the fonts and pick the first one that can handle Chinese * @return the first font that can handle Chinese, or null if there is none */ public static Font pickFirstFontForLanguage() { Font result = null; Vector languageFontNames = getFontNamesForLanguage(getLanguage()); if (languageFontNames.size() > 0) { //pick the first Chinese-capable font, and size it appropriately result = new Font((String) languageFontNames.elementAt(0), getLanguagePropertyInt(getLanguage(), FONT_STYLE), getLanguagePropertyInt(getLanguage(), FONT_SIZE)); } return result; } /** * Blow away the existing font menu items and recreate them. * This is necessary when switching languages, because the available fonts will change * @param frame */ protected static void recreateFontMenuItems(JFrame frame) { JMenuBar menuBar = frame.getJMenuBar(); //it would be better if we could reference this by name JMenu fontMenu = menuBar.getMenu(FONT_MENU_INDEX); fontMenu.removeAll(); initializeFontMenu(fontMenu); } /** * add menu items for each available font for the current language, if the language is picky * about fonts * * @param fontMenu */ public static void initializeFontMenu(JMenu fontMenu) { //if only current language does not require font choice, disable the menu and hide it if ("FALSE".equals(getLanguageProperty(REQUIRES_FONT_CHOICE))) { fontMenu.setEnabled(false); fontMenu.setVisible(false); return; } Vector fontNames = getFontNamesForLanguage(getLanguage()); if (fontNames.size() < 1) { fontMenu.setEnabled(false); fontMenu.setVisible(false); return; } for (int i = 0; i < fontNames.size(); i++) { String fontName = (String) fontNames.elementAt(i); JMenuItem menuItem = new JMenuItem(fontName); menuItem.setFont(createFont(fontName)); FontActionListener listener = new FontActionListener(fontName); menuItem.addActionListener(listener); fontMenu.add(menuItem); } } protected static Font createFont(String fontName) { return new Font(fontName, getLanguagePropertyInt(getLanguage(), FONT_STYLE), getLanguagePropertyInt(getLanguage(), FONT_SIZE)); } /** * actionlistener class for language menu items */ protected static class LanguageActionListener implements ActionListener { Locale _locale; public LanguageActionListener(Locale locale) { _locale = locale; } public void actionPerformed(ActionEvent event) { Localizer.changeLocale(_locale); } } /** * actionlistener class for font menu items */ protected static class FontActionListener implements ActionListener { String _fontName; public FontActionListener(String fontName) { _fontName = fontName; } public void actionPerformed(ActionEvent event) { Localizer.changeFont(_fontName); } } }