source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java @ 5241

Revision 5082, 67.8 KB checked in by simon04, 2 months ago (diff)

fix #5395 - add "Add selection to relation" to popup menu of relation toggle dialog

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