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

Last change on this file was 19413, checked in by stoecker, 11 months ago

see #24342 - add properties.filter.visible hidden setting

  • Property svn:eol-style set to native
File size: 65.2 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 boolean filterVisible = Config.getPref().getBoolean("properties.filter.visible", true);
827 tagTableFilter.setVisible(hasTags && filterVisible);
828 selectSth.setVisible(!hasSelection);
829 pluginHook.setVisible(hasSelection);
830 }
831
832 private void updateActionsEnabledState() {
833 addAction.updateEnabledState();
834 editAction.updateEnabledState();
835 deleteAction.updateEnabledState();
836 }
837
838 private void updateTitle(Collection<? extends IPrimitive> primitives) {
839 int newSelSize = primitives.size();
840 if (tagData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
841 if (newSelSize > 1) {
842 setTitle(tr("Objects: {2} / Tags: {0} / Memberships: {1}",
843 tagData.getRowCount(), membershipData.getRowCount(), newSelSize));
844 } else {
845 setTitle(tr("Tags: {0} / Memberships: {1}",
846 tagData.getRowCount(), membershipData.getRowCount()));
847 }
848 } else {
849 setTitle(tr("Tags/Memberships"));
850 }
851 }
852
853 /* ---------------------------------------------------------------------------------- */
854 /* PreferenceChangedListener */
855 /* ---------------------------------------------------------------------------------- */
856
857 /**
858 * Reloads data when the {@code display.discardable-keys} preference changes
859 */
860 @Override
861 public void preferenceChanged(PreferenceChangeEvent e) {
862 super.preferenceChanged(e);
863 if (PROP_DISPLAY_DISCARDABLE_KEYS.getKey().equals(e.getKey()) && MainApplication.getLayerManager().getActiveData() != null) {
864 updateSelection();
865 }
866 }
867
868 /* ---------------------------------------------------------------------------------- */
869 /* TaggingPresetListener */
870 /* ---------------------------------------------------------------------------------- */
871
872 /**
873 * Updates the preset list when Presets preference changes.
874 */
875 @Override
876 public void taggingPresetsModified() {
877 if (MainApplication.getLayerManager().getActiveData() != null) {
878 updateSelection();
879 }
880 }
881
882 /* ---------------------------------------------------------------------------------- */
883 /* ActiveLayerChangeListener */
884 /* ---------------------------------------------------------------------------------- */
885 @Override
886 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
887 if (e.getSource().getEditLayer() == null) {
888 editHelper.saveTagsIfNeeded();
889 editHelper.resetSelection();
890 }
891 // it is time to save history of tags
892 updateSelection();
893
894 // Listen for active layer visibility change to enable/disable hover preview
895 // Remove previous listener first (order matters if we are somehow getting a layer change event
896 // switching from one layer to the same layer)
897 Layer prevLayer = e.getPreviousDataLayer();
898 if (prevLayer != null) {
899 prevLayer.removePropertyChangeListener(this);
900 }
901
902 Layer newLayer = e.getSource().getActiveDataLayer();
903 if (newLayer != null) {
904 newLayer.addPropertyChangeListener(this);
905 if (newLayer.isVisible() && Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get())) {
906 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
907 } else {
908 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
909 }
910 }
911 }
912
913 @Override
914 public void propertyChange(PropertyChangeEvent e) {
915 if (Layer.VISIBLE_PROP.equals(e.getPropertyName())) {
916 boolean isVisible = (boolean) e.getNewValue();
917
918 // Disable hover preview when primitives are invisible
919 if (isVisible && Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get())) {
920 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
921 } else {
922 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
923 }
924 }
925 }
926
927 @Override
928 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
929 updateSelection();
930 }
931
932 /**
933 * Replies the tag popup menu handler.
934 * @return The tag popup menu handler
935 */
936 public PopupMenuHandler getPropertyPopupMenuHandler() {
937 return tagMenuHandler;
938 }
939
940 /**
941 * Returns the selected tag. Value is empty if several tags are selected for a given key.
942 * @return The current selected tag
943 */
944 public Tag getSelectedProperty() {
945 Tags tags = getSelectedProperties();
946 return tags == null ? null : new Tag(
947 tags.getKey(),
948 tags.getValues().size() > 1 ? "" : tags.getValues().iterator().next());
949 }
950
951 /**
952 * Returns the selected tags. Contains all values if several are selected for a given key.
953 * @return The current selected tags
954 * @since 15376
955 */
956 public Tags getSelectedProperties() {
957 int row = tagTable.getSelectedRow();
958 if (row == -1) return null;
959 Map<String, Integer> map = editHelper.getDataValues(row);
960 return new Tags(editHelper.getDataKey(row), map.keySet());
961 }
962
963 /**
964 * Visits all combinations of the selected keys/values.
965 * @param visitor the visitor
966 * @since 15707
967 */
968 public void visitSelectedProperties(KeyValueVisitor visitor) {
969 for (int row : tagTable.getSelectedRows()) {
970 final String key = editHelper.getDataKey(row);
971 Set<String> values = editHelper.getDataValues(row).keySet();
972 values.forEach(value -> visitor.visitKeyValue(null, key, value));
973 }
974 }
975
976 /**
977 * Replies the membership popup menu handler.
978 * @return The membership popup menu handler
979 */
980 public PopupMenuHandler getMembershipPopupMenuHandler() {
981 return membershipMenuHandler;
982 }
983
984 /**
985 * Returns the selected relation membership.
986 * @return The current selected relation membership
987 */
988 public IRelation<?> getSelectedMembershipRelation() {
989 int row = membershipTable.getSelectedRow();
990 return row > -1 ? (IRelation<?>) membershipData.getValueAt(row, 0) : null;
991 }
992
993 /**
994 * Returns all selected relation memberships.
995 * @return The selected relation memberships
996 * @since 15707
997 */
998 public Collection<IRelation<?>> getSelectedMembershipRelations() {
999 return Arrays.stream(membershipTable.getSelectedRows())
1000 .mapToObj(row -> (IRelation<?>) membershipData.getValueAt(row, 0))
1001 .collect(Collectors.toList());
1002 }
1003
1004 /**
1005 * Adds a custom table cell renderer to render cells of the tags table.
1006 *
1007 * If the renderer is not capable performing a {@link TableCellRenderer#getTableCellRendererComponent},
1008 * it should return {@code null} to fall back to the
1009 * {@link PropertiesCellRenderer#getTableCellRendererComponent default implementation}.
1010 * @param renderer the renderer to add
1011 * @since 9149
1012 */
1013 public void addCustomPropertiesCellRenderer(TableCellRenderer renderer) {
1014 cellRenderer.addCustomRenderer(renderer);
1015 }
1016
1017 /**
1018 * Removes a custom table cell renderer.
1019 * @param renderer the renderer to remove
1020 * @since 9149
1021 */
1022 public void removeCustomPropertiesCellRenderer(TableCellRenderer renderer) {
1023 cellRenderer.removeCustomRenderer(renderer);
1024 }
1025
1026 static final class MemberOfCellRenderer extends DefaultTableCellRenderer {
1027 @Override
1028 public Component getTableCellRendererComponent(JTable table, Object value,
1029 boolean isSelected, boolean hasFocus, int row, int column) {
1030 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
1031 if (value == null)
1032 return this;
1033 if (c instanceof JLabel) {
1034 JLabel label = (JLabel) c;
1035 IRelation<?> r = (IRelation<?>) value;
1036 label.setText(r.getDisplayName(DefaultNameFormatter.getInstance()));
1037 if (r.isDisabledAndHidden()) {
1038 label.setFont(label.getFont().deriveFont(Font.ITALIC));
1039 }
1040 }
1041 return c;
1042 }
1043 }
1044
1045 static final class RoleCellRenderer extends DefaultTableCellRenderer {
1046 @Override
1047 public Component getTableCellRendererComponent(JTable table, Object value,
1048 boolean isSelected, boolean hasFocus, int row, int column) {
1049 if (value == null)
1050 return this;
1051 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
1052 boolean isDisabledAndHidden = ((IRelation<?>) table.getValueAt(row, 0)).isDisabledAndHidden();
1053 if (c instanceof JLabel) {
1054 JLabel label = (JLabel) c;
1055 label.setText(((MemberInfo) value).getRoleString());
1056 if (isDisabledAndHidden) {
1057 label.setFont(label.getFont().deriveFont(Font.ITALIC));
1058 }
1059 }
1060 return c;
1061 }
1062 }
1063
1064 static final class PositionCellRenderer extends DefaultTableCellRenderer {
1065 @Override
1066 public Component getTableCellRendererComponent(JTable table, Object value,
1067 boolean isSelected, boolean hasFocus, int row, int column) {
1068 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
1069 IRelation<?> relation = (IRelation<?>) table.getValueAt(row, 0);
1070 boolean isDisabledAndHidden = relation != null && relation.isDisabledAndHidden();
1071 if (c instanceof JLabel) {
1072 JLabel label = (JLabel) c;
1073 MemberInfo member = (MemberInfo) table.getValueAt(row, 1);
1074 if (member != null) {
1075 label.setText(member.getPositionString());
1076 }
1077 if (isDisabledAndHidden) {
1078 label.setFont(label.getFont().deriveFont(Font.ITALIC));
1079 }
1080 }
1081 return c;
1082 }
1083 }
1084
1085 static final class BlankSpaceMenuLauncher extends PopupMenuLauncher {
1086 BlankSpaceMenuLauncher(JPopupMenu menu) {
1087 super(menu);
1088 }
1089
1090 @Override
1091 protected boolean checkSelection(Component component, Point p) {
1092 if (component instanceof JTable) {
1093 return ((JTable) component).rowAtPoint(p) == -1;
1094 }
1095 return true;
1096 }
1097 }
1098
1099 static final class TaggingPresetCommandHandler implements TaggingPresetHandler {
1100 @Override
1101 public void updateTags(List<Tag> tags) {
1102 Command command = TaggingPreset.createCommand(getSelection(), tags);
1103 if (command != null) {
1104 UndoRedoHandler.getInstance().add(command);
1105 }
1106 }
1107
1108 @Override
1109 public Collection<OsmPrimitive> getSelection() {
1110 return OsmDataManager.getInstance().getInProgressSelection();
1111 }
1112 }
1113
1114 /**
1115 * Class that watches for mouse clicks
1116 * @author imi
1117 */
1118 public class MouseClickWatch extends MouseAdapter {
1119 @Override
1120 public void mouseClicked(MouseEvent e) {
1121 if (e.getClickCount() < 2) {
1122 // single click, clear selection in other table not clicked in
1123 if (e.getSource() == tagTable) {
1124 membershipTable.clearSelection();
1125 } else if (e.getSource() == membershipTable) {
1126 tagTable.clearSelection();
1127 }
1128 } else if (e.getSource() == tagTable) {
1129 // double click, edit or add tag
1130 int row = tagTable.rowAtPoint(e.getPoint());
1131 if (row > -1) {
1132 boolean focusOnKey = tagTable.columnAtPoint(e.getPoint()) == 0;
1133 editHelper.editTag(row, focusOnKey);
1134 } else {
1135 editHelper.addTag();
1136 btnAdd.requestFocusInWindow();
1137 }
1138 } else if (e.getSource() == membershipTable) {
1139 int row = membershipTable.rowAtPoint(e.getPoint());
1140 int col = membershipTable.columnAtPoint(e.getPoint());
1141 if (row > -1 && col == 1) {
1142 final Relation relation = (Relation) membershipData.getValueAt(row, 0);
1143 final MemberInfo memberInfo = (MemberInfo) membershipData.getValueAt(row, 1);
1144 RelationRoleEditor.editRole(relation, memberInfo);
1145 } else if (row > -1) {
1146 editMembership(row);
1147 }
1148 } else {
1149 editHelper.addTag();
1150 btnAdd.requestFocusInWindow();
1151 }
1152 }
1153
1154 @Override
1155 public void mousePressed(MouseEvent e) {
1156 if (e.getSource() == tagTable) {
1157 membershipTable.clearSelection();
1158 } else if (e.getSource() == membershipTable) {
1159 tagTable.clearSelection();
1160 }
1161 }
1162 }
1163
1164 static class MemberInfo {
1165 private final List<IRelationMember<?>> role = new ArrayList<>();
1166 private Set<IPrimitive> members = new HashSet<>();
1167 private List<Integer> position = new ArrayList<>();
1168 private Collection<? extends IPrimitive> selection;
1169 private String positionString;
1170 private String roleString;
1171
1172 MemberInfo(Collection<? extends IPrimitive> selection) {
1173 this.selection = selection;
1174 }
1175
1176 void add(IRelationMember<?> r, Integer p) {
1177 role.add(r);
1178 members.add(r.getMember());
1179 position.add(p);
1180 }
1181
1182 String getPositionString() {
1183 if (positionString == null) {
1184 positionString = Utils.getPositionListString(position);
1185 // if not all objects from the selection are member of this relation
1186 if (selection.stream().anyMatch(p -> !members.contains(p))) {
1187 positionString += ",\u2717";
1188 }
1189 members = null;
1190 position = null;
1191 selection = null;
1192 }
1193 return Utils.shortenString(positionString, 20);
1194 }
1195
1196 List<IRelationMember<?>> getRole() {
1197 return Collections.unmodifiableList(role);
1198 }
1199
1200 String getRoleString() {
1201 if (roleString == null) {
1202 for (IRelationMember<?> r : role) {
1203 if (roleString == null) {
1204 roleString = r.getRole();
1205 } else if (!roleString.equals(r.getRole())) {
1206 roleString = KeyedItem.DIFFERENT_I18N;
1207 break;
1208 }
1209 }
1210 }
1211 return roleString;
1212 }
1213
1214 @Override
1215 public String toString() {
1216 return String.format("MemberInfo{roles='%s', positions='%s'}", roleString, positionString);
1217 }
1218 }
1219
1220 /**
1221 * Class that allows fast creation of read-only table model with String columns
1222 */
1223 public static class ReadOnlyTableModel extends DefaultTableModel {
1224 @Override
1225 public boolean isCellEditable(int row, int column) {
1226 return false;
1227 }
1228
1229 @Override
1230 public Class<?> getColumnClass(int columnIndex) {
1231 return String.class;
1232 }
1233 }
1234
1235 /**
1236 * Action handling delete button press in properties dialog.
1237 */
1238 class DeleteAction extends JosmAction implements ListSelectionListener {
1239
1240 private static final String DELETE_FROM_RELATION_PREF = "delete_from_relation";
1241
1242 DeleteAction() {
1243 super(tr("Delete"), /* ICON() */ "dialogs/delete", tr("Delete the selected key in all objects"),
1244 Shortcut.registerShortcut("properties:delete", tr("Delete Tags"), KeyEvent.VK_D,
1245 Shortcut.ALT_CTRL_SHIFT), false);
1246 updateEnabledState();
1247 }
1248
1249 protected void deleteTags(int... rows) {
1250 // convert list of rows to HashMap (and find gap for nextKey)
1251 Map<String, String> tags = new HashMap<>(Utils.hashMapInitialCapacity(rows.length));
1252 int nextKeyIndex = rows[0];
1253 for (int row : rows) {
1254 String key = editHelper.getDataKey(row);
1255 if (row == nextKeyIndex + 1) {
1256 nextKeyIndex = row; // no gap yet
1257 }
1258 tags.put(key, null);
1259 }
1260
1261 // find key to select after deleting other tags
1262 String nextKey = null;
1263 int rowCount = tagData.getRowCount();
1264 if (rowCount > rows.length) {
1265 if (nextKeyIndex == rows[rows.length-1]) {
1266 // no gap found, pick next or previous key in list
1267 nextKeyIndex = nextKeyIndex + 1 < rowCount ? nextKeyIndex + 1 : rows[0] - 1;
1268 } else {
1269 // gap found
1270 nextKeyIndex++;
1271 }
1272 // We use unfiltered indexes here. So don't use getDataKey()
1273 nextKey = (String) tagData.getValueAt(nextKeyIndex, 0);
1274 }
1275
1276 Collection<OsmPrimitive> sel = OsmDataManager.getInstance().getInProgressSelection();
1277 UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, tags));
1278
1279 membershipTable.clearSelection();
1280 if (nextKey != null) {
1281 tagTable.changeSelection(findViewRow(tagTable, tagData, nextKey), 0, false, false);
1282 }
1283 }
1284
1285 protected void deleteFromRelation(int row) {
1286 Relation cur = (Relation) membershipData.getValueAt(row, 0);
1287
1288 Relation nextRelation = null;
1289 int rowCount = membershipTable.getRowCount();
1290 if (rowCount > 1) {
1291 nextRelation = (Relation) membershipData.getValueAt(row + 1 < rowCount ? row + 1 : row - 1, 0);
1292 }
1293
1294 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
1295 tr("Change relation"),
1296 tr("Delete from relation"), tr("Cancel"));
1297 ed.setButtonIcons("dialogs/delete", "cancel");
1298 ed.setContent(tr("Really delete selection from relation {0}?", cur.getDisplayName(DefaultNameFormatter.getInstance())));
1299 ed.toggleEnable(DELETE_FROM_RELATION_PREF);
1300
1301 if (ed.showDialog().getValue() != 1)
1302 return;
1303
1304 List<RelationMember> members = cur.getMembers();
1305 for (OsmPrimitive primitive: OsmDataManager.getInstance().getInProgressSelection()) {
1306 members.removeIf(rm -> rm.getMember() == primitive);
1307 }
1308 UndoRedoHandler.getInstance().add(new ChangeMembersCommand(cur, members));
1309
1310 tagTable.clearSelection();
1311 if (nextRelation != null) {
1312 membershipTable.changeSelection(findViewRow(membershipTable, membershipData, nextRelation), 0, false, false);
1313 }
1314 }
1315
1316 @Override
1317 public void actionPerformed(ActionEvent e) {
1318 if (tagTable.getSelectedRowCount() > 0) {
1319 int[] rows = tagTable.getSelectedRows();
1320 deleteTags(rows);
1321 } else if (membershipTable.getSelectedRowCount() > 0) {
1322 ConditionalOptionPaneUtil.startBulkOperation(DELETE_FROM_RELATION_PREF);
1323 int[] rows = membershipTable.getSelectedRows();
1324 // delete from last relation to conserve row numbers in the table
1325 for (int i = rows.length-1; i >= 0; i--) {
1326 deleteFromRelation(rows[i]);
1327 }
1328 ConditionalOptionPaneUtil.endBulkOperation(DELETE_FROM_RELATION_PREF);
1329 }
1330 }
1331
1332 @Override
1333 protected final void updateEnabledState() {
1334 DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1335 setEnabled(ds != null && !ds.isLocked() &&
1336 ((tagTable != null && tagTable.getSelectedRowCount() >= 1)
1337 || (membershipTable != null && membershipTable.getSelectedRowCount() > 0)
1338 ));
1339 }
1340
1341 @Override
1342 public void valueChanged(ListSelectionEvent e) {
1343 updateEnabledState();
1344 }
1345 }
1346
1347 /**
1348 * Action handling add button press in properties dialog.
1349 */
1350 class AddAction extends JosmAction {
1351 AtomicBoolean isPerforming = new AtomicBoolean(false);
1352 AddAction() {
1353 super(tr("Add"), /* ICON() */ "dialogs/add", tr("Add a new key/value pair to all objects"),
1354 Shortcut.registerShortcut("properties:add", tr("Add Tag"), KeyEvent.VK_A,
1355 Shortcut.ALT), false);
1356 }
1357
1358 @Override
1359 public void actionPerformed(ActionEvent e) {
1360 if (!/*successful*/isPerforming.compareAndSet(false, true)) {
1361 return;
1362 }
1363 try {
1364 editHelper.addTag();
1365 btnAdd.requestFocusInWindow();
1366 } finally {
1367 isPerforming.set(false);
1368 }
1369 }
1370
1371 @Override
1372 protected final void updateEnabledState() {
1373 DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1374 setEnabled(ds != null && !ds.isLocked() &&
1375 !Utils.isEmpty(OsmDataManager.getInstance().getInProgressSelection()));
1376 }
1377 }
1378
1379 /**
1380 * Action handling edit button press in properties dialog.
1381 */
1382 class EditAction extends JosmAction implements ListSelectionListener {
1383 AtomicBoolean isPerforming = new AtomicBoolean(false);
1384 EditAction() {
1385 super(tr("Edit"), /* ICON() */ "dialogs/edit", tr("Edit the value of the selected key for all objects"),
1386 Shortcut.registerShortcut("properties:edit", tr("Edit: {0}", tr("Edit Tags")), KeyEvent.VK_S,
1387 Shortcut.ALT), false);
1388 updateEnabledState();
1389 }
1390
1391 @Override
1392 public void actionPerformed(ActionEvent e) {
1393 if (!/*successful*/isPerforming.compareAndSet(false, true)) {
1394 return;
1395 }
1396 try {
1397 if (tagTable.getSelectedRowCount() == 1) {
1398 int row = tagTable.getSelectedRow();
1399 editHelper.editTag(row, false);
1400 } else if (membershipTable.getSelectedRowCount() == 1) {
1401 int row = membershipTable.getSelectedRow();
1402 editMembership(row);
1403 }
1404 } finally {
1405 isPerforming.set(false);
1406 }
1407 }
1408
1409 @Override
1410 protected void updateEnabledState() {
1411 DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1412 setEnabled(ds != null && !ds.isLocked() &&
1413 ((tagTable != null && tagTable.getSelectedRowCount() == 1)
1414 ^ (membershipTable != null && membershipTable.getSelectedRowCount() == 1)
1415 ));
1416 }
1417
1418 @Override
1419 public void valueChanged(ListSelectionEvent e) {
1420 updateEnabledState();
1421 }
1422 }
1423
1424 class PasteValueAction extends AbstractAction {
1425 PasteValueAction() {
1426 putValue(NAME, tr("Paste Value"));
1427 putValue(SHORT_DESCRIPTION, tr("Paste the value of the selected tag from clipboard"));
1428 new ImageProvider("paste").getResource().attachImageIcon(this, true);
1429 }
1430
1431 @Override
1432 public void actionPerformed(ActionEvent ae) {
1433 if (tagTable.getSelectedRowCount() != 1)
1434 return;
1435 String key = editHelper.getDataKey(tagTable.getSelectedRow());
1436 Collection<OsmPrimitive> sel = OsmDataManager.getInstance().getInProgressSelection();
1437 String clipboard = ClipboardUtils.getClipboardStringContent();
1438 if (sel.isEmpty() || clipboard == null || sel.iterator().next().getDataSet().isLocked())
1439 return;
1440 UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, key, Utils.strip(clipboard)));
1441 }
1442 }
1443
1444 class SearchAction extends AbstractAction {
1445 private final boolean sameType;
1446
1447 SearchAction(boolean sameType) {
1448 this.sameType = sameType;
1449 if (sameType) {
1450 putValue(NAME, tr("Search Key/Value/Type"));
1451 putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag, restrict to type (i.e., node/way/relation)"));
1452 new ImageProvider("dialogs/search").getResource().attachImageIcon(this, true);
1453 } else {
1454 putValue(NAME, tr("Search Key/Value"));
1455 putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag"));
1456 new ImageProvider("dialogs/search").getResource().attachImageIcon(this, true);
1457 }
1458 }
1459
1460 @Override
1461 public void actionPerformed(ActionEvent e) {
1462 if (tagTable.getSelectedRowCount() != 1)
1463 return;
1464 String key = editHelper.getDataKey(tagTable.getSelectedRow());
1465 Collection<? extends IPrimitive> sel = OsmDataManager.getInstance().getInProgressISelection();
1466 if (sel.isEmpty())
1467 return;
1468 final SearchSetting ss = createSearchSetting(key, sel, sameType);
1469 searchStateless(ss);
1470 }
1471 }
1472
1473 static SearchSetting createSearchSetting(String key, Collection<? extends IPrimitive> sel, boolean sameType) {
1474 String sep = "";
1475 StringBuilder s = new StringBuilder();
1476 Set<String> consideredTokens = new TreeSet<>();
1477 for (IPrimitive p : sel) {
1478 String val = p.get(key);
1479 if (val == null || (!sameType && consideredTokens.contains(val))) {
1480 continue;
1481 }
1482 String t = "";
1483 if (!sameType) {
1484 t = "";
1485 } else if (p instanceof Node) {
1486 t = "type:node ";
1487 } else if (p instanceof Way) {
1488 t = "type:way ";
1489 } else if (p instanceof Relation) {
1490 t = "type:relation ";
1491 }
1492 String token = t + val;
1493 if (consideredTokens.add(token)) {
1494 s.append(sep).append('(').append(t).append(SearchCompiler.buildSearchStringForTag(key, val)).append(')');
1495 sep = " OR ";
1496 }
1497 }
1498
1499 final SearchSetting ss = new SearchSetting();
1500 ss.text = s.toString();
1501 ss.caseSensitive = true;
1502 return ss;
1503 }
1504
1505 /**
1506 * Clears the row selection when it is filtered away by the row sorter.
1507 */
1508 private final class RemoveHiddenSelection implements ListSelectionListener, RowSorterListener {
1509
1510 void removeHiddenSelection() {
1511 try {
1512 tagRowSorter.convertRowIndexToModel(tagTable.getSelectedRow());
1513 } catch (IndexOutOfBoundsException e) {
1514 Logging.trace(e);
1515 Logging.trace("Clearing tagTable selection");
1516 tagTable.clearSelection();
1517 }
1518 }
1519
1520 @Override
1521 public void valueChanged(ListSelectionEvent event) {
1522 removeHiddenSelection();
1523 }
1524
1525 @Override
1526 public void sorterChanged(RowSorterEvent e) {
1527 removeHiddenSelection();
1528 }
1529 }
1530
1531 private final class HoverPreviewPropListener implements ValueChangeListener<Boolean> {
1532 @Override
1533 public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
1534 if (Boolean.TRUE.equals(e.getProperty().get()) && isDialogShowing()) {
1535 MainApplication.getMap().mapView.addPrimitiveHoverListener(PropertiesDialog.this);
1536 } else if (Boolean.FALSE.equals(e.getProperty().get())) {
1537 MainApplication.getMap().mapView.removePrimitiveHoverListener(PropertiesDialog.this);
1538 }
1539 }
1540 }
1541
1542 /*
1543 * Ensure HoverListener is re-added when selection priority is disabled while something is selected.
1544 * Otherwise user would need to change selection to see the preference change take effect.
1545 */
1546 private final class HoverPreviewPreferSelectionPropListener implements ValueChangeListener<Boolean> {
1547 @Override
1548 public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
1549 if (Boolean.FALSE.equals(e.getProperty().get()) &&
1550 Boolean.TRUE.equals(PROP_PREVIEW_ON_HOVER.get()) &&
1551 isDialogShowing()) {
1552 MainApplication.getMap().mapView.addPrimitiveHoverListener(PropertiesDialog.this);
1553 }
1554 }
1555 }
1556}
Note: See TracBrowser for help on using the repository browser.