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

Last change on this file since 17032 was 17032, checked in by Klumbumbus, 4 years ago

Add missing icons in Tags/Membership Dialog context menu, fix icons of HelpAction and SelectInRelationListAction

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