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

Last change on this file was 19014, checked in by GerdP, 6 weeks ago

fix #23527: Memory leak in relation editor

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