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

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

fix #17221 - display layer number in layer list, to ease using toggle layer shortcuts

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