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

Last change on this file since 13434 was 13434, checked in by Don-vip, 6 years ago

see #8039, see #10456 - support read-only data layers

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