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