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

Last change on this file since 12393 was 12393, checked in by michael2402, 7 years ago

Hide color filter sliders if no image layer is selected.

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