source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java@ 13661

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

fix #15711 - Vizualize imagery layers with non-zero offsets (patch by skorbut)

  • Property svn:eol-style set to native
File size: 49.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.Font;
10import java.awt.GraphicsEnvironment;
11import java.awt.event.ActionEvent;
12import java.awt.event.InputEvent;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.beans.PropertyChangeEvent;
16import java.beans.PropertyChangeListener;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collections;
20import java.util.List;
21import java.util.Objects;
22import java.util.concurrent.CopyOnWriteArrayList;
23
24import javax.swing.AbstractAction;
25import javax.swing.DefaultCellEditor;
26import javax.swing.DefaultListSelectionModel;
27import javax.swing.DropMode;
28import javax.swing.ImageIcon;
29import javax.swing.JCheckBox;
30import javax.swing.JComponent;
31import javax.swing.JLabel;
32import javax.swing.JTable;
33import javax.swing.KeyStroke;
34import javax.swing.ListSelectionModel;
35import javax.swing.UIManager;
36import javax.swing.event.ListDataEvent;
37import javax.swing.event.ListSelectionEvent;
38import javax.swing.table.AbstractTableModel;
39import javax.swing.table.DefaultTableCellRenderer;
40import javax.swing.table.TableCellRenderer;
41import javax.swing.table.TableModel;
42
43import org.openstreetmap.josm.Main;
44import org.openstreetmap.josm.actions.MergeLayerAction;
45import org.openstreetmap.josm.data.coor.EastNorth;
46import org.openstreetmap.josm.data.imagery.OffsetBookmark;
47import org.openstreetmap.josm.data.preferences.AbstractProperty;
48import org.openstreetmap.josm.gui.MainApplication;
49import org.openstreetmap.josm.gui.MapFrame;
50import org.openstreetmap.josm.gui.MapView;
51import org.openstreetmap.josm.gui.SideButton;
52import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction;
53import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction;
54import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction;
55import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler;
56import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction;
57import org.openstreetmap.josm.gui.dialogs.layer.MergeAction;
58import org.openstreetmap.josm.gui.dialogs.layer.MoveDownAction;
59import org.openstreetmap.josm.gui.dialogs.layer.MoveUpAction;
60import org.openstreetmap.josm.gui.dialogs.layer.ShowHideLayerAction;
61import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
62import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
63import org.openstreetmap.josm.gui.layer.Layer;
64import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
65import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
66import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
67import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
68import org.openstreetmap.josm.gui.layer.MainLayerManager;
69import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
70import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
71import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
72import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent;
73import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener;
74import org.openstreetmap.josm.gui.util.MultikeyActionsHandler;
75import org.openstreetmap.josm.gui.util.MultikeyShortcutAction.MultikeyInfo;
76import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
77import org.openstreetmap.josm.gui.widgets.JosmTextField;
78import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
79import org.openstreetmap.josm.gui.widgets.ScrollableTable;
80import org.openstreetmap.josm.spi.preferences.Config;
81import org.openstreetmap.josm.tools.ImageProvider;
82import org.openstreetmap.josm.tools.InputMapUtils;
83import org.openstreetmap.josm.tools.Shortcut;
84
85/**
86 * This is a toggle dialog which displays the list of layers. Actions allow to
87 * change the ordering of the layers, to hide/show layers, to activate layers,
88 * and to delete layers.
89 * <p>
90 * Support for multiple {@link LayerListDialog} is currently not complete but intended for the future.
91 * @since 17
92 */
93public class LayerListDialog extends ToggleDialog implements DisplaySettingsChangeListener {
94 /** the unique instance of the dialog */
95 private static volatile LayerListDialog instance;
96
97 /**
98 * Creates the instance of the dialog. It's connected to the layer manager
99 *
100 * @param layerManager the layer manager
101 * @since 11885 (signature)
102 */
103 public static void createInstance(MainLayerManager layerManager) {
104 if (instance != null)
105 throw new IllegalStateException("Dialog was already created");
106 instance = new LayerListDialog(layerManager);
107 }
108
109 /**
110 * Replies the instance of the dialog
111 *
112 * @return the instance of the dialog
113 * @throws IllegalStateException if the dialog is not created yet
114 * @see #createInstance(MainLayerManager)
115 */
116 public static LayerListDialog getInstance() {
117 if (instance == null)
118 throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first");
119 return instance;
120 }
121
122 /** the model for the layer list */
123 private final LayerListModel model;
124
125 /** the list of layers (technically its a JTable, but appears like a list) */
126 private final LayerList layerList;
127
128 private final ActivateLayerAction activateLayerAction;
129 private final ShowHideLayerAction showHideLayerAction;
130
131 //TODO This duplicates ShowHide actions functionality
132 /** stores which layer index to toggle and executes the ShowHide action if the layer is present */
133 private final class ToggleLayerIndexVisibility extends AbstractAction {
134 private final int layerIndex;
135
136 ToggleLayerIndexVisibility(int layerIndex) {
137 this.layerIndex = layerIndex;
138 }
139
140 @Override
141 public void actionPerformed(ActionEvent e) {
142 final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1);
143 if (l != null) {
144 l.toggleVisible();
145 }
146 }
147 }
148
149 private final transient Shortcut[] visibilityToggleShortcuts = new Shortcut[10];
150 private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10];
151
152 /**
153 * The {@link MainLayerManager} this list is for.
154 */
155 private final transient MainLayerManager layerManager;
156
157 /**
158 * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts
159 * to toggle the visibility of the first ten layers.
160 */
161 private void createVisibilityToggleShortcuts() {
162 for (int i = 0; i < 10; i++) {
163 final int i1 = i + 1;
164 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
165 visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + i1,
166 tr("Toggle visibility of layer: {0}", i1), KeyEvent.VK_0 + (i1 % 10), Shortcut.ALT);
167 visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i);
168 MainApplication.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
169 }
170 }
171
172 /**
173 * Creates a layer list and attach it to the given layer manager.
174 * @param layerManager The layer manager this list is for
175 * @since 10467
176 */
177 public LayerListDialog(MainLayerManager layerManager) {
178 super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."),
179 Shortcut.registerShortcut("subwindow:layers", tr("Toggle: {0}", tr("Layers")), KeyEvent.VK_L,
180 Shortcut.ALT_SHIFT), 100, true);
181 this.layerManager = layerManager;
182
183 // create the models
184 //
185 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
186 selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
187 model = new LayerListModel(layerManager, selectionModel);
188
189 // create the list control
190 //
191 layerList = new LayerList(model);
192 layerList.setSelectionModel(selectionModel);
193 layerList.addMouseListener(new PopupMenuHandler());
194 layerList.setBackground(UIManager.getColor("Button.background"));
195 layerList.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
196 layerList.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
197 layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
198 layerList.setTableHeader(null);
199 layerList.setShowGrid(false);
200 layerList.setIntercellSpacing(new Dimension(0, 0));
201 layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer());
202 layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox()));
203 layerList.getColumnModel().getColumn(0).setMaxWidth(12);
204 layerList.getColumnModel().getColumn(0).setPreferredWidth(12);
205 layerList.getColumnModel().getColumn(0).setResizable(false);
206
207 layerList.getColumnModel().getColumn(1).setCellRenderer(new NativeScaleLayerCellRenderer());
208 layerList.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(new NativeScaleLayerCheckBox()));
209 layerList.getColumnModel().getColumn(1).setMaxWidth(12);
210 layerList.getColumnModel().getColumn(1).setPreferredWidth(12);
211 layerList.getColumnModel().getColumn(1).setResizable(false);
212
213 layerList.getColumnModel().getColumn(2).setCellRenderer(new OffsetLayerCellRenderer());
214 layerList.getColumnModel().getColumn(2).setCellEditor(new DefaultCellEditor(new OffsetLayerCheckBox()));
215 layerList.getColumnModel().getColumn(2).setMaxWidth(16);
216 layerList.getColumnModel().getColumn(2).setPreferredWidth(16);
217 layerList.getColumnModel().getColumn(2).setResizable(false);
218
219 layerList.getColumnModel().getColumn(3).setCellRenderer(new LayerVisibleCellRenderer());
220 layerList.getColumnModel().getColumn(3).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
221 layerList.getColumnModel().getColumn(3).setMaxWidth(16);
222 layerList.getColumnModel().getColumn(3).setPreferredWidth(16);
223 layerList.getColumnModel().getColumn(3).setResizable(false);
224
225 layerList.getColumnModel().getColumn(4).setCellRenderer(new LayerNameCellRenderer());
226 layerList.getColumnModel().getColumn(4).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField()));
227 // Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458)
228 for (KeyStroke ks : new KeyStroke[] {
229 KeyStroke.getKeyStroke(KeyEvent.VK_C, Main.platform.getMenuShortcutKeyMaskEx()),
230 KeyStroke.getKeyStroke(KeyEvent.VK_V, Main.platform.getMenuShortcutKeyMaskEx()),
231 KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK),
232 KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK),
233 KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK),
234 KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK),
235 KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK),
236 KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK),
237 KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK),
238 KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK),
239 KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
240 KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
241 KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
242 KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0),
243 }) {
244 layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object());
245 }
246
247 // init the model
248 //
249 model.populate();
250 model.setSelectedLayer(layerManager.getActiveLayer());
251 model.addLayerListModelListener(
252 new LayerListModelListener() {
253 @Override
254 public void makeVisible(int row, Layer layer) {
255 layerList.scrollToVisible(row, 0);
256 layerList.repaint();
257 }
258
259 @Override
260 public void refresh() {
261 layerList.repaint();
262 }
263 }
264 );
265
266 // -- move up action
267 MoveUpAction moveUpAction = new MoveUpAction(model);
268 adaptTo(moveUpAction, model);
269 adaptTo(moveUpAction, selectionModel);
270
271 // -- move down action
272 MoveDownAction moveDownAction = new MoveDownAction(model);
273 adaptTo(moveDownAction, model);
274 adaptTo(moveDownAction, selectionModel);
275
276 // -- activate action
277 activateLayerAction = new ActivateLayerAction(model);
278 activateLayerAction.updateEnabledState();
279 MultikeyActionsHandler.getInstance().addAction(activateLayerAction);
280 adaptTo(activateLayerAction, selectionModel);
281
282 JumpToMarkerActions.initialize();
283
284 // -- show hide action
285 showHideLayerAction = new ShowHideLayerAction(model);
286 MultikeyActionsHandler.getInstance().addAction(showHideLayerAction);
287 adaptTo(showHideLayerAction, selectionModel);
288
289 LayerVisibilityAction visibilityAction = new LayerVisibilityAction(model);
290 adaptTo(visibilityAction, selectionModel);
291 SideButton visibilityButton = new SideButton(visibilityAction, false);
292 visibilityAction.setCorrespondingSideButton(visibilityButton);
293
294 // -- delete layer action
295 DeleteLayerAction deleteLayerAction = new DeleteLayerAction(model);
296 layerList.getActionMap().put("deleteLayer", deleteLayerAction);
297 adaptTo(deleteLayerAction, selectionModel);
298 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
299 KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"
300 );
301 getActionMap().put("delete", deleteLayerAction);
302
303 // Activate layer on Enter key press
304 InputMapUtils.addEnterAction(layerList, new AbstractAction() {
305 @Override
306 public void actionPerformed(ActionEvent e) {
307 activateLayerAction.actionPerformed(null);
308 layerList.requestFocus();
309 }
310 });
311
312 // Show/Activate layer on Enter key press
313 InputMapUtils.addSpacebarAction(layerList, showHideLayerAction);
314
315 createLayout(layerList, true, Arrays.asList(
316 new SideButton(moveUpAction, false),
317 new SideButton(moveDownAction, false),
318 new SideButton(activateLayerAction, false),
319 visibilityButton,
320 new SideButton(deleteLayerAction, false)
321 ));
322
323 createVisibilityToggleShortcuts();
324 }
325
326 /**
327 * Gets the layer manager this dialog is for.
328 * @return The layer manager.
329 * @since 10288
330 */
331 public MainLayerManager getLayerManager() {
332 return layerManager;
333 }
334
335 @Override
336 public void showNotify() {
337 layerManager.addActiveLayerChangeListener(activateLayerAction);
338 layerManager.addAndFireLayerChangeListener(model);
339 layerManager.addAndFireActiveLayerChangeListener(model);
340 model.populate();
341 }
342
343 @Override
344 public void hideNotify() {
345 layerManager.removeAndFireLayerChangeListener(model);
346 layerManager.removeActiveLayerChangeListener(model);
347 layerManager.removeActiveLayerChangeListener(activateLayerAction);
348 }
349
350 /**
351 * Returns the layer list model.
352 * @return the layer list model
353 */
354 public LayerListModel getModel() {
355 return model;
356 }
357
358 /**
359 * Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that
360 * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
361 * on every {@link ListSelectionEvent}.
362 *
363 * @param listener the listener
364 * @param listSelectionModel the source emitting {@link ListSelectionEvent}s
365 */
366 protected void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) {
367 listSelectionModel.addListSelectionListener(e -> listener.updateEnabledState());
368 }
369
370 /**
371 * Wires <code>listener</code> to <code>listModel</code> in such a way, that
372 * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
373 * on every {@link ListDataEvent}.
374 *
375 * @param listener the listener
376 * @param listModel the source emitting {@link ListDataEvent}s
377 */
378 protected void adaptTo(final IEnabledStateUpdating listener, LayerListModel listModel) {
379 listModel.addTableModelListener(e -> listener.updateEnabledState());
380 }
381
382 @Override
383 public void destroy() {
384 for (int i = 0; i < 10; i++) {
385 MainApplication.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
386 }
387 MultikeyActionsHandler.getInstance().removeAction(activateLayerAction);
388 MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction);
389 JumpToMarkerActions.unregisterActions();
390 super.destroy();
391 instance = null;
392 }
393
394 private static class ActiveLayerCheckBox extends JCheckBox {
395 ActiveLayerCheckBox() {
396 setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
397 ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
398 ImageIcon active = ImageProvider.get("dialogs/layerlist", "active");
399 setIcon(blank);
400 setSelectedIcon(active);
401 setRolloverIcon(blank);
402 setRolloverSelectedIcon(active);
403 setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed"));
404 }
405 }
406
407 private static class LayerVisibleCheckBox extends JCheckBox {
408 private final ImageIcon iconEye;
409 private final ImageIcon iconEyeTranslucent;
410 private boolean isTranslucent;
411
412 /**
413 * Constructs a new {@code LayerVisibleCheckBox}.
414 */
415 LayerVisibleCheckBox() {
416 setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
417 iconEye = ImageProvider.get("dialogs/layerlist", "eye");
418 iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent");
419 setIcon(ImageProvider.get("dialogs/layerlist", "eye-off"));
420 setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed"));
421 setSelectedIcon(iconEye);
422 isTranslucent = false;
423 }
424
425 public void setTranslucent(boolean isTranslucent) {
426 if (this.isTranslucent == isTranslucent) return;
427 if (isTranslucent) {
428 setSelectedIcon(iconEyeTranslucent);
429 } else {
430 setSelectedIcon(iconEye);
431 }
432 this.isTranslucent = isTranslucent;
433 }
434
435 public void updateStatus(Layer layer) {
436 boolean visible = layer.isVisible();
437 setSelected(visible);
438 setTranslucent(layer.getOpacity() < 1.0);
439 setToolTipText(visible ?
440 tr("layer is currently visible (click to hide layer)") :
441 tr("layer is currently hidden (click to show layer)"));
442 }
443 }
444
445 private static class NativeScaleLayerCheckBox extends JCheckBox {
446 NativeScaleLayerCheckBox() {
447 setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
448 ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
449 ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale");
450 setIcon(blank);
451 setSelectedIcon(active);
452 }
453 }
454
455 private static class OffsetLayerCheckBox extends JCheckBox {
456 OffsetLayerCheckBox() {
457 setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
458 ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
459 ImageIcon withOffset = ImageProvider.get("dialogs/layerlist", "offset");
460 setIcon(blank);
461 setSelectedIcon(withOffset);
462 }
463 }
464
465 private static class ActiveLayerCellRenderer implements TableCellRenderer {
466 private final JCheckBox cb;
467
468 /**
469 * Constructs a new {@code ActiveLayerCellRenderer}.
470 */
471 ActiveLayerCellRenderer() {
472 cb = new ActiveLayerCheckBox();
473 }
474
475 @Override
476 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
477 boolean active = value != null && (Boolean) value;
478 cb.setSelected(active);
479 cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)"));
480 return cb;
481 }
482 }
483
484 private static class LayerVisibleCellRenderer implements TableCellRenderer {
485 private final LayerVisibleCheckBox cb;
486
487 /**
488 * Constructs a new {@code LayerVisibleCellRenderer}.
489 */
490 LayerVisibleCellRenderer() {
491 this.cb = new LayerVisibleCheckBox();
492 }
493
494 @Override
495 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
496 if (value != null) {
497 cb.updateStatus((Layer) value);
498 }
499 return cb;
500 }
501 }
502
503 private static class LayerVisibleCellEditor extends DefaultCellEditor {
504 private final LayerVisibleCheckBox cb;
505
506 LayerVisibleCellEditor(LayerVisibleCheckBox cb) {
507 super(cb);
508 this.cb = cb;
509 }
510
511 @Override
512 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
513 cb.updateStatus((Layer) value);
514 return cb;
515 }
516 }
517
518 private static class NativeScaleLayerCellRenderer implements TableCellRenderer {
519 private final JCheckBox cb;
520
521 /**
522 * Constructs a new {@code ActiveLayerCellRenderer}.
523 */
524 NativeScaleLayerCellRenderer() {
525 cb = new NativeScaleLayerCheckBox();
526 }
527
528 @Override
529 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
530 Layer layer = (Layer) value;
531 if (layer instanceof NativeScaleLayer) {
532 boolean active = ((NativeScaleLayer) layer) == MainApplication.getMap().mapView.getNativeScaleLayer();
533 cb.setSelected(active);
534 cb.setToolTipText(active
535 ? tr("scale follows native resolution of this layer")
536 : tr("scale follows native resolution of another layer (click to set this layer)")
537 );
538 } else {
539 cb.setSelected(false);
540 cb.setToolTipText(tr("this layer has no native resolution"));
541 }
542 return cb;
543 }
544 }
545
546 private static class OffsetLayerCellRenderer implements TableCellRenderer {
547 private final JCheckBox cb;
548
549 /**
550 * Constructs a new {@code OffsetLayerCellRenderer}.
551 */
552 OffsetLayerCellRenderer() {
553 cb = new OffsetLayerCheckBox();
554 cb.setEnabled(false);
555 }
556
557 @Override
558 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
559 Layer layer = (Layer) value;
560 if (layer instanceof AbstractTileSourceLayer<?>) {
561 if (EastNorth.ZERO.equals(((AbstractTileSourceLayer<?>) layer).getDisplaySettings().getDisplacement())) {
562 cb.setSelected(false);
563 cb.setEnabled(false); // TODO: allow reselecting checkbox and thereby setting the old offset again
564 cb.setToolTipText(tr("layer is without a user-defined offset"));
565 } else {
566 cb.setSelected(true);
567 cb.setEnabled(true);
568 cb.setToolTipText(tr("layer has a user-defined offset (click to remove offset)"));
569 }
570
571 } else {
572 cb.setSelected(false);
573 cb.setEnabled(false);
574 cb.setToolTipText(tr("this layer can not have an offset"));
575 }
576 return cb;
577 }
578 }
579
580 private class LayerNameCellRenderer extends DefaultTableCellRenderer {
581
582 protected boolean isActiveLayer(Layer layer) {
583 return getLayerManager().getActiveLayer() == layer;
584 }
585
586 @Override
587 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
588 if (value == null)
589 return this;
590 Layer layer = (Layer) value;
591 JLabel label = (JLabel) super.getTableCellRendererComponent(table,
592 layer.getName(), isSelected, hasFocus, row, column);
593 if (isActiveLayer(layer)) {
594 label.setFont(label.getFont().deriveFont(Font.BOLD));
595 }
596 if (Config.getPref().getBoolean("dialog.layer.colorname", true)) {
597 AbstractProperty<Color> prop = layer.getColorProperty();
598 Color c = prop == null ? null : prop.get();
599 if (c == null || !model.getLayers().stream()
600 .map(Layer::getColorProperty)
601 .filter(Objects::nonNull)
602 .map(AbstractProperty::get)
603 .anyMatch(oc -> oc != null && !oc.equals(c))) {
604 /* not more than one color, don't use coloring */
605 label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground"));
606 } else {
607 label.setForeground(c);
608 }
609 }
610 label.setIcon(layer.getIcon());
611 label.setToolTipText(layer.getToolTipText());
612 return label;
613 }
614 }
615
616 private static class LayerNameCellEditor extends DefaultCellEditor {
617 LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) {
618 super(tf);
619 }
620
621 @Override
622 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
623 JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
624 tf.setText(value == null ? "" : ((Layer) value).getName());
625 return tf;
626 }
627 }
628
629 class PopupMenuHandler extends PopupMenuLauncher {
630 @Override
631 public void showMenu(MouseEvent evt) {
632 menu = new LayerListPopup(getModel().getSelectedLayers());
633 super.showMenu(evt);
634 }
635 }
636
637 /**
638 * Observer interface to be implemented by views using {@link LayerListModel}.
639 */
640 public interface LayerListModelListener {
641
642 /**
643 * Fired when a layer is made visible.
644 * @param index the layer index
645 * @param layer the layer
646 */
647 void makeVisible(int index, Layer layer);
648
649
650 /**
651 * Fired when something has changed in the layer list model.
652 */
653 void refresh();
654 }
655
656 /**
657 * The layer list model. The model manages a list of layers and provides methods for
658 * moving layers up and down, for toggling their visibility, and for activating a layer.
659 *
660 * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects
661 * to be configured with a {@link DefaultListSelectionModel}. The selection model is used
662 * to update the selection state of views depending on messages sent to the model.
663 *
664 * The model manages a list of {@link LayerListModelListener} which are mainly notified if
665 * the model requires views to make a specific list entry visible.
666 *
667 * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to
668 * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}.
669 */
670 public static final class LayerListModel extends AbstractTableModel
671 implements LayerChangeListener, ActiveLayerChangeListener, PropertyChangeListener {
672 /** manages list selection state*/
673 private final DefaultListSelectionModel selectionModel;
674 private final CopyOnWriteArrayList<LayerListModelListener> listeners;
675 private LayerList layerList;
676 private final MainLayerManager layerManager;
677
678 /**
679 * constructor
680 * @param layerManager The layer manager to use for the list.
681 * @param selectionModel the list selection model
682 */
683 LayerListModel(MainLayerManager layerManager, DefaultListSelectionModel selectionModel) {
684 this.layerManager = layerManager;
685 this.selectionModel = selectionModel;
686 listeners = new CopyOnWriteArrayList<>();
687 }
688
689 void setLayerList(LayerList layerList) {
690 this.layerList = layerList;
691 }
692
693 /**
694 * The layer manager this model is for.
695 * @return The layer manager.
696 */
697 public MainLayerManager getLayerManager() {
698 return layerManager;
699 }
700
701 /**
702 * Adds a listener to this model
703 *
704 * @param listener the listener
705 */
706 public void addLayerListModelListener(LayerListModelListener listener) {
707 if (listener != null) {
708 listeners.addIfAbsent(listener);
709 }
710 }
711
712 /**
713 * removes a listener from this model
714 * @param listener the listener
715 */
716 public void removeLayerListModelListener(LayerListModelListener listener) {
717 listeners.remove(listener);
718 }
719
720 /**
721 * Fires a make visible event to listeners
722 *
723 * @param index the index of the row to make visible
724 * @param layer the layer at this index
725 * @see LayerListModelListener#makeVisible(int, Layer)
726 */
727 private void fireMakeVisible(int index, Layer layer) {
728 for (LayerListModelListener listener : listeners) {
729 listener.makeVisible(index, layer);
730 }
731 }
732
733 /**
734 * Fires a refresh event to listeners of this model
735 *
736 * @see LayerListModelListener#refresh()
737 */
738 private void fireRefresh() {
739 for (LayerListModelListener listener : listeners) {
740 listener.refresh();
741 }
742 }
743
744 /**
745 * Populates the model with the current layers managed by {@link MapView}.
746 */
747 public void populate() {
748 for (Layer layer: getLayers()) {
749 // make sure the model is registered exactly once
750 layer.removePropertyChangeListener(this);
751 layer.addPropertyChangeListener(this);
752 }
753 fireTableDataChanged();
754 }
755
756 /**
757 * Marks <code>layer</code> as selected layer. Ignored, if layer is null.
758 *
759 * @param layer the layer.
760 */
761 public void setSelectedLayer(Layer layer) {
762 if (layer == null)
763 return;
764 int idx = getLayers().indexOf(layer);
765 if (idx >= 0) {
766 selectionModel.setSelectionInterval(idx, idx);
767 }
768 ensureSelectedIsVisible();
769 }
770
771 /**
772 * Replies the list of currently selected layers. Never null, but may be empty.
773 *
774 * @return the list of currently selected layers. Never null, but may be empty.
775 */
776 public List<Layer> getSelectedLayers() {
777 List<Layer> selected = new ArrayList<>();
778 List<Layer> layers = getLayers();
779 for (int i = 0; i < layers.size(); i++) {
780 if (selectionModel.isSelectedIndex(i)) {
781 selected.add(layers.get(i));
782 }
783 }
784 return selected;
785 }
786
787 /**
788 * Replies a the list of indices of the selected rows. Never null, but may be empty.
789 *
790 * @return the list of indices of the selected rows. Never null, but may be empty.
791 */
792 public List<Integer> getSelectedRows() {
793 List<Integer> selected = new ArrayList<>();
794 for (int i = 0; i < getLayers().size(); i++) {
795 if (selectionModel.isSelectedIndex(i)) {
796 selected.add(i);
797 }
798 }
799 return selected;
800 }
801
802 /**
803 * Invoked if a layer managed by {@link MapView} is removed
804 *
805 * @param layer the layer which is removed
806 */
807 private void onRemoveLayer(Layer layer) {
808 if (layer == null)
809 return;
810 layer.removePropertyChangeListener(this);
811 final int size = getRowCount();
812 final List<Integer> rows = getSelectedRows();
813
814 if (rows.isEmpty() && size > 0) {
815 selectionModel.setSelectionInterval(size-1, size-1);
816 }
817 fireTableDataChanged();
818 fireRefresh();
819 ensureActiveSelected();
820 }
821
822 /**
823 * Invoked when a layer managed by {@link MapView} is added
824 *
825 * @param layer the layer
826 */
827 private void onAddLayer(Layer layer) {
828 if (layer == null)
829 return;
830 layer.addPropertyChangeListener(this);
831 fireTableDataChanged();
832 int idx = getLayers().indexOf(layer);
833 if (layerList != null) {
834 layerList.setRowHeight(idx, Math.max(16, layer.getIcon().getIconHeight()));
835 }
836 selectionModel.setSelectionInterval(idx, idx);
837 ensureSelectedIsVisible();
838 if (layer instanceof AbstractTileSourceLayer<?>) {
839 ((AbstractTileSourceLayer<?>) layer).getDisplaySettings().addSettingsChangeListener(LayerListDialog.getInstance());
840 }
841 }
842
843 /**
844 * Replies the first layer. Null if no layers are present
845 *
846 * @return the first layer. Null if no layers are present
847 */
848 public Layer getFirstLayer() {
849 if (getRowCount() == 0)
850 return null;
851 return getLayers().get(0);
852 }
853
854 /**
855 * Replies the layer at position <code>index</code>
856 *
857 * @param index the index
858 * @return the layer at position <code>index</code>. Null,
859 * if index is out of range.
860 */
861 public Layer getLayer(int index) {
862 if (index < 0 || index >= getRowCount())
863 return null;
864 return getLayers().get(index);
865 }
866
867 /**
868 * Replies true if the currently selected layers can move up by one position
869 *
870 * @return true if the currently selected layers can move up by one position
871 */
872 public boolean canMoveUp() {
873 List<Integer> sel = getSelectedRows();
874 return !sel.isEmpty() && sel.get(0) > 0;
875 }
876
877 /**
878 * Move up the currently selected layers by one position
879 *
880 */
881 public void moveUp() {
882 if (!canMoveUp())
883 return;
884 List<Integer> sel = getSelectedRows();
885 List<Layer> layers = getLayers();
886 MapView mapView = MainApplication.getMap().mapView;
887 for (int row : sel) {
888 Layer l1 = layers.get(row);
889 mapView.moveLayer(l1, row-1);
890 }
891 fireTableDataChanged();
892 selectionModel.setValueIsAdjusting(true);
893 selectionModel.clearSelection();
894 for (int row : sel) {
895 selectionModel.addSelectionInterval(row-1, row-1);
896 }
897 selectionModel.setValueIsAdjusting(false);
898 ensureSelectedIsVisible();
899 }
900
901 /**
902 * Replies true if the currently selected layers can move down by one position
903 *
904 * @return true if the currently selected layers can move down by one position
905 */
906 public boolean canMoveDown() {
907 List<Integer> sel = getSelectedRows();
908 return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1;
909 }
910
911 /**
912 * Move down the currently selected layers by one position
913 */
914 public void moveDown() {
915 if (!canMoveDown())
916 return;
917 List<Integer> sel = getSelectedRows();
918 Collections.reverse(sel);
919 List<Layer> layers = getLayers();
920 MapView mapView = MainApplication.getMap().mapView;
921 for (int row : sel) {
922 Layer l1 = layers.get(row);
923 mapView.moveLayer(l1, row+1);
924 }
925 fireTableDataChanged();
926 selectionModel.setValueIsAdjusting(true);
927 selectionModel.clearSelection();
928 for (int row : sel) {
929 selectionModel.addSelectionInterval(row+1, row+1);
930 }
931 selectionModel.setValueIsAdjusting(false);
932 ensureSelectedIsVisible();
933 }
934
935 /**
936 * Make sure the first of the selected layers is visible in the views of this model.
937 */
938 private void ensureSelectedIsVisible() {
939 int index = selectionModel.getMinSelectionIndex();
940 if (index < 0)
941 return;
942 List<Layer> layers = getLayers();
943 if (index >= layers.size())
944 return;
945 Layer layer = layers.get(index);
946 fireMakeVisible(index, layer);
947 }
948
949 /**
950 * Replies a list of layers which are possible merge targets for <code>source</code>
951 *
952 * @param source the source layer
953 * @return a list of layers which are possible merge targets
954 * for <code>source</code>. Never null, but can be empty.
955 */
956 public List<Layer> getPossibleMergeTargets(Layer source) {
957 List<Layer> targets = new ArrayList<>();
958 if (source == null) {
959 return targets;
960 }
961 for (Layer target : getLayers()) {
962 if (source == target) {
963 continue;
964 }
965 if (target.isMergable(source) && source.isMergable(target)) {
966 targets.add(target);
967 }
968 }
969 return targets;
970 }
971
972 /**
973 * Replies the list of layers currently managed by {@link MapView}.
974 * Never null, but can be empty.
975 *
976 * @return the list of layers currently managed by {@link MapView}.
977 * Never null, but can be empty.
978 */
979 public List<Layer> getLayers() {
980 return getLayerManager().getLayers();
981 }
982
983 /**
984 * Ensures that at least one layer is selected in the layer dialog
985 *
986 */
987 private void ensureActiveSelected() {
988 List<Layer> layers = getLayers();
989 if (layers.isEmpty())
990 return;
991 final Layer activeLayer = getActiveLayer();
992 if (activeLayer != null) {
993 // there's an active layer - select it and make it visible
994 int idx = layers.indexOf(activeLayer);
995 selectionModel.setSelectionInterval(idx, idx);
996 ensureSelectedIsVisible();
997 } else {
998 // no active layer - select the first one and make it visible
999 selectionModel.setSelectionInterval(0, 0);
1000 ensureSelectedIsVisible();
1001 }
1002 }
1003
1004 /**
1005 * Replies the active layer. null, if no active layer is available
1006 *
1007 * @return the active layer. null, if no active layer is available
1008 */
1009 private Layer getActiveLayer() {
1010 return getLayerManager().getActiveLayer();
1011 }
1012
1013 /* ------------------------------------------------------------------------------ */
1014 /* Interface TableModel */
1015 /* ------------------------------------------------------------------------------ */
1016
1017 @Override
1018 public int getRowCount() {
1019 List<Layer> layers = getLayers();
1020 return layers == null ? 0 : layers.size();
1021 }
1022
1023 @Override
1024 public int getColumnCount() {
1025 return 5;
1026 }
1027
1028 @Override
1029 public Object getValueAt(int row, int col) {
1030 List<Layer> layers = getLayers();
1031 if (row >= 0 && row < layers.size()) {
1032 switch (col) {
1033 case 0: return layers.get(row) == getActiveLayer();
1034 case 1:
1035 case 2:
1036 case 3:
1037 case 4: return layers.get(row);
1038 default: // Do nothing
1039 }
1040 }
1041 return null;
1042 }
1043
1044 @Override
1045 public boolean isCellEditable(int row, int col) {
1046 return col != 0 || getActiveLayer() != getLayers().get(row);
1047 }
1048
1049 @Override
1050 public void setValueAt(Object value, int row, int col) {
1051 List<Layer> layers = getLayers();
1052 if (row < layers.size()) {
1053 Layer l = layers.get(row);
1054 switch (col) {
1055 case 0:
1056 getLayerManager().setActiveLayer(l);
1057 l.setVisible(true);
1058 break;
1059 case 1:
1060 MapFrame map = MainApplication.getMap();
1061 NativeScaleLayer oldLayer = map.mapView.getNativeScaleLayer();
1062 if (oldLayer == l) {
1063 map.mapView.setNativeScaleLayer(null);
1064 } else if (l instanceof NativeScaleLayer) {
1065 map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
1066 if (oldLayer instanceof Layer) {
1067 int idx = getLayers().indexOf((Layer) oldLayer);
1068 if (idx >= 0) {
1069 fireTableCellUpdated(idx, col);
1070 }
1071 }
1072 }
1073 break;
1074 case 2:
1075 // reset layer offset
1076 if (l instanceof AbstractTileSourceLayer<?>) {
1077 AbstractTileSourceLayer<?> abstractTileSourceLayer = (AbstractTileSourceLayer<?>) l;
1078 OffsetBookmark offsetBookmark = abstractTileSourceLayer.getDisplaySettings().getOffsetBookmark();
1079 if (offsetBookmark != null) {
1080 offsetBookmark.setDisplacement(EastNorth.ZERO);
1081 abstractTileSourceLayer.getDisplaySettings().setOffsetBookmark(offsetBookmark);
1082 }
1083 }
1084 break;
1085 case 3:
1086 l.setVisible((Boolean) value);
1087 break;
1088 case 4:
1089 l.rename((String) value);
1090 break;
1091 default:
1092 throw new IllegalArgumentException("Wrong column: " + col);
1093 }
1094 fireTableCellUpdated(row, col);
1095 }
1096 }
1097
1098 /* ------------------------------------------------------------------------------ */
1099 /* Interface ActiveLayerChangeListener */
1100 /* ------------------------------------------------------------------------------ */
1101 @Override
1102 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1103 Layer oldLayer = e.getPreviousActiveLayer();
1104 if (oldLayer != null) {
1105 int idx = getLayers().indexOf(oldLayer);
1106 if (idx >= 0) {
1107 fireTableRowsUpdated(idx, idx);
1108 }
1109 }
1110
1111 Layer newLayer = getActiveLayer();
1112 if (newLayer != null) {
1113 int idx = getLayers().indexOf(newLayer);
1114 if (idx >= 0) {
1115 fireTableRowsUpdated(idx, idx);
1116 }
1117 }
1118 ensureActiveSelected();
1119 }
1120
1121 /* ------------------------------------------------------------------------------ */
1122 /* Interface LayerChangeListener */
1123 /* ------------------------------------------------------------------------------ */
1124 @Override
1125 public void layerAdded(LayerAddEvent e) {
1126 onAddLayer(e.getAddedLayer());
1127 }
1128
1129 @Override
1130 public void layerRemoving(LayerRemoveEvent e) {
1131 onRemoveLayer(e.getRemovedLayer());
1132 }
1133
1134 @Override
1135 public void layerOrderChanged(LayerOrderChangeEvent e) {
1136 fireTableDataChanged();
1137 }
1138
1139 /* ------------------------------------------------------------------------------ */
1140 /* Interface PropertyChangeListener */
1141 /* ------------------------------------------------------------------------------ */
1142 @Override
1143 public void propertyChange(PropertyChangeEvent evt) {
1144 if (evt.getSource() instanceof Layer) {
1145 Layer layer = (Layer) evt.getSource();
1146 final int idx = getLayers().indexOf(layer);
1147 if (idx < 0)
1148 return;
1149 fireRefresh();
1150 }
1151 }
1152 }
1153
1154 /**
1155 * This component displays a list of layers and provides the methods needed by {@link LayerListModel}.
1156 */
1157 static class LayerList extends ScrollableTable {
1158
1159 LayerList(LayerListModel dataModel) {
1160 super(dataModel);
1161 dataModel.setLayerList(this);
1162 if (!GraphicsEnvironment.isHeadless()) {
1163 setDragEnabled(true);
1164 }
1165 setDropMode(DropMode.INSERT_ROWS);
1166 setTransferHandler(new LayerListTransferHandler());
1167 }
1168
1169 @Override
1170 public LayerListModel getModel() {
1171 return (LayerListModel) super.getModel();
1172 }
1173 }
1174
1175 /**
1176 * Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}.
1177 *
1178 * @return the action
1179 */
1180 public ShowHideLayerAction createShowHideLayerAction() {
1181 return new ShowHideLayerAction(model);
1182 }
1183
1184 /**
1185 * Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}.
1186 *
1187 * @return the action
1188 */
1189 public DeleteLayerAction createDeleteLayerAction() {
1190 return new DeleteLayerAction(model);
1191 }
1192
1193 /**
1194 * Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1195 *
1196 * @param layer the layer
1197 * @return the action
1198 */
1199 public ActivateLayerAction createActivateLayerAction(Layer layer) {
1200 return new ActivateLayerAction(layer, model);
1201 }
1202
1203 /**
1204 * Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1205 *
1206 * @param layer the layer
1207 * @return the action
1208 */
1209 public MergeAction createMergeLayerAction(Layer layer) {
1210 return new MergeAction(layer, model);
1211 }
1212
1213 /**
1214 * Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1215 *
1216 * @param layer the layer
1217 * @return the action
1218 */
1219 public DuplicateAction createDuplicateLayerAction(Layer layer) {
1220 return new DuplicateAction(layer, model);
1221 }
1222
1223 /**
1224 * Returns the layer at given index, or {@code null}.
1225 * @param index the index
1226 * @return the layer at given index, or {@code null} if index out of range
1227 */
1228 public static Layer getLayerForIndex(int index) {
1229 List<Layer> layers = MainApplication.getLayerManager().getLayers();
1230
1231 if (index < layers.size() && index >= 0)
1232 return layers.get(index);
1233 else
1234 return null;
1235 }
1236
1237 /**
1238 * Returns a list of info on all layers of a given class.
1239 * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose,
1240 * to allow asking for layers implementing some interface
1241 * @return list of info on all layers assignable from {@code layerClass}
1242 */
1243 public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
1244 List<MultikeyInfo> result = new ArrayList<>();
1245
1246 List<Layer> layers = MainApplication.getLayerManager().getLayers();
1247
1248 int index = 0;
1249 for (Layer l: layers) {
1250 if (layerClass.isAssignableFrom(l.getClass())) {
1251 result.add(new MultikeyInfo(index, l.getName()));
1252 }
1253 index++;
1254 }
1255
1256 return result;
1257 }
1258
1259 /**
1260 * Determines if a layer is valid (contained in global layer list).
1261 * @param l the layer
1262 * @return {@code true} if layer {@code l} is contained in current layer list
1263 */
1264 public static boolean isLayerValid(Layer l) {
1265 if (l == null)
1266 return false;
1267
1268 return MainApplication.getLayerManager().containsLayer(l);
1269 }
1270
1271 /**
1272 * Returns info about layer.
1273 * @param l the layer
1274 * @return info about layer {@code l}
1275 */
1276 public static MultikeyInfo getLayerInfo(Layer l) {
1277 if (l == null)
1278 return null;
1279
1280 int index = MainApplication.getLayerManager().getLayers().indexOf(l);
1281 if (index < 0)
1282 return null;
1283
1284 return new MultikeyInfo(index, l.getName());
1285 }
1286
1287 @Override
1288 public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
1289 if ("displacement".equals(e.getChangedSetting())) {
1290 layerList.repaint();
1291 }
1292 }
1293}
Note: See TracBrowser for help on using the repository browser.