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