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.TagEditorModel; 081import org.openstreetmap.josm.gui.tagging.TagEditorPanel; 082import org.openstreetmap.josm.gui.tagging.TaggingPreset; 083import org.openstreetmap.josm.gui.tagging.TaggingPresetType; 084import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField; 085import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; 086import org.openstreetmap.josm.io.OnlineResource; 087import org.openstreetmap.josm.tools.CheckParameterUtil; 088import org.openstreetmap.josm.tools.ImageProvider; 089import org.openstreetmap.josm.tools.Shortcut; 090import org.openstreetmap.josm.tools.WindowGeometry; 091 092/** 093 * This dialog is for editing relations. 094 * @since 343 095 */ 096public class GenericRelationEditor extends RelationEditor { 097 /** the tag table and its model */ 098 private TagEditorPanel tagEditorPanel; 099 private ReferringRelationsBrowser referrerBrowser; 100 private ReferringRelationsBrowserModel referrerModel; 101 102 /** the member table */ 103 private MemberTable memberTable; 104 private MemberTableModel memberTableModel; 105 106 /** the model for the selection table */ 107 private SelectionTable selectionTable; 108 private SelectionTableModel selectionTableModel; 109 110 private AutoCompletingTextField tfRole; 111 112 /** the menu item in the windows menu. Required to properly 113 * hide on dialog close. 114 */ 115 private JMenuItem windowMenuItem; 116 117 /** 118 * Creates a new relation editor for the given relation. The relation will be saved if the user 119 * selects "ok" in the editor. 120 * 121 * If no relation is given, will create an editor for a new relation. 122 * 123 * @param layer the {@link OsmDataLayer} the new or edited relation belongs to 124 * @param relation relation to edit, or null to create a new one. 125 * @param selectedMembers a collection of members which shall be selected initially 126 */ 127 public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) { 128 super(layer, relation, selectedMembers); 129 130 setRememberWindowGeometry(getClass().getName() + ".geometry", 131 WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650))); 132 133 final PresetHandler presetHandler = new PresetHandler() { 134 135 @Override 136 public void updateTags(List<Tag> tags) { 137 tagEditorPanel.getModel().updateTags(tags); 138 } 139 140 @Override 141 public Collection<OsmPrimitive> getSelection() { 142 Relation relation = new Relation(); 143 tagEditorPanel.getModel().applyToPrimitive(relation); 144 return Collections.<OsmPrimitive>singletonList(relation); 145 } 146 }; 147 148 // init the various models 149 // 150 memberTableModel = new MemberTableModel(getLayer(), presetHandler); 151 memberTableModel.register(); 152 selectionTableModel = new SelectionTableModel(getLayer()); 153 selectionTableModel.register(); 154 referrerModel = new ReferringRelationsBrowserModel(relation); 155 156 tagEditorPanel = new TagEditorPanel(presetHandler); 157 158 // populate the models 159 // 160 if (relation != null) { 161 tagEditorPanel.getModel().initFromPrimitive(relation); 162 this.memberTableModel.populate(relation); 163 if (!getLayer().data.getRelations().contains(relation)) { 164 // treat it as a new relation if it doesn't exist in the 165 // data set yet. 166 setRelation(null); 167 } 168 } else { 169 tagEditorPanel.getModel().clear(); 170 this.memberTableModel.populate(null); 171 } 172 tagEditorPanel.getModel().ensureOneTag(); 173 174 JSplitPane pane = buildSplitPane(relation); 175 pane.setPreferredSize(new Dimension(100, 100)); 176 177 JPanel pnl = new JPanel(); 178 pnl.setLayout(new BorderLayout()); 179 pnl.add(pane, BorderLayout.CENTER); 180 pnl.setBorder(BorderFactory.createRaisedBevelBorder()); 181 182 getContentPane().setLayout(new BorderLayout()); 183 JTabbedPane tabbedPane = new JTabbedPane(); 184 tabbedPane.add(tr("Tags and Members"), pnl); 185 referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel); 186 tabbedPane.add(tr("Parent Relations"), referrerBrowser); 187 tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation)); 188 tabbedPane.addChangeListener( 189 new ChangeListener() { 190 @Override 191 public void stateChanged(ChangeEvent e) { 192 JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource(); 193 int index = sourceTabbedPane.getSelectedIndex(); 194 String title = sourceTabbedPane.getTitleAt(index); 195 if (title.equals(tr("Parent Relations"))) { 196 referrerBrowser.init(); 197 } 198 } 199 } 200 ); 201 202 getContentPane().add(buildToolBar(), BorderLayout.NORTH); 203 getContentPane().add(tabbedPane, BorderLayout.CENTER); 204 getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH); 205 206 setSize(findMaxDialogSize()); 207 208 addWindowListener( 209 new WindowAdapter() { 210 @Override 211 public void windowOpened(WindowEvent e) { 212 cleanSelfReferences(); 213 } 214 } 215 ); 216 registerCopyPasteAction(tagEditorPanel.getPasteAction(), 217 "PASTE_TAGS", 218 Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke()); 219 registerCopyPasteAction(new PasteMembersAction(), "PASTE_MEMBERS", Shortcut.getPasteKeyStroke()); 220 registerCopyPasteAction(new CopyMembersAction(), "COPY_MEMBERS", Shortcut.getCopyKeyStroke()); 221 222 tagEditorPanel.setNextFocusComponent(memberTable); 223 selectionTable.setFocusable(false); 224 memberTableModel.setSelectedMembers(selectedMembers); 225 HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/RelationEditor")); 226 } 227 228 /** 229 * Creates the toolbar 230 * 231 * @return the toolbar 232 */ 233 protected JToolBar buildToolBar() { 234 JToolBar tb = new JToolBar(); 235 tb.setFloatable(false); 236 tb.add(new ApplyAction()); 237 tb.add(new DuplicateRelationAction()); 238 DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction(); 239 addPropertyChangeListener(deleteAction); 240 tb.add(deleteAction); 241 return tb; 242 } 243 244 /** 245 * builds the panel with the OK and the Cancel button 246 * 247 * @return the panel with the OK and the Cancel button 248 */ 249 protected JPanel buildOkCancelButtonPanel() { 250 JPanel pnl = new JPanel(); 251 pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); 252 253 pnl.add(new SideButton(new OKAction())); 254 pnl.add(new SideButton(new CancelAction())); 255 pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor")))); 256 return pnl; 257 } 258 259 /** 260 * builds the panel with the tag editor 261 * 262 * @return the panel with the tag editor 263 */ 264 protected JPanel buildTagEditorPanel() { 265 JPanel pnl = new JPanel(); 266 pnl.setLayout(new GridBagLayout()); 267 268 GridBagConstraints gc = new GridBagConstraints(); 269 gc.gridx = 0; 270 gc.gridy = 0; 271 gc.gridheight = 1; 272 gc.gridwidth = 1; 273 gc.fill = GridBagConstraints.HORIZONTAL; 274 gc.anchor = GridBagConstraints.FIRST_LINE_START; 275 gc.weightx = 1.0; 276 gc.weighty = 0.0; 277 pnl.add(new JLabel(tr("Tags")), gc); 278 279 gc.gridx = 0; 280 gc.gridy = 1; 281 gc.fill = GridBagConstraints.BOTH; 282 gc.anchor = GridBagConstraints.CENTER; 283 gc.weightx = 1.0; 284 gc.weighty = 1.0; 285 pnl.add(tagEditorPanel, gc); 286 return pnl; 287 } 288 289 /** 290 * builds the panel for the relation member editor 291 * 292 * @return the panel for the relation member editor 293 */ 294 protected JPanel buildMemberEditorPanel() { 295 final JPanel pnl = new JPanel(new GridBagLayout()); 296 // setting up the member table 297 memberTable = new MemberTable(getLayer(), getRelation(), 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, getRelation()); 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(new BorderLayout()); 441 MemberRoleCellEditor ce = (MemberRoleCellEditor)memberTable.getColumnModel().getColumn(0).getCellEditor(); 442 selectionTable = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel)); 443 selectionTable.setMemberTableModel(memberTableModel); 444 selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height); 445 pnl.add(new JScrollPane(selectionTable), 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(Relation relation) { 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) 740 throws IllegalArgumentException { 741 CheckParameterUtil.ensureParameterNotNull(orig, "orig"); 742 try { 743 final Collection<TaggingPreset> presets = TaggingPreset.getMatchingPresets( 744 EnumSet.of(TaggingPresetType.RELATION), orig.getKeys(), false); 745 Relation relation = new Relation(orig); 746 boolean modified = false; 747 for (OsmPrimitive p : primitivesToAdd) { 748 if (p instanceof Relation && orig.equals(p)) { 749 warnOfCircularReferences(p); 750 continue; 751 } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p)) 752 && !confirmAddingPrimitive(p)) { 753 continue; 754 } 755 final String role = presets.isEmpty() ? null : presets.iterator().next().suggestRoleForOsmPrimitive(p); 756 relation.addMember(new RelationMember(role == null ? "" : role, p)); 757 modified = true; 758 } 759 return modified ? new ChangeCommand(orig, relation) : null; 760 } catch (AddAbortException ign) { 761 return null; 762 } 763 } 764 765 abstract class AddFromSelectionAction extends AbstractAction { 766 protected boolean isPotentialDuplicate(OsmPrimitive primitive) { 767 return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive)); 768 } 769 770 protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException { 771 if (primitives == null || primitives.isEmpty()) 772 return primitives; 773 List<OsmPrimitive> ret = new ArrayList<>(); 774 ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation"); 775 for (OsmPrimitive primitive : primitives) { 776 if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) { 777 warnOfCircularReferences(primitive); 778 continue; 779 } 780 if (isPotentialDuplicate(primitive)) { 781 if (confirmAddingPrimitive(primitive)) { 782 ret.add(primitive); 783 } 784 continue; 785 } else { 786 ret.add(primitive); 787 } 788 } 789 ConditionalOptionPaneUtil.endBulkOperation("add_primitive_to_relation"); 790 return ret; 791 } 792 } 793 794 class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener { 795 public AddSelectedAtStartAction() { 796 putValue(SHORT_DESCRIPTION, 797 tr("Add all objects selected in the current dataset before the first member")); 798 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright")); 799 refreshEnabled(); 800 } 801 802 protected void refreshEnabled() { 803 setEnabled(selectionTableModel.getRowCount() > 0); 804 } 805 806 @Override 807 public void actionPerformed(ActionEvent e) { 808 try { 809 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 810 memberTableModel.addMembersAtBeginning(toAdd); 811 } catch(AddAbortException ex) { 812 // do nothing 813 } 814 } 815 816 @Override 817 public void tableChanged(TableModelEvent e) { 818 refreshEnabled(); 819 } 820 } 821 822 class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener { 823 public AddSelectedAtEndAction() { 824 putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member")); 825 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright")); 826 refreshEnabled(); 827 } 828 829 protected void refreshEnabled() { 830 setEnabled(selectionTableModel.getRowCount() > 0); 831 } 832 833 @Override 834 public void actionPerformed(ActionEvent e) { 835 try { 836 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 837 memberTableModel.addMembersAtEnd(toAdd); 838 } catch(AddAbortException ex) { 839 // do nothing 840 } 841 } 842 843 @Override 844 public void tableChanged(TableModelEvent e) { 845 refreshEnabled(); 846 } 847 } 848 849 class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { 850 public AddSelectedBeforeSelection() { 851 putValue(SHORT_DESCRIPTION, 852 tr("Add all objects selected in the current dataset before the first selected member")); 853 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright")); 854 refreshEnabled(); 855 } 856 857 protected void refreshEnabled() { 858 setEnabled(selectionTableModel.getRowCount() > 0 859 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); 860 } 861 862 @Override 863 public void actionPerformed(ActionEvent e) { 864 try { 865 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 866 memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel 867 .getSelectionModel().getMinSelectionIndex()); 868 } catch(AddAbortException ex) { 869 // do nothing 870 } 871 872 } 873 874 @Override 875 public void tableChanged(TableModelEvent e) { 876 refreshEnabled(); 877 } 878 879 @Override 880 public void valueChanged(ListSelectionEvent e) { 881 refreshEnabled(); 882 } 883 } 884 885 class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { 886 public AddSelectedAfterSelection() { 887 putValue(SHORT_DESCRIPTION, 888 tr("Add all objects selected in the current dataset after the last selected member")); 889 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright")); 890 refreshEnabled(); 891 } 892 893 protected void refreshEnabled() { 894 setEnabled(selectionTableModel.getRowCount() > 0 895 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); 896 } 897 898 @Override 899 public void actionPerformed(ActionEvent e) { 900 try { 901 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 902 memberTableModel.addMembersAfterIdx(toAdd, memberTableModel 903 .getSelectionModel().getMaxSelectionIndex()); 904 } catch(AddAbortException ex) { 905 // do nothing 906 } 907 } 908 909 @Override 910 public void tableChanged(TableModelEvent e) { 911 refreshEnabled(); 912 } 913 914 @Override 915 public void valueChanged(ListSelectionEvent e) { 916 refreshEnabled(); 917 } 918 } 919 920 class RemoveSelectedAction extends AbstractAction implements TableModelListener { 921 public RemoveSelectedAction() { 922 putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects")); 923 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers")); 924 updateEnabledState(); 925 } 926 927 protected void updateEnabledState() { 928 DataSet ds = getLayer().data; 929 if (ds == null || ds.getSelected().isEmpty()) { 930 setEnabled(false); 931 return; 932 } 933 // only enable the action if we have members referring to the 934 // selected primitives 935 // 936 setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected())); 937 } 938 939 @Override 940 public void actionPerformed(ActionEvent e) { 941 memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection()); 942 } 943 944 @Override 945 public void tableChanged(TableModelEvent e) { 946 updateEnabledState(); 947 } 948 } 949 950 /** 951 * Selects members in the relation editor which refer to primitives in the current 952 * selection of the context layer. 953 * 954 */ 955 class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener { 956 public SelectedMembersForSelectionAction() { 957 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection")); 958 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers")); 959 updateEnabledState(); 960 } 961 962 protected void updateEnabledState() { 963 boolean enabled = selectionTableModel.getRowCount() > 0 964 && !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty(); 965 966 if (enabled) { 967 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection",memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size())); 968 } else { 969 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection")); 970 } 971 setEnabled(enabled); 972 } 973 974 @Override 975 public void actionPerformed(ActionEvent e) { 976 memberTableModel.selectMembersReferringTo(getLayer().data.getSelected()); 977 } 978 979 @Override 980 public void tableChanged(TableModelEvent e) { 981 updateEnabledState(); 982 983 } 984 } 985 986 /** 987 * Selects primitives in the layer this editor belongs to. The selected primitives are 988 * equal to the set of primitives the currently selected relation members refer to. 989 * 990 */ 991 class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener { 992 public SelectPrimitivesForSelectedMembersAction() { 993 putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members")); 994 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives")); 995 updateEnabledState(); 996 } 997 998 protected void updateEnabledState() { 999 setEnabled(memberTable.getSelectedRowCount() > 0); 1000 } 1001 1002 @Override 1003 public void actionPerformed(ActionEvent e) { 1004 getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives()); 1005 } 1006 1007 @Override 1008 public void valueChanged(ListSelectionEvent e) { 1009 updateEnabledState(); 1010 } 1011 } 1012 1013 class SortAction extends AbstractAction implements TableModelListener { 1014 public SortAction() { 1015 String tooltip = tr("Sort the relation members"); 1016 putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort")); 1017 putValue(NAME, tr("Sort")); 1018 Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"), 1019 KeyEvent.VK_END, Shortcut.ALT); 1020 sc.setAccelerator(this); 1021 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1022 updateEnabledState(); 1023 } 1024 1025 @Override 1026 public void actionPerformed(ActionEvent e) { 1027 memberTableModel.sort(); 1028 } 1029 1030 protected void updateEnabledState() { 1031 setEnabled(memberTableModel.getRowCount() > 0); 1032 } 1033 1034 @Override 1035 public void tableChanged(TableModelEvent e) { 1036 updateEnabledState(); 1037 } 1038 } 1039 1040 class ReverseAction extends AbstractAction implements TableModelListener { 1041 public ReverseAction() { 1042 putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members")); 1043 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse")); 1044 putValue(NAME, tr("Reverse")); 1045 // Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"), 1046 // KeyEvent.VK_END, Shortcut.ALT) 1047 updateEnabledState(); 1048 } 1049 1050 @Override 1051 public void actionPerformed(ActionEvent e) { 1052 memberTableModel.reverse(); 1053 } 1054 1055 protected void updateEnabledState() { 1056 setEnabled(memberTableModel.getRowCount() > 0); 1057 } 1058 1059 @Override 1060 public void tableChanged(TableModelEvent e) { 1061 updateEnabledState(); 1062 } 1063 } 1064 1065 class MoveUpAction extends AbstractAction implements ListSelectionListener { 1066 public MoveUpAction() { 1067 String tooltip = tr("Move the currently selected members up"); 1068 putValue(SMALL_ICON, ImageProvider.get("dialogs", "moveup")); 1069 Shortcut sc = Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Up"), 1070 KeyEvent.VK_UP, Shortcut.ALT); 1071 sc.setAccelerator(this); 1072 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1073 setEnabled(false); 1074 } 1075 1076 @Override 1077 public void actionPerformed(ActionEvent e) { 1078 memberTableModel.moveUp(memberTable.getSelectedRows()); 1079 } 1080 1081 @Override 1082 public void valueChanged(ListSelectionEvent e) { 1083 setEnabled(memberTableModel.canMoveUp(memberTable.getSelectedRows())); 1084 } 1085 } 1086 1087 class MoveDownAction extends AbstractAction implements ListSelectionListener { 1088 public MoveDownAction() { 1089 String tooltip = tr("Move the currently selected members down"); 1090 putValue(SMALL_ICON, ImageProvider.get("dialogs", "movedown")); 1091 Shortcut sc = Shortcut.registerShortcut("relationeditor:movedown", tr("Relation Editor: Move Down"), 1092 KeyEvent.VK_DOWN, Shortcut.ALT); 1093 sc.setAccelerator(this); 1094 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1095 setEnabled(false); 1096 } 1097 1098 @Override 1099 public void actionPerformed(ActionEvent e) { 1100 memberTableModel.moveDown(memberTable.getSelectedRows()); 1101 } 1102 1103 @Override 1104 public void valueChanged(ListSelectionEvent e) { 1105 setEnabled(memberTableModel.canMoveDown(memberTable.getSelectedRows())); 1106 } 1107 } 1108 1109 class RemoveAction extends AbstractAction implements ListSelectionListener { 1110 public RemoveAction() { 1111 String tooltip = tr("Remove the currently selected members from this relation"); 1112 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 1113 putValue(NAME, tr("Remove")); 1114 Shortcut sc = Shortcut.registerShortcut("relationeditor:remove", tr("Relation Editor: Remove"), 1115 KeyEvent.VK_DELETE, Shortcut.ALT); 1116 sc.setAccelerator(this); 1117 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1118 setEnabled(false); 1119 } 1120 1121 @Override 1122 public void actionPerformed(ActionEvent e) { 1123 memberTableModel.remove(memberTable.getSelectedRows()); 1124 } 1125 1126 @Override 1127 public void valueChanged(ListSelectionEvent e) { 1128 setEnabled(memberTableModel.canRemove(memberTable.getSelectedRows())); 1129 } 1130 } 1131 1132 class DeleteCurrentRelationAction extends AbstractAction implements PropertyChangeListener{ 1133 public DeleteCurrentRelationAction() { 1134 putValue(SHORT_DESCRIPTION, tr("Delete the currently edited relation")); 1135 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 1136 putValue(NAME, tr("Delete")); 1137 updateEnabledState(); 1138 } 1139 1140 public void run() { 1141 Relation toDelete = getRelation(); 1142 if (toDelete == null) 1143 return; 1144 org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation( 1145 getLayer(), 1146 toDelete 1147 ); 1148 } 1149 1150 @Override 1151 public void actionPerformed(ActionEvent e) { 1152 run(); 1153 } 1154 1155 protected void updateEnabledState() { 1156 setEnabled(getRelationSnapshot() != null); 1157 } 1158 1159 @Override 1160 public void propertyChange(PropertyChangeEvent evt) { 1161 if (evt.getPropertyName().equals(RELATION_SNAPSHOT_PROP)) { 1162 updateEnabledState(); 1163 } 1164 } 1165 } 1166 1167 abstract class SavingAction extends AbstractAction { 1168 /** 1169 * apply updates to a new relation 1170 */ 1171 protected void applyNewRelation() { 1172 final Relation newRelation = new Relation(); 1173 tagEditorPanel.getModel().applyToPrimitive(newRelation); 1174 memberTableModel.applyToRelation(newRelation); 1175 List<RelationMember> newMembers = new ArrayList<>(); 1176 for (RelationMember rm: newRelation.getMembers()) { 1177 if (!rm.getMember().isDeleted()) { 1178 newMembers.add(rm); 1179 } 1180 } 1181 if (newRelation.getMembersCount() != newMembers.size()) { 1182 newRelation.setMembers(newMembers); 1183 String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" + 1184 "was open. They have been removed from the relation members list."); 1185 JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE); 1186 } 1187 // If the user wanted to create a new relation, but hasn't added any members or 1188 // tags, don't add an empty relation 1189 if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys()) 1190 return; 1191 Main.main.undoRedo.add(new AddCommand(getLayer(),newRelation)); 1192 1193 // make sure everybody is notified about the changes 1194 // 1195 getLayer().data.fireSelectionChanged(); 1196 GenericRelationEditor.this.setRelation(newRelation); 1197 RelationDialogManager.getRelationDialogManager().updateContext( 1198 getLayer(), 1199 getRelation(), 1200 GenericRelationEditor.this 1201 ); 1202 SwingUtilities.invokeLater(new Runnable() { 1203 @Override 1204 public void run() { 1205 // Relation list gets update in EDT so selecting my be postponed to following EDT run 1206 Main.map.relationListDialog.selectRelation(newRelation); 1207 } 1208 }); 1209 } 1210 1211 /** 1212 * Apply the updates for an existing relation which has been changed 1213 * outside of the relation editor. 1214 * 1215 */ 1216 protected void applyExistingConflictingRelation() { 1217 Relation editedRelation = new Relation(getRelation()); 1218 tagEditorPanel.getModel().applyToPrimitive(editedRelation); 1219 memberTableModel.applyToRelation(editedRelation); 1220 Conflict<Relation> conflict = new Conflict<>(getRelation(), editedRelation); 1221 Main.main.undoRedo.add(new ConflictAddCommand(getLayer(),conflict)); 1222 } 1223 1224 /** 1225 * Apply the updates for an existing relation which has not been changed 1226 * outside of the relation editor. 1227 * 1228 */ 1229 protected void applyExistingNonConflictingRelation() { 1230 Relation editedRelation = new Relation(getRelation()); 1231 tagEditorPanel.getModel().applyToPrimitive(editedRelation); 1232 memberTableModel.applyToRelation(editedRelation); 1233 Main.main.undoRedo.add(new ChangeCommand(getRelation(), editedRelation)); 1234 getLayer().data.fireSelectionChanged(); 1235 // this will refresh the snapshot and update the dialog title 1236 // 1237 setRelation(getRelation()); 1238 } 1239 1240 protected boolean confirmClosingBecauseOfDirtyState() { 1241 ButtonSpec [] options = new ButtonSpec[] { 1242 new ButtonSpec( 1243 tr("Yes, create a conflict and close"), 1244 ImageProvider.get("ok"), 1245 tr("Click to create a conflict and close this relation editor") , 1246 null /* no specific help topic */ 1247 ), 1248 new ButtonSpec( 1249 tr("No, continue editing"), 1250 ImageProvider.get("cancel"), 1251 tr("Click to return to the relation editor and to resume relation editing") , 1252 null /* no specific help topic */ 1253 ) 1254 }; 1255 1256 int ret = HelpAwareOptionPane.showOptionDialog( 1257 Main.parent, 1258 tr("<html>This relation has been changed outside of the editor.<br>" 1259 + "You cannot apply your changes and continue editing.<br>" 1260 + "<br>" 1261 + "Do you want to create a conflict and close the editor?</html>"), 1262 tr("Conflict in data"), 1263 JOptionPane.WARNING_MESSAGE, 1264 null, 1265 options, 1266 options[0], // OK is default 1267 "/Dialog/RelationEditor#RelationChangedOutsideOfEditor" 1268 ); 1269 return ret == 0; 1270 } 1271 1272 protected void warnDoubleConflict() { 1273 JOptionPane.showMessageDialog( 1274 Main.parent, 1275 tr("<html>Layer ''{0}'' already has a conflict for object<br>" 1276 + "''{1}''.<br>" 1277 + "Please resolve this conflict first, then try again.</html>", 1278 getLayer().getName(), 1279 getRelation().getDisplayName(DefaultNameFormatter.getInstance()) 1280 ), 1281 tr("Double conflict"), 1282 JOptionPane.WARNING_MESSAGE 1283 ); 1284 } 1285 } 1286 1287 class ApplyAction extends SavingAction { 1288 public ApplyAction() { 1289 putValue(SHORT_DESCRIPTION, tr("Apply the current updates")); 1290 putValue(SMALL_ICON, ImageProvider.get("save")); 1291 putValue(NAME, tr("Apply")); 1292 setEnabled(true); 1293 } 1294 1295 public void run() { 1296 if (getRelation() == null) { 1297 applyNewRelation(); 1298 } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) 1299 || tagEditorPanel.getModel().isDirty()) { 1300 if (isDirtyRelation()) { 1301 if (confirmClosingBecauseOfDirtyState()) { 1302 if (getLayer().getConflicts().hasConflictForMy(getRelation())) { 1303 warnDoubleConflict(); 1304 return; 1305 } 1306 applyExistingConflictingRelation(); 1307 setVisible(false); 1308 } 1309 } else { 1310 applyExistingNonConflictingRelation(); 1311 } 1312 } 1313 } 1314 1315 @Override 1316 public void actionPerformed(ActionEvent e) { 1317 run(); 1318 } 1319 } 1320 1321 class OKAction extends SavingAction { 1322 public OKAction() { 1323 putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog")); 1324 putValue(SMALL_ICON, ImageProvider.get("ok")); 1325 putValue(NAME, tr("OK")); 1326 setEnabled(true); 1327 } 1328 1329 public void run() { 1330 Main.pref.put("relation.editor.generic.lastrole", tfRole.getText()); 1331 memberTable.stopHighlighting(); 1332 if (getRelation() == null) { 1333 applyNewRelation(); 1334 } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) 1335 || tagEditorPanel.getModel().isDirty()) { 1336 if (isDirtyRelation()) { 1337 if (confirmClosingBecauseOfDirtyState()) { 1338 if (getLayer().getConflicts().hasConflictForMy(getRelation())) { 1339 warnDoubleConflict(); 1340 return; 1341 } 1342 applyExistingConflictingRelation(); 1343 } else 1344 return; 1345 } else { 1346 applyExistingNonConflictingRelation(); 1347 } 1348 } 1349 setVisible(false); 1350 } 1351 1352 @Override 1353 public void actionPerformed(ActionEvent e) { 1354 run(); 1355 } 1356 } 1357 1358 class CancelAction extends SavingAction { 1359 public CancelAction() { 1360 putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog")); 1361 putValue(SMALL_ICON, ImageProvider.get("cancel")); 1362 putValue(NAME, tr("Cancel")); 1363 1364 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) 1365 .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE"); 1366 getRootPane().getActionMap().put("ESCAPE", this); 1367 setEnabled(true); 1368 } 1369 1370 @Override 1371 public void actionPerformed(ActionEvent e) { 1372 memberTable.stopHighlighting(); 1373 TagEditorModel tagModel = tagEditorPanel.getModel(); 1374 Relation snapshot = getRelationSnapshot(); 1375 if ( (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty()) 1376 && !(snapshot == null && tagModel.getTags().isEmpty())) { 1377 //give the user a chance to save the changes 1378 int ret = confirmClosingByCancel(); 1379 if (ret == 0) { //Yes, save the changes 1380 //copied from OKAction.run() 1381 Main.pref.put("relation.editor.generic.lastrole", tfRole.getText()); 1382 if (getRelation() == null) { 1383 applyNewRelation(); 1384 } else if (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty()) { 1385 if (isDirtyRelation()) { 1386 if (confirmClosingBecauseOfDirtyState()) { 1387 if (getLayer().getConflicts().hasConflictForMy(getRelation())) { 1388 warnDoubleConflict(); 1389 return; 1390 } 1391 applyExistingConflictingRelation(); 1392 } else 1393 return; 1394 } else { 1395 applyExistingNonConflictingRelation(); 1396 } 1397 } 1398 } else if (ret == 2) //Cancel, continue editing 1399 return; 1400 //in case of "No, discard", there is no extra action to be performed here. 1401 } 1402 setVisible(false); 1403 } 1404 1405 protected int confirmClosingByCancel() { 1406 ButtonSpec [] options = new ButtonSpec[] { 1407 new ButtonSpec( 1408 tr("Yes, save the changes and close"), 1409 ImageProvider.get("ok"), 1410 tr("Click to save the changes and close this relation editor") , 1411 null /* no specific help topic */ 1412 ), 1413 new ButtonSpec( 1414 tr("No, discard the changes and close"), 1415 ImageProvider.get("cancel"), 1416 tr("Click to discard the changes and close this relation editor") , 1417 null /* no specific help topic */ 1418 ), 1419 new ButtonSpec( 1420 tr("Cancel, continue editing"), 1421 ImageProvider.get("cancel"), 1422 tr("Click to return to the relation editor and to resume relation editing") , 1423 null /* no specific help topic */ 1424 ) 1425 }; 1426 1427 return HelpAwareOptionPane.showOptionDialog( 1428 Main.parent, 1429 tr("<html>The relation has been changed.<br>" 1430 + "<br>" 1431 + "Do you want to save your changes?</html>"), 1432 tr("Unsaved changes"), 1433 JOptionPane.WARNING_MESSAGE, 1434 null, 1435 options, 1436 options[0], // OK is default, 1437 "/Dialog/RelationEditor#DiscardChanges" 1438 ); 1439 } 1440 } 1441 1442 class AddTagAction extends AbstractAction { 1443 public AddTagAction() { 1444 putValue(SHORT_DESCRIPTION, tr("Add an empty tag")); 1445 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); 1446 setEnabled(true); 1447 } 1448 1449 @Override 1450 public void actionPerformed(ActionEvent e) { 1451 tagEditorPanel.getModel().appendNewTag(); 1452 } 1453 } 1454 1455 class DownloadIncompleteMembersAction extends AbstractAction implements TableModelListener { 1456 public DownloadIncompleteMembersAction() { 1457 String tooltip = tr("Download all incomplete members"); 1458 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincomplete")); 1459 putValue(NAME, tr("Download Members")); 1460 Shortcut sc = Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"), 1461 KeyEvent.VK_HOME, Shortcut.ALT); 1462 sc.setAccelerator(this); 1463 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1464 updateEnabledState(); 1465 } 1466 1467 @Override 1468 public void actionPerformed(ActionEvent e) { 1469 if (!isEnabled()) 1470 return; 1471 Main.worker.submit(new DownloadRelationMemberTask( 1472 getRelation(), 1473 memberTableModel.getIncompleteMemberPrimitives(), 1474 getLayer(), 1475 GenericRelationEditor.this) 1476 ); 1477 } 1478 1479 protected void updateEnabledState() { 1480 setEnabled(memberTableModel.hasIncompleteMembers() && !Main.isOffline(OnlineResource.OSM_API)); 1481 } 1482 1483 @Override 1484 public void tableChanged(TableModelEvent e) { 1485 updateEnabledState(); 1486 } 1487 } 1488 1489 class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener, TableModelListener{ 1490 public DownloadSelectedIncompleteMembersAction() { 1491 putValue(SHORT_DESCRIPTION, tr("Download selected incomplete members")); 1492 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected")); 1493 putValue(NAME, tr("Download Members")); 1494 // Shortcut.register Shortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"), 1495 // KeyEvent.VK_K, Shortcut.ALT) 1496 updateEnabledState(); 1497 } 1498 1499 @Override 1500 public void actionPerformed(ActionEvent e) { 1501 if (!isEnabled()) 1502 return; 1503 Main.worker.submit(new DownloadRelationMemberTask( 1504 getRelation(), 1505 memberTableModel.getSelectedIncompleteMemberPrimitives(), 1506 getLayer(), 1507 GenericRelationEditor.this) 1508 ); 1509 } 1510 1511 protected void updateEnabledState() { 1512 setEnabled(memberTableModel.hasIncompleteSelectedMembers() && !Main.isOffline(OnlineResource.OSM_API)); 1513 } 1514 1515 @Override 1516 public void valueChanged(ListSelectionEvent e) { 1517 updateEnabledState(); 1518 } 1519 1520 @Override 1521 public void tableChanged(TableModelEvent e) { 1522 updateEnabledState(); 1523 } 1524 } 1525 1526 class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener { 1527 public SetRoleAction() { 1528 putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members")); 1529 putValue(SMALL_ICON, ImageProvider.get("apply")); 1530 putValue(NAME, tr("Apply Role")); 1531 refreshEnabled(); 1532 } 1533 1534 protected void refreshEnabled() { 1535 setEnabled(memberTable.getSelectedRowCount() > 0); 1536 } 1537 1538 protected boolean isEmptyRole() { 1539 return tfRole.getText() == null || tfRole.getText().trim().isEmpty(); 1540 } 1541 1542 protected boolean confirmSettingEmptyRole(int onNumMembers) { 1543 String message = "<html>" 1544 + trn("You are setting an empty role on {0} object.", 1545 "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers) 1546 + "<br>" 1547 + tr("This is equal to deleting the roles of these objects.") + 1548 "<br>" 1549 + tr("Do you really want to apply the new role?") + "</html>"; 1550 String [] options = new String[] { 1551 tr("Yes, apply it"), 1552 tr("No, do not apply") 1553 }; 1554 int ret = ConditionalOptionPaneUtil.showOptionDialog( 1555 "relation_editor.confirm_applying_empty_role", 1556 Main.parent, 1557 message, 1558 tr("Confirm empty role"), 1559 JOptionPane.YES_NO_OPTION, 1560 JOptionPane.WARNING_MESSAGE, 1561 options, 1562 options[0] 1563 ); 1564 switch(ret) { 1565 case JOptionPane.YES_OPTION: return true; 1566 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return true; 1567 default: 1568 return false; 1569 } 1570 } 1571 1572 @Override 1573 public void actionPerformed(ActionEvent e) { 1574 if (isEmptyRole()) { 1575 if (! confirmSettingEmptyRole(memberTable.getSelectedRowCount())) 1576 return; 1577 } 1578 memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText()); 1579 } 1580 1581 @Override 1582 public void valueChanged(ListSelectionEvent e) { 1583 refreshEnabled(); 1584 } 1585 1586 @Override 1587 public void changedUpdate(DocumentEvent e) { 1588 refreshEnabled(); 1589 } 1590 1591 @Override 1592 public void insertUpdate(DocumentEvent e) { 1593 refreshEnabled(); 1594 } 1595 1596 @Override 1597 public void removeUpdate(DocumentEvent e) { 1598 refreshEnabled(); 1599 } 1600 } 1601 1602 /** 1603 * Creates a new relation with a copy of the current editor state. 1604 */ 1605 class DuplicateRelationAction extends AbstractAction { 1606 public DuplicateRelationAction() { 1607 putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window")); 1608 // FIXME provide an icon 1609 putValue(SMALL_ICON, ImageProvider.get("duplicate")); 1610 putValue(NAME, tr("Duplicate")); 1611 setEnabled(true); 1612 } 1613 1614 @Override 1615 public void actionPerformed(ActionEvent e) { 1616 Relation copy = new Relation(); 1617 tagEditorPanel.getModel().applyToPrimitive(copy); 1618 memberTableModel.applyToRelation(copy); 1619 RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers()); 1620 editor.setVisible(true); 1621 } 1622 } 1623 1624 /** 1625 * Action for editing the currently selected relation. 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 class MemberTableDblClickAdapter extends MouseAdapter { 1736 @Override 1737 public void mouseClicked(MouseEvent e) { 1738 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 1739 new EditAction().run(); 1740 } 1741 } 1742 } 1743}