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

Last change on this file since 11848 was 11848, checked in by Don-vip, 7 years ago

fix #14613 - Special HTML characters not escaped in GUI error messages

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