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

Last change on this file since 14470 was 14470, checked in by GerdP, 3 months ago

see #17040 Fix various memory leaks
Not sure if this will break Unit tests. Many of them don't work on my PC with a clean copy.

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