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