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

Last change on this file since 14153 was 14153, checked in by Don-vip, 6 years ago

see #15229 - deprecate Main.parent and Main itself

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