source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java@ 6025

Last change on this file since 6025 was 6025, checked in by akks, 11 years ago

Highlight relation or object when user clicks the list in Relation List, Selection List or Properties dialog
[ idea by Felis Pimeja ] + added HighlighHelper class

  • Property svn:eol-style set to native
File size: 25.0 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.BorderLayout;
7import java.awt.Color;
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.awt.event.MouseEvent;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.AbstractListModel;
21import javax.swing.DefaultListSelectionModel;
22import javax.swing.JComponent;
23import javax.swing.JList;
24import javax.swing.JPanel;
25import javax.swing.JPopupMenu;
26import javax.swing.JScrollPane;
27import javax.swing.KeyStroke;
28import javax.swing.ListSelectionModel;
29import javax.swing.UIManager;
30import javax.swing.event.DocumentEvent;
31import javax.swing.event.DocumentListener;
32import javax.swing.event.ListSelectionEvent;
33import javax.swing.event.ListSelectionListener;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.actions.relation.AddSelectionToRelations;
37import org.openstreetmap.josm.actions.relation.DeleteRelationsAction;
38import org.openstreetmap.josm.actions.relation.DownloadMembersAction;
39import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
40import org.openstreetmap.josm.actions.relation.DuplicateRelationAction;
41import org.openstreetmap.josm.actions.relation.EditRelationAction;
42import org.openstreetmap.josm.actions.relation.SelectMembersAction;
43import org.openstreetmap.josm.actions.relation.SelectRelationAction;
44import org.openstreetmap.josm.actions.search.SearchCompiler;
45import org.openstreetmap.josm.data.osm.DataSet;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.osm.Relation;
48import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
49import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
50import org.openstreetmap.josm.data.osm.event.DataSetListener;
51import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
52import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
53import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
54import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
55import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
56import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
57import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
58import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
59import org.openstreetmap.josm.gui.DefaultNameFormatter;
60import org.openstreetmap.josm.gui.MapView;
61import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
62import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
63import org.openstreetmap.josm.gui.PopupMenuHandler;
64import org.openstreetmap.josm.gui.SideButton;
65import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
66import org.openstreetmap.josm.gui.layer.Layer;
67import org.openstreetmap.josm.gui.layer.OsmDataLayer;
68import org.openstreetmap.josm.gui.util.HighlightHelper;
69import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
70import org.openstreetmap.josm.gui.widgets.JosmTextField;
71import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
72import org.openstreetmap.josm.tools.ImageProvider;
73import org.openstreetmap.josm.tools.InputMapUtils;
74import org.openstreetmap.josm.tools.Predicate;
75import org.openstreetmap.josm.tools.Shortcut;
76import org.openstreetmap.josm.tools.Utils;
77
78/**
79 * A dialog showing all known relations, with buttons to add, edit, and
80 * delete them.
81 *
82 * We don't have such dialogs for nodes, segments, and ways, because those
83 * objects are visible on the map and can be selected there. Relations are not.
84 */
85public class RelationListDialog extends ToggleDialog implements DataSetListener {
86 /** The display list. */
87 private final JList displaylist;
88 /** the list model used */
89 private final RelationListModel model;
90
91 private final NewAction newAction;
92
93 /** the popup menu and its handler */
94 private final JPopupMenu popupMenu = new JPopupMenu();
95 private final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
96
97 private final JosmTextField filter;
98
99 // Actions
100 /** the edit action */
101 private final EditRelationAction editAction = new EditRelationAction();
102 /** the delete action */
103 private final DeleteRelationsAction deleteRelationsAction = new DeleteRelationsAction();
104 /** the duplicate action */
105 private final DuplicateRelationAction duplicateAction = new DuplicateRelationAction();
106 private final DownloadMembersAction downloadMembersAction = new DownloadMembersAction();
107 private final DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction();
108 private final SelectMembersAction selectMembersAction = new SelectMembersAction(false);
109 private final SelectMembersAction addMembersToSelectionAction = new SelectMembersAction(true);
110 private final SelectRelationAction selectRelationAction = new SelectRelationAction(false);
111 private final SelectRelationAction addRelationToSelectionAction = new SelectRelationAction(true);
112 /** add all selected primitives to the given relations */
113 private final AddSelectionToRelations addSelectionToRelations = new AddSelectionToRelations();
114
115 HighlightHelper highlightHelper = new HighlightHelper();
116
117 /**
118 * Constructs <code>RelationListDialog</code>
119 */
120 public RelationListDialog() {
121 super(tr("Relations"), "relationlist", tr("Open a list of all relations."),
122 Shortcut.registerShortcut("subwindow:relations", tr("Toggle: {0}", tr("Relations")),
123 KeyEvent.VK_R, Shortcut.ALT_SHIFT), 150);
124
125 // create the list of relations
126 //
127 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
128 model = new RelationListModel(selectionModel);
129 displaylist = new JList(model);
130 displaylist.setSelectionModel(selectionModel);
131 displaylist.setCellRenderer(new OsmPrimitivRenderer() {
132 /**
133 * Don't show the default tooltip in the relation list.
134 */
135 @Override
136 protected String getComponentToolTipText(OsmPrimitive value) {
137 return null;
138 }
139 });
140 displaylist.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
141 displaylist.addMouseListener(new MouseEventHandler());
142
143 // the new action
144 //
145 newAction = new NewAction();
146
147 filter = setupFilter();
148
149 displaylist.addListSelectionListener(new ListSelectionListener() {
150 @Override
151 public void valueChanged(ListSelectionEvent e) {
152 updateActionsRelationLists();
153 }
154 });
155
156 // Setup popup menu handler
157 setupPopupMenuHandler();
158
159 JPanel pane = new JPanel(new BorderLayout());
160 pane.add(filter, BorderLayout.NORTH);
161 pane.add(new JScrollPane(displaylist), BorderLayout.CENTER);
162 createLayout(pane, false, Arrays.asList(new SideButton[]{
163 new SideButton(newAction, false),
164 new SideButton(editAction, false),
165 new SideButton(duplicateAction, false),
166 new SideButton(deleteRelationsAction, false),
167 new SideButton(selectRelationAction, false)
168 }));
169
170 // activate DEL in the list of relations
171 //displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "deleteRelation");
172 //displaylist.getActionMap().put("deleteRelation", deleteAction);
173
174 InputMapUtils.unassignCtrlShiftUpDown(displaylist, JComponent.WHEN_FOCUSED);
175
176 // Select relation on Ctrl-Enter
177 InputMapUtils.addEnterAction(displaylist, selectRelationAction);
178
179 // Edit relation on Ctrl-Enter
180 displaylist.getActionMap().put("edit", editAction);
181 displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_MASK), "edit");
182
183 updateActionsRelationLists();
184 }
185
186 // inform all actions about list of relations they need
187 private void updateActionsRelationLists() {
188 List<Relation> sel = model.getSelectedRelations();
189 popupMenuHandler.setPrimitives(sel);
190
191 //update highlights
192 if (Main.isDisplayingMapView()) {
193 highlightHelper.highlightOnly(sel);
194 Main.map.mapView.repaint();
195 }
196 }
197
198 @Override public void showNotify() {
199 MapView.addLayerChangeListener(newAction);
200 newAction.updateEnabledState();
201 DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT);
202 DataSet.addSelectionListener(addSelectionToRelations);
203 dataChanged(null);
204 }
205
206 @Override public void hideNotify() {
207 MapView.removeLayerChangeListener(newAction);
208 DatasetEventManager.getInstance().removeDatasetListener(this);
209 DataSet.removeSelectionListener(addSelectionToRelations);
210 }
211
212 private void resetFilter() {
213 filter.setText(null);
214 }
215
216 /**
217 * Initializes the relation list dialog from a layer. If <code>layer</code> is null
218 * or if it isn't an {@link OsmDataLayer} the dialog is reset to an empty dialog.
219 * Otherwise it is initialized with the list of non-deleted and visible relations
220 * in the layer's dataset.
221 *
222 * @param layer the layer. May be null.
223 */
224 protected void initFromLayer(Layer layer) {
225 if (layer == null || ! (layer instanceof OsmDataLayer)) {
226 model.setRelations(null);
227 return;
228 }
229 OsmDataLayer l = (OsmDataLayer)layer;
230 model.setRelations(l.data.getRelations());
231 model.updateTitle();
232 updateActionsRelationLists();
233 }
234
235 /**
236 * @return The selected relation in the list
237 */
238 private Relation getSelected() {
239 if(model.getSize() == 1) {
240 displaylist.setSelectedIndex(0);
241 }
242 return (Relation) displaylist.getSelectedValue();
243 }
244
245 /**
246 * Selects the relation <code>relation</code> in the list of relations.
247 *
248 * @param relation the relation
249 */
250 public void selectRelation(Relation relation) {
251 selectRelations(Collections.singleton(relation));
252 }
253
254 /**
255 * Selects the relations in the list of relations.
256 * @param relations the relations to be selected
257 */
258 public void selectRelations(Collection<Relation> relations) {
259 if (relations == null || relations.isEmpty()) {
260 model.setSelectedRelations(null);
261 } else {
262 model.setSelectedRelations(relations);
263 Integer i = model.getVisibleRelationIndex(relations.iterator().next());
264 if (i != null) { // Not all relations have to be in the list (for example when the relation list is hidden, it's not updated with new relations)
265 displaylist.scrollRectToVisible(displaylist.getCellBounds(i, i));
266 }
267 }
268 }
269
270 private JosmTextField setupFilter() {
271 final JosmTextField f = new DisableShortcutsOnFocusGainedTextField();
272 f.setToolTipText(tr("Relation list filter"));
273 f.getDocument().addDocumentListener(new DocumentListener() {
274
275 private void setFilter() {
276 try {
277 f.setBackground(UIManager.getColor("TextField.background"));
278 f.setToolTipText(tr("Relation list filter"));
279 model.setFilter(SearchCompiler.compile(filter.getText(), false, false));
280 } catch (SearchCompiler.ParseError ex) {
281 f.setBackground(new Color(255, 224, 224));
282 f.setToolTipText(ex.getMessage());
283 model.setFilter(new SearchCompiler.Always());
284 }
285 }
286
287 @Override
288 public void insertUpdate(DocumentEvent e) {
289 setFilter();
290 }
291
292 @Override
293 public void removeUpdate(DocumentEvent e) {
294 setFilter();
295 }
296
297 @Override
298 public void changedUpdate(DocumentEvent e) {
299 setFilter();
300 }
301 });
302 return f;
303 }
304
305 class MouseEventHandler extends PopupMenuLauncher {
306
307 public MouseEventHandler() {
308 super(popupMenu);
309 }
310
311 @Override
312 public void mouseExited(MouseEvent me) {
313 highlightHelper.clear();
314 }
315
316 protected void setCurrentRelationAsSelection() {
317 Main.main.getCurrentDataSet().setSelected((Relation)displaylist.getSelectedValue());
318 }
319
320 protected void editCurrentRelation() {
321 EditRelationAction.launchEditor(getSelected());
322 }
323
324 @Override public void mouseClicked(MouseEvent e) {
325 if (Main.main.getEditLayer() == null) return;
326 if (isDoubleClick(e)) {
327 if (e.isControlDown()) {
328 editCurrentRelation();
329 } else {
330 setCurrentRelationAsSelection();
331 }
332 }
333 }
334 }
335
336 /**
337 * The action for creating a new relation
338 *
339 */
340 static class NewAction extends AbstractAction implements LayerChangeListener{
341 public NewAction() {
342 putValue(SHORT_DESCRIPTION,tr("Create a new relation"));
343 putValue(NAME, tr("New"));
344 putValue(SMALL_ICON, ImageProvider.get("dialogs", "addrelation"));
345 updateEnabledState();
346 }
347
348 public void run() {
349 RelationEditor.getEditor(Main.main.getEditLayer(),null, null).setVisible(true);
350 }
351
352 @Override
353 public void actionPerformed(ActionEvent e) {
354 run();
355 }
356
357 protected void updateEnabledState() {
358 setEnabled(Main.main != null && Main.main.getEditLayer() != null);
359 }
360
361 @Override
362 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
363 updateEnabledState();
364 }
365
366 @Override
367 public void layerAdded(Layer newLayer) {
368 updateEnabledState();
369 }
370
371 @Override
372 public void layerRemoved(Layer oldLayer) {
373 updateEnabledState();
374 }
375 }
376
377 /**
378 * The list model for the list of relations displayed in the relation list
379 * dialog.
380 *
381 */
382 private class RelationListModel extends AbstractListModel {
383 private final ArrayList<Relation> relations = new ArrayList<Relation>();
384 private ArrayList<Relation> filteredRelations;
385 private DefaultListSelectionModel selectionModel;
386 private SearchCompiler.Match filter;
387
388 public RelationListModel(DefaultListSelectionModel selectionModel) {
389 this.selectionModel = selectionModel;
390 }
391
392 public Relation getRelation(int idx) {
393 return relations.get(idx);
394 }
395
396 public void sort() {
397 Collections.sort(
398 relations,
399 DefaultNameFormatter.getInstance().getRelationComparator()
400 );
401 }
402
403 private boolean isValid(Relation r) {
404 return !r.isDeleted() && r.isVisible() && !r.isIncomplete();
405 }
406
407 public void setRelations(Collection<Relation> relations) {
408 List<Relation> sel = getSelectedRelations();
409 this.relations.clear();
410 this.filteredRelations = null;
411 if (relations == null) {
412 selectionModel.clearSelection();
413 fireContentsChanged(this,0,getSize());
414 return;
415 }
416 for (Relation r: relations) {
417 if (isValid(r)) {
418 this.relations.add(r);
419 }
420 }
421 sort();
422 updateFilteredRelations();
423 fireIntervalAdded(this, 0, getSize());
424 setSelectedRelations(sel);
425 }
426
427 /**
428 * Add all relations in <code>addedPrimitives</code> to the model for the
429 * relation list dialog
430 *
431 * @param addedPrimitives the collection of added primitives. May include nodes,
432 * ways, and relations.
433 */
434 public void addRelations(Collection<? extends OsmPrimitive> addedPrimitives) {
435 boolean added = false;
436 for (OsmPrimitive p: addedPrimitives) {
437 if (! (p instanceof Relation)) {
438 continue;
439 }
440
441 Relation r = (Relation)p;
442 if (relations.contains(r)) {
443 continue;
444 }
445 if (isValid(r)) {
446 relations.add(r);
447 added = true;
448 }
449 }
450 if (added) {
451 List<Relation> sel = getSelectedRelations();
452 sort();
453 updateFilteredRelations();
454 fireIntervalAdded(this, 0, getSize());
455 setSelectedRelations(sel);
456 }
457 }
458
459 /**
460 * Removes all relations in <code>removedPrimitives</code> from the model
461 *
462 * @param removedPrimitives the removed primitives. May include nodes, ways,
463 * and relations
464 */
465 public void removeRelations(Collection<? extends OsmPrimitive> removedPrimitives) {
466 if (removedPrimitives == null) return;
467 // extract the removed relations
468 //
469 Set<Relation> removedRelations = new HashSet<Relation>();
470 for (OsmPrimitive p: removedPrimitives) {
471 if (! (p instanceof Relation)) {
472 continue;
473 }
474 removedRelations.add((Relation)p);
475 }
476 if (removedRelations.isEmpty())
477 return;
478 int size = relations.size();
479 relations.removeAll(removedRelations);
480 if (filteredRelations != null) {
481 filteredRelations.removeAll(removedRelations);
482 }
483 if (size != relations.size()) {
484 List<Relation> sel = getSelectedRelations();
485 sort();
486 fireContentsChanged(this, 0, getSize());
487 setSelectedRelations(sel);
488 }
489 }
490
491 private void updateFilteredRelations() {
492 if (filter != null) {
493 filteredRelations = new ArrayList<Relation>(Utils.filter(relations, new Predicate<Relation>() {
494 @Override
495 public boolean evaluate(Relation r) {
496 return filter.match(r);
497 }
498 }));
499 } else if (filteredRelations != null) {
500 filteredRelations = null;
501 }
502 }
503
504 public void setFilter(final SearchCompiler.Match filter) {
505 this.filter = filter;
506 updateFilteredRelations();
507 List<Relation> sel = getSelectedRelations();
508 fireContentsChanged(this, 0, getSize());
509 setSelectedRelations(sel);
510 updateTitle();
511 }
512
513 private List<Relation> getVisibleRelations() {
514 return filteredRelations == null ? relations : filteredRelations;
515 }
516
517 private Relation getVisibleRelation(int index) {
518 if (index < 0 || index >= getVisibleRelations().size()) return null;
519 return getVisibleRelations().get(index);
520 }
521
522 @Override
523 public Object getElementAt(int index) {
524 return getVisibleRelation(index);
525 }
526
527 @Override
528 public int getSize() {
529 return getVisibleRelations().size();
530 }
531
532 /**
533 * Replies the list of selected relations. Empty list,
534 * if there are no selected relations.
535 *
536 * @return the list of selected, non-new relations.
537 */
538 public List<Relation> getSelectedRelations() {
539 ArrayList<Relation> ret = new ArrayList<Relation>();
540 for (int i=0; i<getSize();i++) {
541 if (!selectionModel.isSelectedIndex(i)) {
542 continue;
543 }
544 ret.add(getVisibleRelation(i));
545 }
546 return ret;
547 }
548
549 /**
550 * Sets the selected relations.
551 *
552 * @param sel the list of selected relations
553 */
554 public void setSelectedRelations(Collection<Relation> sel) {
555 selectionModel.clearSelection();
556 if (sel == null || sel.isEmpty())
557 return;
558 if (!getVisibleRelations().containsAll(sel)) {
559 resetFilter();
560 }
561 for (Relation r: sel) {
562 Integer i = getVisibleRelationIndex(r);
563 if (i != null) {
564 selectionModel.addSelectionInterval(i,i);
565 }
566 }
567 }
568
569 /**
570 * Returns the index of the relation
571 * @param rel The relation to look for
572 *
573 * @return index of relation (null if it cannot be found)
574 */
575 public Integer getRelationIndex(Relation rel) {
576 int i = relations.indexOf(rel);
577 if (i<0)
578 return null;
579 return i;
580 }
581
582 private Integer getVisibleRelationIndex(Relation rel) {
583 int i = getVisibleRelations().indexOf(rel);
584 if (i<0)
585 return null;
586 return i;
587 }
588
589 public void updateTitle() {
590 if (relations.size() > 0 && relations.size() != getSize()) {
591 RelationListDialog.this.setTitle(tr("Relations: {0}/{1}", getSize(), relations.size()));
592 } else if (getSize() > 0) {
593 RelationListDialog.this.setTitle(tr("Relations: {0}", getSize()));
594 } else {
595 RelationListDialog.this.setTitle(tr("Relations"));
596 }
597 }
598 }
599
600 private final void setupPopupMenuHandler() {
601
602 // -- select action
603 popupMenuHandler.addAction(selectRelationAction);
604 popupMenuHandler.addAction(addRelationToSelectionAction);
605
606 // -- select members action
607 popupMenuHandler.addAction(selectMembersAction);
608 popupMenuHandler.addAction(addMembersToSelectionAction);
609
610 popupMenuHandler.addSeparator();
611 // -- download members action
612 popupMenuHandler.addAction(downloadMembersAction);
613
614 // -- download incomplete members action
615 popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction);
616
617 popupMenuHandler.addSeparator();
618 popupMenuHandler.addAction(editAction).setVisible(false);
619 popupMenuHandler.addAction(duplicateAction).setVisible(false);
620 popupMenuHandler.addAction(deleteRelationsAction).setVisible(false);
621
622 popupMenuHandler.addAction(addSelectionToRelations);
623 }
624
625 /* ---------------------------------------------------------------------------------- */
626 /* Methods that can be called from plugins */
627 /* ---------------------------------------------------------------------------------- */
628
629 /**
630 * Replies the popup menu handler.
631 * @return The popup menu handler
632 */
633 public PopupMenuHandler getPopupMenuHandler() {
634 return popupMenuHandler;
635 }
636
637 public Collection<Relation> getSelectedRelations() {
638 return model.getSelectedRelations();
639 }
640
641 /* ---------------------------------------------------------------------------------- */
642 /* DataSetListener */
643 /* ---------------------------------------------------------------------------------- */
644
645 @Override
646 public void nodeMoved(NodeMovedEvent event) {/* irrelevant in this context */}
647
648 @Override
649 public void wayNodesChanged(WayNodesChangedEvent event) {/* irrelevant in this context */}
650
651 @Override
652 public void primitivesAdded(final PrimitivesAddedEvent event) {
653 model.addRelations(event.getPrimitives());
654 model.updateTitle();
655 }
656
657 @Override
658 public void primitivesRemoved(final PrimitivesRemovedEvent event) {
659 model.removeRelations(event.getPrimitives());
660 model.updateTitle();
661 }
662
663 @Override
664 public void relationMembersChanged(final RelationMembersChangedEvent event) {
665 List<Relation> sel = model.getSelectedRelations();
666 model.sort();
667 model.setSelectedRelations(sel);
668 displaylist.repaint();
669 }
670
671 @Override
672 public void tagsChanged(TagsChangedEvent event) {
673 OsmPrimitive prim = event.getPrimitive();
674 if (prim == null || ! (prim instanceof Relation))
675 return;
676 // trigger a sort of the relation list because the display name may
677 // have changed
678 //
679 List<Relation> sel = model.getSelectedRelations();
680 model.sort();
681 model.setSelectedRelations(sel);
682 displaylist.repaint();
683 }
684
685 @Override
686 public void dataChanged(DataChangedEvent event) {
687 initFromLayer(Main.main.getEditLayer());
688 }
689
690 @Override
691 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignore */}
692}
Note: See TracBrowser for help on using the repository browser.