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

Last change on this file since 18801 was 18801, checked in by taylor.smock, 10 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

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