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

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

see #16047 - move method to ImageProvider

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