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

Last change on this file since 11055 was 11055, checked in by michael2402, 8 years ago

Fix #13703: Allow mouse wheel to change visibility slider value.

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