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