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