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