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

Last change on this file was 19398, checked in by stoecker, 2 months ago

Auto relation refresh, fix #21840 - patch by Woazboat

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