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