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

Last change on this file since 17007 was 17007, checked in by simon04, 4 years ago

fix #19753 - NPE in CopyAllKeyValueAction due to shortcut re-assignment

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