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;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.BorderLayout;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.GridBagConstraints;
012import java.awt.GridBagLayout;
013import java.awt.event.ActionEvent;
014import java.awt.event.FocusAdapter;
015import java.awt.event.FocusEvent;
016import java.awt.event.InputEvent;
017import java.awt.event.KeyEvent;
018import java.awt.event.MouseAdapter;
019import java.awt.event.MouseEvent;
020import java.awt.event.WindowAdapter;
021import java.awt.event.WindowEvent;
022import java.beans.PropertyChangeEvent;
023import java.beans.PropertyChangeListener;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.EnumSet;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Set;
031
032import javax.swing.AbstractAction;
033import javax.swing.BorderFactory;
034import javax.swing.InputMap;
035import javax.swing.JComponent;
036import javax.swing.JLabel;
037import javax.swing.JMenu;
038import javax.swing.JMenuItem;
039import javax.swing.JOptionPane;
040import javax.swing.JPanel;
041import javax.swing.JScrollPane;
042import javax.swing.JSplitPane;
043import javax.swing.JTabbedPane;
044import javax.swing.JToolBar;
045import javax.swing.KeyStroke;
046import javax.swing.SwingUtilities;
047import javax.swing.event.ChangeEvent;
048import javax.swing.event.ChangeListener;
049import javax.swing.event.DocumentEvent;
050import javax.swing.event.DocumentListener;
051import javax.swing.event.ListSelectionEvent;
052import javax.swing.event.ListSelectionListener;
053import javax.swing.event.TableModelEvent;
054import javax.swing.event.TableModelListener;
055
056import org.openstreetmap.josm.Main;
057import org.openstreetmap.josm.actions.CopyAction;
058import org.openstreetmap.josm.actions.JosmAction;
059import org.openstreetmap.josm.command.AddCommand;
060import org.openstreetmap.josm.command.ChangeCommand;
061import org.openstreetmap.josm.command.Command;
062import org.openstreetmap.josm.command.conflict.ConflictAddCommand;
063import org.openstreetmap.josm.data.conflict.Conflict;
064import org.openstreetmap.josm.data.osm.DataSet;
065import org.openstreetmap.josm.data.osm.OsmPrimitive;
066import org.openstreetmap.josm.data.osm.PrimitiveData;
067import org.openstreetmap.josm.data.osm.Relation;
068import org.openstreetmap.josm.data.osm.RelationMember;
069import org.openstreetmap.josm.data.osm.Tag;
070import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
071import org.openstreetmap.josm.gui.DefaultNameFormatter;
072import org.openstreetmap.josm.gui.HelpAwareOptionPane;
073import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
074import org.openstreetmap.josm.gui.MainMenu;
075import org.openstreetmap.josm.gui.SideButton;
076import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
077import org.openstreetmap.josm.gui.help.HelpUtil;
078import org.openstreetmap.josm.gui.layer.OsmDataLayer;
079import org.openstreetmap.josm.gui.tagging.PresetHandler;
080import org.openstreetmap.josm.gui.tagging.TagEditorModel;
081import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
082import org.openstreetmap.josm.gui.tagging.TaggingPreset;
083import org.openstreetmap.josm.gui.tagging.TaggingPresetType;
084import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
085import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
086import org.openstreetmap.josm.io.OnlineResource;
087import org.openstreetmap.josm.tools.CheckParameterUtil;
088import org.openstreetmap.josm.tools.ImageProvider;
089import org.openstreetmap.josm.tools.Shortcut;
090import org.openstreetmap.josm.tools.WindowGeometry;
091
092/**
093 * This dialog is for editing relations.
094 * @since 343
095 */
096public class GenericRelationEditor extends RelationEditor  {
097    /** the tag table and its model */
098    private TagEditorPanel tagEditorPanel;
099    private ReferringRelationsBrowser referrerBrowser;
100    private ReferringRelationsBrowserModel referrerModel;
101
102    /** the member table */
103    private MemberTable memberTable;
104    private MemberTableModel memberTableModel;
105
106    /** the model for the selection table */
107    private SelectionTable selectionTable;
108    private SelectionTableModel selectionTableModel;
109
110    private AutoCompletingTextField tfRole;
111
112    /** the menu item in the windows menu. Required to properly
113     * hide on dialog close.
114     */
115    private JMenuItem windowMenuItem;
116
117    /**
118     * Creates a new relation editor for the given relation. The relation will be saved if the user
119     * selects "ok" in the editor.
120     *
121     * If no relation is given, will create an editor for a new relation.
122     *
123     * @param layer the {@link OsmDataLayer} the new or edited relation belongs to
124     * @param relation relation to edit, or null to create a new one.
125     * @param selectedMembers a collection of members which shall be selected initially
126     */
127    public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) {
128        super(layer, relation, selectedMembers);
129
130        setRememberWindowGeometry(getClass().getName() + ".geometry",
131                WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650)));
132
133        final PresetHandler presetHandler = new PresetHandler() {
134
135            @Override
136            public void updateTags(List<Tag> tags) {
137                tagEditorPanel.getModel().updateTags(tags);
138            }
139
140            @Override
141            public Collection<OsmPrimitive> getSelection() {
142                Relation relation = new Relation();
143                tagEditorPanel.getModel().applyToPrimitive(relation);
144                return Collections.<OsmPrimitive>singletonList(relation);
145            }
146        };
147
148        // init the various models
149        //
150        memberTableModel = new MemberTableModel(getLayer(), presetHandler);
151        memberTableModel.register();
152        selectionTableModel = new SelectionTableModel(getLayer());
153        selectionTableModel.register();
154        referrerModel = new ReferringRelationsBrowserModel(relation);
155
156        tagEditorPanel = new TagEditorPanel(presetHandler);
157
158        // populate the models
159        //
160        if (relation != null) {
161            tagEditorPanel.getModel().initFromPrimitive(relation);
162            this.memberTableModel.populate(relation);
163            if (!getLayer().data.getRelations().contains(relation)) {
164                // treat it as a new relation if it doesn't exist in the
165                // data set yet.
166                setRelation(null);
167            }
168        } else {
169            tagEditorPanel.getModel().clear();
170            this.memberTableModel.populate(null);
171        }
172        tagEditorPanel.getModel().ensureOneTag();
173
174        JSplitPane pane = buildSplitPane(relation);
175        pane.setPreferredSize(new Dimension(100, 100));
176
177        JPanel pnl = new JPanel();
178        pnl.setLayout(new BorderLayout());
179        pnl.add(pane, BorderLayout.CENTER);
180        pnl.setBorder(BorderFactory.createRaisedBevelBorder());
181
182        getContentPane().setLayout(new BorderLayout());
183        JTabbedPane tabbedPane = new JTabbedPane();
184        tabbedPane.add(tr("Tags and Members"), pnl);
185        referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel);
186        tabbedPane.add(tr("Parent Relations"), referrerBrowser);
187        tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation));
188        tabbedPane.addChangeListener(
189                new ChangeListener() {
190                    @Override
191                    public void stateChanged(ChangeEvent e) {
192                        JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource();
193                        int index = sourceTabbedPane.getSelectedIndex();
194                        String title = sourceTabbedPane.getTitleAt(index);
195                        if (title.equals(tr("Parent Relations"))) {
196                            referrerBrowser.init();
197                        }
198                    }
199                }
200        );
201
202        getContentPane().add(buildToolBar(), BorderLayout.NORTH);
203        getContentPane().add(tabbedPane, BorderLayout.CENTER);
204        getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH);
205
206        setSize(findMaxDialogSize());
207
208        addWindowListener(
209                new WindowAdapter() {
210                    @Override
211                    public void windowOpened(WindowEvent e) {
212                        cleanSelfReferences();
213                    }
214                }
215        );
216        registerCopyPasteAction(tagEditorPanel.getPasteAction(),
217                "PASTE_TAGS",
218                Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke());
219        registerCopyPasteAction(new PasteMembersAction(), "PASTE_MEMBERS", Shortcut.getPasteKeyStroke());
220        registerCopyPasteAction(new CopyMembersAction(), "COPY_MEMBERS", Shortcut.getCopyKeyStroke());
221
222        tagEditorPanel.setNextFocusComponent(memberTable);
223        selectionTable.setFocusable(false);
224        memberTableModel.setSelectedMembers(selectedMembers);
225        HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/RelationEditor"));
226    }
227
228    /**
229     * Creates the toolbar
230     *
231     * @return the toolbar
232     */
233    protected JToolBar buildToolBar() {
234        JToolBar tb  = new JToolBar();
235        tb.setFloatable(false);
236        tb.add(new ApplyAction());
237        tb.add(new DuplicateRelationAction());
238        DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction();
239        addPropertyChangeListener(deleteAction);
240        tb.add(deleteAction);
241        return tb;
242    }
243
244    /**
245     * builds the panel with the OK and the Cancel button
246     *
247     * @return the panel with the OK and the Cancel button
248     */
249    protected JPanel buildOkCancelButtonPanel() {
250        JPanel pnl = new JPanel();
251        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
252
253        pnl.add(new SideButton(new OKAction()));
254        pnl.add(new SideButton(new CancelAction()));
255        pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
256        return pnl;
257    }
258
259    /**
260     * builds the panel with the tag editor
261     *
262     * @return the panel with the tag editor
263     */
264    protected JPanel buildTagEditorPanel() {
265        JPanel pnl = new JPanel();
266        pnl.setLayout(new GridBagLayout());
267
268        GridBagConstraints gc = new GridBagConstraints();
269        gc.gridx = 0;
270        gc.gridy = 0;
271        gc.gridheight = 1;
272        gc.gridwidth = 1;
273        gc.fill = GridBagConstraints.HORIZONTAL;
274        gc.anchor = GridBagConstraints.FIRST_LINE_START;
275        gc.weightx = 1.0;
276        gc.weighty = 0.0;
277        pnl.add(new JLabel(tr("Tags")), gc);
278
279        gc.gridx = 0;
280        gc.gridy = 1;
281        gc.fill = GridBagConstraints.BOTH;
282        gc.anchor = GridBagConstraints.CENTER;
283        gc.weightx = 1.0;
284        gc.weighty = 1.0;
285        pnl.add(tagEditorPanel, gc);
286        return pnl;
287    }
288
289    /**
290     * builds the panel for the relation member editor
291     *
292     * @return the panel for the relation member editor
293     */
294    protected JPanel buildMemberEditorPanel() {
295        final JPanel pnl = new JPanel(new GridBagLayout());
296        // setting up the member table
297        memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel);
298        memberTable.addMouseListener(new MemberTableDblClickAdapter());
299        memberTableModel.addMemberModelListener(memberTable);
300
301        final JScrollPane scrollPane = new JScrollPane(memberTable);
302
303        GridBagConstraints gc = new GridBagConstraints();
304        gc.gridx = 0;
305        gc.gridy = 0;
306        gc.gridwidth = 2;
307        gc.fill = GridBagConstraints.HORIZONTAL;
308        gc.anchor = GridBagConstraints.FIRST_LINE_START;
309        gc.weightx = 1.0;
310        gc.weighty = 0.0;
311        pnl.add(new JLabel(tr("Members")), gc);
312
313        gc.gridx = 0;
314        gc.gridy = 1;
315        gc.gridheight = 2;
316        gc.gridwidth = 1;
317        gc.fill = GridBagConstraints.VERTICAL;
318        gc.anchor = GridBagConstraints.NORTHWEST;
319        gc.weightx = 0.0;
320        gc.weighty = 1.0;
321        pnl.add(buildLeftButtonPanel(), gc);
322
323        gc.gridx = 1;
324        gc.gridy = 1;
325        gc.gridheight = 1;
326        gc.fill = GridBagConstraints.BOTH;
327        gc.anchor = GridBagConstraints.CENTER;
328        gc.weightx = 0.6;
329        gc.weighty = 1.0;
330        pnl.add(scrollPane, gc);
331
332        // --- role editing
333        JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
334        p3.add(new JLabel(tr("Apply Role:")));
335        tfRole = new AutoCompletingTextField(10);
336        tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
337        tfRole.addFocusListener(new FocusAdapter() {
338            @Override
339            public void focusGained(FocusEvent e) {
340                tfRole.selectAll();
341            }
342        });
343        tfRole.setAutoCompletionList(new AutoCompletionList());
344        tfRole.addFocusListener(
345                new FocusAdapter() {
346                    @Override
347                    public void focusGained(FocusEvent e) {
348                        AutoCompletionList list = tfRole.getAutoCompletionList();
349                        if (list != null) {
350                            list.clear();
351                            getLayer().data.getAutoCompletionManager().populateWithMemberRoles(list, getRelation());
352                        }
353                    }
354                }
355        );
356        tfRole.setText(Main.pref.get("relation.editor.generic.lastrole", ""));
357        p3.add(tfRole);
358        SetRoleAction setRoleAction = new SetRoleAction();
359        memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction);
360        tfRole.getDocument().addDocumentListener(setRoleAction);
361        tfRole.addActionListener(setRoleAction);
362        memberTableModel.getSelectionModel().addListSelectionListener(
363                new ListSelectionListener() {
364                    @Override
365                    public void valueChanged(ListSelectionEvent e) {
366                        tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
367                    }
368                }
369        );
370        tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
371        SideButton btnApply = new SideButton(setRoleAction);
372        btnApply.setPreferredSize(new Dimension(20,20));
373        btnApply.setText("");
374        p3.add(btnApply);
375
376        gc.gridx = 1;
377        gc.gridy = 2;
378        gc.fill = GridBagConstraints.HORIZONTAL;
379        gc.anchor = GridBagConstraints.LAST_LINE_START;
380        gc.weightx = 1.0;
381        gc.weighty = 0.0;
382        pnl.add(p3, gc);
383
384        JPanel pnl2 = new JPanel();
385        pnl2.setLayout(new GridBagLayout());
386
387        gc.gridx = 0;
388        gc.gridy = 0;
389        gc.gridheight = 1;
390        gc.gridwidth = 3;
391        gc.fill = GridBagConstraints.HORIZONTAL;
392        gc.anchor = GridBagConstraints.FIRST_LINE_START;
393        gc.weightx = 1.0;
394        gc.weighty = 0.0;
395        pnl2.add(new JLabel(tr("Selection")), gc);
396
397        gc.gridx = 0;
398        gc.gridy = 1;
399        gc.gridheight = 1;
400        gc.gridwidth = 1;
401        gc.fill = GridBagConstraints.VERTICAL;
402        gc.anchor = GridBagConstraints.NORTHWEST;
403        gc.weightx = 0.0;
404        gc.weighty = 1.0;
405        pnl2.add(buildSelectionControlButtonPanel(), gc);
406
407        gc.gridx = 1;
408        gc.gridy = 1;
409        gc.weightx = 1.0;
410        gc.weighty = 1.0;
411        gc.fill = GridBagConstraints.BOTH;
412        pnl2.add(buildSelectionTablePanel(), gc);
413
414        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
415        splitPane.setLeftComponent(pnl);
416        splitPane.setRightComponent(pnl2);
417        splitPane.setOneTouchExpandable(false);
418        addWindowListener(new WindowAdapter() {
419            @Override
420            public void windowOpened(WindowEvent e) {
421                // has to be called when the window is visible, otherwise
422                // no effect
423                splitPane.setDividerLocation(0.6);
424            }
425        });
426
427        JPanel pnl3 = new JPanel();
428        pnl3.setLayout(new BorderLayout());
429        pnl3.add(splitPane, BorderLayout.CENTER);
430
431        return pnl3;
432    }
433
434    /**
435     * builds the panel with the table displaying the currently selected primitives
436     *
437     * @return panel with current selection
438     */
439    protected JPanel buildSelectionTablePanel() {
440        JPanel pnl = new JPanel(new BorderLayout());
441        MemberRoleCellEditor ce = (MemberRoleCellEditor)memberTable.getColumnModel().getColumn(0).getCellEditor();
442        selectionTable = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel));
443        selectionTable.setMemberTableModel(memberTableModel);
444        selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height);
445        pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER);
446        return pnl;
447    }
448
449    /**
450     * builds the {@link JSplitPane} which divides the editor in an upper and a lower half
451     *
452     * @return the split panel
453     */
454    protected JSplitPane buildSplitPane(Relation relation) {
455        final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
456        pane.setTopComponent(buildTagEditorPanel());
457        pane.setBottomComponent(buildMemberEditorPanel());
458        pane.setOneTouchExpandable(true);
459        addWindowListener(new WindowAdapter() {
460            @Override
461            public void windowOpened(WindowEvent e) {
462                // has to be called when the window is visible, otherwise
463                // no effect
464                pane.setDividerLocation(0.3);
465            }
466        });
467        return pane;
468    }
469
470    /**
471     * build the panel with the buttons on the left
472     *
473     * @return left button panel
474     */
475    protected JToolBar buildLeftButtonPanel() {
476        JToolBar tb = new JToolBar();
477        tb.setOrientation(JToolBar.VERTICAL);
478        tb.setFloatable(false);
479
480        // -- move up action
481        MoveUpAction moveUpAction = new MoveUpAction();
482        memberTableModel.getSelectionModel().addListSelectionListener(moveUpAction);
483        tb.add(moveUpAction);
484        memberTable.getActionMap().put("moveUp", moveUpAction);
485
486        // -- move down action
487        MoveDownAction moveDownAction = new MoveDownAction();
488        memberTableModel.getSelectionModel().addListSelectionListener(moveDownAction);
489        tb.add(moveDownAction);
490        memberTable.getActionMap().put("moveDown", moveDownAction);
491
492        tb.addSeparator();
493
494        // -- edit action
495        EditAction editAction = new EditAction();
496        memberTableModel.getSelectionModel().addListSelectionListener(editAction);
497        tb.add(editAction);
498
499        // -- delete action
500        RemoveAction removeSelectedAction = new RemoveAction();
501        memberTable.getSelectionModel().addListSelectionListener(removeSelectedAction);
502        tb.add(removeSelectedAction);
503        memberTable.getActionMap().put("removeSelected", removeSelectedAction);
504
505        tb.addSeparator();
506        // -- sort action
507        SortAction sortAction = new SortAction();
508        memberTableModel.addTableModelListener(sortAction);
509        tb.add(sortAction);
510
511        // -- reverse action
512        ReverseAction reverseAction = new ReverseAction();
513        memberTableModel.addTableModelListener(reverseAction);
514        tb.add(reverseAction);
515
516        tb.addSeparator();
517
518        // -- download action
519        DownloadIncompleteMembersAction downloadIncompleteMembersAction = new DownloadIncompleteMembersAction();
520        memberTable.getModel().addTableModelListener(downloadIncompleteMembersAction);
521        tb.add(downloadIncompleteMembersAction);
522        memberTable.getActionMap().put("downloadIncomplete", downloadIncompleteMembersAction);
523
524        // -- download selected action
525        DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction();
526        memberTable.getModel().addTableModelListener(downloadSelectedIncompleteMembersAction);
527        memberTable.getSelectionModel().addListSelectionListener(downloadSelectedIncompleteMembersAction);
528        tb.add(downloadSelectedIncompleteMembersAction);
529
530        InputMap inputMap = memberTable.getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
531        inputMap.put((KeyStroke) removeSelectedAction.getValue(AbstractAction.ACCELERATOR_KEY),"removeSelected");
532        inputMap.put((KeyStroke) moveUpAction.getValue(AbstractAction.ACCELERATOR_KEY),"moveUp");
533        inputMap.put((KeyStroke) moveDownAction.getValue(AbstractAction.ACCELERATOR_KEY),"moveDown");
534        inputMap.put((KeyStroke) downloadIncompleteMembersAction.getValue(AbstractAction.ACCELERATOR_KEY),"downloadIncomplete");
535
536        return tb;
537    }
538
539    /**
540     * build the panel with the buttons for adding or removing the current selection
541     *
542     * @return control buttons panel for selection/members
543     */
544    protected JToolBar buildSelectionControlButtonPanel() {
545        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
546        tb.setFloatable(false);
547
548        // -- add at start action
549        AddSelectedAtStartAction addSelectionAction = new AddSelectedAtStartAction();
550        selectionTableModel.addTableModelListener(addSelectionAction);
551        tb.add(addSelectionAction);
552
553        // -- add before selected action
554        AddSelectedBeforeSelection addSelectedBeforeSelectionAction = new AddSelectedBeforeSelection();
555        selectionTableModel.addTableModelListener(addSelectedBeforeSelectionAction);
556        memberTableModel.getSelectionModel().addListSelectionListener(addSelectedBeforeSelectionAction);
557        tb.add(addSelectedBeforeSelectionAction);
558
559        // -- add after selected action
560        AddSelectedAfterSelection addSelectedAfterSelectionAction = new AddSelectedAfterSelection();
561        selectionTableModel.addTableModelListener(addSelectedAfterSelectionAction);
562        memberTableModel.getSelectionModel().addListSelectionListener(addSelectedAfterSelectionAction);
563        tb.add(addSelectedAfterSelectionAction);
564
565        // -- add at end action
566        AddSelectedAtEndAction addSelectedAtEndAction = new AddSelectedAtEndAction();
567        selectionTableModel.addTableModelListener(addSelectedAtEndAction);
568        tb.add(addSelectedAtEndAction);
569
570        tb.addSeparator();
571
572        // -- select members action
573        SelectedMembersForSelectionAction selectMembersForSelectionAction = new SelectedMembersForSelectionAction();
574        selectionTableModel.addTableModelListener(selectMembersForSelectionAction);
575        memberTableModel.addTableModelListener(selectMembersForSelectionAction);
576        tb.add(selectMembersForSelectionAction);
577
578        // -- select action
579        SelectPrimitivesForSelectedMembersAction selectAction = new SelectPrimitivesForSelectedMembersAction();
580        memberTable.getSelectionModel().addListSelectionListener(selectAction);
581        tb.add(selectAction);
582
583        tb.addSeparator();
584
585        // -- remove selected action
586        RemoveSelectedAction removeSelectedAction = new RemoveSelectedAction();
587        selectionTableModel.addTableModelListener(removeSelectedAction);
588        tb.add(removeSelectedAction);
589
590        return tb;
591    }
592
593    @Override
594    protected Dimension findMaxDialogSize() {
595        return new Dimension(700, 650);
596    }
597
598    @Override
599    public void setVisible(boolean visible) {
600        if (visible) {
601            tagEditorPanel.initAutoCompletion(getLayer());
602        }
603        super.setVisible(visible);
604        if (visible) {
605            RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
606            if(windowMenuItem == null) {
607                addToWindowMenu();
608            }
609            tagEditorPanel.requestFocusInWindow();
610        } else {
611            // make sure all registered listeners are unregistered
612            //
613            memberTable.stopHighlighting();
614            selectionTableModel.unregister();
615            memberTableModel.unregister();
616            memberTable.unlinkAsListener();
617            if(windowMenuItem != null) {
618                Main.main.menu.windowMenu.remove(windowMenuItem);
619                windowMenuItem = null;
620            }
621            dispose();
622        }
623    }
624
625    /** adds current relation editor to the windows menu (in the "volatile" group) o*/
626    protected void addToWindowMenu() {
627        String name = getRelation() == null ? tr("New Relation") : getRelation().getLocalName();
628        final String tt = tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''",
629                name, getLayer().getName());
630        name = tr("Relation Editor: {0}", name == null ? getRelation().getId() : name);
631        final JMenu wm = Main.main.menu.windowMenu;
632        final JosmAction focusAction = new JosmAction(name, "dialogs/relationlist", tt, null, false, false) {
633            @Override
634            public void actionPerformed(ActionEvent e) {
635                final RelationEditor r = (RelationEditor) getValue("relationEditor");
636                r.setVisible(true);
637            }
638        };
639        focusAction.putValue("relationEditor", this);
640        windowMenuItem = MainMenu.add(wm, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
641    }
642
643    /**
644     * checks whether the current relation has members referring to itself. If so,
645     * warns the users and provides an option for removing these members.
646     *
647     */
648    protected void cleanSelfReferences() {
649        List<OsmPrimitive> toCheck = new ArrayList<>();
650        toCheck.add(getRelation());
651        if (memberTableModel.hasMembersReferringTo(toCheck)) {
652            int ret = ConditionalOptionPaneUtil.showOptionDialog(
653                    "clean_relation_self_references",
654                    Main.parent,
655                    tr("<html>There is at least one member in this relation referring<br>"
656                            + "to the relation itself.<br>"
657                            + "This creates circular dependencies and is discouraged.<br>"
658                            + "How do you want to proceed with circular dependencies?</html>"),
659                            tr("Warning"),
660                            JOptionPane.YES_NO_OPTION,
661                            JOptionPane.WARNING_MESSAGE,
662                            new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")},
663                            tr("Remove them, clean up relation")
664            );
665            switch(ret) {
666            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return;
667            case JOptionPane.CLOSED_OPTION: return;
668            case JOptionPane.NO_OPTION: return;
669            case JOptionPane.YES_OPTION:
670                memberTableModel.removeMembersReferringTo(toCheck);
671                break;
672            }
673        }
674    }
675
676
677    private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut) {
678        int mods = shortcut.getModifiers();
679        int code = shortcut.getKeyCode();
680        if (code!=KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) {
681            Main.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut);
682            return;
683        }
684        getRootPane().getActionMap().put(actionName, action);
685        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
686        // Assign also to JTables because they have their own Copy&Paste implementation (which is disabled in this case but eats key shortcuts anyway)
687        memberTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
688        memberTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
689        memberTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
690        selectionTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
691        selectionTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
692        selectionTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
693    }
694
695    static class AddAbortException extends Exception {
696    }
697
698    static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException {
699        String msg = tr("<html>This relation already has one or more members referring to<br>"
700                + "the object ''{0}''<br>"
701                + "<br>"
702                + "Do you really want to add another relation member?</html>",
703                primitive.getDisplayName(DefaultNameFormatter.getInstance())
704            );
705        int ret = ConditionalOptionPaneUtil.showOptionDialog(
706                "add_primitive_to_relation",
707                Main.parent,
708                msg,
709                tr("Multiple members referring to same object."),
710                JOptionPane.YES_NO_CANCEL_OPTION,
711                JOptionPane.WARNING_MESSAGE,
712                null,
713                null
714        );
715        switch(ret) {
716        case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION : return true;
717        case JOptionPane.YES_OPTION: return true;
718        case JOptionPane.NO_OPTION: return false;
719        case JOptionPane.CLOSED_OPTION: return false;
720        case JOptionPane.CANCEL_OPTION: throw new AddAbortException();
721        }
722        // should not happen
723        return false;
724    }
725
726    static void warnOfCircularReferences(OsmPrimitive primitive) {
727        String msg = tr("<html>You are trying to add a relation to itself.<br>"
728                + "<br>"
729                + "This creates circular references and is therefore discouraged.<br>"
730                + "Skipping relation ''{0}''.</html>",
731                primitive.getDisplayName(DefaultNameFormatter.getInstance()));
732        JOptionPane.showMessageDialog(
733                Main.parent,
734                msg,
735                tr("Warning"),
736                JOptionPane.WARNING_MESSAGE);
737    }
738
739    public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd)
740            throws IllegalArgumentException {
741        CheckParameterUtil.ensureParameterNotNull(orig, "orig");
742        try {
743            final Collection<TaggingPreset> presets = TaggingPreset.getMatchingPresets(
744                    EnumSet.of(TaggingPresetType.RELATION), orig.getKeys(), false);
745            Relation relation = new Relation(orig);
746            boolean modified = false;
747            for (OsmPrimitive p : primitivesToAdd) {
748                if (p instanceof Relation && orig.equals(p)) {
749                    warnOfCircularReferences(p);
750                    continue;
751                } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p))
752                        && !confirmAddingPrimitive(p)) {
753                    continue;
754                }
755                final String role = presets.isEmpty() ? null : presets.iterator().next().suggestRoleForOsmPrimitive(p);
756                relation.addMember(new RelationMember(role == null ? "" : role, p));
757                modified = true;
758            }
759            return modified ? new ChangeCommand(orig, relation) : null;
760        } catch (AddAbortException ign) {
761            return null;
762        }
763    }
764
765    abstract class AddFromSelectionAction extends AbstractAction {
766        protected boolean isPotentialDuplicate(OsmPrimitive primitive) {
767            return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive));
768        }
769
770        protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException {
771            if (primitives == null || primitives.isEmpty())
772                return primitives;
773            List<OsmPrimitive> ret = new ArrayList<>();
774            ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation");
775            for (OsmPrimitive primitive : primitives) {
776                if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) {
777                    warnOfCircularReferences(primitive);
778                    continue;
779                }
780                if (isPotentialDuplicate(primitive)) {
781                    if (confirmAddingPrimitive(primitive)) {
782                        ret.add(primitive);
783                    }
784                    continue;
785                } else {
786                    ret.add(primitive);
787                }
788            }
789            ConditionalOptionPaneUtil.endBulkOperation("add_primitive_to_relation");
790            return ret;
791        }
792    }
793
794    class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener {
795        public AddSelectedAtStartAction() {
796            putValue(SHORT_DESCRIPTION,
797                    tr("Add all objects selected in the current dataset before the first member"));
798            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright"));
799            refreshEnabled();
800        }
801
802        protected void refreshEnabled() {
803            setEnabled(selectionTableModel.getRowCount() > 0);
804        }
805
806        @Override
807        public void actionPerformed(ActionEvent e) {
808            try {
809                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
810                memberTableModel.addMembersAtBeginning(toAdd);
811            } catch(AddAbortException ex) {
812                // do nothing
813            }
814        }
815
816        @Override
817        public void tableChanged(TableModelEvent e) {
818            refreshEnabled();
819        }
820    }
821
822    class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener {
823        public AddSelectedAtEndAction() {
824            putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member"));
825            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright"));
826            refreshEnabled();
827        }
828
829        protected void refreshEnabled() {
830            setEnabled(selectionTableModel.getRowCount() > 0);
831        }
832
833        @Override
834        public void actionPerformed(ActionEvent e) {
835            try {
836                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
837                memberTableModel.addMembersAtEnd(toAdd);
838            } catch(AddAbortException ex) {
839                // do nothing
840            }
841        }
842
843        @Override
844        public void tableChanged(TableModelEvent e) {
845            refreshEnabled();
846        }
847    }
848
849    class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
850        public AddSelectedBeforeSelection() {
851            putValue(SHORT_DESCRIPTION,
852                    tr("Add all objects selected in the current dataset before the first selected member"));
853            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright"));
854            refreshEnabled();
855        }
856
857        protected void refreshEnabled() {
858            setEnabled(selectionTableModel.getRowCount() > 0
859                    && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
860        }
861
862        @Override
863        public void actionPerformed(ActionEvent e) {
864            try {
865                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
866                memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel
867                        .getSelectionModel().getMinSelectionIndex());
868            } catch(AddAbortException ex) {
869                // do nothing
870            }
871
872        }
873
874        @Override
875        public void tableChanged(TableModelEvent e) {
876            refreshEnabled();
877        }
878
879        @Override
880        public void valueChanged(ListSelectionEvent e) {
881            refreshEnabled();
882        }
883    }
884
885    class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
886        public AddSelectedAfterSelection() {
887            putValue(SHORT_DESCRIPTION,
888                    tr("Add all objects selected in the current dataset after the last selected member"));
889            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright"));
890            refreshEnabled();
891        }
892
893        protected void refreshEnabled() {
894            setEnabled(selectionTableModel.getRowCount() > 0
895                    && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
896        }
897
898        @Override
899        public void actionPerformed(ActionEvent e) {
900            try {
901                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
902                memberTableModel.addMembersAfterIdx(toAdd, memberTableModel
903                        .getSelectionModel().getMaxSelectionIndex());
904            } catch(AddAbortException ex) {
905                // do nothing
906            }
907        }
908
909        @Override
910        public void tableChanged(TableModelEvent e) {
911            refreshEnabled();
912        }
913
914        @Override
915        public void valueChanged(ListSelectionEvent e) {
916            refreshEnabled();
917        }
918    }
919
920    class RemoveSelectedAction extends AbstractAction implements TableModelListener {
921        public RemoveSelectedAction() {
922            putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects"));
923            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers"));
924            updateEnabledState();
925        }
926
927        protected void updateEnabledState() {
928            DataSet ds = getLayer().data;
929            if (ds == null || ds.getSelected().isEmpty()) {
930                setEnabled(false);
931                return;
932            }
933            // only enable the action if we have members referring to the
934            // selected primitives
935            //
936            setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected()));
937        }
938
939        @Override
940        public void actionPerformed(ActionEvent e) {
941            memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection());
942        }
943
944        @Override
945        public void tableChanged(TableModelEvent e) {
946            updateEnabledState();
947        }
948    }
949
950    /**
951     * Selects  members in the relation editor which refer to primitives in the current
952     * selection of the context layer.
953     *
954     */
955    class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener {
956        public SelectedMembersForSelectionAction() {
957            putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
958            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers"));
959            updateEnabledState();
960        }
961
962        protected void updateEnabledState() {
963            boolean enabled = selectionTableModel.getRowCount() > 0
964            &&  !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty();
965
966            if (enabled) {
967                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection",memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size()));
968            } else {
969                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
970            }
971            setEnabled(enabled);
972        }
973
974        @Override
975        public void actionPerformed(ActionEvent e) {
976            memberTableModel.selectMembersReferringTo(getLayer().data.getSelected());
977        }
978
979        @Override
980        public void tableChanged(TableModelEvent e) {
981            updateEnabledState();
982
983        }
984    }
985
986    /**
987     * Selects primitives in the layer this editor belongs to. The selected primitives are
988     * equal to the set of primitives the currently selected relation members refer to.
989     *
990     */
991    class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener {
992        public SelectPrimitivesForSelectedMembersAction() {
993            putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members"));
994            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives"));
995            updateEnabledState();
996        }
997
998        protected void updateEnabledState() {
999            setEnabled(memberTable.getSelectedRowCount() > 0);
1000        }
1001
1002        @Override
1003        public void actionPerformed(ActionEvent e) {
1004            getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives());
1005        }
1006
1007        @Override
1008        public void valueChanged(ListSelectionEvent e) {
1009            updateEnabledState();
1010        }
1011    }
1012
1013    class SortAction extends AbstractAction implements TableModelListener {
1014        public SortAction() {
1015            String tooltip = tr("Sort the relation members");
1016            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort"));
1017            putValue(NAME, tr("Sort"));
1018            Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"),
1019                KeyEvent.VK_END, Shortcut.ALT);
1020            sc.setAccelerator(this);
1021            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1022            updateEnabledState();
1023        }
1024
1025        @Override
1026        public void actionPerformed(ActionEvent e) {
1027            memberTableModel.sort();
1028        }
1029
1030        protected void updateEnabledState() {
1031            setEnabled(memberTableModel.getRowCount() > 0);
1032        }
1033
1034        @Override
1035        public void tableChanged(TableModelEvent e) {
1036            updateEnabledState();
1037        }
1038    }
1039
1040    class ReverseAction extends AbstractAction implements TableModelListener {
1041        public ReverseAction() {
1042            putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members"));
1043            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse"));
1044            putValue(NAME, tr("Reverse"));
1045        //  Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"),
1046        //      KeyEvent.VK_END, Shortcut.ALT)
1047            updateEnabledState();
1048        }
1049
1050        @Override
1051        public void actionPerformed(ActionEvent e) {
1052            memberTableModel.reverse();
1053        }
1054
1055        protected void updateEnabledState() {
1056            setEnabled(memberTableModel.getRowCount() > 0);
1057        }
1058
1059        @Override
1060        public void tableChanged(TableModelEvent e) {
1061            updateEnabledState();
1062        }
1063    }
1064
1065    class MoveUpAction extends AbstractAction implements ListSelectionListener {
1066        public MoveUpAction() {
1067            String tooltip = tr("Move the currently selected members up");
1068            putValue(SMALL_ICON, ImageProvider.get("dialogs", "moveup"));
1069            Shortcut sc = Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Up"),
1070                KeyEvent.VK_UP, Shortcut.ALT);
1071            sc.setAccelerator(this);
1072            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1073            setEnabled(false);
1074        }
1075
1076        @Override
1077        public void actionPerformed(ActionEvent e) {
1078            memberTableModel.moveUp(memberTable.getSelectedRows());
1079        }
1080
1081        @Override
1082        public void valueChanged(ListSelectionEvent e) {
1083            setEnabled(memberTableModel.canMoveUp(memberTable.getSelectedRows()));
1084        }
1085    }
1086
1087    class MoveDownAction extends AbstractAction implements ListSelectionListener {
1088        public MoveDownAction() {
1089            String tooltip = tr("Move the currently selected members down");
1090            putValue(SMALL_ICON, ImageProvider.get("dialogs", "movedown"));
1091            Shortcut sc = Shortcut.registerShortcut("relationeditor:movedown", tr("Relation Editor: Move Down"),
1092                KeyEvent.VK_DOWN, Shortcut.ALT);
1093            sc.setAccelerator(this);
1094            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1095            setEnabled(false);
1096        }
1097
1098        @Override
1099        public void actionPerformed(ActionEvent e) {
1100            memberTableModel.moveDown(memberTable.getSelectedRows());
1101        }
1102
1103        @Override
1104        public void valueChanged(ListSelectionEvent e) {
1105            setEnabled(memberTableModel.canMoveDown(memberTable.getSelectedRows()));
1106        }
1107    }
1108
1109    class RemoveAction extends AbstractAction implements ListSelectionListener {
1110        public RemoveAction() {
1111            String tooltip = tr("Remove the currently selected members from this relation");
1112            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
1113            putValue(NAME, tr("Remove"));
1114            Shortcut sc = Shortcut.registerShortcut("relationeditor:remove", tr("Relation Editor: Remove"),
1115                KeyEvent.VK_DELETE, Shortcut.ALT);
1116            sc.setAccelerator(this);
1117            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1118            setEnabled(false);
1119        }
1120
1121        @Override
1122        public void actionPerformed(ActionEvent e) {
1123            memberTableModel.remove(memberTable.getSelectedRows());
1124        }
1125
1126        @Override
1127        public void valueChanged(ListSelectionEvent e) {
1128            setEnabled(memberTableModel.canRemove(memberTable.getSelectedRows()));
1129        }
1130    }
1131
1132    class DeleteCurrentRelationAction extends AbstractAction implements PropertyChangeListener{
1133        public DeleteCurrentRelationAction() {
1134            putValue(SHORT_DESCRIPTION, tr("Delete the currently edited relation"));
1135            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
1136            putValue(NAME, tr("Delete"));
1137            updateEnabledState();
1138        }
1139
1140        public void run() {
1141            Relation toDelete = getRelation();
1142            if (toDelete == null)
1143                return;
1144            org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation(
1145                    getLayer(),
1146                    toDelete
1147            );
1148        }
1149
1150        @Override
1151        public void actionPerformed(ActionEvent e) {
1152            run();
1153        }
1154
1155        protected void updateEnabledState() {
1156            setEnabled(getRelationSnapshot() != null);
1157        }
1158
1159        @Override
1160        public void propertyChange(PropertyChangeEvent evt) {
1161            if (evt.getPropertyName().equals(RELATION_SNAPSHOT_PROP)) {
1162                updateEnabledState();
1163            }
1164        }
1165    }
1166
1167    abstract class SavingAction extends AbstractAction {
1168        /**
1169         * apply updates to a new relation
1170         */
1171        protected void applyNewRelation() {
1172            final Relation newRelation = new Relation();
1173            tagEditorPanel.getModel().applyToPrimitive(newRelation);
1174            memberTableModel.applyToRelation(newRelation);
1175            List<RelationMember> newMembers = new ArrayList<>();
1176            for (RelationMember rm: newRelation.getMembers()) {
1177                if (!rm.getMember().isDeleted()) {
1178                    newMembers.add(rm);
1179                }
1180            }
1181            if (newRelation.getMembersCount() != newMembers.size()) {
1182                newRelation.setMembers(newMembers);
1183                String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" +
1184                "was open. They have been removed from the relation members list.");
1185                JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1186            }
1187            // If the user wanted to create a new relation, but hasn't added any members or
1188            // tags, don't add an empty relation
1189            if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys())
1190                return;
1191            Main.main.undoRedo.add(new AddCommand(getLayer(),newRelation));
1192
1193            // make sure everybody is notified about the changes
1194            //
1195            getLayer().data.fireSelectionChanged();
1196            GenericRelationEditor.this.setRelation(newRelation);
1197            RelationDialogManager.getRelationDialogManager().updateContext(
1198                    getLayer(),
1199                    getRelation(),
1200                    GenericRelationEditor.this
1201            );
1202            SwingUtilities.invokeLater(new Runnable() {
1203                @Override
1204                public void run() {
1205                    // Relation list gets update in EDT so selecting my be postponed to following EDT run
1206                    Main.map.relationListDialog.selectRelation(newRelation);
1207                }
1208            });
1209        }
1210
1211        /**
1212         * Apply the updates for an existing relation which has been changed
1213         * outside of the relation editor.
1214         *
1215         */
1216        protected void applyExistingConflictingRelation() {
1217            Relation editedRelation = new Relation(getRelation());
1218            tagEditorPanel.getModel().applyToPrimitive(editedRelation);
1219            memberTableModel.applyToRelation(editedRelation);
1220            Conflict<Relation> conflict = new Conflict<>(getRelation(), editedRelation);
1221            Main.main.undoRedo.add(new ConflictAddCommand(getLayer(),conflict));
1222        }
1223
1224        /**
1225         * Apply the updates for an existing relation which has not been changed
1226         * outside of the relation editor.
1227         *
1228         */
1229        protected void applyExistingNonConflictingRelation() {
1230            Relation editedRelation = new Relation(getRelation());
1231            tagEditorPanel.getModel().applyToPrimitive(editedRelation);
1232            memberTableModel.applyToRelation(editedRelation);
1233            Main.main.undoRedo.add(new ChangeCommand(getRelation(), editedRelation));
1234            getLayer().data.fireSelectionChanged();
1235            // this will refresh the snapshot and update the dialog title
1236            //
1237            setRelation(getRelation());
1238        }
1239
1240        protected boolean confirmClosingBecauseOfDirtyState() {
1241            ButtonSpec [] options = new ButtonSpec[] {
1242                    new ButtonSpec(
1243                            tr("Yes, create a conflict and close"),
1244                            ImageProvider.get("ok"),
1245                            tr("Click to create a conflict and close this relation editor") ,
1246                            null /* no specific help topic */
1247                    ),
1248                    new ButtonSpec(
1249                            tr("No, continue editing"),
1250                            ImageProvider.get("cancel"),
1251                            tr("Click to return to the relation editor and to resume relation editing") ,
1252                            null /* no specific help topic */
1253                    )
1254            };
1255
1256            int ret = HelpAwareOptionPane.showOptionDialog(
1257                    Main.parent,
1258                    tr("<html>This relation has been changed outside of the editor.<br>"
1259                            + "You cannot apply your changes and continue editing.<br>"
1260                            + "<br>"
1261                            + "Do you want to create a conflict and close the editor?</html>"),
1262                            tr("Conflict in data"),
1263                            JOptionPane.WARNING_MESSAGE,
1264                            null,
1265                            options,
1266                            options[0], // OK is default
1267                            "/Dialog/RelationEditor#RelationChangedOutsideOfEditor"
1268            );
1269            return ret == 0;
1270        }
1271
1272        protected void warnDoubleConflict() {
1273            JOptionPane.showMessageDialog(
1274                    Main.parent,
1275                    tr("<html>Layer ''{0}'' already has a conflict for object<br>"
1276                            + "''{1}''.<br>"
1277                            + "Please resolve this conflict first, then try again.</html>",
1278                            getLayer().getName(),
1279                            getRelation().getDisplayName(DefaultNameFormatter.getInstance())
1280                    ),
1281                    tr("Double conflict"),
1282                    JOptionPane.WARNING_MESSAGE
1283            );
1284        }
1285    }
1286
1287    class ApplyAction extends SavingAction {
1288        public ApplyAction() {
1289            putValue(SHORT_DESCRIPTION, tr("Apply the current updates"));
1290            putValue(SMALL_ICON, ImageProvider.get("save"));
1291            putValue(NAME, tr("Apply"));
1292            setEnabled(true);
1293        }
1294
1295        public void run() {
1296            if (getRelation() == null) {
1297                applyNewRelation();
1298            } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
1299                    || tagEditorPanel.getModel().isDirty()) {
1300                if (isDirtyRelation()) {
1301                    if (confirmClosingBecauseOfDirtyState()) {
1302                        if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
1303                            warnDoubleConflict();
1304                            return;
1305                        }
1306                        applyExistingConflictingRelation();
1307                        setVisible(false);
1308                    }
1309                } else {
1310                    applyExistingNonConflictingRelation();
1311                }
1312            }
1313        }
1314
1315        @Override
1316        public void actionPerformed(ActionEvent e) {
1317            run();
1318        }
1319    }
1320
1321    class OKAction extends SavingAction {
1322        public OKAction() {
1323            putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog"));
1324            putValue(SMALL_ICON, ImageProvider.get("ok"));
1325            putValue(NAME, tr("OK"));
1326            setEnabled(true);
1327        }
1328
1329        public void run() {
1330            Main.pref.put("relation.editor.generic.lastrole", tfRole.getText());
1331            memberTable.stopHighlighting();
1332            if (getRelation() == null) {
1333                applyNewRelation();
1334            } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
1335                    || tagEditorPanel.getModel().isDirty()) {
1336                if (isDirtyRelation()) {
1337                    if (confirmClosingBecauseOfDirtyState()) {
1338                        if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
1339                            warnDoubleConflict();
1340                            return;
1341                        }
1342                        applyExistingConflictingRelation();
1343                    } else
1344                        return;
1345                } else {
1346                    applyExistingNonConflictingRelation();
1347                }
1348            }
1349            setVisible(false);
1350        }
1351
1352        @Override
1353        public void actionPerformed(ActionEvent e) {
1354            run();
1355        }
1356    }
1357
1358    class CancelAction extends SavingAction {
1359        public CancelAction() {
1360            putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog"));
1361            putValue(SMALL_ICON, ImageProvider.get("cancel"));
1362            putValue(NAME, tr("Cancel"));
1363
1364            getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
1365            .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
1366            getRootPane().getActionMap().put("ESCAPE", this);
1367            setEnabled(true);
1368        }
1369
1370        @Override
1371        public void actionPerformed(ActionEvent e) {
1372            memberTable.stopHighlighting();
1373            TagEditorModel tagModel = tagEditorPanel.getModel();
1374            Relation snapshot = getRelationSnapshot();
1375            if ( (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty())
1376             && !(snapshot == null && tagModel.getTags().isEmpty())) {
1377                //give the user a chance to save the changes
1378                int ret = confirmClosingByCancel();
1379                if (ret == 0) { //Yes, save the changes
1380                    //copied from OKAction.run()
1381                    Main.pref.put("relation.editor.generic.lastrole", tfRole.getText());
1382                    if (getRelation() == null) {
1383                        applyNewRelation();
1384                    } else if (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty()) {
1385                        if (isDirtyRelation()) {
1386                            if (confirmClosingBecauseOfDirtyState()) {
1387                                if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
1388                                    warnDoubleConflict();
1389                                    return;
1390                                }
1391                                applyExistingConflictingRelation();
1392                            } else
1393                                return;
1394                        } else {
1395                            applyExistingNonConflictingRelation();
1396                        }
1397                    }
1398                } else if (ret == 2) //Cancel, continue editing
1399                    return;
1400                //in case of "No, discard", there is no extra action to be performed here.
1401            }
1402            setVisible(false);
1403        }
1404
1405        protected int confirmClosingByCancel() {
1406            ButtonSpec [] options = new ButtonSpec[] {
1407                    new ButtonSpec(
1408                            tr("Yes, save the changes and close"),
1409                            ImageProvider.get("ok"),
1410                            tr("Click to save the changes and close this relation editor") ,
1411                            null /* no specific help topic */
1412                    ),
1413                    new ButtonSpec(
1414                            tr("No, discard the changes and close"),
1415                            ImageProvider.get("cancel"),
1416                            tr("Click to discard the changes and close this relation editor") ,
1417                            null /* no specific help topic */
1418                    ),
1419                    new ButtonSpec(
1420                            tr("Cancel, continue editing"),
1421                            ImageProvider.get("cancel"),
1422                            tr("Click to return to the relation editor and to resume relation editing") ,
1423                            null /* no specific help topic */
1424                    )
1425            };
1426
1427            return HelpAwareOptionPane.showOptionDialog(
1428                    Main.parent,
1429                    tr("<html>The relation has been changed.<br>"
1430                            + "<br>"
1431                            + "Do you want to save your changes?</html>"),
1432                            tr("Unsaved changes"),
1433                            JOptionPane.WARNING_MESSAGE,
1434                            null,
1435                            options,
1436                            options[0], // OK is default,
1437                            "/Dialog/RelationEditor#DiscardChanges"
1438            );
1439        }
1440    }
1441
1442    class AddTagAction extends AbstractAction {
1443        public AddTagAction() {
1444            putValue(SHORT_DESCRIPTION, tr("Add an empty tag"));
1445            putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
1446            setEnabled(true);
1447        }
1448
1449        @Override
1450        public void actionPerformed(ActionEvent e) {
1451            tagEditorPanel.getModel().appendNewTag();
1452        }
1453    }
1454
1455    class DownloadIncompleteMembersAction extends AbstractAction implements TableModelListener {
1456        public DownloadIncompleteMembersAction() {
1457            String tooltip = tr("Download all incomplete members");
1458            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincomplete"));
1459            putValue(NAME, tr("Download Members"));
1460            Shortcut sc = Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"),
1461                KeyEvent.VK_HOME, Shortcut.ALT);
1462            sc.setAccelerator(this);
1463            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1464            updateEnabledState();
1465        }
1466
1467        @Override
1468        public void actionPerformed(ActionEvent e) {
1469            if (!isEnabled())
1470                return;
1471            Main.worker.submit(new DownloadRelationMemberTask(
1472                    getRelation(),
1473                    memberTableModel.getIncompleteMemberPrimitives(),
1474                    getLayer(),
1475                    GenericRelationEditor.this)
1476            );
1477        }
1478
1479        protected void updateEnabledState() {
1480            setEnabled(memberTableModel.hasIncompleteMembers() && !Main.isOffline(OnlineResource.OSM_API));
1481        }
1482
1483        @Override
1484        public void tableChanged(TableModelEvent e) {
1485            updateEnabledState();
1486        }
1487    }
1488
1489    class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener, TableModelListener{
1490        public DownloadSelectedIncompleteMembersAction() {
1491            putValue(SHORT_DESCRIPTION, tr("Download selected incomplete members"));
1492            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected"));
1493            putValue(NAME, tr("Download Members"));
1494        //  Shortcut.register Shortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"),
1495        //      KeyEvent.VK_K, Shortcut.ALT)
1496            updateEnabledState();
1497        }
1498
1499        @Override
1500        public void actionPerformed(ActionEvent e) {
1501            if (!isEnabled())
1502                return;
1503            Main.worker.submit(new DownloadRelationMemberTask(
1504                    getRelation(),
1505                    memberTableModel.getSelectedIncompleteMemberPrimitives(),
1506                    getLayer(),
1507                    GenericRelationEditor.this)
1508            );
1509        }
1510
1511        protected void updateEnabledState() {
1512            setEnabled(memberTableModel.hasIncompleteSelectedMembers() && !Main.isOffline(OnlineResource.OSM_API));
1513        }
1514
1515        @Override
1516        public void valueChanged(ListSelectionEvent e) {
1517            updateEnabledState();
1518        }
1519
1520        @Override
1521        public void tableChanged(TableModelEvent e) {
1522            updateEnabledState();
1523        }
1524    }
1525
1526    class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener {
1527        public SetRoleAction() {
1528            putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members"));
1529            putValue(SMALL_ICON, ImageProvider.get("apply"));
1530            putValue(NAME, tr("Apply Role"));
1531            refreshEnabled();
1532        }
1533
1534        protected void refreshEnabled() {
1535            setEnabled(memberTable.getSelectedRowCount() > 0);
1536        }
1537
1538        protected boolean isEmptyRole() {
1539            return tfRole.getText() == null || tfRole.getText().trim().isEmpty();
1540        }
1541
1542        protected boolean confirmSettingEmptyRole(int onNumMembers) {
1543            String message = "<html>"
1544                + trn("You are setting an empty role on {0} object.",
1545                        "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers)
1546                        + "<br>"
1547                        + tr("This is equal to deleting the roles of these objects.") +
1548                        "<br>"
1549                        + tr("Do you really want to apply the new role?") + "</html>";
1550            String [] options = new String[] {
1551                    tr("Yes, apply it"),
1552                    tr("No, do not apply")
1553            };
1554            int ret = ConditionalOptionPaneUtil.showOptionDialog(
1555                    "relation_editor.confirm_applying_empty_role",
1556                    Main.parent,
1557                    message,
1558                    tr("Confirm empty role"),
1559                    JOptionPane.YES_NO_OPTION,
1560                    JOptionPane.WARNING_MESSAGE,
1561                    options,
1562                    options[0]
1563            );
1564            switch(ret) {
1565            case JOptionPane.YES_OPTION: return true;
1566            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return true;
1567            default:
1568                return false;
1569            }
1570        }
1571
1572        @Override
1573        public void actionPerformed(ActionEvent e) {
1574            if (isEmptyRole()) {
1575                if (! confirmSettingEmptyRole(memberTable.getSelectedRowCount()))
1576                    return;
1577            }
1578            memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText());
1579        }
1580
1581        @Override
1582        public void valueChanged(ListSelectionEvent e) {
1583            refreshEnabled();
1584        }
1585
1586        @Override
1587        public void changedUpdate(DocumentEvent e) {
1588            refreshEnabled();
1589        }
1590
1591        @Override
1592        public void insertUpdate(DocumentEvent e) {
1593            refreshEnabled();
1594        }
1595
1596        @Override
1597        public void removeUpdate(DocumentEvent e) {
1598            refreshEnabled();
1599        }
1600    }
1601
1602    /**
1603     * Creates a new relation with a copy of the current editor state.
1604     */
1605    class DuplicateRelationAction extends AbstractAction {
1606        public DuplicateRelationAction() {
1607            putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window"));
1608            // FIXME provide an icon
1609            putValue(SMALL_ICON, ImageProvider.get("duplicate"));
1610            putValue(NAME, tr("Duplicate"));
1611            setEnabled(true);
1612        }
1613
1614        @Override
1615        public void actionPerformed(ActionEvent e) {
1616            Relation copy = new Relation();
1617            tagEditorPanel.getModel().applyToPrimitive(copy);
1618            memberTableModel.applyToRelation(copy);
1619            RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers());
1620            editor.setVisible(true);
1621        }
1622    }
1623
1624    /**
1625     * Action for editing the currently selected relation.
1626     */
1627    class EditAction extends AbstractAction implements ListSelectionListener {
1628        public EditAction() {
1629            putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to"));
1630            putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
1631            refreshEnabled();
1632        }
1633
1634        protected void refreshEnabled() {
1635            setEnabled(memberTable.getSelectedRowCount() == 1
1636                    && memberTableModel.isEditableRelation(memberTable.getSelectedRow()));
1637        }
1638
1639        protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) {
1640            Collection<RelationMember> members = new HashSet<>();
1641            Collection<OsmPrimitive> selection = getLayer().data.getSelected();
1642            for (RelationMember member: r.getMembers()) {
1643                if (selection.contains(member.getMember())) {
1644                    members.add(member);
1645                }
1646            }
1647            return members;
1648        }
1649
1650        public void run() {
1651            int idx = memberTable.getSelectedRow();
1652            if (idx < 0)
1653                return;
1654            OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx);
1655            if (!(primitive instanceof Relation))
1656                return;
1657            Relation r = (Relation) primitive;
1658            if (r.isIncomplete())
1659                return;
1660
1661            RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r));
1662            editor.setVisible(true);
1663        }
1664
1665        @Override
1666        public void actionPerformed(ActionEvent e) {
1667            if (!isEnabled())
1668                return;
1669            run();
1670        }
1671
1672        @Override
1673        public void valueChanged(ListSelectionEvent e) {
1674            refreshEnabled();
1675        }
1676    }
1677
1678    class PasteMembersAction extends AddFromSelectionAction {
1679
1680        @Override
1681        public void actionPerformed(ActionEvent e) {
1682            try {
1683                List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded();
1684                DataSet ds = getLayer().data;
1685                List<OsmPrimitive> toAdd = new ArrayList<>();
1686                boolean hasNewInOtherLayer = false;
1687
1688                for (PrimitiveData primitive: primitives) {
1689                    OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive);
1690                    if (primitiveInDs != null) {
1691                        toAdd.add(primitiveInDs);
1692                    } else if (!primitive.isNew()) {
1693                        OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true);
1694                        ds.addPrimitive(p);
1695                        toAdd.add(p);
1696                    } else {
1697                        hasNewInOtherLayer = true;
1698                        break;
1699                    }
1700                }
1701
1702                if (hasNewInOtherLayer) {
1703                    JOptionPane.showMessageDialog(Main.parent, tr("Members from paste buffer cannot be added because they are not included in current layer"));
1704                    return;
1705                }
1706
1707                toAdd = filterConfirmedPrimitives(toAdd);
1708                int index = memberTableModel.getSelectionModel().getMaxSelectionIndex();
1709                if (index == -1) {
1710                    index = memberTableModel.getRowCount() - 1;
1711                }
1712                memberTableModel.addMembersAfterIdx(toAdd, index);
1713
1714                tfRole.requestFocusInWindow();
1715
1716            } catch (AddAbortException ex) {
1717                // Do nothing
1718            }
1719        }
1720    }
1721
1722    class CopyMembersAction extends AbstractAction {
1723        @Override
1724        public void actionPerformed(ActionEvent e) {
1725            Set<OsmPrimitive> primitives = new HashSet<>();
1726            for (RelationMember rm: memberTableModel.getSelectedMembers()) {
1727                primitives.add(rm.getMember());
1728            }
1729            if (!primitives.isEmpty()) {
1730                CopyAction.copy(getLayer(), primitives);
1731            }
1732        }
1733    }
1734
1735    class MemberTableDblClickAdapter extends MouseAdapter {
1736        @Override
1737        public void mouseClicked(MouseEvent e) {
1738            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
1739                new EditAction().run();
1740            }
1741        }
1742    }
1743}