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.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.BugReportExceptionHandler; 065import org.openstreetmap.josm.tools.CheckParameterUtil; 066import org.openstreetmap.josm.tools.GBC; 067import org.openstreetmap.josm.tools.ImageProvider; 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 * @return The created panel ready to add other controls. 277 */ 278 public PreferencePanel createPreferenceTab(TabPreferenceSetting caller) { 279 return createPreferenceTab(caller, false); 280 } 281 282 /** 283 * Construct a PreferencePanel for the preference settings. Layout is GridBagLayout 284 * and a centered title label and the description are added. 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); 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 private interface TabIdentifier { 303 boolean identify(TabPreferenceSetting tps, Object param); 304 } 305 306 private void selectTabBy(TabIdentifier method, Object param) { 307 for (int i = 0; i < getTabCount(); i++) { 308 Component c = getComponentAt(i); 309 if (c instanceof PreferenceTab) { 310 PreferenceTab tab = (PreferenceTab) c; 311 if (method.identify(tab.getTabPreferenceSetting(), param)) { 312 setSelectedIndex(i); 313 return; 314 } 315 } 316 } 317 } 318 319 public void selectTabByName(String name) { 320 selectTabBy(new TabIdentifier() { 321 @Override 322 public boolean identify(TabPreferenceSetting tps, Object name) { 323 return name != null && tps != null && tps.getIconName() != null && name.equals(tps.getIconName()); 324 } 325 }, name); 326 } 327 328 public void selectTabByPref(Class<? extends TabPreferenceSetting> clazz) { 329 selectTabBy(new TabIdentifier() { 330 @Override 331 public boolean identify(TabPreferenceSetting tps, Object clazz) { 332 return tps.getClass().isAssignableFrom((Class<?>) clazz); 333 } 334 }, clazz); 335 } 336 337 public boolean selectSubTabByPref(Class<? extends SubPreferenceSetting> clazz) { 338 for (PreferenceSetting setting : settings) { 339 if (clazz.isInstance(setting)) { 340 final SubPreferenceSetting sub = (SubPreferenceSetting) setting; 341 final TabPreferenceSetting tab = sub.getTabPreferenceSetting(PreferenceTabbedPane.this); 342 selectTabBy(new TabIdentifier() { 343 @Override 344 public boolean identify(TabPreferenceSetting tps, Object unused) { 345 return tps.equals(tab); 346 } 347 }, null); 348 return tab.selectSubTab(sub); 349 } 350 } 351 return false; 352 } 353 354 /** 355 * Returns the {@code DisplayPreference} object. 356 * @return the {@code DisplayPreference} object. 357 */ 358 public DisplayPreference getDisplayPreference() { 359 return getSetting(DisplayPreference.class); 360 } 361 362 /** 363 * Returns the {@code MapPreference} object. 364 * @return the {@code MapPreference} object. 365 */ 366 public MapPreference getMapPreference() { 367 return getSetting(MapPreference.class); 368 } 369 370 /** 371 * Returns the {@code PluginPreference} object. 372 * @return the {@code PluginPreference} object. 373 */ 374 public PluginPreference getPluginPreference() { 375 return getSetting(PluginPreference.class); 376 } 377 378 /** 379 * Returns the {@code ImageryPreference} object. 380 * @return the {@code ImageryPreference} object. 381 */ 382 public ImageryPreference getImageryPreference() { 383 return getSetting(ImageryPreference.class); 384 } 385 386 /** 387 * Returns the {@code ShortcutPreference} object. 388 * @return the {@code ShortcutPreference} object. 389 */ 390 public ShortcutPreference getShortcutPreference() { 391 return getSetting(ShortcutPreference.class); 392 } 393 394 /** 395 * Returns the {@code ServerAccessPreference} object. 396 * @return the {@code ServerAccessPreference} object. 397 * @since 6523 398 */ 399 public ServerAccessPreference getServerPreference() { 400 return getSetting(ServerAccessPreference.class); 401 } 402 403 /** 404 * Returns the {@code ValidatorPreference} object. 405 * @return the {@code ValidatorPreference} object. 406 * @since 6665 407 */ 408 public ValidatorPreference getValidatorPreference() { 409 return getSetting(ValidatorPreference.class); 410 } 411 412 /** 413 * Saves preferences. 414 */ 415 public void savePreferences() { 416 // create a task for downloading plugins if the user has activated, yet not downloaded, new plugins 417 // 418 final PluginPreference preference = getPluginPreference(); 419 final Set<PluginInformation> toDownload = preference.getPluginsScheduledForUpdateOrDownload(); 420 final PluginDownloadTask task; 421 if (toDownload != null && !toDownload.isEmpty()) { 422 task = new PluginDownloadTask(this, toDownload, tr("Download plugins")); 423 } else { 424 task = null; 425 } 426 427 // this is the task which will run *after* the plugins are downloaded 428 // 429 final Runnable continuation = new PluginDownloadAfterTask(preference, task, toDownload); 430 431 if (task != null) { 432 // if we have to launch a plugin download task we do it asynchronously, followed 433 // by the remaining "save preferences" activites run on the Swing EDT. 434 // 435 Main.worker.submit(task); 436 Main.worker.submit( 437 new Runnable() { 438 @Override 439 public void run() { 440 SwingUtilities.invokeLater(continuation); 441 } 442 } 443 ); 444 } else { 445 // no need for asynchronous activities. Simply run the remaining "save preference" 446 // activities on this thread (we are already on the Swing EDT 447 // 448 continuation.run(); 449 } 450 } 451 452 /** 453 * If the dialog is closed with Ok, the preferences will be stored to the preferences- 454 * file, otherwise no change of the file happens. 455 */ 456 public PreferenceTabbedPane() { 457 super(JTabbedPane.LEFT, JTabbedPane.SCROLL_TAB_LAYOUT); 458 super.addMouseWheelListener(this); 459 super.getModel().addChangeListener(this); 460 ExpertToggleAction.addExpertModeChangeListener(this); 461 } 462 463 public void buildGui() { 464 Collection<PreferenceSettingFactory> factories = new ArrayList<>(settingsFactories); 465 factories.addAll(PluginHandler.getPreferenceSetting()); 466 factories.add(advancedPreferenceFactory); 467 468 for (PreferenceSettingFactory factory : factories) { 469 PreferenceSetting setting = factory.createPreferenceSetting(); 470 if (setting != null) { 471 settings.add(setting); 472 } 473 } 474 addGUITabs(false); 475 } 476 477 private void addGUITabsForSetting(Icon icon, TabPreferenceSetting tps) { 478 for (PreferenceTab tab : tabs) { 479 if (tab.getTabPreferenceSetting().equals(tps)) { 480 insertGUITabsForSetting(icon, tps, getTabCount()); 481 } 482 } 483 } 484 485 private void insertGUITabsForSetting(Icon icon, TabPreferenceSetting tps, int index) { 486 int position = index; 487 for (PreferenceTab tab : tabs) { 488 if (tab.getTabPreferenceSetting().equals(tps)) { 489 insertTab(null, icon, tab.getComponent(), tps.getTooltip(), position++); 490 } 491 } 492 } 493 494 private void addGUITabs(boolean clear) { 495 boolean expert = ExpertToggleAction.isExpert(); 496 Component sel = getSelectedComponent(); 497 if (clear) { 498 removeAll(); 499 } 500 // Inspect each tab setting 501 for (PreferenceSetting setting : settings) { 502 if (setting instanceof TabPreferenceSetting) { 503 TabPreferenceSetting tps = (TabPreferenceSetting) setting; 504 if (expert || !tps.isExpert()) { 505 // Get icon 506 String iconName = tps.getIconName(); 507 ImageIcon icon = iconName != null && !iconName.isEmpty() ? ImageProvider.get("preferences", iconName) : null; 508 // See #6985 - Force icons to be 48x48 pixels 509 if (icon != null && (icon.getIconHeight() != 48 || icon.getIconWidth() != 48)) { 510 icon = new ImageIcon(icon.getImage().getScaledInstance(48, 48, Image.SCALE_DEFAULT)); 511 } 512 if (settingsInitialized.contains(tps)) { 513 // If it has been initialized, add corresponding tab(s) 514 addGUITabsForSetting(icon, tps); 515 } else { 516 // If it has not been initialized, create an empty tab with only icon and tooltip 517 addTab(null, icon, new PreferencePanel(tps), tps.getTooltip()); 518 } 519 } 520 } else if (!(setting instanceof SubPreferenceSetting)) { 521 Main.warn("Ignoring preferences "+setting); 522 } 523 } 524 try { 525 if (sel != null) { 526 setSelectedComponent(sel); 527 } 528 } catch (IllegalArgumentException e) { 529 Main.warn(e); 530 } 531 } 532 533 @Override 534 public void expertChanged(boolean isExpert) { 535 addGUITabs(true); 536 } 537 538 public List<PreferenceSetting> getSettings() { 539 return settings; 540 } 541 542 @SuppressWarnings("unchecked") 543 public <T> T getSetting(Class<? extends T> clazz) { 544 for (PreferenceSetting setting:settings) { 545 if (clazz.isAssignableFrom(setting.getClass())) 546 return (T) setting; 547 } 548 return null; 549 } 550 551 static { 552 // order is important! 553 settingsFactories.add(new DisplayPreference.Factory()); 554 settingsFactories.add(new DrawingPreference.Factory()); 555 settingsFactories.add(new ColorPreference.Factory()); 556 settingsFactories.add(new LafPreference.Factory()); 557 settingsFactories.add(new LanguagePreference.Factory()); 558 settingsFactories.add(new ServerAccessPreference.Factory()); 559 settingsFactories.add(new AuthenticationPreference.Factory()); 560 settingsFactories.add(new ProxyPreference.Factory()); 561 settingsFactories.add(new MapPreference.Factory()); 562 settingsFactories.add(new ProjectionPreference.Factory()); 563 settingsFactories.add(new MapPaintPreference.Factory()); 564 settingsFactories.add(new TaggingPresetPreference.Factory()); 565 settingsFactories.add(new BackupPreference.Factory()); 566 settingsFactories.add(new PluginPreference.Factory()); 567 settingsFactories.add(Main.toolbar); 568 settingsFactories.add(new AudioPreference.Factory()); 569 settingsFactories.add(new ShortcutPreference.Factory()); 570 settingsFactories.add(new ValidatorPreference.Factory()); 571 settingsFactories.add(new ValidatorTestsPreference.Factory()); 572 settingsFactories.add(new ValidatorTagCheckerRulesPreference.Factory()); 573 settingsFactories.add(new RemoteControlPreference.Factory()); 574 settingsFactories.add(new ImageryPreference.Factory()); 575 } 576 577 /** 578 * This mouse wheel listener reacts when a scroll is carried out over the 579 * tab strip and scrolls one tab/down or up, selecting it immediately. 580 */ 581 @Override 582 public void mouseWheelMoved(MouseWheelEvent wev) { 583 // Ensure the cursor is over the tab strip 584 if (super.indexAtLocation(wev.getPoint().x, wev.getPoint().y) < 0) 585 return; 586 587 // Get currently selected tab 588 int newTab = super.getSelectedIndex() + wev.getWheelRotation(); 589 590 // Ensure the new tab index is sound 591 newTab = newTab < 0 ? 0 : newTab; 592 newTab = newTab >= super.getTabCount() ? super.getTabCount() - 1 : newTab; 593 594 // select new tab 595 super.setSelectedIndex(newTab); 596 } 597 598 @Override 599 public void stateChanged(ChangeEvent e) { 600 int index = getSelectedIndex(); 601 Component sel = getSelectedComponent(); 602 if (index > -1 && sel instanceof PreferenceTab) { 603 PreferenceTab tab = (PreferenceTab) sel; 604 TabPreferenceSetting preferenceSettings = tab.getTabPreferenceSetting(); 605 if (!settingsInitialized.contains(preferenceSettings)) { 606 try { 607 getModel().removeChangeListener(this); 608 preferenceSettings.addGui(this); 609 // Add GUI for sub preferences 610 for (PreferenceSetting setting : settings) { 611 if (setting instanceof SubPreferenceSetting) { 612 SubPreferenceSetting sps = (SubPreferenceSetting) setting; 613 if (sps.getTabPreferenceSetting(this) == preferenceSettings) { 614 try { 615 sps.addGui(this); 616 } catch (SecurityException ex) { 617 Main.error(ex); 618 } catch (Exception ex) { 619 BugReportExceptionHandler.handleException(ex); 620 } finally { 621 settingsInitialized.add(sps); 622 } 623 } 624 } 625 } 626 Icon icon = getIconAt(index); 627 remove(index); 628 insertGUITabsForSetting(icon, preferenceSettings, index); 629 setSelectedIndex(index); 630 } catch (SecurityException ex) { 631 Main.error(ex); 632 } catch (Exception ex) { 633 // allow to change most settings even if e.g. a plugin fails 634 BugReportExceptionHandler.handleException(ex); 635 } finally { 636 settingsInitialized.add(preferenceSettings); 637 getModel().addChangeListener(this); 638 } 639 } 640 } 641 } 642}