source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java

Last change on this file was 19050, checked in by taylor.smock, 2 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 44.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.relation;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.FlowLayout;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.Window;
14import java.awt.datatransfer.Clipboard;
15import java.awt.datatransfer.FlavorListener;
16import java.awt.event.ActionEvent;
17import java.awt.event.FocusAdapter;
18import java.awt.event.FocusEvent;
19import java.awt.event.InputEvent;
20import java.awt.event.KeyEvent;
21import java.awt.event.MouseAdapter;
22import java.awt.event.MouseEvent;
23import java.awt.event.WindowAdapter;
24import java.awt.event.WindowEvent;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.EnumSet;
30import java.util.List;
31import java.util.Set;
32import java.util.stream.Collectors;
33
34import javax.swing.AbstractAction;
35import javax.swing.Action;
36import javax.swing.BorderFactory;
37import javax.swing.InputMap;
38import javax.swing.JButton;
39import javax.swing.JComponent;
40import javax.swing.JLabel;
41import javax.swing.JMenuItem;
42import javax.swing.JOptionPane;
43import javax.swing.JPanel;
44import javax.swing.JRootPane;
45import javax.swing.JScrollPane;
46import javax.swing.JSplitPane;
47import javax.swing.JTabbedPane;
48import javax.swing.JTable;
49import javax.swing.JToolBar;
50import javax.swing.KeyStroke;
51import javax.swing.SwingConstants;
52import javax.swing.event.TableModelListener;
53
54import org.openstreetmap.josm.actions.JosmAction;
55import org.openstreetmap.josm.command.ChangeMembersCommand;
56import org.openstreetmap.josm.command.Command;
57import org.openstreetmap.josm.data.UndoRedoHandler;
58import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueListener;
59import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
60import org.openstreetmap.josm.data.osm.OsmPrimitive;
61import org.openstreetmap.josm.data.osm.Relation;
62import org.openstreetmap.josm.data.osm.RelationMember;
63import org.openstreetmap.josm.data.osm.Tag;
64import org.openstreetmap.josm.data.validation.tests.RelationChecker;
65import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
66import org.openstreetmap.josm.gui.MainApplication;
67import org.openstreetmap.josm.gui.MainMenu;
68import org.openstreetmap.josm.gui.ScrollViewport;
69import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
70import org.openstreetmap.josm.gui.dialogs.relation.actions.AbstractRelationEditorAction;
71import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAfterSelection;
72import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtEndAction;
73import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtStartAction;
74import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedBeforeSelection;
75import org.openstreetmap.josm.gui.dialogs.relation.actions.ApplyAction;
76import org.openstreetmap.josm.gui.dialogs.relation.actions.CancelAction;
77import org.openstreetmap.josm.gui.dialogs.relation.actions.CopyMembersAction;
78import org.openstreetmap.josm.gui.dialogs.relation.actions.DeleteCurrentRelationAction;
79import org.openstreetmap.josm.gui.dialogs.relation.actions.DownloadIncompleteMembersAction;
80import org.openstreetmap.josm.gui.dialogs.relation.actions.DownloadSelectedIncompleteMembersAction;
81import org.openstreetmap.josm.gui.dialogs.relation.actions.DuplicateRelationAction;
82import org.openstreetmap.josm.gui.dialogs.relation.actions.EditAction;
83import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionAccess;
84import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionGroup;
85import org.openstreetmap.josm.gui.dialogs.relation.actions.MoveDownAction;
86import org.openstreetmap.josm.gui.dialogs.relation.actions.MoveUpAction;
87import org.openstreetmap.josm.gui.dialogs.relation.actions.OKAction;
88import org.openstreetmap.josm.gui.dialogs.relation.actions.PasteMembersAction;
89import org.openstreetmap.josm.gui.dialogs.relation.actions.RefreshAction;
90import org.openstreetmap.josm.gui.dialogs.relation.actions.RemoveAction;
91import org.openstreetmap.josm.gui.dialogs.relation.actions.RemoveSelectedAction;
92import org.openstreetmap.josm.gui.dialogs.relation.actions.ReverseAction;
93import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectAction;
94import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectPrimitivesForSelectedMembersAction;
95import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectedMembersForSelectionAction;
96import org.openstreetmap.josm.gui.dialogs.relation.actions.SetRoleAction;
97import org.openstreetmap.josm.gui.dialogs.relation.actions.SortAction;
98import org.openstreetmap.josm.gui.dialogs.relation.actions.SortBelowAction;
99import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
100import org.openstreetmap.josm.gui.help.HelpUtil;
101import org.openstreetmap.josm.gui.layer.OsmDataLayer;
102import org.openstreetmap.josm.gui.tagging.TagEditorModel;
103import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
104import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
105import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
106import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
107import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
108import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
109import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
110import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
111import org.openstreetmap.josm.gui.util.WindowGeometry;
112import org.openstreetmap.josm.spi.preferences.Config;
113import org.openstreetmap.josm.tools.CheckParameterUtil;
114import org.openstreetmap.josm.tools.InputMapUtils;
115import org.openstreetmap.josm.tools.Logging;
116import org.openstreetmap.josm.tools.Shortcut;
117import org.openstreetmap.josm.tools.Utils;
118
119/**
120 * This dialog is for editing relations.
121 * @since 343
122 */
123public class GenericRelationEditor extends RelationEditor implements CommandQueueListener {
124 /** the tag table and its model */
125 private final TagEditorPanel tagEditorPanel;
126 private final ReferringRelationsBrowser referrerBrowser;
127
128 /** the member table and its model */
129 private final MemberTable memberTable;
130 private final MemberTableModel memberTableModel;
131
132 /** the selection table and its model */
133 private final SelectionTable selectionTable;
134 private final SelectionTableModel selectionTableModel;
135
136 private final AutoCompletingTextField tfRole;
137 private final RelationEditorActionAccess actionAccess;
138
139 /**
140 * the menu item in the windows menu. Required to properly hide on dialog close.
141 */
142 private JMenuItem windowMenuItem;
143 /**
144 * Action for performing the {@link RefreshAction}
145 */
146 private final RefreshAction refreshAction;
147 /**
148 * Action for performing the {@link ApplyAction}
149 */
150 private final ApplyAction applyAction;
151 /**
152 * Action for performing the {@link SelectAction}
153 */
154 private final SelectAction selectAction;
155 /**
156 * Action for performing the {@link CancelAction}
157 */
158 private final CancelAction cancelAction;
159 /**
160 * A list of listeners that need to be notified on clipboard content changes.
161 */
162 private final ArrayList<FlavorListener> clipboardListeners = new ArrayList<>();
163
164 private Component selectedTabPane;
165 private final JTabbedPane tabbedPane;
166
167 /**
168 * Creates a new relation editor for the given relation. The relation will be saved if the user
169 * selects "ok" in the editor.
170 * <p>
171 * If no relation is given, will create an editor for a new relation.
172 *
173 * @param layer the {@link OsmDataLayer} the new or edited relation belongs to
174 * @param relation relation to edit, or null to create a new one.
175 * @param selectedMembers a collection of members which shall be selected initially
176 */
177 public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) {
178 super(layer, relation);
179
180 setRememberWindowGeometry(getClass().getName() + ".geometry",
181 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(700, 650)));
182
183 final TaggingPresetHandler presetHandler = new TaggingPresetHandler() {
184
185 @Override
186 public void updateTags(List<Tag> tags) {
187 tagEditorPanel.getModel().updateTags(tags);
188 }
189
190 @Override
191 public Collection<OsmPrimitive> getSelection() {
192 // Creating a new relation will open the window. The relation, in that case, will be null.
193 if (getRelation() == null) {
194 Relation relation = new Relation();
195 tagEditorPanel.getModel().applyToPrimitive(relation);
196 memberTableModel.applyToRelation(relation);
197 return Collections.singletonList(relation);
198 }
199 return Collections.singletonList(getRelation());
200 }
201 };
202
203 // init the various models
204 //
205 memberTableModel = new MemberTableModel(relation, getLayer(), presetHandler);
206 memberTableModel.register();
207 selectionTableModel = new SelectionTableModel(getLayer());
208 selectionTableModel.register();
209 ReferringRelationsBrowserModel referrerModel = new ReferringRelationsBrowserModel(relation);
210
211 tagEditorPanel = new TagEditorPanel(relation, presetHandler);
212 populateModels(relation);
213 tagEditorPanel.getModel().ensureOneTag();
214
215 // setting up the member table
216 memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel);
217 memberTable.addMouseListener(new MemberTableDblClickAdapter());
218 memberTableModel.addMemberModelListener(memberTable);
219
220 MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor();
221 selectionTable = new SelectionTable(selectionTableModel, memberTableModel);
222 selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height);
223
224 LeftButtonToolbar leftButtonToolbar = new LeftButtonToolbar(new RelationEditorActionAccess());
225 tfRole = buildRoleTextField(this);
226
227 JSplitPane pane = buildSplitPane(
228 buildTagEditorPanel(tagEditorPanel),
229 buildMemberEditorPanel(leftButtonToolbar, new RelationEditorActionAccess()),
230 this);
231 pane.setPreferredSize(new Dimension(100, 100));
232
233 JPanel pnl = new JPanel(new BorderLayout());
234 pnl.add(pane, BorderLayout.CENTER);
235 pnl.setBorder(BorderFactory.createRaisedBevelBorder());
236
237 getContentPane().setLayout(new BorderLayout());
238 tabbedPane = new JTabbedPane();
239 tabbedPane.add(tr("Tags and Members"), pnl);
240 referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel);
241 tabbedPane.add(tr("Parent Relations"), referrerBrowser);
242 tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation));
243 selectedTabPane = tabbedPane.getSelectedComponent();
244 tabbedPane.addChangeListener(e -> {
245 JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource();
246 int index = sourceTabbedPane.getSelectedIndex();
247 String title = sourceTabbedPane.getTitleAt(index);
248 if (title.equals(tr("Parent Relations"))) {
249 referrerBrowser.init();
250 }
251 // see #20228
252 boolean selIsTagsAndMembers = sourceTabbedPane.getSelectedComponent() == pnl;
253 if (selectedTabPane == pnl && !selIsTagsAndMembers) {
254 unregisterMain();
255 } else if (selectedTabPane != pnl && selIsTagsAndMembers) {
256 registerMain();
257 }
258 selectedTabPane = sourceTabbedPane.getSelectedComponent();
259 });
260
261 actionAccess = new RelationEditorActionAccess();
262
263 refreshAction = new RefreshAction(actionAccess);
264 applyAction = new ApplyAction(actionAccess);
265 selectAction = new SelectAction(actionAccess);
266 // Action for performing the {@link DuplicateRelationAction}
267 final DuplicateRelationAction duplicateAction = new DuplicateRelationAction(actionAccess);
268 // Action for performing the {@link DeleteCurrentRelationAction}
269 final DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction(actionAccess);
270
271 this.memberTableModel.addTableModelListener(applyAction);
272 this.tagEditorPanel.getModel().addTableModelListener(applyAction);
273
274 addPropertyChangeListener(deleteAction);
275
276 // Action for performing the {@link OKAction}
277 final OKAction okAction = new OKAction(actionAccess);
278 cancelAction = new CancelAction(actionAccess);
279
280 getContentPane().add(buildToolBar(refreshAction, applyAction, selectAction, duplicateAction, deleteAction), BorderLayout.NORTH);
281 getContentPane().add(tabbedPane, BorderLayout.CENTER);
282 getContentPane().add(buildOkCancelButtonPanel(okAction, deleteAction, cancelAction), BorderLayout.SOUTH);
283
284 setSize(findMaxDialogSize());
285
286 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
287 addWindowListener(
288 new WindowAdapter() {
289 @Override
290 public void windowOpened(WindowEvent e) {
291 cleanSelfReferences(memberTableModel, getRelation());
292 }
293
294 @Override
295 public void windowClosing(WindowEvent e) {
296 cancel();
297 }
298 }
299 );
300 InputMapUtils.addCtrlEnterAction(getRootPane(), okAction);
301 // CHECKSTYLE.OFF: LineLength
302 registerCopyPasteAction(tagEditorPanel.getPasteAction(), "PASTE_TAGS",
303 Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke(),
304 getRootPane(), memberTable, selectionTable);
305 // CHECKSTYLE.ON: LineLength
306
307 KeyStroke key = Shortcut.getPasteKeyStroke();
308 if (key != null) {
309 // handle uncommon situation, that user has no keystroke assigned to paste
310 registerCopyPasteAction(new PasteMembersAction(actionAccess) {
311 private static final long serialVersionUID = 1L;
312
313 @Override
314 public void actionPerformed(ActionEvent e) {
315 super.actionPerformed(e);
316 tfRole.requestFocusInWindow();
317 }
318 }, "PASTE_MEMBERS", key, getRootPane(), memberTable, selectionTable);
319 }
320 key = Shortcut.getCopyKeyStroke();
321 if (key != null) {
322 // handle uncommon situation, that user has no keystroke assigned to copy
323 registerCopyPasteAction(new CopyMembersAction(actionAccess),
324 "COPY_MEMBERS", key, getRootPane(), memberTable, selectionTable);
325 }
326 tagEditorPanel.setNextFocusComponent(memberTable);
327 selectionTable.setFocusable(false);
328 memberTableModel.setSelectedMembers(selectedMembers);
329 HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/RelationEditor"));
330 UndoRedoHandler.getInstance().addCommandQueueListener(this);
331 }
332
333 private void registerMain() {
334 selectionTableModel.register();
335 memberTableModel.register();
336 memberTable.registerListeners();
337 }
338
339 private void unregisterMain() {
340 selectionTableModel.unregister();
341 memberTableModel.unregister();
342 memberTable.unregisterListeners();
343 }
344
345 @Override
346 public void reloadDataFromRelation() {
347 setRelation(getRelation());
348 populateModels(getRelation());
349 refreshAction.updateEnabledState();
350 }
351
352 private void populateModels(Relation relation) {
353 if (relation != null) {
354 tagEditorPanel.getModel().initFromPrimitive(relation);
355 memberTableModel.populate(relation);
356 if (!getLayer().data.getRelations().contains(relation)) {
357 // treat it as a new relation if it doesn't exist in the data set yet.
358 setRelation(null);
359 }
360 } else {
361 tagEditorPanel.getModel().clear();
362 memberTableModel.populate(null);
363 }
364 }
365
366 /**
367 * Apply changes.
368 * @see ApplyAction
369 */
370 public void apply() {
371 applyAction.actionPerformed(null);
372 }
373
374 /**
375 * Select relation.
376 * @see SelectAction
377 * @since 12933
378 */
379 public void select() {
380 selectAction.actionPerformed(null);
381 }
382
383 /**
384 * Cancel changes.
385 * @see CancelAction
386 */
387 public void cancel() {
388 cancelAction.actionPerformed(null);
389 }
390
391 /**
392 * Creates the toolbar
393 * @param actions relation toolbar actions
394 * @return the toolbar
395 * @since 12933
396 */
397 protected static JToolBar buildToolBar(AbstractRelationEditorAction... actions) {
398 JToolBar tb = new JToolBar();
399 tb.setFloatable(false);
400 for (AbstractRelationEditorAction action : actions) {
401 tb.add(action);
402 }
403 return tb;
404 }
405
406 /**
407 * builds the panel with the OK and the Cancel button
408 * @param okAction OK action
409 * @param deleteAction Delete action
410 * @param cancelAction Cancel action
411 *
412 * @return the panel with the OK and the Cancel button
413 */
414 protected final JPanel buildOkCancelButtonPanel(OKAction okAction, DeleteCurrentRelationAction deleteAction,
415 CancelAction cancelAction) {
416 final JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
417 final JButton okButton = new JButton(okAction);
418 final JButton deleteButton = new JButton(deleteAction);
419 okButton.setPreferredSize(deleteButton.getPreferredSize());
420 pnl.add(okButton);
421 pnl.add(deleteButton);
422 pnl.add(new JButton(cancelAction));
423 pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
424 // Keep users from saving invalid relations -- a relation MUST have at least a tag with the key "type"
425 // AND must contain at least one other OSM object.
426 final TableModelListener listener = l -> updateOkPanel(okButton, deleteButton);
427 listener.tableChanged(null);
428 this.memberTableModel.addTableModelListener(listener);
429 this.tagEditorPanel.getModel().addTableModelListener(listener);
430 return pnl;
431 }
432
433 /**
434 * Update the OK panel area with a temporary relation that looks if it were to be saved now.
435 * @param okButton The OK button
436 * @param deleteButton The delete button
437 */
438 private void updateOkPanel(JButton okButton, JButton deleteButton) {
439 boolean useful = this.actionAccess.wouldRelationBeUseful();
440 okButton.setVisible(useful || this.getRelationSnapshot() == null);
441 deleteButton.setVisible(!useful && this.getRelationSnapshot() != null);
442 if (this.getRelationSnapshot() == null && !useful) {
443 okButton.setText(tr("Delete"));
444 } else {
445 okButton.setText(tr("OK"));
446 }
447 }
448
449 /**
450 * builds the panel with the tag editor
451 * @param tagEditorPanel tag editor panel
452 *
453 * @return the panel with the tag editor
454 */
455 protected static JPanel buildTagEditorPanel(TagEditorPanel tagEditorPanel) {
456 JPanel pnl = new JPanel(new GridBagLayout());
457
458 GridBagConstraints gc = new GridBagConstraints();
459 gc.gridx = 0;
460 gc.gridy = 0;
461 gc.gridheight = 1;
462 gc.gridwidth = 1;
463 gc.fill = GridBagConstraints.HORIZONTAL;
464 gc.anchor = GridBagConstraints.FIRST_LINE_START;
465 gc.weightx = 1.0;
466 gc.weighty = 0.0;
467 pnl.add(new JLabel(tr("Tags")), gc);
468
469 gc.gridx = 0;
470 gc.gridy = 1;
471 gc.fill = GridBagConstraints.BOTH;
472 gc.anchor = GridBagConstraints.CENTER;
473 gc.weightx = 1.0;
474 gc.weighty = 1.0;
475 pnl.add(tagEditorPanel, gc);
476 return pnl;
477 }
478
479 /**
480 * builds the role text field
481 * @param re relation editor
482 * @return the role text field
483 */
484 protected static AutoCompletingTextField buildRoleTextField(final IRelationEditor re) {
485 final AutoCompletingTextField tfRole = new AutoCompletingTextField(10);
486 tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
487 tfRole.addFocusListener(new FocusAdapter() {
488 @Override
489 public void focusGained(FocusEvent e) {
490 tfRole.selectAll();
491 }
492 });
493 tfRole.setAutoCompletionList(new AutoCompletionList());
494 tfRole.addFocusListener(
495 new FocusAdapter() {
496 @Override
497 public void focusGained(FocusEvent e) {
498 AutoCompletionList list = tfRole.getAutoCompletionList();
499 if (list != null) {
500 list.clear();
501 AutoCompletionManager.of(re.getLayer().data).populateWithMemberRoles(list, re.getRelation());
502 }
503 }
504 }
505 );
506 tfRole.setText(Config.getPref().get("relation.editor.generic.lastrole", ""));
507 return tfRole;
508 }
509
510 /**
511 * builds the panel for the relation member editor
512 * @param leftButtonToolbar left button toolbar
513 * @param editorAccess The relation editor
514 *
515 * @return the panel for the relation member editor
516 */
517 static JPanel buildMemberEditorPanel(
518 LeftButtonToolbar leftButtonToolbar, IRelationEditorActionAccess editorAccess) {
519 final JPanel pnl = new JPanel(new GridBagLayout());
520 final JScrollPane scrollPane = new JScrollPane(editorAccess.getMemberTable());
521
522 GridBagConstraints gc = new GridBagConstraints();
523 gc.gridx = 0;
524 gc.gridy = 0;
525 gc.gridwidth = 2;
526 gc.fill = GridBagConstraints.HORIZONTAL;
527 gc.anchor = GridBagConstraints.FIRST_LINE_START;
528 gc.weightx = 1.0;
529 gc.weighty = 0.0;
530 pnl.add(new JLabel(tr("Members")), gc);
531
532 gc.gridx = 0;
533 gc.gridy = 1;
534 gc.gridheight = 2;
535 gc.gridwidth = 1;
536 gc.fill = GridBagConstraints.VERTICAL;
537 gc.anchor = GridBagConstraints.NORTHWEST;
538 gc.weightx = 0.0;
539 gc.weighty = 1.0;
540 pnl.add(new ScrollViewport(leftButtonToolbar, ScrollViewport.VERTICAL_DIRECTION), gc);
541
542 gc.gridx = 1;
543 gc.gridy = 1;
544 gc.gridheight = 1;
545 gc.fill = GridBagConstraints.BOTH;
546 gc.anchor = GridBagConstraints.CENTER;
547 gc.weightx = 0.6;
548 gc.weighty = 1.0;
549 pnl.add(scrollPane, gc);
550
551 // --- role editing
552 JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
553 p3.add(new JLabel(tr("Apply Role:")));
554 p3.add(editorAccess.getTextFieldRole());
555 SetRoleAction setRoleAction = new SetRoleAction(editorAccess);
556 editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(setRoleAction);
557 editorAccess.getTextFieldRole().getDocument().addDocumentListener(setRoleAction);
558 editorAccess.getTextFieldRole().addActionListener(setRoleAction);
559 editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(
560 e -> editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0)
561 );
562 editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0);
563 JButton btnApply = new JButton(setRoleAction);
564 btnApply.setPreferredSize(new Dimension(20, 20));
565 btnApply.setText("");
566 p3.add(btnApply);
567
568 gc.gridx = 1;
569 gc.gridy = 2;
570 gc.fill = GridBagConstraints.HORIZONTAL;
571 gc.anchor = GridBagConstraints.LAST_LINE_START;
572 gc.weightx = 1.0;
573 gc.weighty = 0.0;
574 pnl.add(p3, gc);
575
576 JPanel pnl2 = new JPanel(new GridBagLayout());
577
578 gc.gridx = 0;
579 gc.gridy = 0;
580 gc.gridheight = 1;
581 gc.gridwidth = 3;
582 gc.fill = GridBagConstraints.HORIZONTAL;
583 gc.anchor = GridBagConstraints.FIRST_LINE_START;
584 gc.weightx = 1.0;
585 gc.weighty = 0.0;
586 pnl2.add(new JLabel(tr("Selection")), gc);
587
588 gc.gridx = 0;
589 gc.gridy = 1;
590 gc.gridheight = 1;
591 gc.gridwidth = 1;
592 gc.fill = GridBagConstraints.VERTICAL;
593 gc.anchor = GridBagConstraints.NORTHWEST;
594 gc.weightx = 0.0;
595 gc.weighty = 1.0;
596 pnl2.add(new ScrollViewport(buildSelectionControlButtonToolbar(editorAccess),
597 ScrollViewport.VERTICAL_DIRECTION), gc);
598
599 gc.gridx = 1;
600 gc.gridy = 1;
601 gc.weightx = 1.0;
602 gc.weighty = 1.0;
603 gc.fill = GridBagConstraints.BOTH;
604 pnl2.add(buildSelectionTablePanel(editorAccess.getSelectionTable()), gc);
605
606 final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
607 splitPane.setLeftComponent(pnl);
608 splitPane.setRightComponent(pnl2);
609 splitPane.setOneTouchExpandable(false);
610 if (editorAccess.getEditor() instanceof Window) {
611 ((Window) editorAccess.getEditor()).addWindowListener(new WindowAdapter() {
612 @Override
613 public void windowOpened(WindowEvent e) {
614 // has to be called when the window is visible, otherwise no effect
615 splitPane.setDividerLocation(0.6);
616 }
617 });
618 }
619
620 JPanel pnl3 = new JPanel(new BorderLayout());
621 pnl3.add(splitPane, BorderLayout.CENTER);
622
623 return pnl3;
624 }
625
626 /**
627 * builds the panel with the table displaying the currently selected primitives
628 * @param selectionTable selection table
629 *
630 * @return panel with current selection
631 */
632 protected static JPanel buildSelectionTablePanel(SelectionTable selectionTable) {
633 JPanel pnl = new JPanel(new BorderLayout());
634 pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER);
635 return pnl;
636 }
637
638 /**
639 * builds the {@link JSplitPane} which divides the editor in an upper and a lower half
640 * @param top top panel
641 * @param bottom bottom panel
642 * @param re relation editor
643 *
644 * @return the split panel
645 */
646 protected static JSplitPane buildSplitPane(JPanel top, JPanel bottom, IRelationEditor re) {
647 final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
648 pane.setTopComponent(top);
649 pane.setBottomComponent(bottom);
650 pane.setOneTouchExpandable(true);
651 if (re instanceof Window) {
652 ((Window) re).addWindowListener(new WindowAdapter() {
653 @Override
654 public void windowOpened(WindowEvent e) {
655 // has to be called when the window is visible, otherwise no effect
656 pane.setDividerLocation(0.3);
657 }
658 });
659 }
660 return pane;
661 }
662
663 /**
664 * The toolbar with the buttons on the left
665 */
666 static class LeftButtonToolbar extends JToolBar {
667 private static final long serialVersionUID = 1L;
668
669 /**
670 * Constructs a new {@code LeftButtonToolbar}.
671 * @param editorAccess relation editor
672 */
673 LeftButtonToolbar(IRelationEditorActionAccess editorAccess) {
674 setOrientation(SwingConstants.VERTICAL);
675 setFloatable(false);
676
677 List<IRelationEditorActionGroup> groups = new ArrayList<>();
678 // Move
679 groups.add(buildNativeGroup(10,
680 new MoveUpAction(editorAccess, "moveUp"),
681 new MoveDownAction(editorAccess, "moveDown")
682 ));
683 // Edit
684 groups.add(buildNativeGroup(20,
685 new EditAction(editorAccess),
686 new RemoveAction(editorAccess, "removeSelected")
687 ));
688 // Sort
689 groups.add(buildNativeGroup(30,
690 new SortAction(editorAccess),
691 new SortBelowAction(editorAccess)
692 ));
693 // Reverse
694 groups.add(buildNativeGroup(40,
695 new ReverseAction(editorAccess)
696 ));
697 // Download
698 groups.add(buildNativeGroup(50,
699 new DownloadIncompleteMembersAction(editorAccess, "downloadIncomplete"),
700 new DownloadSelectedIncompleteMembersAction(editorAccess)
701 ));
702 groups.addAll(RelationEditorHooks.getMemberActions());
703
704 IRelationEditorActionGroup.fillToolbar(this, groups, editorAccess);
705
706
707 InputMap inputMap = editorAccess.getMemberTable().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
708 inputMap.put((KeyStroke) new RemoveAction(editorAccess, "removeSelected")
709 .getValue(Action.ACCELERATOR_KEY), "removeSelected");
710 inputMap.put((KeyStroke) new MoveUpAction(editorAccess, "moveUp")
711 .getValue(Action.ACCELERATOR_KEY), "moveUp");
712 inputMap.put((KeyStroke) new MoveDownAction(editorAccess, "moveDown")
713 .getValue(Action.ACCELERATOR_KEY), "moveDown");
714 inputMap.put((KeyStroke) new DownloadIncompleteMembersAction(
715 editorAccess, "downloadIncomplete").getValue(Action.ACCELERATOR_KEY), "downloadIncomplete");
716 }
717 }
718
719 /**
720 * build the toolbar with the buttons for adding or removing the current selection
721 * @param editorAccess relation editor
722 *
723 * @return control buttons panel for selection/members
724 */
725 protected static JToolBar buildSelectionControlButtonToolbar(IRelationEditorActionAccess editorAccess) {
726 JToolBar tb = new JToolBar(SwingConstants.VERTICAL);
727 tb.setFloatable(false);
728
729 List<IRelationEditorActionGroup> groups = new ArrayList<>();
730 groups.add(buildNativeGroup(10,
731 new AddSelectedAtStartAction(editorAccess),
732 new AddSelectedBeforeSelection(editorAccess),
733 new AddSelectedAfterSelection(editorAccess),
734 new AddSelectedAtEndAction(editorAccess)
735 ));
736 groups.add(buildNativeGroup(20,
737 new SelectedMembersForSelectionAction(editorAccess),
738 new SelectPrimitivesForSelectedMembersAction(editorAccess)
739 ));
740 groups.add(buildNativeGroup(30,
741 new RemoveSelectedAction(editorAccess)
742 ));
743 groups.addAll(RelationEditorHooks.getSelectActions());
744
745 IRelationEditorActionGroup.fillToolbar(tb, groups, editorAccess);
746 return tb;
747 }
748
749 private static IRelationEditorActionGroup buildNativeGroup(int order, AbstractRelationEditorAction... actions) {
750 return new IRelationEditorActionGroup() {
751 @Override
752 public int order() {
753 return order;
754 }
755
756 @Override
757 public List<AbstractRelationEditorAction> getActions(IRelationEditorActionAccess editorAccess) {
758 return Arrays.asList(actions);
759 }
760 };
761 }
762
763 @Override
764 protected Dimension findMaxDialogSize() {
765 return new Dimension(700, 650);
766 }
767
768 @Override
769 public void setVisible(boolean visible) {
770 if (isVisible() == visible) {
771 return;
772 }
773 if (visible) {
774 tagEditorPanel.initAutoCompletion(getLayer());
775 }
776 super.setVisible(visible);
777 Clipboard clipboard = ClipboardUtils.getClipboard();
778 if (visible) {
779 RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
780 if (windowMenuItem == null) {
781 windowMenuItem = addToWindowMenu(this, getLayer().getName());
782 }
783 tagEditorPanel.requestFocusInWindow();
784 for (FlavorListener listener : clipboardListeners) {
785 clipboard.addFlavorListener(listener);
786 }
787 } else {
788 // make sure all registered listeners are unregistered
789 //
790 memberTable.stopHighlighting();
791 if (tabbedPane != null && tr("Tags and Members").equals(tabbedPane.getTitleAt(tabbedPane.getSelectedIndex()))) {
792 unregisterMain();
793 }
794 if (windowMenuItem != null) {
795 MainApplication.getMenu().windowMenu.remove(windowMenuItem);
796 windowMenuItem = null;
797 }
798 for (FlavorListener listener : clipboardListeners) {
799 clipboard.removeFlavorListener(listener);
800 }
801 dispose();
802 }
803 }
804
805 /**
806 * Adds current relation editor to the windows menu (in the "volatile" group)
807 * @param re relation editor
808 * @param layerName layer name
809 * @return created menu item
810 */
811 protected static JMenuItem addToWindowMenu(IRelationEditor re, String layerName) {
812 Relation r = re.getRelation();
813 String name = r == null ? tr("New relation") : r.getLocalName();
814 JosmAction focusAction = new JosmAction(
815 tr("Relation Editor: {0}", name == null && r != null ? r.getId() : name),
816 "dialogs/relationlist",
817 tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''", name, layerName),
818 null, false, false) {
819 private static final long serialVersionUID = 1L;
820
821 @Override
822 public void actionPerformed(ActionEvent e) {
823 ((RelationEditor) getValue("relationEditor")).setVisible(true);
824 }
825 };
826 focusAction.putValue("relationEditor", re);
827 return MainMenu.add(MainApplication.getMenu().windowMenu, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
828 }
829
830 /**
831 * checks whether the current relation has members referring to itself. If so,
832 * warns the users and provides an option for removing these members.
833 * @param memberTableModel member table model
834 * @param relation relation
835 */
836 protected static void cleanSelfReferences(MemberTableModel memberTableModel, Relation relation) {
837 List<OsmPrimitive> toCheck = new ArrayList<>();
838 toCheck.add(relation);
839 if (memberTableModel.hasMembersReferringTo(toCheck)) {
840 int ret = ConditionalOptionPaneUtil.showOptionDialog(
841 "clean_relation_self_references",
842 MainApplication.getMainFrame(),
843 tr("<html>There is at least one member in this relation referring<br>"
844 + "to the relation itself.<br>"
845 + "This creates circular dependencies and is discouraged.<br>"
846 + "How do you want to proceed with circular dependencies?</html>"),
847 tr("Warning"),
848 JOptionPane.YES_NO_OPTION,
849 JOptionPane.WARNING_MESSAGE,
850 new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")},
851 tr("Remove them, clean up relation")
852 );
853 switch (ret) {
854 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
855 case JOptionPane.CLOSED_OPTION:
856 case JOptionPane.NO_OPTION:
857 return;
858 case JOptionPane.YES_OPTION:
859 memberTableModel.removeMembersReferringTo(toCheck);
860 break;
861 default: // Do nothing
862 }
863 }
864 }
865
866 private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut,
867 JRootPane rootPane, JTable... tables) {
868 if (shortcut == null) {
869 Logging.warn("No shortcut provided for the Paste action in Relation editor dialog");
870 } else {
871 int mods = shortcut.getModifiers();
872 int code = shortcut.getKeyCode();
873 if (code != KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) {
874 Logging.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut);
875 return;
876 }
877 }
878 rootPane.getActionMap().put(actionName, action);
879 if (shortcut != null) {
880 rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
881 // Assign also to JTables because they have their own Copy&Paste implementation
882 // (which is disabled in this case but eats key shortcuts anyway)
883 for (JTable table : tables) {
884 table.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
885 table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
886 table.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
887 }
888 }
889 if (action instanceof FlavorListener) {
890 clipboardListeners.add((FlavorListener) action);
891 }
892 }
893
894 @Override
895 public void dispose() {
896 refreshAction.destroy();
897 UndoRedoHandler.getInstance().removeCommandQueueListener(this);
898 super.dispose(); // call before setting relation to null, see #20304
899 setRelation(null);
900 selectedTabPane = null;
901 }
902
903 /**
904 * Exception thrown when user aborts add operation.
905 */
906 public static class AddAbortException extends Exception {
907 }
908
909 /**
910 * Asks confirmation before adding a primitive.
911 * @param primitive primitive to add
912 * @return {@code true} is user confirms the operation, {@code false} otherwise
913 * @throws AddAbortException if user aborts operation
914 */
915 public static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException {
916 String msg = tr("<html>This relation already has one or more members referring to<br>"
917 + "the object ''{0}''<br>"
918 + "<br>"
919 + "Do you really want to add another relation member?</html>",
920 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance()))
921 );
922 int ret = ConditionalOptionPaneUtil.showOptionDialog(
923 "add_primitive_to_relation",
924 MainApplication.getMainFrame(),
925 msg,
926 tr("Multiple members referring to same object."),
927 JOptionPane.YES_NO_CANCEL_OPTION,
928 JOptionPane.WARNING_MESSAGE,
929 null,
930 null
931 );
932 switch (ret) {
933 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
934 case JOptionPane.YES_OPTION:
935 return true;
936 case JOptionPane.NO_OPTION:
937 case JOptionPane.CLOSED_OPTION:
938 return false;
939 case JOptionPane.CANCEL_OPTION:
940 default:
941 throw new AddAbortException();
942 }
943 }
944
945 /**
946 * Warn about circular references.
947 * @param primitive the concerned primitive
948 */
949 public static void warnOfCircularReferences(OsmPrimitive primitive) {
950 warnOfCircularReferences(primitive, Collections.emptyList());
951 }
952
953 /**
954 * Warn about circular references.
955 * @param primitive the concerned primitive
956 * @param loop list of relation that form the circular dependencies.
957 * Only used to report the loop if more than one relation is involved.
958 * @since 16651
959 */
960 public static void warnOfCircularReferences(OsmPrimitive primitive, List<Relation> loop) {
961 final String msg;
962 DefaultNameFormatter df = DefaultNameFormatter.getInstance();
963 if (loop.size() <= 2) {
964 msg = tr("<html>You are trying to add a relation to itself.<br>"
965 + "<br>"
966 + "This generates a circular dependency of parent/child elements and is therefore discouraged.<br>"
967 + "Skipping relation ''{0}''.</html>",
968 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(df)));
969 } else {
970 msg = tr("<html>You are trying to add a child relation which refers to the parent relation.<br>"
971 + "<br>"
972 + "This generates a circular dependency of parent/child elements and is therefore discouraged.<br>"
973 + "Skipping relation ''{0}''." + "<br>"
974 + "Relations that would generate the circular dependency:<br>{1}</html>",
975 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(df)),
976 loop.stream().map(p -> Utils.escapeReservedCharactersHTML(p.getDisplayName(df)))
977 .collect(Collectors.joining(" -> <br>")));
978 }
979 JOptionPane.showMessageDialog(
980 MainApplication.getMainFrame(),
981 msg,
982 tr("Warning"),
983 JOptionPane.WARNING_MESSAGE);
984 }
985
986 /**
987 * Adds primitives to a given relation.
988 * @param orig The relation to modify
989 * @param primitivesToAdd The primitives to add as relation members
990 * @return The resulting command
991 * @throws IllegalArgumentException if orig is null
992 */
993 public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
994 CheckParameterUtil.ensureParameterNotNull(orig, "orig");
995 try {
996 final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(
997 EnumSet.of(TaggingPresetType.forPrimitive(orig)), orig.getKeys(), false);
998 Relation target = new Relation(orig);
999 boolean modified = false;
1000 for (OsmPrimitive p : primitivesToAdd) {
1001 if (p instanceof Relation) {
1002 List<Relation> loop = RelationChecker.checkAddMember(target, (Relation) p);
1003 if (!loop.isEmpty() && loop.get(0).equals(loop.get(loop.size() - 1))) {
1004 warnOfCircularReferences(p, loop);
1005 continue;
1006 }
1007 } else if (MemberTableModel.hasMembersReferringTo(target.getMembers(), Collections.singleton(p))
1008 && !confirmAddingPrimitive(p)) {
1009 continue;
1010 }
1011 final Set<String> roles = findSuggestedRoles(presets, p);
1012 target.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p));
1013 modified = true;
1014 }
1015 List<RelationMember> members = new ArrayList<>(target.getMembers());
1016 target.setMembers(null); // see #19885
1017 return modified ? new ChangeMembersCommand(orig, members) : null;
1018 } catch (AddAbortException ign) {
1019 Logging.trace(ign);
1020 return null;
1021 }
1022 }
1023
1024 protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) {
1025 return presets.stream()
1026 .map(preset -> preset.suggestRoleForOsmPrimitive(p))
1027 .filter(role -> !Utils.isEmpty(role))
1028 .collect(Collectors.toSet());
1029 }
1030
1031 class MemberTableDblClickAdapter extends MouseAdapter {
1032 @Override
1033 public void mouseClicked(MouseEvent e) {
1034 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
1035 new EditAction(actionAccess).actionPerformed(null);
1036 }
1037 }
1038 }
1039
1040 private final class RelationEditorActionAccess implements IRelationEditorActionAccess {
1041
1042 @Override
1043 public MemberTable getMemberTable() {
1044 return memberTable;
1045 }
1046
1047 @Override
1048 public MemberTableModel getMemberTableModel() {
1049 return memberTableModel;
1050 }
1051
1052 @Override
1053 public SelectionTable getSelectionTable() {
1054 return selectionTable;
1055 }
1056
1057 @Override
1058 public SelectionTableModel getSelectionTableModel() {
1059 return selectionTableModel;
1060 }
1061
1062 @Override
1063 public IRelationEditor getEditor() {
1064 return GenericRelationEditor.this;
1065 }
1066
1067 @Override
1068 public TagEditorModel getTagModel() {
1069 return tagEditorPanel.getModel();
1070 }
1071
1072 @Override
1073 public AutoCompletingTextField getTextFieldRole() {
1074 return tfRole;
1075 }
1076
1077 }
1078
1079 @Override
1080 public void commandChanged(int queueSize, int redoSize) {
1081 Relation r = getRelation();
1082 if (r != null && r.getDataSet() == null) {
1083 // see #19915
1084 setRelation(null);
1085 applyAction.updateEnabledState();
1086 }
1087 }
1088}
Note: See TracBrowser for help on using the repository browser.