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