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