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