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

Last change on this file since 17358 was 17358, checked in by GerdP, 4 years ago

see #19885: memory leak with "temporary" objects in validator and actions

  • (hopefully) fix memory leaks in complex actions
  • handle complex cases with presets and RelationEditor

I hope these changes don't break plugins which extend or overwrite RelationEditor

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