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