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