001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.gui.preferences; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.Component; 007 import java.awt.Font; 008 import java.awt.GridBagLayout; 009 import java.awt.Image; 010 import java.awt.event.MouseWheelEvent; 011 import java.awt.event.MouseWheelListener; 012 import java.util.ArrayList; 013 import java.util.Collection; 014 import java.util.LinkedList; 015 import java.util.List; 016 017 import javax.swing.BorderFactory; 018 import javax.swing.Icon; 019 import javax.swing.ImageIcon; 020 import javax.swing.JLabel; 021 import javax.swing.JOptionPane; 022 import javax.swing.JPanel; 023 import javax.swing.JScrollPane; 024 import javax.swing.JTabbedPane; 025 import javax.swing.SwingUtilities; 026 import javax.swing.event.ChangeEvent; 027 import javax.swing.event.ChangeListener; 028 029 import org.openstreetmap.josm.Main; 030 import org.openstreetmap.josm.actions.ExpertToggleAction; 031 import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener; 032 import org.openstreetmap.josm.gui.preferences.advanced.AdvancedPreference; 033 import org.openstreetmap.josm.gui.preferences.display.ColorPreference; 034 import org.openstreetmap.josm.gui.preferences.display.DisplayPreference; 035 import org.openstreetmap.josm.gui.preferences.display.DrawingPreference; 036 import org.openstreetmap.josm.gui.preferences.display.LafPreference; 037 import org.openstreetmap.josm.gui.preferences.display.LanguagePreference; 038 import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 039 import org.openstreetmap.josm.gui.preferences.map.BackupPreference; 040 import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference; 041 import org.openstreetmap.josm.gui.preferences.map.MapPreference; 042 import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; 043 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 044 import org.openstreetmap.josm.gui.preferences.shortcut.ShortcutPreference; 045 import org.openstreetmap.josm.plugins.PluginDownloadTask; 046 import org.openstreetmap.josm.plugins.PluginHandler; 047 import org.openstreetmap.josm.plugins.PluginInformation; 048 import org.openstreetmap.josm.tools.BugReportExceptionHandler; 049 import org.openstreetmap.josm.tools.CheckParameterUtil; 050 import org.openstreetmap.josm.tools.GBC; 051 import org.openstreetmap.josm.tools.ImageProvider; 052 053 /** 054 * The preference settings. 055 * 056 * @author imi 057 */ 058 public class PreferenceTabbedPane extends JTabbedPane implements MouseWheelListener, ExpertModeChangeListener, ChangeListener { 059 /** 060 * Allows PreferenceSettings to do validation of entered values when ok was pressed. 061 * If data is invalid then event can return false to cancel closing of preferences dialog. 062 * 063 */ 064 public interface ValidationListener { 065 /** 066 * 067 * @return True if preferences can be saved 068 */ 069 boolean validatePreferences(); 070 } 071 072 private static interface PreferenceTab { 073 public TabPreferenceSetting getTabPreferenceSetting(); 074 public Component getComponent(); 075 } 076 077 public static class PreferencePanel extends JPanel implements PreferenceTab { 078 private final TabPreferenceSetting preferenceSetting; 079 080 private PreferencePanel(TabPreferenceSetting preferenceSetting) { 081 super(new GridBagLayout()); 082 CheckParameterUtil.ensureParameterNotNull(preferenceSetting); 083 this.preferenceSetting = preferenceSetting; 084 buildPanel(); 085 } 086 087 protected void buildPanel() { 088 setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 089 add(new JLabel(preferenceSetting.getTitle()), GBC.eol().insets(0,5,0,10).anchor(GBC.NORTHWEST)); 090 091 JLabel descLabel = new JLabel("<html>"+preferenceSetting.getDescription()+"</html>"); 092 descLabel.setFont(descLabel.getFont().deriveFont(Font.ITALIC)); 093 add(descLabel, GBC.eol().insets(5,0,5,20).fill(GBC.HORIZONTAL)); 094 } 095 096 @Override 097 public final TabPreferenceSetting getTabPreferenceSetting() { 098 return preferenceSetting; 099 } 100 101 @Override 102 public Component getComponent() { 103 return this; 104 } 105 } 106 107 public static class PreferenceScrollPane extends JScrollPane implements PreferenceTab { 108 private final TabPreferenceSetting preferenceSetting; 109 110 private PreferenceScrollPane(Component view, TabPreferenceSetting preferenceSetting) { 111 super(view); 112 this.preferenceSetting = preferenceSetting; 113 } 114 115 private PreferenceScrollPane(PreferencePanel preferencePanel) { 116 super(preferencePanel.getComponent()); 117 this.preferenceSetting = preferencePanel.getTabPreferenceSetting(); 118 } 119 120 @Override 121 public final TabPreferenceSetting getTabPreferenceSetting() { 122 return preferenceSetting; 123 } 124 125 @Override 126 public Component getComponent() { 127 return this; 128 } 129 } 130 131 // all created tabs 132 private final List<PreferenceTab> tabs = new ArrayList<PreferenceTab>(); 133 private final static Collection<PreferenceSettingFactory> settingsFactory = new LinkedList<PreferenceSettingFactory>(); 134 private final List<PreferenceSetting> settings = new ArrayList<PreferenceSetting>(); 135 136 // distinct list of tabs that have been initialized (we do not initialize tabs until they are displayed to speed up dialog startup) 137 private final List<PreferenceSetting> settingsInitialized = new ArrayList<PreferenceSetting>(); 138 139 List<ValidationListener> validationListeners = new ArrayList<ValidationListener>(); 140 141 /** 142 * Add validation listener to currently open preferences dialog. Calling to removeValidationListener is not necessary, all listeners will 143 * be automatically removed when dialog is closed 144 * @param validationListener 145 */ 146 public void addValidationListener(ValidationListener validationListener) { 147 validationListeners.add(validationListener); 148 } 149 150 /** 151 * Construct a PreferencePanel for the preference settings. Layout is GridBagLayout 152 * and a centered title label and the description are added. 153 * @return The created panel ready to add other controls. 154 */ 155 public PreferencePanel createPreferenceTab(TabPreferenceSetting caller) { 156 return createPreferenceTab(caller, false); 157 } 158 159 /** 160 * Construct a PreferencePanel for the preference settings. Layout is GridBagLayout 161 * and a centered title label and the description are added. 162 * @param inScrollPane if <code>true</code> the added tab will show scroll bars 163 * if the panel content is larger than the available space 164 * @return The created panel ready to add other controls. 165 */ 166 public PreferencePanel createPreferenceTab(TabPreferenceSetting caller, boolean inScrollPane) { 167 CheckParameterUtil.ensureParameterNotNull(caller); 168 PreferencePanel p = new PreferencePanel(caller); 169 170 PreferenceTab tab = p; 171 if (inScrollPane) { 172 PreferenceScrollPane sp = new PreferenceScrollPane(p); 173 tab = sp; 174 } 175 tabs.add(tab); 176 return p; 177 } 178 179 private static interface TabIdentifier { 180 public boolean identify(TabPreferenceSetting tps, Object param); 181 } 182 183 private void selectTabBy(TabIdentifier method, Object param) { 184 for (int i=0; i<getTabCount(); i++) { 185 Component c = getComponentAt(i); 186 if (c instanceof PreferenceTab) { 187 PreferenceTab tab = (PreferenceTab) c; 188 if (method.identify(tab.getTabPreferenceSetting(), param)) { 189 setSelectedIndex(i); 190 return; 191 } 192 } 193 } 194 } 195 196 public void selectTabByName(String name) { 197 selectTabBy(new TabIdentifier(){ 198 @Override 199 public boolean identify(TabPreferenceSetting tps, Object name) { 200 return tps.getIconName().equals(name); 201 }}, name); 202 } 203 204 public void selectTabByPref(Class<? extends TabPreferenceSetting> clazz) { 205 selectTabBy(new TabIdentifier(){ 206 @Override 207 public boolean identify(TabPreferenceSetting tps, Object clazz) { 208 return tps.getClass().isAssignableFrom((Class<?>) clazz); 209 }}, clazz); 210 } 211 212 public final DisplayPreference getDisplayPreference() { 213 return getSetting(DisplayPreference.class); 214 } 215 216 public final MapPreference getMapPreference() { 217 return getSetting(MapPreference.class); 218 } 219 220 public final PluginPreference getPluginPreference() { 221 return getSetting(PluginPreference.class); 222 } 223 224 public final ImageryPreference getImageryPreference() { 225 return getSetting(ImageryPreference.class); 226 } 227 228 public void savePreferences() { 229 if(Main.applet) 230 return; 231 // create a task for downloading plugins if the user has activated, yet not downloaded, 232 // new plugins 233 // 234 final PluginPreference preference = getPluginPreference(); 235 final List<PluginInformation> toDownload = preference.getPluginsScheduledForUpdateOrDownload(); 236 final PluginDownloadTask task; 237 if (toDownload != null && ! toDownload.isEmpty()) { 238 task = new PluginDownloadTask(this, toDownload, tr("Download plugins")); 239 } else { 240 task = null; 241 } 242 243 // this is the task which will run *after* the plugins are downloaded 244 // 245 final Runnable continuation = new Runnable() { 246 public void run() { 247 boolean requiresRestart = false; 248 if (task != null && !task.isCanceled()) { 249 if (!task.getDownloadedPlugins().isEmpty()) { 250 requiresRestart = true; 251 } 252 } 253 254 for (PreferenceSetting setting : settingsInitialized) { 255 if (setting.ok()) { 256 requiresRestart = true; 257 } 258 } 259 260 // build the messages. We only display one message, including the status 261 // information from the plugin download task and - if necessary - a hint 262 // to restart JOSM 263 // 264 StringBuilder sb = new StringBuilder(); 265 sb.append("<html>"); 266 if (task != null && !task.isCanceled()) { 267 sb.append(PluginPreference.buildDownloadSummary(task)); 268 } 269 if (requiresRestart) { 270 sb.append(tr("You have to restart JOSM for some settings to take effect.")); 271 } 272 sb.append("</html>"); 273 274 // display the message, if necessary 275 // 276 if ((task != null && !task.isCanceled()) || requiresRestart) { 277 JOptionPane.showMessageDialog( 278 Main.parent, 279 sb.toString(), 280 tr("Warning"), 281 JOptionPane.WARNING_MESSAGE 282 ); 283 } 284 Main.parent.repaint(); 285 } 286 }; 287 288 if (task != null) { 289 // if we have to launch a plugin download task we do it asynchronously, followed 290 // by the remaining "save preferences" activites run on the Swing EDT. 291 // 292 Main.worker.submit(task); 293 Main.worker.submit( 294 new Runnable() { 295 public void run() { 296 SwingUtilities.invokeLater(continuation); 297 } 298 } 299 ); 300 } else { 301 // no need for asynchronous activities. Simply run the remaining "save preference" 302 // activities on this thread (we are already on the Swing EDT 303 // 304 continuation.run(); 305 } 306 } 307 308 /** 309 * If the dialog is closed with Ok, the preferences will be stored to the preferences- 310 * file, otherwise no change of the file happens. 311 */ 312 public PreferenceTabbedPane() { 313 super(JTabbedPane.LEFT, JTabbedPane.SCROLL_TAB_LAYOUT); 314 super.addMouseWheelListener(this); 315 super.getModel().addChangeListener(this); 316 ExpertToggleAction.addExpertModeChangeListener(this); 317 } 318 319 public void buildGui() { 320 for (PreferenceSettingFactory factory : settingsFactory) { 321 PreferenceSetting setting = factory.createPreferenceSetting(); 322 if (setting != null) { 323 settings.add(setting); 324 } 325 } 326 addGUITabs(false); 327 } 328 329 private void addGUITabsForSetting(Icon icon, TabPreferenceSetting tps) { 330 for (PreferenceTab tab : tabs) { 331 if (tab.getTabPreferenceSetting().equals(tps)) { 332 insertGUITabsForSetting(icon, tps, getTabCount()); 333 } 334 } 335 } 336 337 private void insertGUITabsForSetting(Icon icon, TabPreferenceSetting tps, int index) { 338 int position = index; 339 for (PreferenceTab tab : tabs) { 340 if (tab.getTabPreferenceSetting().equals(tps)) { 341 insertTab(null, icon, tab.getComponent(), tps.getTooltip(), position++); 342 } 343 } 344 } 345 346 private void addGUITabs(boolean clear) { 347 boolean expert = ExpertToggleAction.isExpert(); 348 Component sel = getSelectedComponent(); 349 if (clear) { 350 removeAll(); 351 } 352 // Inspect each tab setting 353 for (PreferenceSetting setting : settings) { 354 if (setting instanceof TabPreferenceSetting) { 355 TabPreferenceSetting tps = (TabPreferenceSetting) setting; 356 if (expert || !tps.isExpert()) { 357 // Get icon 358 String iconName = tps.getIconName(); 359 ImageIcon icon = iconName != null && iconName.length() > 0 ? ImageProvider.get("preferences", iconName) : null; 360 // See #6985 - Force icons to be 48x48 pixels 361 if (icon != null && (icon.getIconHeight() != 48 || icon.getIconWidth() != 48)) { 362 icon = new ImageIcon(icon.getImage().getScaledInstance(48, 48, Image.SCALE_DEFAULT)); 363 } 364 if (settingsInitialized.contains(tps)) { 365 // If it has been initialized, add corresponding tab(s) 366 addGUITabsForSetting(icon, tps); 367 } else { 368 // If it has not been initialized, create an empty tab with only icon and tooltip 369 addTab(null, icon, new PreferencePanel(tps), tps.getTooltip()); 370 } 371 } 372 } 373 } 374 try { 375 if (sel != null) { 376 setSelectedComponent(sel); 377 } 378 } catch (IllegalArgumentException e) {} 379 } 380 381 @Override 382 public void expertChanged(boolean isExpert) { 383 addGUITabs(true); 384 } 385 386 public List<PreferenceSetting> getSettings() { 387 return settings; 388 } 389 390 @SuppressWarnings("unchecked") 391 public <T> T getSetting(Class<? extends T> clazz) { 392 for (PreferenceSetting setting:settings) { 393 if (clazz.isAssignableFrom(setting.getClass())) 394 return (T)setting; 395 } 396 return null; 397 } 398 399 static { 400 // order is important! 401 settingsFactory.add(new DisplayPreference.Factory()); 402 settingsFactory.add(new DrawingPreference.Factory()); 403 settingsFactory.add(new ColorPreference.Factory()); 404 settingsFactory.add(new LafPreference.Factory()); 405 settingsFactory.add(new LanguagePreference.Factory()); 406 settingsFactory.add(new ServerAccessPreference.Factory()); 407 settingsFactory.add(new MapPreference.Factory()); 408 settingsFactory.add(new ProjectionPreference.Factory()); 409 settingsFactory.add(new MapPaintPreference.Factory()); 410 settingsFactory.add(new TaggingPresetPreference.Factory()); 411 settingsFactory.add(new BackupPreference.Factory()); 412 if(!Main.applet) { 413 settingsFactory.add(new PluginPreference.Factory()); 414 } 415 settingsFactory.add(Main.toolbar); 416 settingsFactory.add(new AudioPreference.Factory()); 417 settingsFactory.add(new ShortcutPreference.Factory()); 418 settingsFactory.add(new ValidatorPreference.Factory()); 419 settingsFactory.add(new RemoteControlPreference.Factory()); 420 settingsFactory.add(new ImageryPreference.Factory()); 421 422 PluginHandler.getPreferenceSetting(settingsFactory); 423 424 // always the last: advanced tab 425 settingsFactory.add(new AdvancedPreference.Factory()); 426 } 427 428 /** 429 * This mouse wheel listener reacts when a scroll is carried out over the 430 * tab strip and scrolls one tab/down or up, selecting it immediately. 431 */ 432 public void mouseWheelMoved(MouseWheelEvent wev) { 433 // Ensure the cursor is over the tab strip 434 if(super.indexAtLocation(wev.getPoint().x, wev.getPoint().y) < 0) 435 return; 436 437 // Get currently selected tab 438 int newTab = super.getSelectedIndex() + wev.getWheelRotation(); 439 440 // Ensure the new tab index is sound 441 newTab = newTab < 0 ? 0 : newTab; 442 newTab = newTab >= super.getTabCount() ? super.getTabCount() - 1 : newTab; 443 444 // select new tab 445 super.setSelectedIndex(newTab); 446 } 447 448 @Override 449 public void stateChanged(ChangeEvent e) { 450 int index = getSelectedIndex(); 451 Component sel = getSelectedComponent(); 452 if (index > -1 && sel instanceof PreferenceTab) { 453 PreferenceTab tab = (PreferenceTab) sel; 454 TabPreferenceSetting preferenceSettings = tab.getTabPreferenceSetting(); 455 if (!settingsInitialized.contains(preferenceSettings)) { 456 try { 457 getModel().removeChangeListener(this); 458 preferenceSettings.addGui(this); 459 // Add GUI for sub preferences 460 for (PreferenceSetting setting : settings) { 461 if (setting instanceof SubPreferenceSetting) { 462 SubPreferenceSetting sps = (SubPreferenceSetting) setting; 463 if (sps.getTabPreferenceSetting(this) == preferenceSettings) { 464 try { 465 sps.addGui(this); 466 } catch (SecurityException ex) { 467 ex.printStackTrace(); 468 } catch (Throwable ex) { 469 BugReportExceptionHandler.handleException(ex); 470 } finally { 471 settingsInitialized.add(sps); 472 } 473 } 474 } 475 } 476 Icon icon = getIconAt(index); 477 remove(index); 478 insertGUITabsForSetting(icon, preferenceSettings, index); 479 setSelectedIndex(index); 480 } catch (SecurityException ex) { 481 ex.printStackTrace(); 482 } catch (Throwable ex) { 483 // allow to change most settings even if e.g. a plugin fails 484 BugReportExceptionHandler.handleException(ex); 485 } finally { 486 settingsInitialized.add(preferenceSettings); 487 getModel().addChangeListener(this); 488 } 489 } 490 } 491 } 492 }