source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/layer/LayerVisibilityAction.java @ 12396

Last change on this file since 12396 was 12396, checked in by michael2402, 22 months ago

LayerVisibilityAction: Align the texts more nicely.

  • Property svn:eol-style set to native
File size: 20.4 KB
RevLine 
[10144]1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
[12395]6import java.awt.BorderLayout;
7import java.awt.Color;
[10144]8import java.awt.Component;
[12395]9import java.awt.Dimension;
[10144]10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
[12395]12import java.awt.event.MouseAdapter;
13import java.awt.event.MouseEvent;
[11055]14import java.awt.event.MouseWheelEvent;
[10144]15import java.util.ArrayList;
16import java.util.Collection;
[12395]17import java.util.HashMap;
[10144]18import java.util.List;
[12395]19import java.util.stream.Collectors;
[10144]20
21import javax.swing.AbstractAction;
22import javax.swing.BorderFactory;
[12396]23import javax.swing.Icon;
[10144]24import javax.swing.ImageIcon;
25import javax.swing.JCheckBox;
[12392]26import javax.swing.JComponent;
[10144]27import javax.swing.JLabel;
28import javax.swing.JMenuItem;
29import javax.swing.JPanel;
30import javax.swing.JPopupMenu;
31import javax.swing.JSlider;
[12396]32import javax.swing.UIManager;
[12395]33import javax.swing.border.Border;
[10144]34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.gui.SideButton;
[10600]37import org.openstreetmap.josm.gui.dialogs.IEnabledStateUpdating;
[10144]38import org.openstreetmap.josm.gui.dialogs.LayerListDialog.LayerListModel;
[12395]39import org.openstreetmap.josm.gui.layer.GpxLayer;
[10144]40import org.openstreetmap.josm.gui.layer.ImageryLayer;
41import org.openstreetmap.josm.gui.layer.Layer;
42import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
[10547]43import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
[10144]44import org.openstreetmap.josm.tools.GBC;
45import org.openstreetmap.josm.tools.ImageProvider;
46import org.openstreetmap.josm.tools.Utils;
47
48/**
49 * This is a menu that includes all settings for the layer visibility. It combines gamma/opacity sliders and the visible-checkbox.
50 *
51 * @author Michael Zangl
52 */
53public final class LayerVisibilityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
[11913]54    private static final String DIALOGS_LAYERLIST = "dialogs/layerlist";
[10245]55    private static final int SLIDER_STEPS = 100;
[11055]56    /**
57     * Steps the value is changed by a mouse wheel change (one full click)
58     */
59    private static final int SLIDER_WHEEL_INCREMENT = 5;
[10144]60    private static final double MAX_SHARPNESS_FACTOR = 2;
61    private static final double MAX_COLORFUL_FACTOR = 2;
62    private final LayerListModel model;
63    private final JPopupMenu popup;
64    private SideButton sideButton;
[12392]65    /**
66     * The real content, just to add a border
67     */
68    private final JPanel content = new JPanel();
[10144]69    final OpacitySlider opacitySlider = new OpacitySlider();
[12392]70    private final ArrayList<LayerVisibilityMenuEntry> sliders = new ArrayList<>();
[10144]71
72    /**
73     * Creates a new {@link LayerVisibilityAction}
74     * @param model The list to get the selection from.
75     */
76    public LayerVisibilityAction(LayerListModel model) {
77        this.model = model;
78        popup = new JPopupMenu();
[11206]79        // prevent popup close on mouse wheel move
80        popup.addMouseWheelListener(MouseWheelEvent::consume);
[10144]81
82        popup.add(content);
83        content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
84        content.setLayout(new GridBagLayout());
85
[11913]86        new ImageProvider(DIALOGS_LAYERLIST, "visibility").getResource().attachImageIcon(this, true);
[10144]87        putValue(SHORT_DESCRIPTION, tr("Change visibility of the selected layer."));
88
[12392]89        addContentEntry(new VisibilityCheckbox());
[10144]90
[12392]91        addContentEntry(opacitySlider);
92        addContentEntry(new ColorfulnessSlider());
93        addContentEntry(new GammaFilterSlider());
94        addContentEntry(new SharpnessSlider());
[12395]95        addContentEntry(new ColorSelector());
[10144]96    }
97
[12392]98    private void addContentEntry(LayerVisibilityMenuEntry slider) {
[12396]99        content.add(slider.getPanel(), GBC.eop().fill(GBC.HORIZONTAL));
[10144]100        sliders.add(slider);
101    }
102
[10763]103    void setVisibleFlag(boolean visible) {
[10144]104        for (Layer l : model.getSelectedLayers()) {
105            l.setVisible(visible);
106        }
107        updateValues();
108    }
109
110    @Override
111    public void actionPerformed(ActionEvent e) {
112        updateValues();
113        if (e.getSource() == sideButton) {
114            popup.show(sideButton, 0, sideButton.getHeight());
115        } else {
116            // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden).
117            // In that case, show it in the middle of screen (because opacityButton is not visible)
118            popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2);
119        }
120    }
121
[10763]122    void updateValues() {
[10144]123        List<Layer> layers = model.getSelectedLayers();
124
125        boolean allVisible = true;
126        boolean allHidden = true;
127        for (Layer l : layers) {
128            allVisible &= l.isVisible();
129            allHidden &= !l.isVisible();
130        }
131
[12392]132        for (LayerVisibilityMenuEntry slider : sliders) {
133            slider.updateLayers(layers, allVisible, allHidden);
[10144]134        }
135    }
136
137    @Override
138    public boolean supportLayers(List<Layer> layers) {
139        return !layers.isEmpty();
140    }
141
142    @Override
143    public Component createMenuComponent() {
144        return new JMenuItem(this);
145    }
146
147    @Override
148    public void updateEnabledState() {
149        setEnabled(!model.getSelectedLayers().isEmpty());
150    }
151
152    /**
153     * Sets the corresponding side button.
154     * @param sideButton the corresponding side button
155     */
156    public void setCorrespondingSideButton(SideButton sideButton) {
157        this.sideButton = sideButton;
158    }
159
160    /**
[12392]161     * An entry in the visibility settings dropdown.
162     * @author Michael Zangl
163     */
164    private interface LayerVisibilityMenuEntry {
165
166        /**
167         * Update the displayed value depending on the current layers
168         * @param layers The layers
169         * @param allVisible <code>true</code> if all layers are visible
170         * @param allHidden <code>true</code> if all layers are hidden
171         */
172        void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden);
173
174        /**
175         * Get the panel that should be added to the menu
176         * @return The panel
177         */
178        JComponent getPanel();
179    }
180
181    private class VisibilityCheckbox extends JCheckBox implements LayerVisibilityMenuEntry {
182
183        VisibilityCheckbox() {
184            super(tr("Show layer"));
[12396]185
186            // Align all texts
187            Icon icon = UIManager.getIcon("CheckBox.icon");
188            int iconWidth = icon == null ? 20 : icon.getIconWidth();
189            setBorder(BorderFactory.createEmptyBorder(0, Math.max(24 + 5 - iconWidth, 0), 0, 0));
[12392]190            addChangeListener(e -> setVisibleFlag(isSelected()));
191        }
192
193        @Override
194        public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
195            setEnabled(!layers.isEmpty());
196            // TODO: Indicate tristate.
197            setSelected(allVisible && !allHidden);
198        }
199
200        @Override
201        public JComponent getPanel() {
202            return this;
203        }
204    }
205
206    /**
[10144]207     * This is a slider for a filter value.
208     * @author Michael Zangl
209     *
210     * @param <T> The layer type.
211     */
[12392]212    private abstract class AbstractFilterSlider<T extends Layer> extends JPanel implements LayerVisibilityMenuEntry {
[10144]213        private final double minValue;
214        private final double maxValue;
215        private final Class<T> layerClassFilter;
216
[12392]217        protected final JSlider slider = new JSlider(JSlider.HORIZONTAL);
218
[10144]219        /**
220         * Create a new filter slider.
221         * @param minValue The minimum value to map to the left side.
222         * @param maxValue The maximum value to map to the right side.
223         * @param layerClassFilter The type of layer influenced by this filter.
224         */
[12392]225        AbstractFilterSlider(double minValue, double maxValue, Class<T> layerClassFilter) {
226            super(new GridBagLayout());
[10144]227            this.minValue = minValue;
228            this.maxValue = maxValue;
229            this.layerClassFilter = layerClassFilter;
[12392]230
231            add(new JLabel(getIcon()), GBC.std().span(1, 2).insets(0, 0, 5, 0));
[12396]232            add(new JLabel(getLabel()), GBC.eol().insets(5, 0, 5, 0));
[12392]233            add(slider, GBC.eol());
234            addMouseWheelListener(this::mouseWheelMoved);
235
236            slider.setMaximum(SLIDER_STEPS);
[10144]237            int tick = convertFromRealValue(1);
[12392]238            slider.setMinorTickSpacing(tick);
239            slider.setMajorTickSpacing(tick);
240            slider.setPaintTicks(true);
[10144]241
[12392]242            slider.addChangeListener(e -> onStateChanged());
[10144]243        }
244
245        /**
246         * Called whenever the state of the slider was changed.
[12392]247         * @see JSlider#getValueIsAdjusting()
[10144]248         * @see #getRealValue()
249         */
250        protected void onStateChanged() {
251            Collection<T> layers = filterLayers(model.getSelectedLayers());
252            for (T layer : layers) {
253                applyValueToLayer(layer);
254            }
255        }
256
[11055]257        protected void mouseWheelMoved(MouseWheelEvent e) {
[11206]258            e.consume();
259            if (!isEnabled()) {
260                // ignore mouse wheel in disabled state.
261                return;
262            }
[11223]263            double rotation = -1 * e.getPreciseWheelRotation();
[12392]264            double destinationValue = slider.getValue() + rotation * SLIDER_WHEEL_INCREMENT;
[11055]265            if (rotation < 0) {
266                destinationValue = Math.floor(destinationValue);
267            } else {
268                destinationValue = Math.ceil(destinationValue);
269            }
[12392]270            slider.setValue(Utils.clamp((int) destinationValue, slider.getMinimum(), slider.getMaximum()));
[11055]271        }
272
[11510]273        abstract void applyValueToLayer(T layer);
[10144]274
275        protected double getRealValue() {
[12392]276            return convertToRealValue(slider.getValue());
[10144]277        }
278
279        protected double convertToRealValue(int value) {
280            double s = (double) value / SLIDER_STEPS;
281            return s * maxValue + (1-s) * minValue;
282        }
283
284        protected void setRealValue(double value) {
[12392]285            slider.setValue(convertFromRealValue(value));
[10144]286        }
287
288        protected int convertFromRealValue(double value) {
289            int i = (int) ((value - minValue) / (maxValue - minValue) * SLIDER_STEPS + .5);
[12392]290            return Utils.clamp(i, slider.getMinimum(), slider.getMaximum());
[10144]291        }
292
293        public abstract ImageIcon getIcon();
294
295        public abstract String getLabel();
296
[12392]297        @Override
298        public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
[10144]299            Collection<? extends Layer> usedLayers = filterLayers(layers);
[12393]300            setVisible(!usedLayers.isEmpty());
[12392]301            if (!usedLayers.stream().anyMatch(Layer::isVisible)) {
302                slider.setEnabled(false);
[10144]303            } else {
[12392]304                slider.setEnabled(true);
[10144]305                updateSliderWhileEnabled(usedLayers, allHidden);
306            }
307        }
308
309        protected Collection<T> filterLayers(List<Layer> layers) {
310            return Utils.filteredCollection(layers, layerClassFilter);
311        }
312
313        protected abstract void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden);
[12392]314
315        @Override
316        public JComponent getPanel() {
317            return this;
318        }
[10144]319    }
320
321    /**
322     * This slider allows you to change the opacity of a layer.
323     *
324     * @author Michael Zangl
325     * @see Layer#setOpacity(double)
326     */
[12392]327    class OpacitySlider extends AbstractFilterSlider<Layer> {
[10144]328        /**
329         * Creaate a new {@link OpacitySlider}.
330         */
331        OpacitySlider() {
332            super(0, 1, Layer.class);
[12392]333            slider.setToolTipText(tr("Adjust opacity of the layer."));
[10144]334        }
335
336        @Override
337        protected void onStateChanged() {
[12392]338            if (getRealValue() <= 0.001 && !slider.getValueIsAdjusting()) {
[10144]339                setVisibleFlag(false);
340            } else {
341                super.onStateChanged();
342            }
343        }
344
345        @Override
[11206]346        protected void mouseWheelMoved(MouseWheelEvent e) {
[11223]347            if (!isEnabled() && !filterLayers(model.getSelectedLayers()).isEmpty() && e.getPreciseWheelRotation() < 0) {
[11206]348                // make layer visible and set the value.
349                // this allows users to use the mouse wheel to make the layer visible if it was hidden previously.
350                e.consume();
351                setVisibleFlag(true);
352            } else {
353                super.mouseWheelMoved(e);
354            }
355        }
356
357        @Override
[10144]358        protected void applyValueToLayer(Layer layer) {
359            layer.setOpacity(getRealValue());
360        }
361
362        @Override
363        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
364            double opacity = 0;
365            for (Layer l : usedLayers) {
366                opacity += l.getOpacity();
367            }
368            opacity /= usedLayers.size();
369            if (opacity == 0) {
370                opacity = 1;
371                setVisibleFlag(true);
372            }
373            setRealValue(opacity);
374        }
375
376        @Override
377        public String getLabel() {
378            return tr("Opacity");
379        }
380
381        @Override
382        public ImageIcon getIcon() {
[11913]383            return ImageProvider.get(DIALOGS_LAYERLIST, "transparency");
[10144]384        }
385
386        @Override
387        public String toString() {
[10300]388            return "OpacitySlider [getRealValue()=" + getRealValue() + ']';
[10144]389        }
390    }
391
392    /**
393     * This slider allows you to change the gamma value of a layer.
394     *
395     * @author Michael Zangl
[10547]396     * @see ImageryFilterSettings#setGamma(double)
[10144]397     */
[12392]398    private class GammaFilterSlider extends AbstractFilterSlider<ImageryLayer> {
[10144]399
400        /**
401         * Create a new {@link GammaFilterSlider}
402         */
403        GammaFilterSlider() {
[10148]404            super(-1, 1, ImageryLayer.class);
[12392]405            slider.setToolTipText(tr("Adjust gamma value of the layer."));
[10144]406        }
407
408        @Override
409        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
[10547]410            double gamma = ((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getGamma();
[10148]411            setRealValue(mapGammaToInterval(gamma));
[10144]412        }
413
414        @Override
415        protected void applyValueToLayer(ImageryLayer layer) {
[10547]416            layer.getFilterSettings().setGamma(mapIntervalToGamma(getRealValue()));
[10144]417        }
418
419        @Override
420        public ImageIcon getIcon() {
[11913]421           return ImageProvider.get(DIALOGS_LAYERLIST, "gamma");
[10144]422        }
423
424        @Override
425        public String getLabel() {
426            return tr("Gamma");
427        }
[10148]428
429        /**
430         * Maps a number x from the range (-1,1) to a gamma value.
431         * Gamma value is in the range (0, infinity).
432         * Gamma values of 3 and 1/3 have opposite effects, so the mapping
433         * should be symmetric in that sense.
434         * @param x the slider value in the range (-1,1)
435         * @return the gamma value
436         */
437        private double mapIntervalToGamma(double x) {
438            // properties of the mapping:
439            // g(-1) = 0
440            // g(0) = 1
441            // g(1) = infinity
442            // g(-x) = 1 / g(x)
443            return (1 + x) / (1 - x);
444        }
445
446        private double mapGammaToInterval(double gamma) {
447            return (gamma - 1) / (gamma + 1);
448        }
[10144]449    }
450
451    /**
452     * This slider allows you to change the sharpness of a layer.
453     *
454     * @author Michael Zangl
[10547]455     * @see ImageryFilterSettings#setSharpenLevel(double)
[10144]456     */
[12392]457    private class SharpnessSlider extends AbstractFilterSlider<ImageryLayer> {
[10144]458
459        /**
460         * Creates a new {@link SharpnessSlider}
461         */
462        SharpnessSlider() {
463            super(0, MAX_SHARPNESS_FACTOR, ImageryLayer.class);
[12392]464            slider.setToolTipText(tr("Adjust sharpness/blur value of the layer."));
[10144]465        }
466
467        @Override
468        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
[10547]469            setRealValue(((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getSharpenLevel());
[10144]470        }
471
472        @Override
473        protected void applyValueToLayer(ImageryLayer layer) {
[10547]474            layer.getFilterSettings().setSharpenLevel(getRealValue());
[10144]475        }
476
477        @Override
478        public ImageIcon getIcon() {
[11913]479           return ImageProvider.get(DIALOGS_LAYERLIST, "sharpness");
[10144]480        }
481
482        @Override
483        public String getLabel() {
484            return tr("Sharpness");
485        }
486    }
487
488    /**
489     * This slider allows you to change the colorfulness of a layer.
490     *
491     * @author Michael Zangl
[10547]492     * @see ImageryFilterSettings#setColorfulness(double)
[10144]493     */
[12392]494    private class ColorfulnessSlider extends AbstractFilterSlider<ImageryLayer> {
[10144]495
496        /**
497         * Create a new {@link ColorfulnessSlider}
498         */
499        ColorfulnessSlider() {
500            super(0, MAX_COLORFUL_FACTOR, ImageryLayer.class);
[12392]501            slider.setToolTipText(tr("Adjust colorfulness of the layer."));
[10144]502        }
503
504        @Override
505        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
[10547]506            setRealValue(((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getColorfulness());
[10144]507        }
508
509        @Override
510        protected void applyValueToLayer(ImageryLayer layer) {
[10547]511            layer.getFilterSettings().setColorfulness(getRealValue());
[10144]512        }
513
514        @Override
515        public ImageIcon getIcon() {
[11913]516           return ImageProvider.get(DIALOGS_LAYERLIST, "colorfulness");
[10144]517        }
518
519        @Override
520        public String getLabel() {
521            return tr("Colorfulness");
522        }
523    }
[12395]524
525    /**
526     * Allows to select the color for the GPX layer
527     * @author Michael Zangl
528     */
529    private class ColorSelector extends JPanel implements LayerVisibilityMenuEntry {
530
531        private final Border NORMAL_BORDER = BorderFactory.createEmptyBorder(2, 2, 2, 2);
532        private final Border SELECTED_BORDER = BorderFactory.createLineBorder(Color.BLACK, 2);
533
534        // TODO: Nicer color palette
535        private final Color[] COLORS = new Color[] {
536                Color.RED,
537                Color.ORANGE,
538                Color.YELLOW,
539                Color.GREEN,
540                Color.BLUE,
541                Color.CYAN,
542                Color.GRAY,
543        };
544        private final HashMap<Color, JPanel> panels = new HashMap<>();
545
546        public ColorSelector() {
547            super(new GridBagLayout());
[12396]548            add(new JLabel(tr("Color")), GBC.eol().insets(24 + 10, 0, 0, 0));
[12395]549            for (Color color : COLORS) {
550                addPanelForColor(color);
551            }
552        }
553
554        private void addPanelForColor(Color color) {
555            JPanel innerPanel = new JPanel();
556            innerPanel.setBackground(color);
557
558            JPanel colorPanel = new JPanel(new BorderLayout());
559            colorPanel.setBorder(NORMAL_BORDER);
560            colorPanel.add(innerPanel);
561            colorPanel.setMinimumSize(new Dimension(20, 20));
562            colorPanel.addMouseListener(new MouseAdapter() {
563                @Override
564                public void mouseClicked(MouseEvent e) {
565                    List<Layer> layers = model.getSelectedLayers();
566                    for(Layer l : layers) {
567                        if (l instanceof GpxLayer) {
568                            l.getColorProperty().put(color);
569                        }
570                    }
571                    highlightColor(color);
572                }
573            });
574            add(colorPanel, GBC.std().weight(1, 1).fill().insets(5));
575            panels.put(color, colorPanel);
576        }
577
578        @Override
579        public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
580            List<Color> colors = layers.stream().filter(l -> l instanceof GpxLayer)
581                    .map(l -> ((GpxLayer)l).getColorProperty().get())
582                    .distinct()
583                    .collect(Collectors.toList());
584            if (colors.size() == 1) {
585                setVisible(true);
586                highlightColor(colors.get(0));
587            } else if (colors.size() > 0) {
588                setVisible(true);
589                highlightColor(null);
590            } else {
591                // no GPX layer
592                setVisible(false);
593            }
594        }
595
596        private void highlightColor(Color color) {
597            panels.values().forEach(panel -> panel.setBorder(NORMAL_BORDER));
598            if (color != null) {
599                JPanel selected = panels.get(color);
600                if (selected != null) {
601                    selected.setBorder(SELECTED_BORDER);
602                }
603            }
604            repaint();
605        }
606
607        @Override
608        public JComponent getPanel() {
609            return this;
610        }
611    }
[10144]612}
Note: See TracBrowser for help on using the repository browser.