source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java@ 19050

Last change on this file since 19050 was 19050, checked in by taylor.smock, 6 weeks ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 65.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.properties;
3
4import static org.openstreetmap.josm.actions.search.SearchAction.searchStateless;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Component;
8import java.awt.Container;
9import java.awt.Font;
10import java.awt.GridBagConstraints;
11import java.awt.GridBagLayout;
12import java.awt.Point;
13import java.awt.event.ActionEvent;
14import java.awt.event.KeyEvent;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.util.ArrayList;
20import java.util.Arrays;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.EnumSet;
24import java.util.HashMap;
25import java.util.HashSet;
26import java.util.List;
27import java.util.Map;
28import java.util.Map.Entry;
29import java.util.Set;
30import java.util.TreeMap;
31import java.util.TreeSet;
32import java.util.concurrent.atomic.AtomicBoolean;
33import java.util.stream.Collectors;
34
35import javax.swing.AbstractAction;
36import javax.swing.JComponent;
37import javax.swing.JLabel;
38import javax.swing.JMenuItem;
39import javax.swing.JPanel;
40import javax.swing.JPopupMenu;
41import javax.swing.JScrollPane;
42import javax.swing.JTable;
43import javax.swing.KeyStroke;
44import javax.swing.ListSelectionModel;
45import javax.swing.event.ListSelectionEvent;
46import javax.swing.event.ListSelectionListener;
47import javax.swing.event.PopupMenuEvent;
48import javax.swing.event.RowSorterEvent;
49import javax.swing.event.RowSorterListener;
50import javax.swing.table.DefaultTableCellRenderer;
51import javax.swing.table.DefaultTableModel;
52import javax.swing.table.TableCellRenderer;
53import javax.swing.table.TableColumnModel;
54import javax.swing.table.TableModel;
55import javax.swing.table.TableRowSorter;
56
57import org.openstreetmap.josm.actions.JosmAction;
58import org.openstreetmap.josm.actions.relation.DeleteRelationsAction;
59import org.openstreetmap.josm.actions.relation.DuplicateRelationAction;
60import org.openstreetmap.josm.actions.relation.EditRelationAction;
61import org.openstreetmap.josm.command.ChangeMembersCommand;
62import org.openstreetmap.josm.command.ChangePropertyCommand;
63import org.openstreetmap.josm.command.Command;
64import org.openstreetmap.josm.data.UndoRedoHandler;
65import org.openstreetmap.josm.data.coor.LatLon;
66import org.openstreetmap.josm.data.osm.AbstractPrimitive;
67import org.openstreetmap.josm.data.osm.DataSelectionListener;
68import org.openstreetmap.josm.data.osm.DataSet;
69import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
70import org.openstreetmap.josm.data.osm.IPrimitive;
71import org.openstreetmap.josm.data.osm.IRelation;
72import org.openstreetmap.josm.data.osm.IRelationMember;
73import org.openstreetmap.josm.data.osm.KeyValueVisitor;
74import org.openstreetmap.josm.data.osm.Node;
75import org.openstreetmap.josm.data.osm.OsmDataManager;
76import org.openstreetmap.josm.data.osm.OsmPrimitive;
77import org.openstreetmap.josm.data.osm.Relation;
78import org.openstreetmap.josm.data.osm.RelationMember;
79import org.openstreetmap.josm.data.osm.Tag;
80import org.openstreetmap.josm.data.osm.Tags;
81import org.openstreetmap.josm.data.osm.Way;
82import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
83import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
84import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
85import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
86import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
87import org.openstreetmap.josm.data.osm.search.SearchCompiler;
88import org.openstreetmap.josm.data.osm.search.SearchSetting;
89import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeEvent;
90import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
91import org.openstreetmap.josm.data.preferences.BooleanProperty;
92import org.openstreetmap.josm.data.preferences.CachingProperty;
93import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
94import org.openstreetmap.josm.gui.ExtendedDialog;
95import org.openstreetmap.josm.gui.MainApplication;
96import org.openstreetmap.josm.gui.PopupMenuHandler;
97import org.openstreetmap.josm.gui.PrimitiveHoverListener;
98import org.openstreetmap.josm.gui.SideButton;
99import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
100import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
101import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
102import org.openstreetmap.josm.gui.dialogs.relation.RelationPopupMenus;
103import org.openstreetmap.josm.gui.help.HelpUtil;
104import org.openstreetmap.josm.gui.layer.Layer;
105import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
106import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
107import org.openstreetmap.josm.gui.layer.OsmDataLayer;
108import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
109import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
110import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetListener;
111import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
112import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
113import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
114import org.openstreetmap.josm.gui.util.AbstractTag2LinkPopupListener;
115import org.openstreetmap.josm.gui.util.HighlightHelper;
116import org.openstreetmap.josm.gui.util.TableHelper;
117import org.openstreetmap.josm.gui.widgets.CompileSearchTextDecorator;
118import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
119import org.openstreetmap.josm.gui.widgets.FilterField;
120import org.openstreetmap.josm.gui.widgets.JosmTextField;
121import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
122import org.openstreetmap.josm.spi.preferences.Config;
123import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
124import org.openstreetmap.josm.tools.AlphanumComparator;
125import org.openstreetmap.josm.tools.GBC;
126import org.openstreetmap.josm.tools.ImageProvider;
127import org.openstreetmap.josm.tools.InputMapUtils;
128import org.openstreetmap.josm.tools.Logging;
129import org.openstreetmap.josm.tools.Shortcut;
130import org.openstreetmap.josm.tools.TaginfoRegionalInstance;
131import org.openstreetmap.josm.tools.Territories;
132import org.openstreetmap.josm.tools.Utils;
133
134/**
135 * This dialog displays the tags of the current selected primitives.
136 *
137 * If no object is selected, the dialog list is empty.
138 * If only one is selected, all tags of this object are selected.
139 * If more than one object is selected, the sum of all tags is displayed. If the
140 * different objects share the same tag, the shared value is displayed. If they have
141 * different values, all of them are put in a combo box and the string "<different>"
142 * is displayed in italic.
143 *
144 * Below the list, the user can click on an add, modify and delete tag button to
145 * edit the table selection value.
146 *
147 * The command is applied to all selected entries.
148 *
149 * @author imi
150 */
151public class PropertiesDialog extends ToggleDialog
152implements DataSelectionListener, ActiveLayerChangeListener, PropertyChangeListener,
153 DataSetListenerAdapter.Listener, TaggingPresetListener, PrimitiveHoverListener {
154 private static final BooleanProperty PROP_DISPLAY_DISCARDABLE_KEYS = new BooleanProperty("display.discardable-keys", false);
155
156 /**
157 * hook for roadsigns plugin to display a small button in the upper right corner of this dialog
158 */
159 public static final JPanel pluginHook = new JPanel();
160
161 /**
162 * The tag data of selected objects.
163 */
164 private final ReadOnlyTableModel tagData = new ReadOnlyTableModel();
165 private final PropertiesCellRenderer cellRenderer = new PropertiesCellRenderer();
166 private final transient TableRowSorter<ReadOnlyTableModel> tagRowSorter = new TableRowSorter<>(tagData);
167 private final JosmTextField tagTableFilter;
168
169 /**
170 * The membership data of selected objects.
171 */
172 private final DefaultTableModel membershipData = new ReadOnlyTableModel();
173
174 /**
175 * The tags table.
176 */
177 private final JTable tagTable = new JTable(tagData);
178
179 /**
180 * The membership table.
181 */
182 private final JTable membershipTable = new JTable(membershipData);
183
184 /** JPanel containing both previous tables */
185 private final JPanel bothTables = new JPanel(new GridBagLayout());
186
187 // Popup menus
188 private final JPopupMenu tagMenu = new JPopupMenu();
189 private final JPopupMenu membershipMenu = new JPopupMenu();
190 private final JPopupMenu blankSpaceMenu = new JPopupMenu();
191
192 // Popup menu handlers
193 private final transient PopupMenuHandler tagMenuHandler = new PopupMenuHandler(tagMenu);
194 private final transient PopupMenuHandler membershipMenuHandler = new PopupMenuHandler(membershipMenu);
195 private final transient PopupMenuHandler blankSpaceMenuHandler = new PopupMenuHandler(blankSpaceMenu);
196
197 private final List<JMenuItem> tagMenuTagInfoNatItems = new ArrayList<>();
198 private final List<JMenuItem> membershipMenuTagInfoNatItems = new ArrayList<>();
199
200 private final transient Map<String, Map<String, Integer>> valueCount = new TreeMap<>();
201 /**
202 * This sub-object is responsible for all adding and editing of tags
203 */
204 private final transient TagEditHelper editHelper = new TagEditHelper(tagTable, tagData, valueCount);
205
206 private final transient DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
207 private final HelpAction helpTagAction = new HelpTagAction(tagTable, editHelper::getDataKey, editHelper::getDataValues);
208 private final HelpAction helpRelAction = new HelpMembershipAction(membershipTable, x -> (IRelation<?>) membershipData.getValueAt(x, 0));
209 private final TaginfoAction taginfoAction = new TaginfoAction(
210 tagTable, editHelper::getDataKey, editHelper::getDataValues,
211 membershipTable, x -> (IRelation<?>) membershipData.getValueAt(x, 0));
212 private final TaginfoAction tagHistoryAction = taginfoAction.toTagHistoryAction();
213 private final Collection<TaginfoAction> taginfoNationalActions = new ArrayList<>();
214 private transient int taginfoNationalHash;
215 private final PasteValueAction pasteValueAction = new PasteValueAction();
216 private final CopyValueAction copyValueAction = new CopyValueAction(
217 tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection);
218 private final CopyKeyValueAction copyKeyValueAction = new CopyKeyValueAction(
219 tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection);
220 private final CopyAllKeyValueAction copyAllKeyValueAction = new CopyAllKeyValueAction(
221 tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection).registerShortcut(); /* NO-SHORTCUT */
222 private final SearchAction searchActionSame = new SearchAction(true);
223 private final SearchAction searchActionAny = new SearchAction(false);
224 private final AddAction addAction = new AddAction();
225 private final EditAction editAction = new EditAction();
226 private final DeleteAction deleteAction = new DeleteAction();
227 private final JosmAction[] josmActions = {addAction, editAction, deleteAction};
228
229 private final transient HighlightHelper highlightHelper = new HighlightHelper();
230
231 /**
232 * The Add button (needed to be able to disable it)
233 */
234 private final SideButton btnAdd = new SideButton(addAction);
235 /**
236 * The Edit button (needed to be able to disable it)
237 */
238 private final SideButton btnEdit = new SideButton(editAction);
239 /**
240 * The Delete button (needed to be able to disable it)
241 */
242 private final SideButton btnDel = new SideButton(deleteAction);
243 /**
244 * Matching preset display class
245 */
246 private final PresetListPanel presets = new PresetListPanel();
247
248 /**
249 * Text to display when nothing selected.
250 */
251 private final JLabel selectSth = new JLabel("<html><p>"
252 + tr("Select objects for which to change tags.") + "</p></html>");
253
254 private final transient TaggingPresetHandler presetHandler = new TaggingPresetCommandHandler();
255
256 private PopupMenuLauncher popupMenuLauncher;
257
258 private static final BooleanProperty PROP_AUTORESIZE_TAGS_TABLE = new BooleanProperty("propertiesdialog.autoresizeTagsTable", false);
259
260 /**
261 * Show tags and relation memberships of objects in the properties dialog when hovering over them with the mouse pointer
262 * @since 18574
263 */
264 public static final BooleanProperty PROP_PREVIEW_ON_HOVER = new BooleanProperty("propertiesdialog.preview-on-hover", true);
265 private final HoverPreviewPropListener hoverPreviewPropListener = new HoverPreviewPropListener();
266
267 /**
268 * Always show information for selected objects when something is selected instead of the hovered object
269 * @since 18574
270 */
271 public static final CachingProperty<Boolean> PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION =
272 new BooleanProperty("propertiesdialog.preview-on-hover.always-show-selected", true).cached();
273 private final HoverPreviewPreferSelectionPropListener hoverPreviewPrioritizeSelectionPropListener =
274 new HoverPreviewPreferSelectionPropListener();
275
276 /**
277 * Create a new PropertiesDialog
278 */
279 public PropertiesDialog() {
280 super(tr("Tags/Memberships"), "propertiesdialog", tr("Tags for selected objects."),
281 Shortcut.registerShortcut("subwindow:properties", tr("Windows: {0}", tr("Tags/Memberships")), KeyEvent.VK_P,
282 Shortcut.ALT_SHIFT), 150, true);
283
284 setupTagsMenu();
285 buildTagsTable();
286
287 setupMembershipMenu();
288 buildMembershipTable();
289
290 tagTableFilter = setupFilter();
291
292 // combine both tables and wrap them in a scrollPane
293 boolean top = Config.getPref().getBoolean("properties.presets.top", true);
294 boolean presetsVisible = Config.getPref().getBoolean("properties.presets.visible", true);
295 if (presetsVisible && top) {
296 bothTables.add(presets, GBC.std().fill(GridBagConstraints.HORIZONTAL).insets(5, 2, 5, 2).anchor(GridBagConstraints.NORTHWEST));
297 double epsilon = Double.MIN_VALUE; // need to set a weight or else anchor value is ignored
298 bothTables.add(pluginHook, GBC.eol().insets(0, 1, 1, 1).anchor(GridBagConstraints.NORTHEAST).weight(epsilon, epsilon));
299 }
300 bothTables.add(selectSth, GBC.eol().fill().insets(10, 10, 10, 10));
301 bothTables.add(tagTableFilter, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
302 bothTables.add(tagTable.getTableHeader(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
303 bothTables.add(tagTable, GBC.eol().fill(GridBagConstraints.BOTH));
304 bothTables.add(membershipTable.getTableHeader(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
305 bothTables.add(membershipTable, GBC.eol().fill(GridBagConstraints.BOTH));
306 if (presetsVisible && !top) {
307 bothTables.add(presets, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(5, 2, 5, 2));
308 }
309
310 setupBlankSpaceMenu();
311 setupKeyboardShortcuts();
312
313 // Let the actions know when selection in the tables change
314 tagTable.getSelectionModel().addListSelectionListener(editAction);
315 membershipTable.getSelectionModel().addListSelectionListener(editAction);
316 tagTable.getSelectionModel().addListSelectionListener(deleteAction);
317 membershipTable.getSelectionModel().addListSelectionListener(deleteAction);
318
319 JScrollPane scrollPane = (JScrollPane) createLayout(bothTables, true,
320 Arrays.asList(this.btnAdd, this.btnEdit, this.btnDel));
321
322 MouseClickWatch mouseClickWatch = new MouseClickWatch();
323 tagTable.addMouseListener(mouseClickWatch);
324 membershipTable.addMouseListener(mouseClickWatch);
325 scrollPane.addMouseListener(mouseClickWatch);
326
327 selectSth.setPreferredSize(scrollPane.getSize());
328 presets.setSize(scrollPane.getSize());
329
330 editHelper.loadTagsIfNeeded();
331
332 TaggingPresets.addListener(this);
333
334 PROP_PREVIEW_ON_HOVER.addListener(hoverPreviewPropListener);
335 PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.addListener(hoverPreviewPrioritizeSelectionPropListener);
336 }
337
338 @Override
339 public String helpTopic() {
340 return HelpUtil.ht("/Dialog/TagsMembership");
341 }
342
343 private void buildTagsTable() {
344 // setting up the tags table
345 TableHelper.setFont(tagTable, getClass());
346 tagData.setColumnIdentifiers(new String[]{tr("Key"), tr("Value")});
347 tagTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
348 tagTable.getTableHeader().setReorderingAllowed(false);
349
350 tagTable.getColumnModel().getColumn(0).setCellRenderer(cellRenderer);
351 tagTable.getColumnModel().getColumn(1).setCellRenderer(cellRenderer);
352 tagTable.setRowSorter(tagRowSorter);
353
354 final RemoveHiddenSelection removeHiddenSelection = new RemoveHiddenSelection();
355 tagTable.getSelectionModel().addListSelectionListener(removeHiddenSelection);
356 tagRowSorter.addRowSorterListener(removeHiddenSelection);
357 tagRowSorter.setComparator(0, AlphanumComparator.getInstance());
358 tagRowSorter.setComparator(1, (o1, o2) -> {
359 if (o1 instanceof Map && o2 instanceof Map) {
360 final String v1 = ((Map) o1).size() == 1 ? (String) ((Map) o1).keySet().iterator().next() : KeyedItem.DIFFERENT_I18N;
361 final String v2 = ((Map) o2).size() == 1 ? (String) ((Map) o2).keySet().iterator().next() : KeyedItem.DIFFERENT_I18N;
362 return AlphanumComparator.getInstance().compare(v1, v2);
363 } else {
364 return AlphanumComparator.getInstance().compare(String.valueOf(o1), String.valueOf(o2));
365 }
366 });
367 }
368
369 private void buildMembershipTable() {
370 TableHelper.setFont(membershipTable, getClass());
371 membershipData.setColumnIdentifiers(new String[]{tr("Member Of"), tr("Role"), tr("Position")});
372 membershipTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
373
374 TableColumnModel mod = membershipTable.getColumnModel();
375 membershipTable.getTableHeader().setReorderingAllowed(false);
376 mod.getColumn(0).setCellRenderer(new MemberOfCellRenderer());
377 mod.getColumn(1).setCellRenderer(new RoleCellRenderer());
378 mod.getColumn(2).setCellRenderer(new PositionCellRenderer());
379 mod.getColumn(2).setPreferredWidth(20);
380 mod.getColumn(1).setPreferredWidth(40);
381 mod.getColumn(0).setPreferredWidth(200);
382 }
383
384 /**
385 * Creates the popup menu @field blankSpaceMenu and its launcher on main panel.
386 */
387 private void setupBlankSpaceMenu() {
388 if (Config.getPref().getBoolean("properties.menu.add_edit_delete", true)) {
389 blankSpaceMenuHandler.addAction(addAction);
390 PopupMenuLauncher launcher = new BlankSpaceMenuLauncher(blankSpaceMenu);
391 bothTables.addMouseListener(launcher);
392 tagTable.addMouseListener(launcher);
393 }
394 }
395
396 private void destroyTaginfoNationalActions() {
397 membershipMenuTagInfoNatItems.forEach(membershipMenu::remove);
398 membershipMenuTagInfoNatItems.clear();
399 tagMenuTagInfoNatItems.forEach(tagMenu::remove);
400 tagMenuTagInfoNatItems.clear();
401 taginfoNationalActions.clear();
402 }
403
404 private void setupTaginfoNationalActions(Collection<? extends IPrimitive> newSel) {
405 if (newSel.isEmpty()) {
406 return;
407 }
408 final LatLon center = newSel.iterator().next().getBBox().getCenter();
409 List<TaginfoRegionalInstance> regionalInstances = Territories.getRegionalTaginfoUrls(center);
410 int newHashCode = regionalInstances.hashCode();
411 if (newHashCode == taginfoNationalHash) {
412 // taginfoNationalActions are still valid
413 return;
414 }
415 taginfoNationalHash = newHashCode;
416 destroyTaginfoNationalActions();
417 regionalInstances.stream()
418 .map(taginfo -> taginfoAction.withTaginfoUrl(tr("Go to Taginfo ({0})", taginfo.toString()), taginfo.getUrl()))
419 .forEach(taginfoNationalActions::add);
420 taginfoNationalActions.stream().map(membershipMenu::add).forEach(membershipMenuTagInfoNatItems::add);
421 taginfoNationalActions.stream().map(tagMenu::add).forEach(tagMenuTagInfoNatItems::add);
422 }
423
424 /**
425 * Creates the popup menu @field membershipMenu and its launcher on membership table.
426 */
427 private void setupMembershipMenu() {
428 // setting up the membership table
429 if (Config.getPref().getBoolean("properties.menu.add_edit_delete", true)) {
430 membershipMenuHandler.addAction(editAction);
431 membershipMenuHandler.addAction(deleteAction);
432 membershipMenu.addSeparator();
433 }
434 RelationPopupMenus.setupHandler(membershipMenuHandler,
435 EditRelationAction.class, DuplicateRelationAction.class, DeleteRelationsAction.class);
436 membershipMenu.addSeparator();
437 membershipMenu.add(helpRelAction);
438 membershipMenu.add(taginfoAction);
439
440 membershipMenu.addPopupMenuListener(new AbstractTag2LinkPopupListener() {
441 @Override
442 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
443 getSelectedMembershipRelations().forEach(relation ->
444 relation.visitKeys((primitive, key, value) -> addLinks(membershipMenu, key, value)));
445 }
446 });
447
448 popupMenuLauncher = new PopupMenuLauncher(membershipMenu) {
449 @Override
450 protected int checkTableSelection(JTable table, Point p) {
451 int row = super.checkTableSelection(table, p);
452 List<IRelation<?>> rels = Arrays.stream(table.getSelectedRows())
453 .mapToObj(i -> (IRelation<?>) table.getValueAt(i, 0))
454 .collect(Collectors.toList());
455 membershipMenuHandler.setPrimitives(rels);
456 return row;
457 }
458
459 @Override
460 public void mouseClicked(MouseEvent e) {
461 //update highlights
462 if (MainApplication.isDisplayingMapView()) {
463 int row = membershipTable.rowAtPoint(e.getPoint());
464 if (row >= 0 && highlightHelper.highlightOnly((Relation) membershipTable.getValueAt(row, 0))) {
465 MainApplication.getMap().mapView.repaint();
466 }
467 }
468 super.mouseClicked(e);
469 }
470
471 @Override
472 public void mouseExited(MouseEvent me) {
473 highlightHelper.clear();
474 }
475 };
476 membershipTable.addMouseListener(popupMenuLauncher);
477 }
478
479 /**
480 * Creates the popup menu @field tagMenu and its launcher on tag table.
481 */
482 private void setupTagsMenu() {
483 if (Config.getPref().getBoolean("properties.menu.add_edit_delete", true)) {
484 tagMenu.add(addAction);
485 tagMenu.add(editAction);
486 tagMenu.add(deleteAction);
487 tagMenu.addSeparator();
488 }
489 tagMenu.add(pasteValueAction);
490 tagMenu.add(copyValueAction);
491 tagMenu.add(copyKeyValueAction);
492 tagMenu.addPopupMenuListener(copyKeyValueAction);
493 tagMenu.add(copyAllKeyValueAction);
494 tagMenu.addSeparator();
495 tagMenu.add(searchActionAny);
496 tagMenu.add(searchActionSame);
497 tagMenu.addSeparator();
498 tagMenu.add(helpTagAction);
499 tagMenu.add(tagHistoryAction);
500 tagMenu.add(taginfoAction);
501 tagMenu.addPopupMenuListener(new AbstractTag2LinkPopupListener() {
502 @Override
503 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
504 visitSelectedProperties((primitive, key, value) -> addLinks(tagMenu, key, value));
505 }
506 });
507
508 tagTable.addMouseListener(new PopupMenuLauncher(tagMenu));
509 }
510
511 /**
512 * Sets a filter to restrict the displayed properties.
513 * @param filter the filter
514 * @since 8980
515 */
516 public void setFilter(final SearchCompiler.Match filter) {
517 this.tagRowSorter.setRowFilter(new SearchBasedRowFilter(filter));
518 }
519
520 /**
521 * Assigns all needed keys like Enter and Spacebar to most important actions.
522 */
523 private void setupKeyboardShortcuts() {
524
525 // ENTER = editAction, open "edit" dialog
526 InputMapUtils.addEnterActionWhenAncestor(tagTable, editAction);
527 InputMapUtils.addEnterActionWhenAncestor(membershipTable, editAction);
528
529 // INSERT button = addAction, open "add tag" dialog
530 tagTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
531 .put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "onTableInsert");
532 tagTable.getActionMap().put("onTableInsert", addAction);
533
534 // unassign some standard shortcuts for JTable to allow upload / download / image browsing
535 InputMapUtils.unassignCtrlShiftUpDown(tagTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
536 InputMapUtils.unassignPageUpDown(tagTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
537
538 // unassign some standard shortcuts for correct copy-pasting, fix #8508
539 tagTable.setTransferHandler(null);
540
541 tagTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
542 .put(Shortcut.getCopyKeyStroke(), "onCopy");
543 tagTable.getActionMap().put("onCopy", copyKeyValueAction);
544
545 // allow using enter to add tags for all look&feel configurations
546 InputMapUtils.enableEnter(this.btnAdd);
547
548 // DEL button = deleteAction
549 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
550 KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"
551 );
552 getActionMap().put("delete", deleteAction);
553
554 // F1 button = custom help action
555 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
556 HelpAction.getKeyStroke(), "onHelp");
557 getActionMap().put("onHelp", new AbstractAction() {
558 @Override
559 public void actionPerformed(ActionEvent e) {
560 if (membershipTable.getSelectedRowCount() == 1) {
561 helpRelAction.actionPerformed(e);
562 } else {
563 helpTagAction.actionPerformed(e);
564 }
565 }
566 });
567 }
568
569 private JosmTextField setupFilter() {
570 final JosmTextField f = new DisableShortcutsOnFocusGainedTextField();
571 FilterField.setSearchIcon(f);
572 f.setToolTipText(tr("Tag filter"));
573 final CompileSearchTextDecorator decorator = CompileSearchTextDecorator.decorate(f);
574 f.addPropertyChangeListener("filter", evt -> setFilter(decorator.getMatch()));
575 return f;
576 }
577
578 /**
579 * This simply fires up an {@link RelationEditor} for the relation shown; everything else
580 * is the editor's business.
581 *
582 * @param row position
583 */
584 private void editMembership(int row) {
585 Relation relation = (Relation) membershipData.getValueAt(row, 0);
586 MainApplication.getMap().relationListDialog.selectRelation(relation);
587 OsmDataLayer layer = MainApplication.getLayerManager().getActiveDataLayer();
588 if (!layer.isLocked()) {
589 List<RelationMember> members = ((MemberInfo) membershipData.getValueAt(row, 1)).role.stream()
590 .filter(RelationMember.class::isInstance)
591 .map(RelationMember.class::cast)
592 .collect(Collectors.toList());
593 RelationEditor.getEditor(layer, relation, members).setVisible(true);
594 }
595 }
596
597 private static int findViewRow(JTable table, TableModel model, Object value) {
598 for (int i = 0; i < model.getRowCount(); i++) {
599 if (model.getValueAt(i, 0).equals(value))
600 return table.convertRowIndexToView(i);
601 }
602 return -1;
603 }
604
605 /**
606 * Update selection status, call {@link #selectionChanged} function.
607 */
608 private void updateSelection() {
609 // Parameter is ignored in this class
610 selectionChanged(null);
611 }
612
613 @Override
614 public void showNotify() {
615 DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IN_EDT_CONSOLIDATED);
616 SelectionEventManager.getInstance().addSelectionListenerForEdt(this);
617 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
618 if (Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get()))
619 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
620 for (JosmAction action : josmActions) {
621 MainApplication.registerActionShortcut(action);
622 }
623 updateSelection();
624 }
625
626 @Override
627 public void hideNotify() {
628 DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
629 SelectionEventManager.getInstance().removeSelectionListener(this);
630 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
631 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
632 for (JosmAction action : josmActions) {
633 MainApplication.unregisterActionShortcut(action);
634 }
635 }
636
637 @Override
638 public void setVisible(boolean b) {
639 super.setVisible(b);
640 if (b && MainApplication.getLayerManager().getActiveData() != null) {
641 updateSelection();
642 }
643 }
644
645 @Override
646 public void destroy() {
647 membershipMenuHandler.setPrimitives(Collections.emptyList());
648 destroyTaginfoNationalActions();
649 membershipTable.removeMouseListener(popupMenuLauncher);
650 super.destroy();
651 TaggingPresets.removeListener(this);
652 PROP_PREVIEW_ON_HOVER.removeListener(hoverPreviewPropListener);
653 PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.removeListener(hoverPreviewPrioritizeSelectionPropListener);
654 Container parent = pluginHook.getParent();
655 if (parent != null) {
656 parent.remove(pluginHook);
657 }
658 }
659
660 @Override
661 public void selectionChanged(SelectionChangeEvent event) {
662 if (!isVisible())
663 return;
664 if (tagTable == null)
665 return; // selection changed may be received in base class constructor before init
666 if (tagTable.getCellEditor() != null) {
667 tagTable.getCellEditor().cancelCellEditing();
668 }
669
670 // Ignore parameter as we do not want to operate always on real selection here, especially in draw mode
671 Collection<? extends IPrimitive> newSel = OsmDataManager.getInstance().getInProgressISelection();
672
673 // Temporarily disable listening to primitive mouse hover events while we have a selection as that takes priority
674 if (Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get())) {
675 if (newSel.isEmpty()) {
676 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
677 } else if (Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get())) {
678 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
679 }
680 }
681
682 updateUi(newSel);
683 }
684
685 @Override
686 public void primitiveHovered(PrimitiveHoverEvent e) {
687 Collection<? extends IPrimitive> selection = OsmDataManager.getInstance().getInProgressISelection();
688 if (Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get()) && !selection.isEmpty())
689 return;
690
691 if (e.getHoveredPrimitive() != null) {
692 updateUi(e.getHoveredPrimitive());
693 } else {
694 updateUi(selection);
695 }
696 }
697
698 private void autoresizeTagTable() {
699 if (Boolean.TRUE.equals(PROP_AUTORESIZE_TAGS_TABLE.get())) {
700 // resize table's columns to fit content
701 TableHelper.computeColumnsWidth(tagTable);
702 }
703 }
704
705 private void updateUi(IPrimitive primitive) {
706 updateUi(primitive == null ? Collections.emptyList() :
707 Collections.singletonList(primitive));
708 }
709
710 private void updateUi(Collection<? extends IPrimitive> primitives) {
711 IRelation<?> selectedRelation = null;
712 String selectedTag = editHelper.getChangedKey(); // select last added or last edited key by default
713 if (selectedTag == null && tagTable.getSelectedRowCount() == 1) {
714 selectedTag = editHelper.getDataKey(tagTable.getSelectedRow());
715 }
716 if (membershipTable.getSelectedRowCount() == 1) {
717 selectedRelation = (IRelation<?>) membershipData.getValueAt(membershipTable.getSelectedRow(), 0);
718 }
719
720 updateTagTableData(primitives);
721 updateMembershipTableData(primitives);
722
723 updateMembershipTableVisibility();
724 updateActionsEnabledState();
725 updateTagTableVisibility(primitives);
726
727 setupTaginfoNationalActions(primitives);
728 autoresizeTagTable();
729
730 int selectedIndex;
731 if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
732 tagTable.changeSelection(selectedIndex, 0, false, false);
733 } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
734 membershipTable.changeSelection(selectedIndex, 0, false, false);
735 } else if (tagData.getRowCount() > 0) {
736 tagTable.changeSelection(0, 0, false, false);
737 } else if (membershipData.getRowCount() > 0) {
738 membershipTable.changeSelection(0, 0, false, false);
739 }
740
741 updateTitle(primitives);
742 }
743
744 private void updateTagTableData(Collection<? extends IPrimitive> primitives) {
745 int newSelSize = primitives.size();
746
747 // re-load tag data
748 tagData.setRowCount(0);
749
750 final boolean displayDiscardableKeys = PROP_DISPLAY_DISCARDABLE_KEYS.get();
751 final Map<String, Integer> keyCount = new HashMap<>();
752 final Map<String, String> tags = new HashMap<>();
753 valueCount.clear();
754 Set<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
755 for (IPrimitive osm : primitives) {
756 types.add(TaggingPresetType.forPrimitive(osm));
757 osm.visitKeys((p, key, value) -> {
758 if (displayDiscardableKeys || !AbstractPrimitive.getDiscardableKeys().contains(key)) {
759 keyCount.put(key, keyCount.containsKey(key) ? keyCount.get(key) + 1 : 1);
760 if (valueCount.containsKey(key)) {
761 Map<String, Integer> v = valueCount.get(key);
762 v.put(value, v.containsKey(value) ? v.get(value) + 1 : 1);
763 } else {
764 Map<String, Integer> v = new TreeMap<>();
765 v.put(value, 1);
766 valueCount.put(key, v);
767 }
768 }
769 });
770 }
771 for (Entry<String, Map<String, Integer>> e : valueCount.entrySet()) {
772 int count = e.getValue().values().stream().mapToInt(i -> i).sum();
773 if (count < newSelSize) {
774 e.getValue().put("", newSelSize - count);
775 }
776 tagData.addRow(new Object[]{e.getKey(), e.getValue()});
777 tags.put(e.getKey(), e.getValue().size() == 1
778 ? e.getValue().keySet().iterator().next() : KeyedItem.DIFFERENT_I18N);
779 }
780
781 presets.updatePresets(types, tags, presetHandler);
782 }
783
784 private void updateMembershipTableData(Collection<? extends IPrimitive> primitives) {
785 membershipData.setRowCount(0);
786
787 Map<IRelation<?>, MemberInfo> roles = new HashMap<>();
788 for (IPrimitive primitive : primitives) {
789 for (IPrimitive ref : primitive.getReferrers(true)) {
790 if (ref instanceof IRelation && !ref.isIncomplete() && !ref.isDeleted()) {
791 IRelation<?> r = (IRelation<?>) ref;
792 MemberInfo mi = roles.computeIfAbsent(r, ignore -> new MemberInfo(primitives));
793 int i = 1;
794 for (IRelationMember<?> m : r.getMembers()) {
795 if (m.getMember() == primitive) {
796 mi.add(m, i);
797 }
798 ++i;
799 }
800 }
801 }
802 }
803
804 List<IRelation<?>> sortedRelations = new ArrayList<>(roles.keySet());
805 sortedRelations.sort((o1, o2) -> {
806 int comp = Boolean.compare(o1.isDisabledAndHidden(), o2.isDisabledAndHidden());
807 return comp != 0 ? comp : DefaultNameFormatter.getInstance().getRelationComparator().compare(o1, o2);
808 });
809
810 for (IRelation<?> r: sortedRelations) {
811 membershipData.addRow(new Object[]{r, roles.get(r)});
812 }
813 }
814
815 private void updateMembershipTableVisibility() {
816 membershipTable.getTableHeader().setVisible(membershipData.getRowCount() > 0);
817 membershipTable.setVisible(membershipData.getRowCount() > 0);
818 }
819
820 private void updateTagTableVisibility(Collection<? extends IPrimitive> primitives) {
821 boolean hasSelection = !primitives.isEmpty();
822 boolean hasTags = hasSelection && tagData.getRowCount() > 0;
823
824 tagTable.setVisible(hasTags);
825 tagTable.getTableHeader().setVisible(hasTags);
826 tagTableFilter.setVisible(hasTags);
827 selectSth.setVisible(!hasSelection);
828 pluginHook.setVisible(hasSelection);
829 }
830
831 private void updateActionsEnabledState() {
832 addAction.updateEnabledState();
833 editAction.updateEnabledState();
834 deleteAction.updateEnabledState();
835 }
836
837 private void updateTitle(Collection<? extends IPrimitive> primitives) {
838 int newSelSize = primitives.size();
839 if (tagData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
840 if (newSelSize > 1) {
841 setTitle(tr("Objects: {2} / Tags: {0} / Memberships: {1}",
842 tagData.getRowCount(), membershipData.getRowCount(), newSelSize));
843 } else {
844 setTitle(tr("Tags: {0} / Memberships: {1}",
845 tagData.getRowCount(), membershipData.getRowCount()));
846 }
847 } else {
848 setTitle(tr("Tags/Memberships"));
849 }
850 }
851
852 /* ---------------------------------------------------------------------------------- */
853 /* PreferenceChangedListener */
854 /* ---------------------------------------------------------------------------------- */
855
856 /**
857 * Reloads data when the {@code display.discardable-keys} preference changes
858 */
859 @Override
860 public void preferenceChanged(PreferenceChangeEvent e) {
861 super.preferenceChanged(e);
862 if (PROP_DISPLAY_DISCARDABLE_KEYS.getKey().equals(e.getKey()) && MainApplication.getLayerManager().getActiveData() != null) {
863 updateSelection();
864 }
865 }
866
867 /* ---------------------------------------------------------------------------------- */
868 /* TaggingPresetListener */
869 /* ---------------------------------------------------------------------------------- */
870
871 /**
872 * Updates the preset list when Presets preference changes.
873 */
874 @Override
875 public void taggingPresetsModified() {
876 if (MainApplication.getLayerManager().getActiveData() != null) {
877 updateSelection();
878 }
879 }
880
881 /* ---------------------------------------------------------------------------------- */
882 /* ActiveLayerChangeListener */
883 /* ---------------------------------------------------------------------------------- */
884 @Override
885 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
886 if (e.getSource().getEditLayer() == null) {
887 editHelper.saveTagsIfNeeded();
888 editHelper.resetSelection();
889 }
890 // it is time to save history of tags
891 updateSelection();
892
893 // Listen for active layer visibility change to enable/disable hover preview
894 // Remove previous listener first (order matters if we are somehow getting a layer change event
895 // switching from one layer to the same layer)
896 Layer prevLayer = e.getPreviousDataLayer();
897 if (prevLayer != null) {
898 prevLayer.removePropertyChangeListener(this);
899 }
900
901 Layer newLayer = e.getSource().getActiveDataLayer();
902 if (newLayer != null) {
903 newLayer.addPropertyChangeListener(this);
904 if (newLayer.isVisible() && Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get())) {
905 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
906 } else {
907 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
908 }
909 }
910 }
911
912 @Override
913 public void propertyChange(PropertyChangeEvent e) {
914 if (Layer.VISIBLE_PROP.equals(e.getPropertyName())) {
915 boolean isVisible = (boolean) e.getNewValue();
916
917 // Disable hover preview when primitives are invisible
918 if (isVisible && Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get())) {
919 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
920 } else {
921 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
922 }
923 }
924 }
925
926 @Override
927 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
928 updateSelection();
929 }
930
931 /**
932 * Replies the tag popup menu handler.
933 * @return The tag popup menu handler
934 */
935 public PopupMenuHandler getPropertyPopupMenuHandler() {
936 return tagMenuHandler;
937 }
938
939 /**
940 * Returns the selected tag. Value is empty if several tags are selected for a given key.
941 * @return The current selected tag
942 */
943 public Tag getSelectedProperty() {
944 Tags tags = getSelectedProperties();
945 return tags == null ? null : new Tag(
946 tags.getKey(),
947 tags.getValues().size() > 1 ? "" : tags.getValues().iterator().next());
948 }
949
950 /**
951 * Returns the selected tags. Contains all values if several are selected for a given key.
952 * @return The current selected tags
953 * @since 15376
954 */
955 public Tags getSelectedProperties() {
956 int row = tagTable.getSelectedRow();
957 if (row == -1) return null;
958 Map<String, Integer> map = editHelper.getDataValues(row);
959 return new Tags(editHelper.getDataKey(row), map.keySet());
960 }
961
962 /**
963 * Visits all combinations of the selected keys/values.
964 * @param visitor the visitor
965 * @since 15707
966 */
967 public void visitSelectedProperties(KeyValueVisitor visitor) {
968 for (int row : tagTable.getSelectedRows()) {
969 final String key = editHelper.getDataKey(row);
970 Set<String> values = editHelper.getDataValues(row).keySet();
971 values.forEach(value -> visitor.visitKeyValue(null, key, value));
972 }
973 }
974
975 /**
976 * Replies the membership popup menu handler.
977 * @return The membership popup menu handler
978 */
979 public PopupMenuHandler getMembershipPopupMenuHandler() {
980 return membershipMenuHandler;
981 }
982
983 /**
984 * Returns the selected relation membership.
985 * @return The current selected relation membership
986 */
987 public IRelation<?> getSelectedMembershipRelation() {
988 int row = membershipTable.getSelectedRow();
989 return row > -1 ? (IRelation<?>) membershipData.getValueAt(row, 0) : null;
990 }
991
992 /**
993 * Returns all selected relation memberships.
994 * @return The selected relation memberships
995 * @since 15707
996 */
997 public Collection<IRelation<?>> getSelectedMembershipRelations() {
998 return Arrays.stream(membershipTable.getSelectedRows())
999 .mapToObj(row -> (IRelation<?>) membershipData.getValueAt(row, 0))
1000 .collect(Collectors.toList());
1001 }
1002
1003 /**
1004 * Adds a custom table cell renderer to render cells of the tags table.
1005 *
1006 * If the renderer is not capable performing a {@link TableCellRenderer#getTableCellRendererComponent},
1007 * it should return {@code null} to fall back to the
1008 * {@link PropertiesCellRenderer#getTableCellRendererComponent default implementation}.
1009 * @param renderer the renderer to add
1010 * @since 9149
1011 */
1012 public void addCustomPropertiesCellRenderer(TableCellRenderer renderer) {
1013 cellRenderer.addCustomRenderer(renderer);
1014 }
1015
1016 /**
1017 * Removes a custom table cell renderer.
1018 * @param renderer the renderer to remove
1019 * @since 9149
1020 */
1021 public void removeCustomPropertiesCellRenderer(TableCellRenderer renderer) {
1022 cellRenderer.removeCustomRenderer(renderer);
1023 }
1024
1025 static final class MemberOfCellRenderer extends DefaultTableCellRenderer {
1026 @Override
1027 public Component getTableCellRendererComponent(JTable table, Object value,
1028 boolean isSelected, boolean hasFocus, int row, int column) {
1029 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
1030 if (value == null)
1031 return this;
1032 if (c instanceof JLabel) {
1033 JLabel label = (JLabel) c;
1034 IRelation<?> r = (IRelation<?>) value;
1035 label.setText(r.getDisplayName(DefaultNameFormatter.getInstance()));
1036 if (r.isDisabledAndHidden()) {
1037 label.setFont(label.getFont().deriveFont(Font.ITALIC));
1038 }
1039 }
1040 return c;
1041 }
1042 }
1043
1044 static final class RoleCellRenderer extends DefaultTableCellRenderer {
1045 @Override
1046 public Component getTableCellRendererComponent(JTable table, Object value,
1047 boolean isSelected, boolean hasFocus, int row, int column) {
1048 if (value == null)
1049 return this;
1050 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
1051 boolean isDisabledAndHidden = ((IRelation<?>) table.getValueAt(row, 0)).isDisabledAndHidden();
1052 if (c instanceof JLabel) {
1053 JLabel label = (JLabel) c;
1054 label.setText(((MemberInfo) value).getRoleString());
1055 if (isDisabledAndHidden) {
1056 label.setFont(label.getFont().deriveFont(Font.ITALIC));
1057 }
1058 }
1059 return c;
1060 }
1061 }
1062
1063 static final class PositionCellRenderer extends DefaultTableCellRenderer {
1064 @Override
1065 public Component getTableCellRendererComponent(JTable table, Object value,
1066 boolean isSelected, boolean hasFocus, int row, int column) {
1067 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
1068 IRelation<?> relation = (IRelation<?>) table.getValueAt(row, 0);
1069 boolean isDisabledAndHidden = relation != null && relation.isDisabledAndHidden();
1070 if (c instanceof JLabel) {
1071 JLabel label = (JLabel) c;
1072 MemberInfo member = (MemberInfo) table.getValueAt(row, 1);
1073 if (member != null) {
1074 label.setText(member.getPositionString());
1075 }
1076 if (isDisabledAndHidden) {
1077 label.setFont(label.getFont().deriveFont(Font.ITALIC));
1078 }
1079 }
1080 return c;
1081 }
1082 }
1083
1084 static final class BlankSpaceMenuLauncher extends PopupMenuLauncher {
1085 BlankSpaceMenuLauncher(JPopupMenu menu) {
1086 super(menu);
1087 }
1088
1089 @Override
1090 protected boolean checkSelection(Component component, Point p) {
1091 if (component instanceof JTable) {
1092 return ((JTable) component).rowAtPoint(p) == -1;
1093 }
1094 return true;
1095 }
1096 }
1097
1098 static final class TaggingPresetCommandHandler implements TaggingPresetHandler {
1099 @Override
1100 public void updateTags(List<Tag> tags) {
1101 Command command = TaggingPreset.createCommand(getSelection(), tags);
1102 if (command != null) {
1103 UndoRedoHandler.getInstance().add(command);
1104 }
1105 }
1106
1107 @Override
1108 public Collection<OsmPrimitive> getSelection() {
1109 return OsmDataManager.getInstance().getInProgressSelection();
1110 }
1111 }
1112
1113 /**
1114 * Class that watches for mouse clicks
1115 * @author imi
1116 */
1117 public class MouseClickWatch extends MouseAdapter {
1118 @Override
1119 public void mouseClicked(MouseEvent e) {
1120 if (e.getClickCount() < 2) {
1121 // single click, clear selection in other table not clicked in
1122 if (e.getSource() == tagTable) {
1123 membershipTable.clearSelection();
1124 } else if (e.getSource() == membershipTable) {
1125 tagTable.clearSelection();
1126 }
1127 } else if (e.getSource() == tagTable) {
1128 // double click, edit or add tag
1129 int row = tagTable.rowAtPoint(e.getPoint());
1130 if (row > -1) {
1131 boolean focusOnKey = tagTable.columnAtPoint(e.getPoint()) == 0;
1132 editHelper.editTag(row, focusOnKey);
1133 } else {
1134 editHelper.addTag();
1135 btnAdd.requestFocusInWindow();
1136 }
1137 } else if (e.getSource() == membershipTable) {
1138 int row = membershipTable.rowAtPoint(e.getPoint());
1139 int col = membershipTable.columnAtPoint(e.getPoint());
1140 if (row > -1 && col == 1) {
1141 final Relation relation = (Relation) membershipData.getValueAt(row, 0);
1142 final MemberInfo memberInfo = (MemberInfo) membershipData.getValueAt(row, 1);
1143 RelationRoleEditor.editRole(relation, memberInfo);
1144 } else if (row > -1) {
1145 editMembership(row);
1146 }
1147 } else {
1148 editHelper.addTag();
1149 btnAdd.requestFocusInWindow();
1150 }
1151 }
1152
1153 @Override
1154 public void mousePressed(MouseEvent e) {
1155 if (e.getSource() == tagTable) {
1156 membershipTable.clearSelection();
1157 } else if (e.getSource() == membershipTable) {
1158 tagTable.clearSelection();
1159 }
1160 }
1161 }
1162
1163 static class MemberInfo {
1164 private final List<IRelationMember<?>> role = new ArrayList<>();
1165 private Set<IPrimitive> members = new HashSet<>();
1166 private List<Integer> position = new ArrayList<>();
1167 private Collection<? extends IPrimitive> selection;
1168 private String positionString;
1169 private String roleString;
1170
1171 MemberInfo(Collection<? extends IPrimitive> selection) {
1172 this.selection = selection;
1173 }
1174
1175 void add(IRelationMember<?> r, Integer p) {
1176 role.add(r);
1177 members.add(r.getMember());
1178 position.add(p);
1179 }
1180
1181 String getPositionString() {
1182 if (positionString == null) {
1183 positionString = Utils.getPositionListString(position);
1184 // if not all objects from the selection are member of this relation
1185 if (selection.stream().anyMatch(p -> !members.contains(p))) {
1186 positionString += ",\u2717";
1187 }
1188 members = null;
1189 position = null;
1190 selection = null;
1191 }
1192 return Utils.shortenString(positionString, 20);
1193 }
1194
1195 List<IRelationMember<?>> getRole() {
1196 return Collections.unmodifiableList(role);
1197 }
1198
1199 String getRoleString() {
1200 if (roleString == null) {
1201 for (IRelationMember<?> r : role) {
1202 if (roleString == null) {
1203 roleString = r.getRole();
1204 } else if (!roleString.equals(r.getRole())) {
1205 roleString = KeyedItem.DIFFERENT_I18N;
1206 break;
1207 }
1208 }
1209 }
1210 return roleString;
1211 }
1212
1213 @Override
1214 public String toString() {
1215 return String.format("MemberInfo{roles='%s', positions='%s'}", roleString, positionString);
1216 }
1217 }
1218
1219 /**
1220 * Class that allows fast creation of read-only table model with String columns
1221 */
1222 public static class ReadOnlyTableModel extends DefaultTableModel {
1223 @Override
1224 public boolean isCellEditable(int row, int column) {
1225 return false;
1226 }
1227
1228 @Override
1229 public Class<?> getColumnClass(int columnIndex) {
1230 return String.class;
1231 }
1232 }
1233
1234 /**
1235 * Action handling delete button press in properties dialog.
1236 */
1237 class DeleteAction extends JosmAction implements ListSelectionListener {
1238
1239 private static final String DELETE_FROM_RELATION_PREF = "delete_from_relation";
1240
1241 DeleteAction() {
1242 super(tr("Delete"), /* ICON() */ "dialogs/delete", tr("Delete the selected key in all objects"),
1243 Shortcut.registerShortcut("properties:delete", tr("Delete Tags"), KeyEvent.VK_D,
1244 Shortcut.ALT_CTRL_SHIFT), false);
1245 updateEnabledState();
1246 }
1247
1248 protected void deleteTags(int... rows) {
1249 // convert list of rows to HashMap (and find gap for nextKey)
1250 Map<String, String> tags = new HashMap<>(Utils.hashMapInitialCapacity(rows.length));
1251 int nextKeyIndex = rows[0];
1252 for (int row : rows) {
1253 String key = editHelper.getDataKey(row);
1254 if (row == nextKeyIndex + 1) {
1255 nextKeyIndex = row; // no gap yet
1256 }
1257 tags.put(key, null);
1258 }
1259
1260 // find key to select after deleting other tags
1261 String nextKey = null;
1262 int rowCount = tagData.getRowCount();
1263 if (rowCount > rows.length) {
1264 if (nextKeyIndex == rows[rows.length-1]) {
1265 // no gap found, pick next or previous key in list
1266 nextKeyIndex = nextKeyIndex + 1 < rowCount ? nextKeyIndex + 1 : rows[0] - 1;
1267 } else {
1268 // gap found
1269 nextKeyIndex++;
1270 }
1271 // We use unfiltered indexes here. So don't use getDataKey()
1272 nextKey = (String) tagData.getValueAt(nextKeyIndex, 0);
1273 }
1274
1275 Collection<OsmPrimitive> sel = OsmDataManager.getInstance().getInProgressSelection();
1276 UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, tags));
1277
1278 membershipTable.clearSelection();
1279 if (nextKey != null) {
1280 tagTable.changeSelection(findViewRow(tagTable, tagData, nextKey), 0, false, false);
1281 }
1282 }
1283
1284 protected void deleteFromRelation(int row) {
1285 Relation cur = (Relation) membershipData.getValueAt(row, 0);
1286
1287 Relation nextRelation = null;
1288 int rowCount = membershipTable.getRowCount();
1289 if (rowCount > 1) {
1290 nextRelation = (Relation) membershipData.getValueAt(row + 1 < rowCount ? row + 1 : row - 1, 0);
1291 }
1292
1293 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
1294 tr("Change relation"),
1295 tr("Delete from relation"), tr("Cancel"));
1296 ed.setButtonIcons("dialogs/delete", "cancel");
1297 ed.setContent(tr("Really delete selection from relation {0}?", cur.getDisplayName(DefaultNameFormatter.getInstance())));
1298 ed.toggleEnable(DELETE_FROM_RELATION_PREF);
1299
1300 if (ed.showDialog().getValue() != 1)
1301 return;
1302
1303 List<RelationMember> members = cur.getMembers();
1304 for (OsmPrimitive primitive: OsmDataManager.getInstance().getInProgressSelection()) {
1305 members.removeIf(rm -> rm.getMember() == primitive);
1306 }
1307 UndoRedoHandler.getInstance().add(new ChangeMembersCommand(cur, members));
1308
1309 tagTable.clearSelection();
1310 if (nextRelation != null) {
1311 membershipTable.changeSelection(findViewRow(membershipTable, membershipData, nextRelation), 0, false, false);
1312 }
1313 }
1314
1315 @Override
1316 public void actionPerformed(ActionEvent e) {
1317 if (tagTable.getSelectedRowCount() > 0) {
1318 int[] rows = tagTable.getSelectedRows();
1319 deleteTags(rows);
1320 } else if (membershipTable.getSelectedRowCount() > 0) {
1321 ConditionalOptionPaneUtil.startBulkOperation(DELETE_FROM_RELATION_PREF);
1322 int[] rows = membershipTable.getSelectedRows();
1323 // delete from last relation to conserve row numbers in the table
1324 for (int i = rows.length-1; i >= 0; i--) {
1325 deleteFromRelation(rows[i]);
1326 }
1327 ConditionalOptionPaneUtil.endBulkOperation(DELETE_FROM_RELATION_PREF);
1328 }
1329 }
1330
1331 @Override
1332 protected final void updateEnabledState() {
1333 DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1334 setEnabled(ds != null && !ds.isLocked() &&
1335 ((tagTable != null && tagTable.getSelectedRowCount() >= 1)
1336 || (membershipTable != null && membershipTable.getSelectedRowCount() > 0)
1337 ));
1338 }
1339
1340 @Override
1341 public void valueChanged(ListSelectionEvent e) {
1342 updateEnabledState();
1343 }
1344 }
1345
1346 /**
1347 * Action handling add button press in properties dialog.
1348 */
1349 class AddAction extends JosmAction {
1350 AtomicBoolean isPerforming = new AtomicBoolean(false);
1351 AddAction() {
1352 super(tr("Add"), /* ICON() */ "dialogs/add", tr("Add a new key/value pair to all objects"),
1353 Shortcut.registerShortcut("properties:add", tr("Add Tag"), KeyEvent.VK_A,
1354 Shortcut.ALT), false);
1355 }
1356
1357 @Override
1358 public void actionPerformed(ActionEvent e) {
1359 if (!/*successful*/isPerforming.compareAndSet(false, true)) {
1360 return;
1361 }
1362 try {
1363 editHelper.addTag();
1364 btnAdd.requestFocusInWindow();
1365 } finally {
1366 isPerforming.set(false);
1367 }
1368 }
1369
1370 @Override
1371 protected final void updateEnabledState() {
1372 DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1373 setEnabled(ds != null && !ds.isLocked() &&
1374 !Utils.isEmpty(OsmDataManager.getInstance().getInProgressSelection()));
1375 }
1376 }
1377
1378 /**
1379 * Action handling edit button press in properties dialog.
1380 */
1381 class EditAction extends JosmAction implements ListSelectionListener {
1382 AtomicBoolean isPerforming = new AtomicBoolean(false);
1383 EditAction() {
1384 super(tr("Edit"), /* ICON() */ "dialogs/edit", tr("Edit the value of the selected key for all objects"),
1385 Shortcut.registerShortcut("properties:edit", tr("Edit: {0}", tr("Edit Tags")), KeyEvent.VK_S,
1386 Shortcut.ALT), false);
1387 updateEnabledState();
1388 }
1389
1390 @Override
1391 public void actionPerformed(ActionEvent e) {
1392 if (!/*successful*/isPerforming.compareAndSet(false, true)) {
1393 return;
1394 }
1395 try {
1396 if (tagTable.getSelectedRowCount() == 1) {
1397 int row = tagTable.getSelectedRow();
1398 editHelper.editTag(row, false);
1399 } else if (membershipTable.getSelectedRowCount() == 1) {
1400 int row = membershipTable.getSelectedRow();
1401 editMembership(row);
1402 }
1403 } finally {
1404 isPerforming.set(false);
1405 }
1406 }
1407
1408 @Override
1409 protected void updateEnabledState() {
1410 DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1411 setEnabled(ds != null && !ds.isLocked() &&
1412 ((tagTable != null && tagTable.getSelectedRowCount() == 1)
1413 ^ (membershipTable != null && membershipTable.getSelectedRowCount() == 1)
1414 ));
1415 }
1416
1417 @Override
1418 public void valueChanged(ListSelectionEvent e) {
1419 updateEnabledState();
1420 }
1421 }
1422
1423 class PasteValueAction extends AbstractAction {
1424 PasteValueAction() {
1425 putValue(NAME, tr("Paste Value"));
1426 putValue(SHORT_DESCRIPTION, tr("Paste the value of the selected tag from clipboard"));
1427 new ImageProvider("paste").getResource().attachImageIcon(this, true);
1428 }
1429
1430 @Override
1431 public void actionPerformed(ActionEvent ae) {
1432 if (tagTable.getSelectedRowCount() != 1)
1433 return;
1434 String key = editHelper.getDataKey(tagTable.getSelectedRow());
1435 Collection<OsmPrimitive> sel = OsmDataManager.getInstance().getInProgressSelection();
1436 String clipboard = ClipboardUtils.getClipboardStringContent();
1437 if (sel.isEmpty() || clipboard == null || sel.iterator().next().getDataSet().isLocked())
1438 return;
1439 UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, key, Utils.strip(clipboard)));
1440 }
1441 }
1442
1443 class SearchAction extends AbstractAction {
1444 private final boolean sameType;
1445
1446 SearchAction(boolean sameType) {
1447 this.sameType = sameType;
1448 if (sameType) {
1449 putValue(NAME, tr("Search Key/Value/Type"));
1450 putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag, restrict to type (i.e., node/way/relation)"));
1451 new ImageProvider("dialogs/search").getResource().attachImageIcon(this, true);
1452 } else {
1453 putValue(NAME, tr("Search Key/Value"));
1454 putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag"));
1455 new ImageProvider("dialogs/search").getResource().attachImageIcon(this, true);
1456 }
1457 }
1458
1459 @Override
1460 public void actionPerformed(ActionEvent e) {
1461 if (tagTable.getSelectedRowCount() != 1)
1462 return;
1463 String key = editHelper.getDataKey(tagTable.getSelectedRow());
1464 Collection<? extends IPrimitive> sel = OsmDataManager.getInstance().getInProgressISelection();
1465 if (sel.isEmpty())
1466 return;
1467 final SearchSetting ss = createSearchSetting(key, sel, sameType);
1468 searchStateless(ss);
1469 }
1470 }
1471
1472 static SearchSetting createSearchSetting(String key, Collection<? extends IPrimitive> sel, boolean sameType) {
1473 String sep = "";
1474 StringBuilder s = new StringBuilder();
1475 Set<String> consideredTokens = new TreeSet<>();
1476 for (IPrimitive p : sel) {
1477 String val = p.get(key);
1478 if (val == null || (!sameType && consideredTokens.contains(val))) {
1479 continue;
1480 }
1481 String t = "";
1482 if (!sameType) {
1483 t = "";
1484 } else if (p instanceof Node) {
1485 t = "type:node ";
1486 } else if (p instanceof Way) {
1487 t = "type:way ";
1488 } else if (p instanceof Relation) {
1489 t = "type:relation ";
1490 }
1491 String token = t + val;
1492 if (consideredTokens.add(token)) {
1493 s.append(sep).append('(').append(t).append(SearchCompiler.buildSearchStringForTag(key, val)).append(')');
1494 sep = " OR ";
1495 }
1496 }
1497
1498 final SearchSetting ss = new SearchSetting();
1499 ss.text = s.toString();
1500 ss.caseSensitive = true;
1501 return ss;
1502 }
1503
1504 /**
1505 * Clears the row selection when it is filtered away by the row sorter.
1506 */
1507 private final class RemoveHiddenSelection implements ListSelectionListener, RowSorterListener {
1508
1509 void removeHiddenSelection() {
1510 try {
1511 tagRowSorter.convertRowIndexToModel(tagTable.getSelectedRow());
1512 } catch (IndexOutOfBoundsException e) {
1513 Logging.trace(e);
1514 Logging.trace("Clearing tagTable selection");
1515 tagTable.clearSelection();
1516 }
1517 }
1518
1519 @Override
1520 public void valueChanged(ListSelectionEvent event) {
1521 removeHiddenSelection();
1522 }
1523
1524 @Override
1525 public void sorterChanged(RowSorterEvent e) {
1526 removeHiddenSelection();
1527 }
1528 }
1529
1530 private final class HoverPreviewPropListener implements ValueChangeListener<Boolean> {
1531 @Override
1532 public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
1533 if (Boolean.TRUE.equals(e.getProperty().get()) && isDialogShowing()) {
1534 MainApplication.getMap().mapView.addPrimitiveHoverListener(PropertiesDialog.this);
1535 } else if (Boolean.FALSE.equals(e.getProperty().get())) {
1536 MainApplication.getMap().mapView.removePrimitiveHoverListener(PropertiesDialog.this);
1537 }
1538 }
1539 }
1540
1541 /*
1542 * Ensure HoverListener is re-added when selection priority is disabled while something is selected.
1543 * Otherwise user would need to change selection to see the preference change take effect.
1544 */
1545 private final class HoverPreviewPreferSelectionPropListener implements ValueChangeListener<Boolean> {
1546 @Override
1547 public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
1548 if (Boolean.FALSE.equals(e.getProperty().get()) &&
1549 Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get()) &&
1550 isDialogShowing()) {
1551 MainApplication.getMap().mapView.addPrimitiveHoverListener(PropertiesDialog.this);
1552 }
1553 }
1554 }
1555}
Note: See TracBrowser for help on using the repository browser.