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