001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.display;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.GridBagLayout;
008import java.time.LocalDate;
009import java.time.ZoneId;
010import java.time.format.DateTimeFormatter;
011import java.time.format.FormatStyle;
012
013import javax.swing.BorderFactory;
014import javax.swing.Box;
015import javax.swing.DefaultListCellRenderer;
016import javax.swing.JCheckBox;
017import javax.swing.JFormattedTextField;
018import javax.swing.JLabel;
019import javax.swing.JList;
020import javax.swing.JScrollPane;
021import javax.swing.JSeparator;
022import javax.swing.JSpinner;
023import javax.swing.ListCellRenderer;
024import javax.swing.LookAndFeel;
025import javax.swing.SpinnerNumberModel;
026import javax.swing.UIManager;
027import javax.swing.UIManager.LookAndFeelInfo;
028
029import org.openstreetmap.josm.actions.ExpertToggleAction;
030import org.openstreetmap.josm.data.preferences.StringProperty;
031import org.openstreetmap.josm.gui.MapFrame;
032import org.openstreetmap.josm.gui.MapMover;
033import org.openstreetmap.josm.gui.NavigatableComponent;
034import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
035
036import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
037import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
038import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
039import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
040import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
041import org.openstreetmap.josm.gui.util.WindowGeometry;
042import org.openstreetmap.josm.gui.widgets.FileChooserManager;
043import org.openstreetmap.josm.gui.widgets.JosmComboBox;
044import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
045import org.openstreetmap.josm.spi.preferences.Config;
046import org.openstreetmap.josm.tools.GBC;
047import org.openstreetmap.josm.tools.Logging;
048import org.openstreetmap.josm.tools.PlatformManager;
049import org.openstreetmap.josm.tools.date.DateUtils;
050
051/**
052 * Look-and-feel preferences.
053 */
054public class LafPreference implements SubPreferenceSetting {
055
056    /**
057     * Look-and-feel property.
058     * @since 11713
059     */
060    public static final StringProperty LAF = new StringProperty("laf", PlatformManager.getPlatform().getDefaultStyle());
061
062    static final class LafListCellRenderer implements ListCellRenderer<LookAndFeelInfo> {
063        private final DefaultListCellRenderer def = new DefaultListCellRenderer();
064
065        @Override
066        public Component getListCellRendererComponent(JList<? extends LookAndFeelInfo> list, LookAndFeelInfo value,
067                int index, boolean isSelected, boolean cellHasFocus) {
068            return def.getListCellRendererComponent(list, value.getName(), index, isSelected, cellHasFocus);
069        }
070    }
071
072    /**
073     * Factory used to create a new {@code LafPreference}.
074     */
075    public static class Factory implements PreferenceSettingFactory {
076        @Override
077        public PreferenceSetting createPreferenceSetting() {
078            return new LafPreference();
079        }
080    }
081
082    /**
083     * ComboBox with all look and feels.
084     */
085    private JosmComboBox<LookAndFeelInfo> lafCombo;
086    VerticallyScrollablePanel panel;
087    private final JCheckBox showSplashScreen = new JCheckBox(tr("Show splash screen at startup"));
088    private final JCheckBox showUser = new JCheckBox(tr("Show user name in title"));
089    private final JCheckBox showID = new JCheckBox(tr("Show object ID in selection lists"));
090    private final JCheckBox showVersion = new JCheckBox(tr("Show object version in selection lists"));
091    private final JCheckBox showCoor = new JCheckBox(tr("Show node coordinates in selection lists"));
092    private final JCheckBox showLocalizedName = new JCheckBox(tr("Show localized name in selection lists"));
093    private final JCheckBox modeless = new JCheckBox(tr("Modeless working (Potlatch style)"));
094    private final JCheckBox dynamicButtons = new JCheckBox(tr("Dynamic buttons in side menus"));
095    private final JCheckBox isoDates = new JCheckBox(tr("Display ISO dates"));
096    private final JCheckBox dialogGeometry = new JCheckBox(tr("Remember dialog geometries"));
097    private final JCheckBox nativeFileChoosers = new JCheckBox(tr("Use native file choosers (nicer, but do not support file filters)"));
098    private final JCheckBox zoomReverseWheel = new JCheckBox(tr("Reverse zoom with mouse wheel"));
099    private final JCheckBox zoomIntermediateSteps = new JCheckBox(tr("Intermediate steps between native resolutions"));
100    private JSpinner spinZoomRatio;
101
102    @Override
103    public void addGui(PreferenceTabbedPane gui) {
104        lafCombo = new JosmComboBox<>(UIManager.getInstalledLookAndFeels());
105
106        // let's try to load additional LookAndFeels and put them into the list
107        if (PlatformManager.isPlatformOsx()) {
108            try {
109                Class<?> cquaqua = Class.forName("ch.randelshofer.quaqua.QuaquaLookAndFeel");
110                Object oquaqua = cquaqua.getConstructor((Class[]) null).newInstance((Object[]) null);
111                // no exception? Then Go!
112                lafCombo.addItem(
113                        new UIManager.LookAndFeelInfo(((LookAndFeel) oquaqua).getName(), "ch.randelshofer.quaqua.QuaquaLookAndFeel")
114                );
115            } catch (ReflectiveOperationException ex) {
116                // just debug, Quaqua may not even be installed...
117                Logging.debug(ex);
118            }
119        }
120
121        String laf = LAF.get();
122        for (int i = 0; i < lafCombo.getItemCount(); ++i) {
123            if (lafCombo.getItemAt(i).getClassName().equals(laf)) {
124                lafCombo.setSelectedIndex(i);
125                break;
126            }
127        }
128
129        lafCombo.setRenderer(new LafListCellRenderer());
130
131        panel = new VerticallyScrollablePanel(new GridBagLayout());
132        panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
133
134        // First the most important setting "Look and Feel" that changes the most
135        panel.add(new JLabel(tr("Look and Feel")), GBC.std().insets(20, 0, 0, 0));
136        panel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
137        panel.add(lafCombo, GBC.eol().fill(GBC.HORIZONTAL));
138        panel.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 10, 0, 10));
139
140        // Show splash screen on startup
141        showSplashScreen.setToolTipText(tr("Show splash screen at startup"));
142        showSplashScreen.setSelected(Config.getPref().getBoolean("draw.splashscreen", true));
143        panel.add(showSplashScreen, GBC.eop().insets(20, 0, 0, 0));
144
145        // Show user name in title
146        showUser.setToolTipText(tr("Show user name in title"));
147        showUser.setSelected(Config.getPref().getBoolean("draw.show-user", false));
148
149        // Show ID in selection
150        showID.setToolTipText(tr("Show object ID in selection lists"));
151        showID.setSelected(Config.getPref().getBoolean("osm-primitives.showid", false));
152
153        // Show version in selection
154        showVersion.setToolTipText(tr("Show object version in selection lists"));
155        showVersion.setSelected(Config.getPref().getBoolean("osm-primitives.showversion", false));
156
157        // Show Coordinates in selection
158        showCoor.setToolTipText(tr("Show node coordinates in selection lists"));
159        showCoor.setSelected(Config.getPref().getBoolean("osm-primitives.showcoor", false));
160
161        // Show localized names
162        showLocalizedName.setToolTipText(tr("Show localized name in selection lists, if available"));
163        showLocalizedName.setSelected(Config.getPref().getBoolean("osm-primitives.localize-name", true));
164        ExpertToggleAction.addVisibilitySwitcher(showLocalizedName);
165
166        modeless.setToolTipText(tr("Do not require to switch modes (potlatch style workflow)"));
167        modeless.setSelected(MapFrame.MODELESS.get());
168        ExpertToggleAction.addVisibilitySwitcher(modeless);
169
170        panel.add(showUser, GBC.eop().insets(20, 0, 0, 0));
171        panel.add(showID, GBC.eop().insets(20, 0, 0, 0));
172        panel.add(showVersion, GBC.eop().insets(20, 0, 0, 0));
173        panel.add(showCoor, GBC.eop().insets(20, 0, 0, 0));
174        panel.add(showLocalizedName, GBC.eop().insets(20, 0, 0, 0));
175        panel.add(modeless, GBC.eop().insets(20, 0, 0, 0));
176
177        dynamicButtons.setToolTipText(tr("Display buttons in right side menus only when mouse is inside the element"));
178        dynamicButtons.setSelected(ToggleDialog.PROP_DYNAMIC_BUTTONS.get());
179        panel.add(dynamicButtons, GBC.eop().insets(20, 0, 0, 0));
180
181        LocalDate today = LocalDate.now(ZoneId.systemDefault());
182        isoDates.setToolTipText(tr("Format dates according to {0}. Today''s date will be displayed as {1} instead of {2}",
183                tr("ISO 8601"),
184                today.toString(),
185                DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(today)));
186        isoDates.setSelected(DateUtils.PROP_ISO_DATES.get());
187        panel.add(isoDates, GBC.eop().insets(20, 0, 0, 0));
188
189        dialogGeometry.setSelected(WindowGeometry.GUI_GEOMETRY_ENABLED.get());
190        panel.add(dialogGeometry, GBC.eop().insets(20, 0, 0, 0));
191
192        nativeFileChoosers.setToolTipText(
193                tr("Use file choosers that behave more like native ones. They look nicer but do not support some features like file filters"));
194        nativeFileChoosers.setSelected(FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.get());
195        panel.add(nativeFileChoosers, GBC.eop().insets(20, 0, 0, 0));
196
197        zoomReverseWheel.setToolTipText(
198                tr("Check if you feel opposite direction more convenient"));
199        zoomReverseWheel.setSelected(MapMover.PROP_ZOOM_REVERSE_WHEEL.get());
200        panel.add(zoomReverseWheel, GBC.eop().insets(20, 0, 0, 0));
201
202        zoomIntermediateSteps.setToolTipText(
203                tr("Divide intervals between native resolution levels to smaller steps if they are much larger than zoom ratio"));
204        zoomIntermediateSteps.setSelected(NavigatableComponent.PROP_ZOOM_INTERMEDIATE_STEPS.get());
205        ExpertToggleAction.addVisibilitySwitcher(zoomIntermediateSteps);
206        panel.add(zoomIntermediateSteps, GBC.eop().insets(20, 0, 0, 0));
207
208        panel.add(Box.createVerticalGlue(), GBC.eol().insets(0, 10, 0, 0));
209
210        double logZoomLevel = Math.log(2) / Math.log(NavigatableComponent.PROP_ZOOM_RATIO.get());
211        logZoomLevel = Math.max(1, logZoomLevel);
212        logZoomLevel = Math.min(5, logZoomLevel);
213        JLabel labelZoomRatio = new JLabel(tr("Zoom steps to get double scale"));
214        spinZoomRatio = new JSpinner(new SpinnerNumberModel(logZoomLevel, 1, 5, 1));
215        Component spinZoomRatioEditor = spinZoomRatio.getEditor();
216        JFormattedTextField jftf = ((JSpinner.DefaultEditor) spinZoomRatioEditor).getTextField();
217        jftf.setColumns(2);
218        String zoomRatioToolTipText = tr("Higher value means more steps needed, therefore zoom steps will be smaller");
219        spinZoomRatio.setToolTipText(zoomRatioToolTipText);
220        labelZoomRatio.setToolTipText(zoomRatioToolTipText);
221        labelZoomRatio.setLabelFor(spinZoomRatio);
222        panel.add(labelZoomRatio, GBC.std().insets(20, 0, 0, 0));
223        panel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
224        panel.add(spinZoomRatio, GBC.eol());
225
226        JScrollPane scrollpane = panel.getVerticalScrollPane();
227        scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
228        gui.getDisplayPreference().addSubTab(this, tr("Look and Feel"), scrollpane);
229    }
230
231    @Override
232    public boolean ok() {
233        boolean mod = false;
234        Config.getPref().putBoolean("draw.splashscreen", showSplashScreen.isSelected());
235        Config.getPref().putBoolean("draw.show-user", showUser.isSelected());
236        Config.getPref().putBoolean("osm-primitives.showid", showID.isSelected());
237        Config.getPref().putBoolean("osm-primitives.showversion", showVersion.isSelected());
238        Config.getPref().putBoolean("osm-primitives.showcoor", showCoor.isSelected());
239        Config.getPref().putBoolean("osm-primitives.localize-name", showLocalizedName.isSelected());
240        MapFrame.MODELESS.put(modeless.isSelected());
241        Config.getPref().putBoolean(ToggleDialog.PROP_DYNAMIC_BUTTONS.getKey(), dynamicButtons.isSelected());
242        Config.getPref().putBoolean(DateUtils.PROP_ISO_DATES.getKey(), isoDates.isSelected());
243        WindowGeometry.GUI_GEOMETRY_ENABLED.put(dialogGeometry.isSelected());
244        Config.getPref().putBoolean(FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.getKey(), nativeFileChoosers.isSelected());
245        MapMover.PROP_ZOOM_REVERSE_WHEEL.put(zoomReverseWheel.isSelected());
246        NavigatableComponent.PROP_ZOOM_INTERMEDIATE_STEPS.put(zoomIntermediateSteps.isSelected());
247        NavigatableComponent.PROP_ZOOM_RATIO.put(Math.pow(2, 1/(double) spinZoomRatio.getModel().getValue()));
248        mod |= LAF.put(((LookAndFeelInfo) lafCombo.getSelectedItem()).getClassName());
249        return mod;
250    }
251
252    @Override
253    public boolean isExpert() {
254        return false;
255    }
256
257    @Override
258    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
259        return gui.getDisplayPreference();
260    }
261
262}