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.BugReportExceptionHandler; 066import org.openstreetmap.josm.tools.CheckParameterUtil; 067import org.openstreetmap.josm.tools.GBC; 068import org.openstreetmap.josm.tools.ImageProvider; 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 PreferenceSetting setting = factory.createPreferenceSetting(); 473 if (setting != null) { 474 settings.add(setting); 475 } 476 } 477 addGUITabs(false); 478 } 479 480 private void addGUITabsForSetting(Icon icon, TabPreferenceSetting tps) { 481 for (PreferenceTab tab : tabs) { 482 if (tab.getTabPreferenceSetting().equals(tps)) { 483 insertGUITabsForSetting(icon, tps, getTabCount()); 484 } 485 } 486 } 487 488 private void insertGUITabsForSetting(Icon icon, TabPreferenceSetting tps, int index) { 489 int position = index; 490 for (PreferenceTab tab : tabs) { 491 if (tab.getTabPreferenceSetting().equals(tps)) { 492 insertTab(null, icon, tab.getComponent(), tps.getTooltip(), position++); 493 } 494 } 495 } 496 497 private void addGUITabs(boolean clear) { 498 boolean expert = ExpertToggleAction.isExpert(); 499 Component sel = getSelectedComponent(); 500 if (clear) { 501 removeAll(); 502 } 503 // Inspect each tab setting 504 for (PreferenceSetting setting : settings) { 505 if (setting instanceof TabPreferenceSetting) { 506 TabPreferenceSetting tps = (TabPreferenceSetting) setting; 507 if (expert || !tps.isExpert()) { 508 // Get icon 509 String iconName = tps.getIconName(); 510 ImageIcon icon = iconName != null && !iconName.isEmpty() ? ImageProvider.get("preferences", iconName) : null; 511 // See #6985 - Force icons to be 48x48 pixels 512 if (icon != null && (icon.getIconHeight() != 48 || icon.getIconWidth() != 48)) { 513 icon = new ImageIcon(icon.getImage().getScaledInstance(48, 48, Image.SCALE_DEFAULT)); 514 } 515 if (settingsInitialized.contains(tps)) { 516 // If it has been initialized, add corresponding tab(s) 517 addGUITabsForSetting(icon, tps); 518 } else { 519 // If it has not been initialized, create an empty tab with only icon and tooltip 520 addTab(null, icon, new PreferencePanel(tps), tps.getTooltip()); 521 } 522 } 523 } else if (!(setting instanceof SubPreferenceSetting)) { 524 Main.warn("Ignoring preferences "+setting); 525 } 526 } 527 try { 528 if (sel != null) { 529 setSelectedComponent(sel); 530 } 531 } catch (IllegalArgumentException e) { 532 Main.warn(e); 533 } 534 } 535 536 @Override 537 public void expertChanged(boolean isExpert) { 538 addGUITabs(true); 539 } 540 541 public List<PreferenceSetting> getSettings() { 542 return settings; 543 } 544 545 @SuppressWarnings("unchecked") 546 public <T> T getSetting(Class<? extends T> clazz) { 547 for (PreferenceSetting setting:settings) { 548 if (clazz.isAssignableFrom(setting.getClass())) 549 return (T) setting; 550 } 551 return null; 552 } 553 554 static { 555 // order is important! 556 settingsFactories.add(new DisplayPreference.Factory()); 557 settingsFactories.add(new DrawingPreference.Factory()); 558 settingsFactories.add(new ColorPreference.Factory()); 559 settingsFactories.add(new LafPreference.Factory()); 560 settingsFactories.add(new LanguagePreference.Factory()); 561 settingsFactories.add(new ServerAccessPreference.Factory()); 562 settingsFactories.add(new AuthenticationPreference.Factory()); 563 settingsFactories.add(new ProxyPreference.Factory()); 564 settingsFactories.add(new OverpassServerPreference.Factory()); 565 settingsFactories.add(new MapPreference.Factory()); 566 settingsFactories.add(new ProjectionPreference.Factory()); 567 settingsFactories.add(new MapPaintPreference.Factory()); 568 settingsFactories.add(new TaggingPresetPreference.Factory()); 569 settingsFactories.add(new BackupPreference.Factory()); 570 settingsFactories.add(new PluginPreference.Factory()); 571 settingsFactories.add(Main.toolbar); 572 settingsFactories.add(new AudioPreference.Factory()); 573 settingsFactories.add(new ShortcutPreference.Factory()); 574 settingsFactories.add(new ValidatorPreference.Factory()); 575 settingsFactories.add(new ValidatorTestsPreference.Factory()); 576 settingsFactories.add(new ValidatorTagCheckerRulesPreference.Factory()); 577 settingsFactories.add(new RemoteControlPreference.Factory()); 578 settingsFactories.add(new ImageryPreference.Factory()); 579 } 580 581 /** 582 * This mouse wheel listener reacts when a scroll is carried out over the 583 * tab strip and scrolls one tab/down or up, selecting it immediately. 584 */ 585 @Override 586 public void mouseWheelMoved(MouseWheelEvent wev) { 587 // Ensure the cursor is over the tab strip 588 if (super.indexAtLocation(wev.getPoint().x, wev.getPoint().y) < 0) 589 return; 590 591 // Get currently selected tab 592 int newTab = super.getSelectedIndex() + wev.getWheelRotation(); 593 594 // Ensure the new tab index is sound 595 newTab = newTab < 0 ? 0 : newTab; 596 newTab = newTab >= super.getTabCount() ? super.getTabCount() - 1 : newTab; 597 598 // select new tab 599 super.setSelectedIndex(newTab); 600 } 601 602 @Override 603 public void stateChanged(ChangeEvent e) { 604 int index = getSelectedIndex(); 605 Component sel = getSelectedComponent(); 606 if (index > -1 && sel instanceof PreferenceTab) { 607 PreferenceTab tab = (PreferenceTab) sel; 608 TabPreferenceSetting preferenceSettings = tab.getTabPreferenceSetting(); 609 if (!settingsInitialized.contains(preferenceSettings)) { 610 try { 611 getModel().removeChangeListener(this); 612 preferenceSettings.addGui(this); 613 // Add GUI for sub preferences 614 for (PreferenceSetting setting : settings) { 615 if (setting instanceof SubPreferenceSetting) { 616 SubPreferenceSetting sps = (SubPreferenceSetting) setting; 617 if (sps.getTabPreferenceSetting(this) == preferenceSettings) { 618 try { 619 sps.addGui(this); 620 } catch (SecurityException ex) { 621 Main.error(ex); 622 } catch (Exception ex) { 623 BugReportExceptionHandler.handleException(ex); 624 } finally { 625 settingsInitialized.add(sps); 626 } 627 } 628 } 629 } 630 Icon icon = getIconAt(index); 631 remove(index); 632 insertGUITabsForSetting(icon, preferenceSettings, index); 633 setSelectedIndex(index); 634 } catch (SecurityException ex) { 635 Main.error(ex); 636 } catch (Exception ex) { 637 // allow to change most settings even if e.g. a plugin fails 638 BugReportExceptionHandler.handleException(ex); 639 } finally { 640 settingsInitialized.add(preferenceSettings); 641 getModel().addChangeListener(this); 642 } 643 } 644 } 645 } 646}