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