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

Last change on this file since 14027 was 14027, checked in by michael2402, 5 months ago

See #16388: New mechanism for plugins to register relation editor actions.

  • Property svn:eol-style set to native
File size: 38.8 KB
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;
6
7import java.awt.BorderLayout;
8import java.awt.Dimension;
9import java.awt.FlowLayout;
10import java.awt.GraphicsEnvironment;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.Window;
14import java.awt.datatransfer.Clipboard;
15import java.awt.datatransfer.FlavorListener;
16import java.awt.event.ActionEvent;
17import java.awt.event.FocusAdapter;
18import java.awt.event.FocusEvent;
19import java.awt.event.InputEvent;
20import java.awt.event.KeyEvent;
21import java.awt.event.MouseAdapter;
22import java.awt.event.MouseEvent;
23import java.awt.event.WindowAdapter;
24import java.awt.event.WindowEvent;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.EnumSet;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Set;
33
34import javax.swing.AbstractAction;
35import javax.swing.BorderFactory;
36import javax.swing.InputMap;
37import javax.swing.JButton;
38import javax.swing.JComponent;
39import javax.swing.JLabel;
40import javax.swing.JMenuItem;
41import javax.swing.JOptionPane;
42import javax.swing.JPanel;
43import javax.swing.JRootPane;
44import javax.swing.JScrollPane;
45import javax.swing.JSplitPane;
46import javax.swing.JTabbedPane;
47import javax.swing.JTable;
48import javax.swing.JToolBar;
49import javax.swing.KeyStroke;
50
51import org.openstreetmap.josm.Main;
52import org.openstreetmap.josm.actions.JosmAction;
53import org.openstreetmap.josm.command.ChangeCommand;
54import org.openstreetmap.josm.command.Command;
55import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
56import org.openstreetmap.josm.data.osm.OsmPrimitive;
57import org.openstreetmap.josm.data.osm.Relation;
58import org.openstreetmap.josm.data.osm.RelationMember;
59import org.openstreetmap.josm.data.osm.Tag;
60import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
61import org.openstreetmap.josm.gui.MainApplication;
62import org.openstreetmap.josm.gui.MainMenu;
63import org.openstreetmap.josm.gui.ScrollViewport;
64import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
65import org.openstreetmap.josm.gui.dialogs.relation.actions.AbstractRelationEditorAction;
66import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAfterSelection;
67import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtEndAction;
68import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtStartAction;
69import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedBeforeSelection;
70import org.openstreetmap.josm.gui.dialogs.relation.actions.ApplyAction;
71import org.openstreetmap.josm.gui.dialogs.relation.actions.CancelAction;
72import org.openstreetmap.josm.gui.dialogs.relation.actions.CopyMembersAction;
73import org.openstreetmap.josm.gui.dialogs.relation.actions.DeleteCurrentRelationAction;
74import org.openstreetmap.josm.gui.dialogs.relation.actions.DownloadIncompleteMembersAction;
75import org.openstreetmap.josm.gui.dialogs.relation.actions.DownloadSelectedIncompleteMembersAction;
76import org.openstreetmap.josm.gui.dialogs.relation.actions.DuplicateRelationAction;
77import org.openstreetmap.josm.gui.dialogs.relation.actions.EditAction;
78import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionAccess;
79import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionGroup;
80import org.openstreetmap.josm.gui.dialogs.relation.actions.MoveDownAction;
81import org.openstreetmap.josm.gui.dialogs.relation.actions.MoveUpAction;
82import org.openstreetmap.josm.gui.dialogs.relation.actions.OKAction;
83import org.openstreetmap.josm.gui.dialogs.relation.actions.PasteMembersAction;
84import org.openstreetmap.josm.gui.dialogs.relation.actions.RefreshAction;
85import org.openstreetmap.josm.gui.dialogs.relation.actions.RemoveAction;
86import org.openstreetmap.josm.gui.dialogs.relation.actions.RemoveSelectedAction;
87import org.openstreetmap.josm.gui.dialogs.relation.actions.ReverseAction;
88import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectAction;
89import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectPrimitivesForSelectedMembersAction;
90import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectedMembersForSelectionAction;
91import org.openstreetmap.josm.gui.dialogs.relation.actions.SetRoleAction;
92import org.openstreetmap.josm.gui.dialogs.relation.actions.SortAction;
93import org.openstreetmap.josm.gui.dialogs.relation.actions.SortBelowAction;
94import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
95import org.openstreetmap.josm.gui.help.HelpUtil;
96import org.openstreetmap.josm.gui.layer.OsmDataLayer;
97import org.openstreetmap.josm.gui.tagging.TagEditorModel;
98import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
99import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
100import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
101import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
102import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
103import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
104import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
105import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
106import org.openstreetmap.josm.gui.util.WindowGeometry;
107import org.openstreetmap.josm.spi.preferences.Config;
108import org.openstreetmap.josm.tools.CheckParameterUtil;
109import org.openstreetmap.josm.tools.Logging;
110import org.openstreetmap.josm.tools.Shortcut;
111import org.openstreetmap.josm.tools.Utils;
112
113/**
114 * This dialog is for editing relations.
115 * @since 343
116 */
117public class GenericRelationEditor extends RelationEditor {
118    /** the tag table and its model */
119    private final TagEditorPanel tagEditorPanel;
120    private final ReferringRelationsBrowser referrerBrowser;
121    private final ReferringRelationsBrowserModel referrerModel;
122
123    /** the member table and its model */
124    private final MemberTable memberTable;
125    private final MemberTableModel memberTableModel;
126
127    /** the selection table and its model */
128    private final SelectionTable selectionTable;
129    private final SelectionTableModel selectionTableModel;
130
131    private final AutoCompletingTextField tfRole;
132
133    /**
134     * the menu item in the windows menu. Required to properly hide on dialog close.
135     */
136    private JMenuItem windowMenuItem;
137    /**
138     * The toolbar with the buttons on the left
139     */
140    private final LeftButtonToolbar leftButtonToolbar;
141    /**
142     * Action for performing the {@link RefreshAction}
143     */
144    private final RefreshAction refreshAction;
145    /**
146     * Action for performing the {@link ApplyAction}
147     */
148    private final ApplyAction applyAction;
149    /**
150     * Action for performing the {@link SelectAction}
151     */
152    private final SelectAction selectAction;
153    /**
154     * Action for performing the {@link DuplicateRelationAction}
155     */
156    private final DuplicateRelationAction duplicateAction;
157    /**
158     * Action for performing the {@link DeleteCurrentRelationAction}
159     */
160    private final DeleteCurrentRelationAction deleteAction;
161    /**
162     * Action for performing the {@link OKAction}
163     */
164    private final OKAction okAction;
165    /**
166     * Action for performing the {@link CancelAction}
167     */
168    private final CancelAction cancelAction;
169    /**
170     * A list of listeners that need to be notified on clipboard content changes.
171     */
172    private final ArrayList<FlavorListener> clipboardListeners = new ArrayList<>();
173
174    /**
175     * Creates a new relation editor for the given relation. The relation will be saved if the user
176     * selects "ok" in the editor.
177     *
178     * If no relation is given, will create an editor for a new relation.
179     *
180     * @param layer the {@link OsmDataLayer} the new or edited relation belongs to
181     * @param relation relation to edit, or null to create a new one.
182     * @param selectedMembers a collection of members which shall be selected initially
183     */
184    public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) {
185        super(layer, relation);
186
187        setRememberWindowGeometry(getClass().getName() + ".geometry",
188                WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650)));
189
190        final TaggingPresetHandler presetHandler = new TaggingPresetHandler() {
191
192            @Override
193            public void updateTags(List<Tag> tags) {
194                tagEditorPanel.getModel().updateTags(tags);
195            }
196
197            @Override
198            public Collection<OsmPrimitive> getSelection() {
199                Relation relation = new Relation();
200                tagEditorPanel.getModel().applyToPrimitive(relation);
201                return Collections.<OsmPrimitive>singletonList(relation);
202            }
203        };
204
205        // init the various models
206        //
207        memberTableModel = new MemberTableModel(relation, getLayer(), presetHandler);
208        memberTableModel.register();
209        selectionTableModel = new SelectionTableModel(getLayer());
210        selectionTableModel.register();
211        referrerModel = new ReferringRelationsBrowserModel(relation);
212
213        tagEditorPanel = new TagEditorPanel(relation, presetHandler);
214        populateModels(relation);
215        tagEditorPanel.getModel().ensureOneTag();
216
217        // setting up the member table
218        memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel);
219        memberTable.addMouseListener(new MemberTableDblClickAdapter());
220        memberTableModel.addMemberModelListener(memberTable);
221
222        MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor();
223        selectionTable = new SelectionTable(selectionTableModel, memberTableModel);
224        selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height);
225
226        leftButtonToolbar = new LeftButtonToolbar(new RelationEditorActionAccess());
227        tfRole = buildRoleTextField(this);
228
229        JSplitPane pane = buildSplitPane(
230                buildTagEditorPanel(tagEditorPanel),
231                buildMemberEditorPanel(leftButtonToolbar, new RelationEditorActionAccess()),
232                this);
233        pane.setPreferredSize(new Dimension(100, 100));
234
235        JPanel pnl = new JPanel(new BorderLayout());
236        pnl.add(pane, BorderLayout.CENTER);
237        pnl.setBorder(BorderFactory.createRaisedBevelBorder());
238
239        getContentPane().setLayout(new BorderLayout());
240        JTabbedPane tabbedPane = new JTabbedPane();
241        tabbedPane.add(tr("Tags and Members"), pnl);
242        referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel);
243        tabbedPane.add(tr("Parent Relations"), referrerBrowser);
244        tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation));
245        tabbedPane.addChangeListener(e -> {
246            JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource();
247            int index = sourceTabbedPane.getSelectedIndex();
248            String title = sourceTabbedPane.getTitleAt(index);
249            if (title.equals(tr("Parent Relations"))) {
250                referrerBrowser.init();
251            }
252        });
253       
254        IRelationEditorActionAccess actionAccess = new RelationEditorActionAccess();
255
256        refreshAction = new RefreshAction(actionAccess);
257        applyAction = new ApplyAction(actionAccess);
258        selectAction = new SelectAction(actionAccess);
259        duplicateAction = new DuplicateRelationAction(actionAccess);
260        deleteAction = new DeleteCurrentRelationAction(actionAccess);
261        addPropertyChangeListener(deleteAction);
262
263        okAction = new OKAction(actionAccess);
264        cancelAction = new CancelAction(actionAccess);
265
266        getContentPane().add(buildToolBar(refreshAction, applyAction, selectAction, duplicateAction, deleteAction), BorderLayout.NORTH);
267        getContentPane().add(tabbedPane, BorderLayout.CENTER);
268        getContentPane().add(buildOkCancelButtonPanel(okAction, cancelAction), BorderLayout.SOUTH);
269
270        setSize(findMaxDialogSize());
271
272        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
273        addWindowListener(
274                new WindowAdapter() {
275                    @Override
276                    public void windowOpened(WindowEvent e) {
277                        cleanSelfReferences(memberTableModel, getRelation());
278                    }
279
280                    @Override
281                    public void windowClosing(WindowEvent e) {
282                        cancel();
283                    }
284                }
285        );
286        // CHECKSTYLE.OFF: LineLength
287        registerCopyPasteAction(tagEditorPanel.getPasteAction(), "PASTE_TAGS",
288                Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke(),
289                getRootPane(), memberTable, selectionTable);
290        // CHECKSTYLE.ON: LineLength
291
292        KeyStroke key = Shortcut.getPasteKeyStroke();
293        if (key != null) {
294            // handle uncommon situation, that user has no keystroke assigned to paste
295            registerCopyPasteAction(new PasteMembersAction(actionAccess) {
296                                private static final long serialVersionUID = 1L;
297
298                                @Override
299                public void actionPerformed(ActionEvent e) {
300                    super.actionPerformed(e);
301                    tfRole.requestFocusInWindow();
302                }
303            }, "PASTE_MEMBERS", key, getRootPane(), memberTable, selectionTable);
304        }
305        key = Shortcut.getCopyKeyStroke();
306        if (key != null) {
307            // handle uncommon situation, that user has no keystroke assigned to copy
308            registerCopyPasteAction(new CopyMembersAction(actionAccess),
309                    "COPY_MEMBERS", key, getRootPane(), memberTable, selectionTable);
310        }
311        tagEditorPanel.setNextFocusComponent(memberTable);
312        selectionTable.setFocusable(false);
313        memberTableModel.setSelectedMembers(selectedMembers);
314        HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/RelationEditor"));
315    }
316
317    @Override
318    public void reloadDataFromRelation() {
319        setRelation(getRelation());
320        populateModels(getRelation());
321        refreshAction.updateEnabledState();
322    }
323
324    private void populateModels(Relation relation) {
325        if (relation != null) {
326            tagEditorPanel.getModel().initFromPrimitive(relation);
327            memberTableModel.populate(relation);
328            if (!getLayer().data.getRelations().contains(relation)) {
329                // treat it as a new relation if it doesn't exist in the data set yet.
330                setRelation(null);
331            }
332        } else {
333            tagEditorPanel.getModel().clear();
334            memberTableModel.populate(null);
335        }
336    }
337
338    /**
339     * Apply changes.
340     * @see ApplyAction
341     */
342    public void apply() {
343        applyAction.actionPerformed(null);
344    }
345
346    /**
347     * Select relation.
348     * @see SelectAction
349     * @since 12933
350     */
351    public void select() {
352        selectAction.actionPerformed(null);
353    }
354
355    /**
356     * Cancel changes.
357     * @see CancelAction
358     */
359    public void cancel() {
360        cancelAction.actionPerformed(null);
361    }
362
363    /**
364     * Creates the toolbar
365     * @param actions relation toolbar actions
366     * @return the toolbar
367     * @since 12933
368     */
369    protected static JToolBar buildToolBar(AbstractRelationEditorAction... actions) {
370        JToolBar tb = new JToolBar();
371        tb.setFloatable(false);
372        for (AbstractRelationEditorAction action : actions) {
373            tb.add(action);
374        }
375        return tb;
376    }
377
378    /**
379     * builds the panel with the OK and the Cancel button
380     * @param okAction OK action
381     * @param cancelAction Cancel action
382     *
383     * @return the panel with the OK and the Cancel button
384     */
385    protected static JPanel buildOkCancelButtonPanel(OKAction okAction, CancelAction cancelAction) {
386        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
387        pnl.add(new JButton(okAction));
388        pnl.add(new JButton(cancelAction));
389        pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
390        return pnl;
391    }
392
393    /**
394     * builds the panel with the tag editor
395     * @param tagEditorPanel tag editor panel
396     *
397     * @return the panel with the tag editor
398     */
399    protected static JPanel buildTagEditorPanel(TagEditorPanel tagEditorPanel) {
400        JPanel pnl = new JPanel(new GridBagLayout());
401
402        GridBagConstraints gc = new GridBagConstraints();
403        gc.gridx = 0;
404        gc.gridy = 0;
405        gc.gridheight = 1;
406        gc.gridwidth = 1;
407        gc.fill = GridBagConstraints.HORIZONTAL;
408        gc.anchor = GridBagConstraints.FIRST_LINE_START;
409        gc.weightx = 1.0;
410        gc.weighty = 0.0;
411        pnl.add(new JLabel(tr("Tags")), gc);
412
413        gc.gridx = 0;
414        gc.gridy = 1;
415        gc.fill = GridBagConstraints.BOTH;
416        gc.anchor = GridBagConstraints.CENTER;
417        gc.weightx = 1.0;
418        gc.weighty = 1.0;
419        pnl.add(tagEditorPanel, gc);
420        return pnl;
421    }
422
423    /**
424     * builds the role text field
425     * @param re relation editor
426     * @return the role text field
427     */
428    protected static AutoCompletingTextField buildRoleTextField(final IRelationEditor re) {
429        final AutoCompletingTextField tfRole = new AutoCompletingTextField(10);
430        tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
431        tfRole.addFocusListener(new FocusAdapter() {
432            @Override
433            public void focusGained(FocusEvent e) {
434                tfRole.selectAll();
435            }
436        });
437        tfRole.setAutoCompletionList(new AutoCompletionList());
438        tfRole.addFocusListener(
439                new FocusAdapter() {
440                    @Override
441                    public void focusGained(FocusEvent e) {
442                        AutoCompletionList list = tfRole.getAutoCompletionList();
443                        if (list != null) {
444                            list.clear();
445                            AutoCompletionManager.of(re.getLayer().data).populateWithMemberRoles(list, re.getRelation());
446                        }
447                    }
448                }
449        );
450        tfRole.setText(Config.getPref().get("relation.editor.generic.lastrole", ""));
451        return tfRole;
452    }
453
454    /**
455     * builds the panel for the relation member editor
456     * @param leftButtonToolbar left button toolbar
457     * @param editorAccess The relation editor
458     *
459     * @return the panel for the relation member editor
460     */
461    protected static JPanel buildMemberEditorPanel(
462            LeftButtonToolbar leftButtonToolbar, IRelationEditorActionAccess editorAccess) {
463        final JPanel pnl = new JPanel(new GridBagLayout());
464        final JScrollPane scrollPane = new JScrollPane(editorAccess.getMemberTable());
465
466        GridBagConstraints gc = new GridBagConstraints();
467        gc.gridx = 0;
468        gc.gridy = 0;
469        gc.gridwidth = 2;
470        gc.fill = GridBagConstraints.HORIZONTAL;
471        gc.anchor = GridBagConstraints.FIRST_LINE_START;
472        gc.weightx = 1.0;
473        gc.weighty = 0.0;
474        pnl.add(new JLabel(tr("Members")), gc);
475
476        gc.gridx = 0;
477        gc.gridy = 1;
478        gc.gridheight = 2;
479        gc.gridwidth = 1;
480        gc.fill = GridBagConstraints.VERTICAL;
481        gc.anchor = GridBagConstraints.NORTHWEST;
482        gc.weightx = 0.0;
483        gc.weighty = 1.0;
484        pnl.add(new ScrollViewport(leftButtonToolbar, ScrollViewport.VERTICAL_DIRECTION), gc);
485
486        gc.gridx = 1;
487        gc.gridy = 1;
488        gc.gridheight = 1;
489        gc.fill = GridBagConstraints.BOTH;
490        gc.anchor = GridBagConstraints.CENTER;
491        gc.weightx = 0.6;
492        gc.weighty = 1.0;
493        pnl.add(scrollPane, gc);
494
495        // --- role editing
496        JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
497        p3.add(new JLabel(tr("Apply Role:")));
498        p3.add(editorAccess.getTextFieldRole());
499        SetRoleAction setRoleAction = new SetRoleAction(editorAccess);
500        editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(setRoleAction);
501        editorAccess.getTextFieldRole().getDocument().addDocumentListener(setRoleAction);
502        editorAccess.getTextFieldRole().addActionListener(setRoleAction);
503        editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(
504                e -> editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0)
505        );
506        editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0);
507        JButton btnApply = new JButton(setRoleAction);
508        btnApply.setPreferredSize(new Dimension(20, 20));
509        btnApply.setText("");
510        p3.add(btnApply);
511
512        gc.gridx = 1;
513        gc.gridy = 2;
514        gc.fill = GridBagConstraints.HORIZONTAL;
515        gc.anchor = GridBagConstraints.LAST_LINE_START;
516        gc.weightx = 1.0;
517        gc.weighty = 0.0;
518        pnl.add(p3, gc);
519
520        JPanel pnl2 = new JPanel(new GridBagLayout());
521
522        gc.gridx = 0;
523        gc.gridy = 0;
524        gc.gridheight = 1;
525        gc.gridwidth = 3;
526        gc.fill = GridBagConstraints.HORIZONTAL;
527        gc.anchor = GridBagConstraints.FIRST_LINE_START;
528        gc.weightx = 1.0;
529        gc.weighty = 0.0;
530        pnl2.add(new JLabel(tr("Selection")), gc);
531
532        gc.gridx = 0;
533        gc.gridy = 1;
534        gc.gridheight = 1;
535        gc.gridwidth = 1;
536        gc.fill = GridBagConstraints.VERTICAL;
537        gc.anchor = GridBagConstraints.NORTHWEST;
538        gc.weightx = 0.0;
539        gc.weighty = 1.0;
540        pnl2.add(new ScrollViewport(buildSelectionControlButtonToolbar(editorAccess),
541                ScrollViewport.VERTICAL_DIRECTION), gc);
542
543        gc.gridx = 1;
544        gc.gridy = 1;
545        gc.weightx = 1.0;
546        gc.weighty = 1.0;
547        gc.fill = GridBagConstraints.BOTH;
548        pnl2.add(buildSelectionTablePanel(editorAccess.getSelectionTable()), gc);
549
550        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
551        splitPane.setLeftComponent(pnl);
552        splitPane.setRightComponent(pnl2);
553        splitPane.setOneTouchExpandable(false);
554        if (editorAccess.getEditor() instanceof Window) {
555            ((Window) editorAccess.getEditor()).addWindowListener(new WindowAdapter() {
556                @Override
557                public void windowOpened(WindowEvent e) {
558                    // has to be called when the window is visible, otherwise no effect
559                    splitPane.setDividerLocation(0.6);
560                }
561            });
562        }
563
564        JPanel pnl3 = new JPanel(new BorderLayout());
565        pnl3.add(splitPane, BorderLayout.CENTER);
566
567        return pnl3;
568    }
569
570    /**
571     * builds the panel with the table displaying the currently selected primitives
572     * @param selectionTable selection table
573     *
574     * @return panel with current selection
575     */
576    protected static JPanel buildSelectionTablePanel(SelectionTable selectionTable) {
577        JPanel pnl = new JPanel(new BorderLayout());
578        pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER);
579        return pnl;
580    }
581
582    /**
583     * builds the {@link JSplitPane} which divides the editor in an upper and a lower half
584     * @param top top panel
585     * @param bottom bottom panel
586     * @param re relation editor
587     *
588     * @return the split panel
589     */
590    protected static JSplitPane buildSplitPane(JPanel top, JPanel bottom, IRelationEditor re) {
591        final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
592        pane.setTopComponent(top);
593        pane.setBottomComponent(bottom);
594        pane.setOneTouchExpandable(true);
595        if (re instanceof Window) {
596            ((Window) re).addWindowListener(new WindowAdapter() {
597                @Override
598                public void windowOpened(WindowEvent e) {
599                    // has to be called when the window is visible, otherwise no effect
600                    pane.setDividerLocation(0.3);
601                }
602            });
603        }
604        return pane;
605    }
606
607    /**
608     * The toolbar with the buttons on the left
609     */
610    static class LeftButtonToolbar extends JToolBar {
611                private static final long serialVersionUID = 1L;
612
613                /**
614         * Constructs a new {@code LeftButtonToolbar}.
615         * @param re relation editor
616         */
617        LeftButtonToolbar(IRelationEditorActionAccess editorAccess) {
618            setOrientation(JToolBar.VERTICAL);
619            setFloatable(false);
620
621            List<IRelationEditorActionGroup> groups = new ArrayList<>();
622            // Move
623            groups.add(buildNativeGroup(10,
624                        new MoveUpAction(editorAccess, "moveUp"),
625                        new MoveDownAction(editorAccess, "moveDown")
626                        ));
627            // Edit
628            groups.add(buildNativeGroup(20,
629                        new EditAction(editorAccess),
630                        new RemoveAction(editorAccess, "removeSelected")
631                        ));
632            // Sort
633            groups.add(buildNativeGroup(30,
634                        new SortAction(editorAccess),
635                        new SortBelowAction(editorAccess)
636                        ));
637            // Reverse
638            groups.add(buildNativeGroup(40,
639                        new ReverseAction(editorAccess)
640                        ));
641            // Download
642            groups.add(buildNativeGroup(50,
643                        new DownloadIncompleteMembersAction(editorAccess, "downloadIncomplete"),
644                        new DownloadSelectedIncompleteMembersAction(editorAccess)
645                        ));
646            groups.addAll(RelationEditorHooks.getMemberActions());
647           
648            IRelationEditorActionGroup.fillToolbar(this, groups, editorAccess);
649           
650
651            InputMap inputMap = editorAccess.getMemberTable().getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
652            inputMap.put((KeyStroke) new RemoveAction(editorAccess, "removeSelected").getValue(AbstractAction.ACCELERATOR_KEY), "removeSelected");
653            inputMap.put((KeyStroke) new MoveUpAction(editorAccess, "moveUp").getValue(AbstractAction.ACCELERATOR_KEY), "moveUp");
654            inputMap.put((KeyStroke) new MoveDownAction(editorAccess, "moveDown").getValue(AbstractAction.ACCELERATOR_KEY), "moveDown");
655            inputMap.put((KeyStroke) new DownloadIncompleteMembersAction(
656                        editorAccess, "downloadIncomplete").getValue(AbstractAction.ACCELERATOR_KEY), "downloadIncomplete");
657        }
658    }
659
660    /**
661     * build the toolbar with the buttons for adding or removing the current selection
662     * @param memberTable member table
663     * @param memberTableModel member table model
664     * @param selectionTableModel selection table model
665     * @param re relation editor
666     *
667     * @return control buttons panel for selection/members
668     */
669    protected static JToolBar buildSelectionControlButtonToolbar(IRelationEditorActionAccess editorAccess) {
670        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
671        tb.setFloatable(false);
672
673                List<IRelationEditorActionGroup> groups = new ArrayList<>();
674                groups.add(buildNativeGroup(10,
675                                new AddSelectedAtStartAction(editorAccess),
676                                new AddSelectedBeforeSelection(editorAccess),
677                                new AddSelectedAfterSelection(editorAccess),
678                                new AddSelectedAtEndAction(editorAccess)
679                                ));
680                groups.add(buildNativeGroup(20,
681                                new SelectedMembersForSelectionAction(editorAccess),
682                                new SelectPrimitivesForSelectedMembersAction(editorAccess)
683                                ));
684                groups.add(buildNativeGroup(30,
685                                new RemoveSelectedAction(editorAccess)
686                                ));
687                groups.addAll(RelationEditorHooks.getSelectActions());
688       
689        IRelationEditorActionGroup.fillToolbar(tb, groups, editorAccess);
690        return tb;
691    }
692   
693    private static IRelationEditorActionGroup buildNativeGroup(int order, AbstractRelationEditorAction... actions) {
694        return new IRelationEditorActionGroup() {
695                @Override
696                public int order() {
697                        return order;
698                }
699               
700                        @Override
701                        public List<AbstractRelationEditorAction> getActions(IRelationEditorActionAccess editorAccess) {
702                                return Arrays.asList(actions);
703                        }
704                };
705    }
706
707    @Override
708    protected Dimension findMaxDialogSize() {
709        return new Dimension(700, 650);
710    }
711
712    @Override
713    public void setVisible(boolean visible) {
714        if (isVisible() == visible) {
715            return;
716        }
717        if (visible) {
718            tagEditorPanel.initAutoCompletion(getLayer());
719        }
720        super.setVisible(visible);
721        Clipboard clipboard = ClipboardUtils.getClipboard();
722        if (visible) {
723            RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
724            if (windowMenuItem == null) {
725                windowMenuItem = addToWindowMenu(this, getLayer().getName());
726            }
727            tagEditorPanel.requestFocusInWindow();
728            for (FlavorListener listener : clipboardListeners) {
729                clipboard.addFlavorListener(listener);
730            }
731        } else {
732            // make sure all registered listeners are unregistered
733            //
734            memberTable.stopHighlighting();
735            selectionTableModel.unregister();
736            memberTableModel.unregister();
737            memberTable.unregisterListeners();
738            if (windowMenuItem != null) {
739                MainApplication.getMenu().windowMenu.remove(windowMenuItem);
740                windowMenuItem = null;
741            }
742            for (FlavorListener listener : clipboardListeners) {
743                clipboard.removeFlavorListener(listener);
744            }
745            dispose();
746        }
747    }
748
749    /**
750     * Adds current relation editor to the windows menu (in the "volatile" group)
751     * @param re relation editor
752     * @param layerName layer name
753     * @return created menu item
754     */
755    protected static JMenuItem addToWindowMenu(IRelationEditor re, String layerName) {
756        Relation r = re.getRelation();
757        String name = r == null ? tr("New relation") : r.getLocalName();
758        JosmAction focusAction = new JosmAction(
759                tr("Relation Editor: {0}", name == null && r != null ? r.getId() : name),
760                "dialogs/relationlist",
761                tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''", name, layerName),
762                null, false, false) {
763                        private static final long serialVersionUID = 1L;
764
765                        @Override
766            public void actionPerformed(ActionEvent e) {
767                ((RelationEditor) getValue("relationEditor")).setVisible(true);
768            }
769        };
770        focusAction.putValue("relationEditor", re);
771        return MainMenu.add(MainApplication.getMenu().windowMenu, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
772    }
773
774    /**
775     * checks whether the current relation has members referring to itself. If so,
776     * warns the users and provides an option for removing these members.
777     * @param memberTableModel member table model
778     * @param relation relation
779     */
780    protected static void cleanSelfReferences(MemberTableModel memberTableModel, Relation relation) {
781        List<OsmPrimitive> toCheck = new ArrayList<>();
782        toCheck.add(relation);
783        if (memberTableModel.hasMembersReferringTo(toCheck)) {
784            int ret = ConditionalOptionPaneUtil.showOptionDialog(
785                    "clean_relation_self_references",
786                    Main.parent,
787                    tr("<html>There is at least one member in this relation referring<br>"
788                            + "to the relation itself.<br>"
789                            + "This creates circular dependencies and is discouraged.<br>"
790                            + "How do you want to proceed with circular dependencies?</html>"),
791                            tr("Warning"),
792                            JOptionPane.YES_NO_OPTION,
793                            JOptionPane.WARNING_MESSAGE,
794                            new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")},
795                            tr("Remove them, clean up relation")
796            );
797            switch(ret) {
798            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
799            case JOptionPane.CLOSED_OPTION:
800            case JOptionPane.NO_OPTION:
801                return;
802            case JOptionPane.YES_OPTION:
803                memberTableModel.removeMembersReferringTo(toCheck);
804                break;
805            default: // Do nothing
806            }
807        }
808    }
809
810    private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut,
811            JRootPane rootPane, JTable... tables) {
812        if (shortcut == null) {
813            Logging.warn("No shortcut provided for the Paste action in Relation editor dialog");
814        } else {
815            int mods = shortcut.getModifiers();
816            int code = shortcut.getKeyCode();
817            if (code != KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) {
818                Logging.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut);
819                return;
820            }
821        }
822        rootPane.getActionMap().put(actionName, action);
823        if (shortcut != null) {
824            rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
825            // Assign also to JTables because they have their own Copy&Paste implementation
826            // (which is disabled in this case but eats key shortcuts anyway)
827            for (JTable table : tables) {
828                table.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
829                table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
830                table.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
831            }
832        }
833        if (action instanceof FlavorListener) {
834            clipboardListeners.add((FlavorListener) action);
835        }
836    }
837
838    /**
839     * Exception thrown when user aborts add operation.
840     */
841    public static class AddAbortException extends Exception {
842    }
843
844    /**
845     * Asks confirmationbefore adding a primitive.
846     * @param primitive primitive to add
847     * @return {@code true} is user confirms the operation, {@code false} otherwise
848     * @throws AddAbortException if user aborts operation
849     */
850    public static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException {
851        String msg = tr("<html>This relation already has one or more members referring to<br>"
852                + "the object ''{0}''<br>"
853                + "<br>"
854                + "Do you really want to add another relation member?</html>",
855                Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance()))
856            );
857        int ret = ConditionalOptionPaneUtil.showOptionDialog(
858                "add_primitive_to_relation",
859                Main.parent,
860                msg,
861                tr("Multiple members referring to same object."),
862                JOptionPane.YES_NO_CANCEL_OPTION,
863                JOptionPane.WARNING_MESSAGE,
864                null,
865                null
866        );
867        switch(ret) {
868        case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
869        case JOptionPane.YES_OPTION:
870            return true;
871        case JOptionPane.NO_OPTION:
872        case JOptionPane.CLOSED_OPTION:
873            return false;
874        case JOptionPane.CANCEL_OPTION:
875        default:
876            throw new AddAbortException();
877        }
878    }
879
880    /**
881     * Warn about circular references.
882     * @param primitive the concerned primitive
883     */
884    public static void warnOfCircularReferences(OsmPrimitive primitive) {
885        String msg = tr("<html>You are trying to add a relation to itself.<br>"
886                + "<br>"
887                + "This creates circular references and is therefore discouraged.<br>"
888                + "Skipping relation ''{0}''.</html>",
889                Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance())));
890        JOptionPane.showMessageDialog(
891                Main.parent,
892                msg,
893                tr("Warning"),
894                JOptionPane.WARNING_MESSAGE);
895    }
896
897    /**
898     * Adds primitives to a given relation.
899     * @param orig The relation to modify
900     * @param primitivesToAdd The primitives to add as relation members
901     * @return The resulting command
902     * @throws IllegalArgumentException if orig is null
903     */
904    public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
905        CheckParameterUtil.ensureParameterNotNull(orig, "orig");
906        try {
907            final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(
908                    EnumSet.of(TaggingPresetType.forPrimitive(orig)), orig.getKeys(), false);
909            Relation relation = new Relation(orig);
910            boolean modified = false;
911            for (OsmPrimitive p : primitivesToAdd) {
912                if (p instanceof Relation && orig.equals(p)) {
913                    if (!GraphicsEnvironment.isHeadless()) {
914                        warnOfCircularReferences(p);
915                    }
916                    continue;
917                } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p))
918                        && !confirmAddingPrimitive(p)) {
919                    continue;
920                }
921                final Set<String> roles = findSuggestedRoles(presets, p);
922                relation.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p));
923                modified = true;
924            }
925            return modified ? new ChangeCommand(orig, relation) : null;
926        } catch (AddAbortException ign) {
927            Logging.trace(ign);
928            return null;
929        }
930    }
931
932    protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) {
933        final Set<String> roles = new HashSet<>();
934        for (TaggingPreset preset : presets) {
935            String role = preset.suggestRoleForOsmPrimitive(p);
936            if (role != null && !role.isEmpty()) {
937                roles.add(role);
938            }
939        }
940        return roles;
941    }
942
943    class MemberTableDblClickAdapter extends MouseAdapter {
944        @Override
945        public void mouseClicked(MouseEvent e) {
946            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
947                new EditAction(new RelationEditorActionAccess()).actionPerformed(null);
948            }
949        }
950    }
951   
952    private class RelationEditorActionAccess implements IRelationEditorActionAccess {
953
954                @Override
955                public MemberTable getMemberTable() {
956                        return memberTable;
957                }
958
959                @Override
960                public MemberTableModel getMemberTableModel() {
961                        return memberTableModel;
962                }
963
964                @Override
965                public SelectionTable getSelectionTable() {
966                        return selectionTable;
967                }
968
969                @Override
970                public SelectionTableModel getSelectionTableModel() {
971                        return selectionTableModel;
972                }
973
974                @Override
975                public IRelationEditor getEditor() {
976                        return GenericRelationEditor.this;
977                }
978
979                @Override
980                public TagEditorModel getTagModel() {
981                        return tagEditorPanel.getModel();
982                }
983
984                @Override
985                public AutoCompletingTextField getTextFieldRole() {
986                        return tfRole;
987                }
988       
989    }
990}
Note: See TracBrowser for help on using the repository browser.