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}