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, 15 months 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.