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