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