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.io.OnlineResource;
089import org.openstreetmap.josm.tools.CheckParameterUtil;
090import org.openstreetmap.josm.tools.ImageProvider;
091import org.openstreetmap.josm.tools.Shortcut;
092import org.openstreetmap.josm.tools.WindowGeometry;
093
094/**
095 * This dialog is for editing relations.
096 * @since 343
097 */
098public class GenericRelationEditor extends RelationEditor  {
099    /** the tag table and its model */
100    private TagEditorPanel tagEditorPanel;
101    private ReferringRelationsBrowser referrerBrowser;
102    private ReferringRelationsBrowserModel referrerModel;
103
104    /** the member table */
105    private MemberTable memberTable;
106    private MemberTableModel memberTableModel;
107
108    /** the model for the selection table */
109    private SelectionTable selectionTable;
110    private SelectionTableModel selectionTableModel;
111
112    private AutoCompletingTextField tfRole;
113
114    /** the menu item in the windows menu. Required to properly
115     * hide on dialog close.
116     */
117    private JMenuItem windowMenuItem;
118    /**
119     * Button for performing the {@link org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor.SortBelowAction}.
120     */
121    private JButton sortBelowButton;
122
123    /**
124     * Creates a new relation editor for the given relation. The relation will be saved if the user
125     * selects "ok" in the editor.
126     *
127     * If no relation is given, will create an editor for a new relation.
128     *
129     * @param layer the {@link OsmDataLayer} the new or edited relation belongs to
130     * @param relation relation to edit, or null to create a new one.
131     * @param selectedMembers a collection of members which shall be selected initially
132     */
133    public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) {
134        super(layer, relation, selectedMembers);
135
136        setRememberWindowGeometry(getClass().getName() + ".geometry",
137                WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650)));
138
139        final TaggingPresetHandler presetHandler = new TaggingPresetHandler() {
140
141            @Override
142            public void updateTags(List<Tag> tags) {
143                tagEditorPanel.getModel().updateTags(tags);
144            }
145
146            @Override
147            public Collection<OsmPrimitive> getSelection() {
148                Relation relation = new Relation();
149                tagEditorPanel.getModel().applyToPrimitive(relation);
150                return Collections.<OsmPrimitive>singletonList(relation);
151            }
152        };
153
154        // init the various models
155        //
156        memberTableModel = new MemberTableModel(getLayer(), presetHandler);
157        memberTableModel.register();
158        selectionTableModel = new SelectionTableModel(getLayer());
159        selectionTableModel.register();
160        referrerModel = new ReferringRelationsBrowserModel(relation);
161
162        tagEditorPanel = new TagEditorPanel(presetHandler);
163
164        // populate the models
165        //
166        if (relation != null) {
167            tagEditorPanel.getModel().initFromPrimitive(relation);
168            this.memberTableModel.populate(relation);
169            if (!getLayer().data.getRelations().contains(relation)) {
170                // treat it as a new relation if it doesn't exist in the
171                // data set yet.
172                setRelation(null);
173            }
174        } else {
175            tagEditorPanel.getModel().clear();
176            this.memberTableModel.populate(null);
177        }
178        tagEditorPanel.getModel().ensureOneTag();
179
180        JSplitPane pane = buildSplitPane();
181        pane.setPreferredSize(new Dimension(100, 100));
182
183        JPanel pnl = new JPanel();
184        pnl.setLayout(new BorderLayout());
185        pnl.add(pane, BorderLayout.CENTER);
186        pnl.setBorder(BorderFactory.createRaisedBevelBorder());
187
188        getContentPane().setLayout(new BorderLayout());
189        JTabbedPane tabbedPane = new JTabbedPane();
190        tabbedPane.add(tr("Tags and Members"), pnl);
191        referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel);
192        tabbedPane.add(tr("Parent Relations"), referrerBrowser);
193        tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation));
194        tabbedPane.addChangeListener(
195                new ChangeListener() {
196                    @Override
197                    public void stateChanged(ChangeEvent e) {
198                        JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource();
199                        int index = sourceTabbedPane.getSelectedIndex();
200                        String title = sourceTabbedPane.getTitleAt(index);
201                        if (title.equals(tr("Parent Relations"))) {
202                            referrerBrowser.init();
203                        }
204                    }
205                }
206        );
207
208        getContentPane().add(buildToolBar(), BorderLayout.NORTH);
209        getContentPane().add(tabbedPane, BorderLayout.CENTER);
210        getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH);
211
212        setSize(findMaxDialogSize());
213
214        addWindowListener(
215                new WindowAdapter() {
216                    @Override
217                    public void windowOpened(WindowEvent e) {
218                        cleanSelfReferences();
219                    }
220                }
221        );
222        registerCopyPasteAction(tagEditorPanel.getPasteAction(),
223                "PASTE_TAGS",
224                // CHECKSTYLE.OFF: LineLength
225                Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke());
226                // CHECKSTYLE.ON: LineLength
227        registerCopyPasteAction(new PasteMembersAction(), "PASTE_MEMBERS", Shortcut.getPasteKeyStroke());
228        registerCopyPasteAction(new CopyMembersAction(), "COPY_MEMBERS", Shortcut.getCopyKeyStroke());
229
230        tagEditorPanel.setNextFocusComponent(memberTable);
231        selectionTable.setFocusable(false);
232        memberTableModel.setSelectedMembers(selectedMembers);
233        HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/RelationEditor"));
234    }
235
236    /**
237     * Creates the toolbar
238     *
239     * @return the toolbar
240     */
241    protected JToolBar buildToolBar() {
242        JToolBar tb  = new JToolBar();
243        tb.setFloatable(false);
244        tb.add(new ApplyAction());
245        tb.add(new DuplicateRelationAction());
246        DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction();
247        addPropertyChangeListener(deleteAction);
248        tb.add(deleteAction);
249        return tb;
250    }
251
252    /**
253     * builds the panel with the OK and the Cancel button
254     *
255     * @return the panel with the OK and the Cancel button
256     */
257    protected JPanel buildOkCancelButtonPanel() {
258        JPanel pnl = new JPanel();
259        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
260
261        pnl.add(new SideButton(new OKAction()));
262        pnl.add(new SideButton(new CancelAction()));
263        pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
264        return pnl;
265    }
266
267    /**
268     * builds the panel with the tag editor
269     *
270     * @return the panel with the tag editor
271     */
272    protected JPanel buildTagEditorPanel() {
273        JPanel pnl = new JPanel();
274        pnl.setLayout(new GridBagLayout());
275
276        GridBagConstraints gc = new GridBagConstraints();
277        gc.gridx = 0;
278        gc.gridy = 0;
279        gc.gridheight = 1;
280        gc.gridwidth = 1;
281        gc.fill = GridBagConstraints.HORIZONTAL;
282        gc.anchor = GridBagConstraints.FIRST_LINE_START;
283        gc.weightx = 1.0;
284        gc.weighty = 0.0;
285        pnl.add(new JLabel(tr("Tags")), gc);
286
287        gc.gridx = 0;
288        gc.gridy = 1;
289        gc.fill = GridBagConstraints.BOTH;
290        gc.anchor = GridBagConstraints.CENTER;
291        gc.weightx = 1.0;
292        gc.weighty = 1.0;
293        pnl.add(tagEditorPanel, gc);
294        return pnl;
295    }
296
297    /**
298     * builds the panel for the relation member editor
299     *
300     * @return the panel for the relation member editor
301     */
302    protected JPanel buildMemberEditorPanel() {
303        final JPanel pnl = new JPanel(new GridBagLayout());
304        // setting up the member table
305        memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel);
306        memberTable.addMouseListener(new MemberTableDblClickAdapter());
307        memberTableModel.addMemberModelListener(memberTable);
308
309        final JScrollPane scrollPane = new JScrollPane(memberTable);
310
311        GridBagConstraints gc = new GridBagConstraints();
312        gc.gridx = 0;
313        gc.gridy = 0;
314        gc.gridwidth = 2;
315        gc.fill = GridBagConstraints.HORIZONTAL;
316        gc.anchor = GridBagConstraints.FIRST_LINE_START;
317        gc.weightx = 1.0;
318        gc.weighty = 0.0;
319        pnl.add(new JLabel(tr("Members")), gc);
320
321        gc.gridx = 0;
322        gc.gridy = 1;
323        gc.gridheight = 2;
324        gc.gridwidth = 1;
325        gc.fill = GridBagConstraints.VERTICAL;
326        gc.anchor = GridBagConstraints.NORTHWEST;
327        gc.weightx = 0.0;
328        gc.weighty = 1.0;
329        pnl.add(buildLeftButtonPanel(), gc);
330
331        gc.gridx = 1;
332        gc.gridy = 1;
333        gc.gridheight = 1;
334        gc.fill = GridBagConstraints.BOTH;
335        gc.anchor = GridBagConstraints.CENTER;
336        gc.weightx = 0.6;
337        gc.weighty = 1.0;
338        pnl.add(scrollPane, gc);
339
340        // --- role editing
341        JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
342        p3.add(new JLabel(tr("Apply Role:")));
343        tfRole = new AutoCompletingTextField(10);
344        tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
345        tfRole.addFocusListener(new FocusAdapter() {
346            @Override
347            public void focusGained(FocusEvent e) {
348                tfRole.selectAll();
349            }
350        });
351        tfRole.setAutoCompletionList(new AutoCompletionList());
352        tfRole.addFocusListener(
353                new FocusAdapter() {
354                    @Override
355                    public void focusGained(FocusEvent e) {
356                        AutoCompletionList list = tfRole.getAutoCompletionList();
357                        if (list != null) {
358                            list.clear();
359                            getLayer().data.getAutoCompletionManager().populateWithMemberRoles(list, getRelation());
360                        }
361                    }
362                }
363        );
364        tfRole.setText(Main.pref.get("relation.editor.generic.lastrole", ""));
365        p3.add(tfRole);
366        SetRoleAction setRoleAction = new SetRoleAction();
367        memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction);
368        tfRole.getDocument().addDocumentListener(setRoleAction);
369        tfRole.addActionListener(setRoleAction);
370        memberTableModel.getSelectionModel().addListSelectionListener(
371                new ListSelectionListener() {
372                    @Override
373                    public void valueChanged(ListSelectionEvent e) {
374                        tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
375                    }
376                }
377        );
378        tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
379        SideButton btnApply = new SideButton(setRoleAction);
380        btnApply.setPreferredSize(new Dimension(20, 20));
381        btnApply.setText("");
382        p3.add(btnApply);
383
384        gc.gridx = 1;
385        gc.gridy = 2;
386        gc.fill = GridBagConstraints.HORIZONTAL;
387        gc.anchor = GridBagConstraints.LAST_LINE_START;
388        gc.weightx = 1.0;
389        gc.weighty = 0.0;
390        pnl.add(p3, gc);
391
392        JPanel pnl2 = new JPanel();
393        pnl2.setLayout(new GridBagLayout());
394
395        gc.gridx = 0;
396        gc.gridy = 0;
397        gc.gridheight = 1;
398        gc.gridwidth = 3;
399        gc.fill = GridBagConstraints.HORIZONTAL;
400        gc.anchor = GridBagConstraints.FIRST_LINE_START;
401        gc.weightx = 1.0;
402        gc.weighty = 0.0;
403        pnl2.add(new JLabel(tr("Selection")), gc);
404
405        gc.gridx = 0;
406        gc.gridy = 1;
407        gc.gridheight = 1;
408        gc.gridwidth = 1;
409        gc.fill = GridBagConstraints.VERTICAL;
410        gc.anchor = GridBagConstraints.NORTHWEST;
411        gc.weightx = 0.0;
412        gc.weighty = 1.0;
413        pnl2.add(buildSelectionControlButtonPanel(), gc);
414
415        gc.gridx = 1;
416        gc.gridy = 1;
417        gc.weightx = 1.0;
418        gc.weighty = 1.0;
419        gc.fill = GridBagConstraints.BOTH;
420        pnl2.add(buildSelectionTablePanel(), gc);
421
422        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
423        splitPane.setLeftComponent(pnl);
424        splitPane.setRightComponent(pnl2);
425        splitPane.setOneTouchExpandable(false);
426        addWindowListener(new WindowAdapter() {
427            @Override
428            public void windowOpened(WindowEvent e) {
429                // has to be called when the window is visible, otherwise
430                // no effect
431                splitPane.setDividerLocation(0.6);
432            }
433        });
434
435        JPanel pnl3 = new JPanel();
436        pnl3.setLayout(new BorderLayout());
437        pnl3.add(splitPane, BorderLayout.CENTER);
438
439        return pnl3;
440    }
441
442    /**
443     * builds the panel with the table displaying the currently selected primitives
444     *
445     * @return panel with current selection
446     */
447    protected JPanel buildSelectionTablePanel() {
448        JPanel pnl = new JPanel(new BorderLayout());
449        MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor();
450        selectionTable = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel));
451        selectionTable.setMemberTableModel(memberTableModel);
452        selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height);
453        pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER);
454        return pnl;
455    }
456
457    /**
458     * builds the {@link JSplitPane} which divides the editor in an upper and a lower half
459     *
460     * @return the split panel
461     */
462    protected JSplitPane buildSplitPane() {
463        final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
464        pane.setTopComponent(buildTagEditorPanel());
465        pane.setBottomComponent(buildMemberEditorPanel());
466        pane.setOneTouchExpandable(true);
467        addWindowListener(new WindowAdapter() {
468            @Override
469            public void windowOpened(WindowEvent e) {
470                // has to be called when the window is visible, otherwise no effect
471                pane.setDividerLocation(0.3);
472            }
473        });
474        return pane;
475    }
476
477    /**
478     * build the panel with the buttons on the left
479     *
480     * @return left button panel
481     */
482    protected JToolBar buildLeftButtonPanel() {
483        JToolBar tb = new JToolBar();
484        tb.setOrientation(JToolBar.VERTICAL);
485        tb.setFloatable(false);
486
487        // -- move up action
488        MoveUpAction moveUpAction = new MoveUpAction();
489        memberTableModel.getSelectionModel().addListSelectionListener(moveUpAction);
490        tb.add(moveUpAction);
491        memberTable.getActionMap().put("moveUp", moveUpAction);
492
493        // -- move down action
494        MoveDownAction moveDownAction = new MoveDownAction();
495        memberTableModel.getSelectionModel().addListSelectionListener(moveDownAction);
496        tb.add(moveDownAction);
497        memberTable.getActionMap().put("moveDown", moveDownAction);
498
499        tb.addSeparator();
500
501        // -- edit action
502        EditAction editAction = new EditAction();
503        memberTableModel.getSelectionModel().addListSelectionListener(editAction);
504        tb.add(editAction);
505
506        // -- delete action
507        RemoveAction removeSelectedAction = new RemoveAction();
508        memberTable.getSelectionModel().addListSelectionListener(removeSelectedAction);
509        tb.add(removeSelectedAction);
510        memberTable.getActionMap().put("removeSelected", removeSelectedAction);
511
512        tb.addSeparator();
513        // -- sort action
514        SortAction sortAction = new SortAction();
515        memberTableModel.addTableModelListener(sortAction);
516        tb.add(sortAction);
517        final SortBelowAction sortBelowAction = new SortBelowAction();
518        memberTableModel.addTableModelListener(sortBelowAction);
519        memberTableModel.getSelectionModel().addListSelectionListener(sortBelowAction);
520        sortBelowButton = tb.add(sortBelowAction);
521
522        // -- reverse action
523        ReverseAction reverseAction = new ReverseAction();
524        memberTableModel.addTableModelListener(reverseAction);
525        tb.add(reverseAction);
526
527        tb.addSeparator();
528
529        // -- download action
530        DownloadIncompleteMembersAction downloadIncompleteMembersAction = new DownloadIncompleteMembersAction();
531        memberTable.getModel().addTableModelListener(downloadIncompleteMembersAction);
532        tb.add(downloadIncompleteMembersAction);
533        memberTable.getActionMap().put("downloadIncomplete", downloadIncompleteMembersAction);
534
535        // -- download selected action
536        DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction();
537        memberTable.getModel().addTableModelListener(downloadSelectedIncompleteMembersAction);
538        memberTable.getSelectionModel().addListSelectionListener(downloadSelectedIncompleteMembersAction);
539        tb.add(downloadSelectedIncompleteMembersAction);
540
541        InputMap inputMap = memberTable.getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
542        inputMap.put((KeyStroke) removeSelectedAction.getValue(AbstractAction.ACCELERATOR_KEY), "removeSelected");
543        inputMap.put((KeyStroke) moveUpAction.getValue(AbstractAction.ACCELERATOR_KEY), "moveUp");
544        inputMap.put((KeyStroke) moveDownAction.getValue(AbstractAction.ACCELERATOR_KEY), "moveDown");
545        inputMap.put((KeyStroke) downloadIncompleteMembersAction.getValue(AbstractAction.ACCELERATOR_KEY), "downloadIncomplete");
546
547        return tb;
548    }
549
550    /**
551     * build the panel with the buttons for adding or removing the current selection
552     *
553     * @return control buttons panel for selection/members
554     */
555    protected JToolBar buildSelectionControlButtonPanel() {
556        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
557        tb.setFloatable(false);
558
559        // -- add at start action
560        AddSelectedAtStartAction addSelectionAction = new AddSelectedAtStartAction();
561        selectionTableModel.addTableModelListener(addSelectionAction);
562        tb.add(addSelectionAction);
563
564        // -- add before selected action
565        AddSelectedBeforeSelection addSelectedBeforeSelectionAction = new AddSelectedBeforeSelection();
566        selectionTableModel.addTableModelListener(addSelectedBeforeSelectionAction);
567        memberTableModel.getSelectionModel().addListSelectionListener(addSelectedBeforeSelectionAction);
568        tb.add(addSelectedBeforeSelectionAction);
569
570        // -- add after selected action
571        AddSelectedAfterSelection addSelectedAfterSelectionAction = new AddSelectedAfterSelection();
572        selectionTableModel.addTableModelListener(addSelectedAfterSelectionAction);
573        memberTableModel.getSelectionModel().addListSelectionListener(addSelectedAfterSelectionAction);
574        tb.add(addSelectedAfterSelectionAction);
575
576        // -- add at end action
577        AddSelectedAtEndAction addSelectedAtEndAction = new AddSelectedAtEndAction();
578        selectionTableModel.addTableModelListener(addSelectedAtEndAction);
579        tb.add(addSelectedAtEndAction);
580
581        tb.addSeparator();
582
583        // -- select members action
584        SelectedMembersForSelectionAction selectMembersForSelectionAction = new SelectedMembersForSelectionAction();
585        selectionTableModel.addTableModelListener(selectMembersForSelectionAction);
586        memberTableModel.addTableModelListener(selectMembersForSelectionAction);
587        tb.add(selectMembersForSelectionAction);
588
589        // -- select action
590        SelectPrimitivesForSelectedMembersAction selectAction = new SelectPrimitivesForSelectedMembersAction();
591        memberTable.getSelectionModel().addListSelectionListener(selectAction);
592        tb.add(selectAction);
593
594        tb.addSeparator();
595
596        // -- remove selected action
597        RemoveSelectedAction removeSelectedAction = new RemoveSelectedAction();
598        selectionTableModel.addTableModelListener(removeSelectedAction);
599        tb.add(removeSelectedAction);
600
601        return tb;
602    }
603
604    @Override
605    protected Dimension findMaxDialogSize() {
606        return new Dimension(700, 650);
607    }
608
609    @Override
610    public void setVisible(boolean visible) {
611        if (visible) {
612            tagEditorPanel.initAutoCompletion(getLayer());
613        }
614        super.setVisible(visible);
615        if (visible) {
616            sortBelowButton.setVisible(ExpertToggleAction.isExpert());
617            RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
618            if (windowMenuItem == null) {
619                addToWindowMenu();
620            }
621            tagEditorPanel.requestFocusInWindow();
622        } else {
623            // make sure all registered listeners are unregistered
624            //
625            memberTable.stopHighlighting();
626            selectionTableModel.unregister();
627            memberTableModel.unregister();
628            memberTable.unlinkAsListener();
629            if (windowMenuItem != null) {
630                Main.main.menu.windowMenu.remove(windowMenuItem);
631                windowMenuItem = null;
632            }
633            dispose();
634        }
635    }
636
637    /** adds current relation editor to the windows menu (in the "volatile" group) o*/
638    protected void addToWindowMenu() {
639        String name = getRelation() == null ? tr("New Relation") : getRelation().getLocalName();
640        final String tt = tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''",
641                name, getLayer().getName());
642        name = tr("Relation Editor: {0}", name == null ? getRelation().getId() : name);
643        final JMenu wm = Main.main.menu.windowMenu;
644        final JosmAction focusAction = new JosmAction(name, "dialogs/relationlist", tt, null, false, false) {
645            @Override
646            public void actionPerformed(ActionEvent e) {
647                final RelationEditor r = (RelationEditor) getValue("relationEditor");
648                r.setVisible(true);
649            }
650        };
651        focusAction.putValue("relationEditor", this);
652        windowMenuItem = MainMenu.add(wm, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
653    }
654
655    /**
656     * checks whether the current relation has members referring to itself. If so,
657     * warns the users and provides an option for removing these members.
658     *
659     */
660    protected void cleanSelfReferences() {
661        List<OsmPrimitive> toCheck = new ArrayList<>();
662        toCheck.add(getRelation());
663        if (memberTableModel.hasMembersReferringTo(toCheck)) {
664            int ret = ConditionalOptionPaneUtil.showOptionDialog(
665                    "clean_relation_self_references",
666                    Main.parent,
667                    tr("<html>There is at least one member in this relation referring<br>"
668                            + "to the relation itself.<br>"
669                            + "This creates circular dependencies and is discouraged.<br>"
670                            + "How do you want to proceed with circular dependencies?</html>"),
671                            tr("Warning"),
672                            JOptionPane.YES_NO_OPTION,
673                            JOptionPane.WARNING_MESSAGE,
674                            new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")},
675                            tr("Remove them, clean up relation")
676            );
677            switch(ret) {
678            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
679            case JOptionPane.CLOSED_OPTION:
680            case JOptionPane.NO_OPTION:
681                return;
682            case JOptionPane.YES_OPTION:
683                memberTableModel.removeMembersReferringTo(toCheck);
684                break;
685            }
686        }
687    }
688
689    private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut) {
690        int mods = shortcut.getModifiers();
691        int code = shortcut.getKeyCode();
692        if (code != KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) {
693            Main.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut);
694            return;
695        }
696        getRootPane().getActionMap().put(actionName, action);
697        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
698        // Assign also to JTables because they have their own Copy&Paste implementation
699        // (which is disabled in this case but eats key shortcuts anyway)
700        memberTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
701        memberTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
702        memberTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
703        selectionTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
704        selectionTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
705        selectionTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
706    }
707
708    static class AddAbortException extends Exception {
709    }
710
711    static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException {
712        String msg = tr("<html>This relation already has one or more members referring to<br>"
713                + "the object ''{0}''<br>"
714                + "<br>"
715                + "Do you really want to add another relation member?</html>",
716                primitive.getDisplayName(DefaultNameFormatter.getInstance())
717            );
718        int ret = ConditionalOptionPaneUtil.showOptionDialog(
719                "add_primitive_to_relation",
720                Main.parent,
721                msg,
722                tr("Multiple members referring to same object."),
723                JOptionPane.YES_NO_CANCEL_OPTION,
724                JOptionPane.WARNING_MESSAGE,
725                null,
726                null
727        );
728        switch(ret) {
729        case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
730        case JOptionPane.YES_OPTION:
731            return true;
732        case JOptionPane.NO_OPTION:
733        case JOptionPane.CLOSED_OPTION:
734            return false;
735        case JOptionPane.CANCEL_OPTION:
736            throw new AddAbortException();
737        }
738        // should not happen
739        return false;
740    }
741
742    static void warnOfCircularReferences(OsmPrimitive primitive) {
743        String msg = tr("<html>You are trying to add a relation to itself.<br>"
744                + "<br>"
745                + "This creates circular references and is therefore discouraged.<br>"
746                + "Skipping relation ''{0}''.</html>",
747                primitive.getDisplayName(DefaultNameFormatter.getInstance()));
748        JOptionPane.showMessageDialog(
749                Main.parent,
750                msg,
751                tr("Warning"),
752                JOptionPane.WARNING_MESSAGE);
753    }
754
755    /**
756     * Adds primitives to a given relation.
757     * @param orig The relation to modify
758     * @param primitivesToAdd The primitives to add as relation members
759     * @return The resulting command
760     * @throws IllegalArgumentException if orig is null
761     */
762    public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
763        CheckParameterUtil.ensureParameterNotNull(orig, "orig");
764        try {
765            final Collection<TaggingPreset> presets = TaggingPreset.getMatchingPresets(
766                    EnumSet.of(TaggingPresetType.RELATION), orig.getKeys(), false);
767            Relation relation = new Relation(orig);
768            boolean modified = false;
769            for (OsmPrimitive p : primitivesToAdd) {
770                if (p instanceof Relation && orig.equals(p)) {
771                    warnOfCircularReferences(p);
772                    continue;
773                } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p))
774                        && !confirmAddingPrimitive(p)) {
775                    continue;
776                }
777                final Set<String> roles = findSuggestedRoles(presets, p);
778                relation.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p));
779                modified = true;
780            }
781            return modified ? new ChangeCommand(orig, relation) : null;
782        } catch (AddAbortException ign) {
783            return null;
784        }
785    }
786
787    protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) {
788        final Set<String> roles = new HashSet<>();
789        for (TaggingPreset preset : presets) {
790            String role = preset.suggestRoleForOsmPrimitive(p);
791            if (role != null && !role.isEmpty()) {
792                roles.add(role);
793            }
794        }
795        return roles;
796    }
797
798    abstract class AddFromSelectionAction extends AbstractAction {
799        protected boolean isPotentialDuplicate(OsmPrimitive primitive) {
800            return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive));
801        }
802
803        protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException {
804            if (primitives == null || primitives.isEmpty())
805                return primitives;
806            List<OsmPrimitive> ret = new ArrayList<>();
807            ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation");
808            for (OsmPrimitive primitive : primitives) {
809                if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) {
810                    warnOfCircularReferences(primitive);
811                    continue;
812                }
813                if (isPotentialDuplicate(primitive)) {
814                    if (confirmAddingPrimitive(primitive)) {
815                        ret.add(primitive);
816                    }
817                    continue;
818                } else {
819                    ret.add(primitive);
820                }
821            }
822            ConditionalOptionPaneUtil.endBulkOperation("add_primitive_to_relation");
823            return ret;
824        }
825    }
826
827    class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener {
828        AddSelectedAtStartAction() {
829            putValue(SHORT_DESCRIPTION,
830                    tr("Add all objects selected in the current dataset before the first member"));
831            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright"));
832            refreshEnabled();
833        }
834
835        protected void refreshEnabled() {
836            setEnabled(selectionTableModel.getRowCount() > 0);
837        }
838
839        @Override
840        public void actionPerformed(ActionEvent e) {
841            try {
842                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
843                memberTableModel.addMembersAtBeginning(toAdd);
844            } catch (AddAbortException ex) {
845                // do nothing
846                if (Main.isTraceEnabled()) {
847                    Main.trace(ex.getMessage());
848                }
849            }
850        }
851
852        @Override
853        public void tableChanged(TableModelEvent e) {
854            refreshEnabled();
855        }
856    }
857
858    class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener {
859        AddSelectedAtEndAction() {
860            putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member"));
861            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright"));
862            refreshEnabled();
863        }
864
865        protected void refreshEnabled() {
866            setEnabled(selectionTableModel.getRowCount() > 0);
867        }
868
869        @Override
870        public void actionPerformed(ActionEvent e) {
871            try {
872                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
873                memberTableModel.addMembersAtEnd(toAdd);
874            } catch (AddAbortException ex) {
875                // do nothing
876                if (Main.isTraceEnabled()) {
877                    Main.trace(ex.getMessage());
878                }
879            }
880        }
881
882        @Override
883        public void tableChanged(TableModelEvent e) {
884            refreshEnabled();
885        }
886    }
887
888    class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
889        /**
890         * Constructs a new {@code AddSelectedBeforeSelection}.
891         */
892        AddSelectedBeforeSelection() {
893            putValue(SHORT_DESCRIPTION,
894                    tr("Add all objects selected in the current dataset before the first selected member"));
895            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright"));
896            refreshEnabled();
897        }
898
899        protected void refreshEnabled() {
900            setEnabled(selectionTableModel.getRowCount() > 0
901                    && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
902        }
903
904        @Override
905        public void actionPerformed(ActionEvent e) {
906            try {
907                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
908                memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel
909                        .getSelectionModel().getMinSelectionIndex());
910            } catch (AddAbortException ex) {
911                // do nothing
912                if (Main.isTraceEnabled()) {
913                    Main.trace(ex.getMessage());
914                }
915            }
916        }
917
918        @Override
919        public void tableChanged(TableModelEvent e) {
920            refreshEnabled();
921        }
922
923        @Override
924        public void valueChanged(ListSelectionEvent e) {
925            refreshEnabled();
926        }
927    }
928
929    class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
930        AddSelectedAfterSelection() {
931            putValue(SHORT_DESCRIPTION,
932                    tr("Add all objects selected in the current dataset after the last selected member"));
933            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright"));
934            refreshEnabled();
935        }
936
937        protected void refreshEnabled() {
938            setEnabled(selectionTableModel.getRowCount() > 0
939                    && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
940        }
941
942        @Override
943        public void actionPerformed(ActionEvent e) {
944            try {
945                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
946                memberTableModel.addMembersAfterIdx(toAdd, memberTableModel
947                        .getSelectionModel().getMaxSelectionIndex());
948            } catch (AddAbortException ex) {
949                // do nothing
950                if (Main.isTraceEnabled()) {
951                    Main.trace(ex.getMessage());
952                }
953            }
954        }
955
956        @Override
957        public void tableChanged(TableModelEvent e) {
958            refreshEnabled();
959        }
960
961        @Override
962        public void valueChanged(ListSelectionEvent e) {
963            refreshEnabled();
964        }
965    }
966
967    class RemoveSelectedAction extends AbstractAction implements TableModelListener {
968        /**
969         * Constructs a new {@code RemoveSelectedAction}.
970         */
971        RemoveSelectedAction() {
972            putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects"));
973            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers"));
974            updateEnabledState();
975        }
976
977        protected void updateEnabledState() {
978            DataSet ds = getLayer().data;
979            if (ds == null || ds.getSelected().isEmpty()) {
980                setEnabled(false);
981                return;
982            }
983            // only enable the action if we have members referring to the
984            // selected primitives
985            //
986            setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected()));
987        }
988
989        @Override
990        public void actionPerformed(ActionEvent e) {
991            memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection());
992        }
993
994        @Override
995        public void tableChanged(TableModelEvent e) {
996            updateEnabledState();
997        }
998    }
999
1000    /**
1001     * Selects  members in the relation editor which refer to primitives in the current
1002     * selection of the context layer.
1003     *
1004     */
1005    class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener {
1006        SelectedMembersForSelectionAction() {
1007            putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
1008            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers"));
1009            updateEnabledState();
1010        }
1011
1012        protected void updateEnabledState() {
1013            boolean enabled = selectionTableModel.getRowCount() > 0
1014            &&  !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty();
1015
1016            if (enabled) {
1017                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection",
1018                        memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size()));
1019            } else {
1020                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
1021            }
1022            setEnabled(enabled);
1023        }
1024
1025        @Override
1026        public void actionPerformed(ActionEvent e) {
1027            memberTableModel.selectMembersReferringTo(getLayer().data.getSelected());
1028        }
1029
1030        @Override
1031        public void tableChanged(TableModelEvent e) {
1032            updateEnabledState();
1033        }
1034    }
1035
1036    /**
1037     * Selects primitives in the layer this editor belongs to. The selected primitives are
1038     * equal to the set of primitives the currently selected relation members refer to.
1039     *
1040     */
1041    class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener {
1042        SelectPrimitivesForSelectedMembersAction() {
1043            putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members"));
1044            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives"));
1045            updateEnabledState();
1046        }
1047
1048        protected void updateEnabledState() {
1049            setEnabled(memberTable.getSelectedRowCount() > 0);
1050        }
1051
1052        @Override
1053        public void actionPerformed(ActionEvent e) {
1054            getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives());
1055        }
1056
1057        @Override
1058        public void valueChanged(ListSelectionEvent e) {
1059            updateEnabledState();
1060        }
1061    }
1062
1063    class SortAction extends AbstractAction implements TableModelListener {
1064        SortAction() {
1065            String tooltip = tr("Sort the relation members");
1066            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort"));
1067            putValue(NAME, tr("Sort"));
1068            Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"),
1069                KeyEvent.VK_END, Shortcut.ALT);
1070            sc.setAccelerator(this);
1071            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1072            updateEnabledState();
1073        }
1074
1075        @Override
1076        public void actionPerformed(ActionEvent e) {
1077            memberTableModel.sort();
1078        }
1079
1080        protected void updateEnabledState() {
1081            setEnabled(memberTableModel.getRowCount() > 0);
1082        }
1083
1084        @Override
1085        public void tableChanged(TableModelEvent e) {
1086            updateEnabledState();
1087        }
1088    }
1089
1090    class SortBelowAction extends AbstractAction implements TableModelListener, ListSelectionListener {
1091        SortBelowAction() {
1092            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort_below"));
1093            putValue(NAME, tr("Sort below"));
1094            putValue(SHORT_DESCRIPTION, tr("Sort the selected relation members and all members below"));
1095            updateEnabledState();
1096        }
1097
1098        @Override
1099        public void actionPerformed(ActionEvent e) {
1100            memberTableModel.sortBelow();
1101        }
1102
1103        protected void updateEnabledState() {
1104            setEnabled(memberTableModel.getRowCount() > 0 && !memberTableModel.getSelectionModel().isSelectionEmpty());
1105        }
1106
1107        @Override
1108        public void tableChanged(TableModelEvent e) {
1109            updateEnabledState();
1110        }
1111
1112        @Override
1113        public void valueChanged(ListSelectionEvent e) {
1114            updateEnabledState();
1115        }
1116    }
1117
1118    class ReverseAction extends AbstractAction implements TableModelListener {
1119        ReverseAction() {
1120            putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members"));
1121            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse"));
1122            putValue(NAME, tr("Reverse"));
1123        //  Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"),
1124        //      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"),
1573        //      KeyEvent.VK_K, Shortcut.ALT)
1574            updateEnabledState();
1575        }
1576
1577        @Override
1578        public void actionPerformed(ActionEvent e) {
1579            if (!isEnabled())
1580                return;
1581            Main.worker.submit(new DownloadRelationMemberTask(
1582                    getRelation(),
1583                    memberTableModel.getSelectedIncompleteMemberPrimitives(),
1584                    getLayer(),
1585                    GenericRelationEditor.this)
1586            );
1587        }
1588
1589        protected void updateEnabledState() {
1590            setEnabled(memberTableModel.hasIncompleteSelectedMembers() && !Main.isOffline(OnlineResource.OSM_API));
1591        }
1592
1593        @Override
1594        public void valueChanged(ListSelectionEvent e) {
1595            updateEnabledState();
1596        }
1597
1598        @Override
1599        public void tableChanged(TableModelEvent e) {
1600            updateEnabledState();
1601        }
1602    }
1603
1604    class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener {
1605        SetRoleAction() {
1606            putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members"));
1607            putValue(SMALL_ICON, ImageProvider.get("apply"));
1608            putValue(NAME, tr("Apply Role"));
1609            refreshEnabled();
1610        }
1611
1612        protected void refreshEnabled() {
1613            setEnabled(memberTable.getSelectedRowCount() > 0);
1614        }
1615
1616        protected boolean isEmptyRole() {
1617            return tfRole.getText() == null || tfRole.getText().trim().isEmpty();
1618        }
1619
1620        protected boolean confirmSettingEmptyRole(int onNumMembers) {
1621            String message = "<html>"
1622                + trn("You are setting an empty role on {0} object.",
1623                        "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers)
1624                        + "<br>"
1625                        + tr("This is equal to deleting the roles of these objects.") +
1626                        "<br>"
1627                        + tr("Do you really want to apply the new role?") + "</html>";
1628            String[] options = new String[] {
1629                    tr("Yes, apply it"),
1630                    tr("No, do not apply")
1631            };
1632            int ret = ConditionalOptionPaneUtil.showOptionDialog(
1633                    "relation_editor.confirm_applying_empty_role",
1634                    Main.parent,
1635                    message,
1636                    tr("Confirm empty role"),
1637                    JOptionPane.YES_NO_OPTION,
1638                    JOptionPane.WARNING_MESSAGE,
1639                    options,
1640                    options[0]
1641            );
1642            switch(ret) {
1643            case JOptionPane.YES_OPTION:
1644            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
1645                return true;
1646            default:
1647                return false;
1648            }
1649        }
1650
1651        @Override
1652        public void actionPerformed(ActionEvent e) {
1653            if (isEmptyRole()) {
1654                if (!confirmSettingEmptyRole(memberTable.getSelectedRowCount()))
1655                    return;
1656            }
1657            memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText());
1658        }
1659
1660        @Override
1661        public void valueChanged(ListSelectionEvent e) {
1662            refreshEnabled();
1663        }
1664
1665        @Override
1666        public void changedUpdate(DocumentEvent e) {
1667            refreshEnabled();
1668        }
1669
1670        @Override
1671        public void insertUpdate(DocumentEvent e) {
1672            refreshEnabled();
1673        }
1674
1675        @Override
1676        public void removeUpdate(DocumentEvent e) {
1677            refreshEnabled();
1678        }
1679    }
1680
1681    /**
1682     * Creates a new relation with a copy of the current editor state.
1683     */
1684    class DuplicateRelationAction extends AbstractAction {
1685        DuplicateRelationAction() {
1686            putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window"));
1687            // FIXME provide an icon
1688            putValue(SMALL_ICON, ImageProvider.get("duplicate"));
1689            putValue(NAME, tr("Duplicate"));
1690            setEnabled(true);
1691        }
1692
1693        @Override
1694        public void actionPerformed(ActionEvent e) {
1695            Relation copy = new Relation();
1696            tagEditorPanel.getModel().applyToPrimitive(copy);
1697            memberTableModel.applyToRelation(copy);
1698            RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers());
1699            editor.setVisible(true);
1700        }
1701    }
1702
1703    /**
1704     * Action for editing the currently selected relation.
1705     */
1706    class EditAction extends AbstractAction implements ListSelectionListener {
1707        EditAction() {
1708            putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to"));
1709            putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
1710            refreshEnabled();
1711        }
1712
1713        protected void refreshEnabled() {
1714            setEnabled(memberTable.getSelectedRowCount() == 1
1715                    && memberTableModel.isEditableRelation(memberTable.getSelectedRow()));
1716        }
1717
1718        protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) {
1719            Collection<RelationMember> members = new HashSet<>();
1720            Collection<OsmPrimitive> selection = getLayer().data.getSelected();
1721            for (RelationMember member: r.getMembers()) {
1722                if (selection.contains(member.getMember())) {
1723                    members.add(member);
1724                }
1725            }
1726            return members;
1727        }
1728
1729        public void run() {
1730            int idx = memberTable.getSelectedRow();
1731            if (idx < 0)
1732                return;
1733            OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx);
1734            if (!(primitive instanceof Relation))
1735                return;
1736            Relation r = (Relation) primitive;
1737            if (r.isIncomplete())
1738                return;
1739
1740            RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r));
1741            editor.setVisible(true);
1742        }
1743
1744        @Override
1745        public void actionPerformed(ActionEvent e) {
1746            if (!isEnabled())
1747                return;
1748            run();
1749        }
1750
1751        @Override
1752        public void valueChanged(ListSelectionEvent e) {
1753            refreshEnabled();
1754        }
1755    }
1756
1757    class PasteMembersAction extends AddFromSelectionAction {
1758
1759        @Override
1760        public void actionPerformed(ActionEvent e) {
1761            try {
1762                List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded();
1763                DataSet ds = getLayer().data;
1764                List<OsmPrimitive> toAdd = new ArrayList<>();
1765                boolean hasNewInOtherLayer = false;
1766
1767                for (PrimitiveData primitive: primitives) {
1768                    OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive);
1769                    if (primitiveInDs != null) {
1770                        toAdd.add(primitiveInDs);
1771                    } else if (!primitive.isNew()) {
1772                        OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true);
1773                        ds.addPrimitive(p);
1774                        toAdd.add(p);
1775                    } else {
1776                        hasNewInOtherLayer = true;
1777                        break;
1778                    }
1779                }
1780
1781                if (hasNewInOtherLayer) {
1782                    JOptionPane.showMessageDialog(Main.parent,
1783                            tr("Members from paste buffer cannot be added because they are not included in current layer"));
1784                    return;
1785                }
1786
1787                toAdd = filterConfirmedPrimitives(toAdd);
1788                int index = memberTableModel.getSelectionModel().getMaxSelectionIndex();
1789                if (index == -1) {
1790                    index = memberTableModel.getRowCount() - 1;
1791                }
1792                memberTableModel.addMembersAfterIdx(toAdd, index);
1793
1794                tfRole.requestFocusInWindow();
1795
1796            } catch (AddAbortException ex) {
1797                // Do nothing
1798                if (Main.isTraceEnabled()) {
1799                    Main.trace(ex.getMessage());
1800                }
1801            }
1802        }
1803    }
1804
1805    class CopyMembersAction extends AbstractAction {
1806        @Override
1807        public void actionPerformed(ActionEvent e) {
1808            Set<OsmPrimitive> primitives = new HashSet<>();
1809            for (RelationMember rm: memberTableModel.getSelectedMembers()) {
1810                primitives.add(rm.getMember());
1811            }
1812            if (!primitives.isEmpty()) {
1813                CopyAction.copy(getLayer(), primitives);
1814            }
1815        }
1816    }
1817
1818    class MemberTableDblClickAdapter extends MouseAdapter {
1819        @Override
1820        public void mouseClicked(MouseEvent e) {
1821            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
1822                new EditAction().run();
1823            }
1824        }
1825    }
1826}