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: 667 case JOptionPane.CLOSED_OPTION: 668 case JOptionPane.NO_OPTION: 669 return; 670 case JOptionPane.YES_OPTION: 671 memberTableModel.removeMembersReferringTo(toCheck); 672 break; 673 } 674 } 675 } 676 677 678 private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut) { 679 int mods = shortcut.getModifiers(); 680 int code = shortcut.getKeyCode(); 681 if (code!=KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) { 682 Main.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut); 683 return; 684 } 685 getRootPane().getActionMap().put(actionName, action); 686 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 687 // Assign also to JTables because they have their own Copy&Paste implementation (which is disabled in this case but eats key shortcuts anyway) 688 memberTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName); 689 memberTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName); 690 memberTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 691 selectionTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName); 692 selectionTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName); 693 selectionTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 694 } 695 696 static class AddAbortException extends Exception { 697 } 698 699 static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException { 700 String msg = tr("<html>This relation already has one or more members referring to<br>" 701 + "the object ''{0}''<br>" 702 + "<br>" 703 + "Do you really want to add another relation member?</html>", 704 primitive.getDisplayName(DefaultNameFormatter.getInstance()) 705 ); 706 int ret = ConditionalOptionPaneUtil.showOptionDialog( 707 "add_primitive_to_relation", 708 Main.parent, 709 msg, 710 tr("Multiple members referring to same object."), 711 JOptionPane.YES_NO_CANCEL_OPTION, 712 JOptionPane.WARNING_MESSAGE, 713 null, 714 null 715 ); 716 switch(ret) { 717 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 718 case JOptionPane.YES_OPTION: 719 return true; 720 case JOptionPane.NO_OPTION: 721 case JOptionPane.CLOSED_OPTION: 722 return false; 723 case JOptionPane.CANCEL_OPTION: 724 throw new AddAbortException(); 725 } 726 // should not happen 727 return false; 728 } 729 730 static void warnOfCircularReferences(OsmPrimitive primitive) { 731 String msg = tr("<html>You are trying to add a relation to itself.<br>" 732 + "<br>" 733 + "This creates circular references and is therefore discouraged.<br>" 734 + "Skipping relation ''{0}''.</html>", 735 primitive.getDisplayName(DefaultNameFormatter.getInstance())); 736 JOptionPane.showMessageDialog( 737 Main.parent, 738 msg, 739 tr("Warning"), 740 JOptionPane.WARNING_MESSAGE); 741 } 742 743 /** 744 * Adds primitives to a given relation. 745 * @param orig The relation to modify 746 * @param primitivesToAdd The primitives to add as relation members 747 * @return The resulting command 748 * @throws IllegalArgumentException if orig is null 749 */ 750 public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) 751 throws IllegalArgumentException { 752 CheckParameterUtil.ensureParameterNotNull(orig, "orig"); 753 try { 754 final Collection<TaggingPreset> presets = TaggingPreset.getMatchingPresets( 755 EnumSet.of(TaggingPresetType.RELATION), orig.getKeys(), false); 756 Relation relation = new Relation(orig); 757 boolean modified = false; 758 for (OsmPrimitive p : primitivesToAdd) { 759 if (p instanceof Relation && orig.equals(p)) { 760 warnOfCircularReferences(p); 761 continue; 762 } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p)) 763 && !confirmAddingPrimitive(p)) { 764 continue; 765 } 766 final Set<String> roles = findSuggestedRoles(presets, p); 767 relation.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p)); 768 modified = true; 769 } 770 return modified ? new ChangeCommand(orig, relation) : null; 771 } catch (AddAbortException ign) { 772 return null; 773 } 774 } 775 776 protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) { 777 final Set<String> roles = new HashSet<>(); 778 for (TaggingPreset preset : presets) { 779 String role = preset.suggestRoleForOsmPrimitive(p); 780 if (role != null && !role.isEmpty()) { 781 roles.add(role); 782 } 783 } 784 return roles; 785 } 786 787 abstract class AddFromSelectionAction extends AbstractAction { 788 protected boolean isPotentialDuplicate(OsmPrimitive primitive) { 789 return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive)); 790 } 791 792 protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException { 793 if (primitives == null || primitives.isEmpty()) 794 return primitives; 795 List<OsmPrimitive> ret = new ArrayList<>(); 796 ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation"); 797 for (OsmPrimitive primitive : primitives) { 798 if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) { 799 warnOfCircularReferences(primitive); 800 continue; 801 } 802 if (isPotentialDuplicate(primitive)) { 803 if (confirmAddingPrimitive(primitive)) { 804 ret.add(primitive); 805 } 806 continue; 807 } else { 808 ret.add(primitive); 809 } 810 } 811 ConditionalOptionPaneUtil.endBulkOperation("add_primitive_to_relation"); 812 return ret; 813 } 814 } 815 816 class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener { 817 public AddSelectedAtStartAction() { 818 putValue(SHORT_DESCRIPTION, 819 tr("Add all objects selected in the current dataset before the first member")); 820 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright")); 821 refreshEnabled(); 822 } 823 824 protected void refreshEnabled() { 825 setEnabled(selectionTableModel.getRowCount() > 0); 826 } 827 828 @Override 829 public void actionPerformed(ActionEvent e) { 830 try { 831 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 832 memberTableModel.addMembersAtBeginning(toAdd); 833 } catch(AddAbortException ex) { 834 // do nothing 835 } 836 } 837 838 @Override 839 public void tableChanged(TableModelEvent e) { 840 refreshEnabled(); 841 } 842 } 843 844 class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener { 845 public AddSelectedAtEndAction() { 846 putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member")); 847 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright")); 848 refreshEnabled(); 849 } 850 851 protected void refreshEnabled() { 852 setEnabled(selectionTableModel.getRowCount() > 0); 853 } 854 855 @Override 856 public void actionPerformed(ActionEvent e) { 857 try { 858 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 859 memberTableModel.addMembersAtEnd(toAdd); 860 } catch(AddAbortException ex) { 861 // do nothing 862 } 863 } 864 865 @Override 866 public void tableChanged(TableModelEvent e) { 867 refreshEnabled(); 868 } 869 } 870 871 class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { 872 public AddSelectedBeforeSelection() { 873 putValue(SHORT_DESCRIPTION, 874 tr("Add all objects selected in the current dataset before the first selected member")); 875 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright")); 876 refreshEnabled(); 877 } 878 879 protected void refreshEnabled() { 880 setEnabled(selectionTableModel.getRowCount() > 0 881 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); 882 } 883 884 @Override 885 public void actionPerformed(ActionEvent e) { 886 try { 887 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 888 memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel 889 .getSelectionModel().getMinSelectionIndex()); 890 } catch(AddAbortException ex) { 891 // do nothing 892 } 893 894 } 895 896 @Override 897 public void tableChanged(TableModelEvent e) { 898 refreshEnabled(); 899 } 900 901 @Override 902 public void valueChanged(ListSelectionEvent e) { 903 refreshEnabled(); 904 } 905 } 906 907 class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { 908 public AddSelectedAfterSelection() { 909 putValue(SHORT_DESCRIPTION, 910 tr("Add all objects selected in the current dataset after the last selected member")); 911 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright")); 912 refreshEnabled(); 913 } 914 915 protected void refreshEnabled() { 916 setEnabled(selectionTableModel.getRowCount() > 0 917 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); 918 } 919 920 @Override 921 public void actionPerformed(ActionEvent e) { 922 try { 923 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 924 memberTableModel.addMembersAfterIdx(toAdd, memberTableModel 925 .getSelectionModel().getMaxSelectionIndex()); 926 } catch(AddAbortException ex) { 927 // do nothing 928 } 929 } 930 931 @Override 932 public void tableChanged(TableModelEvent e) { 933 refreshEnabled(); 934 } 935 936 @Override 937 public void valueChanged(ListSelectionEvent e) { 938 refreshEnabled(); 939 } 940 } 941 942 class RemoveSelectedAction extends AbstractAction implements TableModelListener { 943 public RemoveSelectedAction() { 944 putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects")); 945 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers")); 946 updateEnabledState(); 947 } 948 949 protected void updateEnabledState() { 950 DataSet ds = getLayer().data; 951 if (ds == null || ds.getSelected().isEmpty()) { 952 setEnabled(false); 953 return; 954 } 955 // only enable the action if we have members referring to the 956 // selected primitives 957 // 958 setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected())); 959 } 960 961 @Override 962 public void actionPerformed(ActionEvent e) { 963 memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection()); 964 } 965 966 @Override 967 public void tableChanged(TableModelEvent e) { 968 updateEnabledState(); 969 } 970 } 971 972 /** 973 * Selects members in the relation editor which refer to primitives in the current 974 * selection of the context layer. 975 * 976 */ 977 class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener { 978 public SelectedMembersForSelectionAction() { 979 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection")); 980 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers")); 981 updateEnabledState(); 982 } 983 984 protected void updateEnabledState() { 985 boolean enabled = selectionTableModel.getRowCount() > 0 986 && !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty(); 987 988 if (enabled) { 989 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection",memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size())); 990 } else { 991 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection")); 992 } 993 setEnabled(enabled); 994 } 995 996 @Override 997 public void actionPerformed(ActionEvent e) { 998 memberTableModel.selectMembersReferringTo(getLayer().data.getSelected()); 999 } 1000 1001 @Override 1002 public void tableChanged(TableModelEvent e) { 1003 updateEnabledState(); 1004 1005 } 1006 } 1007 1008 /** 1009 * Selects primitives in the layer this editor belongs to. The selected primitives are 1010 * equal to the set of primitives the currently selected relation members refer to. 1011 * 1012 */ 1013 class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener { 1014 public SelectPrimitivesForSelectedMembersAction() { 1015 putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members")); 1016 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives")); 1017 updateEnabledState(); 1018 } 1019 1020 protected void updateEnabledState() { 1021 setEnabled(memberTable.getSelectedRowCount() > 0); 1022 } 1023 1024 @Override 1025 public void actionPerformed(ActionEvent e) { 1026 getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives()); 1027 } 1028 1029 @Override 1030 public void valueChanged(ListSelectionEvent e) { 1031 updateEnabledState(); 1032 } 1033 } 1034 1035 class SortAction extends AbstractAction implements TableModelListener { 1036 public SortAction() { 1037 String tooltip = tr("Sort the relation members"); 1038 putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort")); 1039 putValue(NAME, tr("Sort")); 1040 Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"), 1041 KeyEvent.VK_END, Shortcut.ALT); 1042 sc.setAccelerator(this); 1043 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1044 updateEnabledState(); 1045 } 1046 1047 @Override 1048 public void actionPerformed(ActionEvent e) { 1049 memberTableModel.sort(); 1050 } 1051 1052 protected void updateEnabledState() { 1053 setEnabled(memberTableModel.getRowCount() > 0); 1054 } 1055 1056 @Override 1057 public void tableChanged(TableModelEvent e) { 1058 updateEnabledState(); 1059 } 1060 } 1061 1062 class ReverseAction extends AbstractAction implements TableModelListener { 1063 public ReverseAction() { 1064 putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members")); 1065 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse")); 1066 putValue(NAME, tr("Reverse")); 1067 // Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"), 1068 // KeyEvent.VK_END, Shortcut.ALT) 1069 updateEnabledState(); 1070 } 1071 1072 @Override 1073 public void actionPerformed(ActionEvent e) { 1074 memberTableModel.reverse(); 1075 } 1076 1077 protected void updateEnabledState() { 1078 setEnabled(memberTableModel.getRowCount() > 0); 1079 } 1080 1081 @Override 1082 public void tableChanged(TableModelEvent e) { 1083 updateEnabledState(); 1084 } 1085 } 1086 1087 class MoveUpAction extends AbstractAction implements ListSelectionListener { 1088 public MoveUpAction() { 1089 String tooltip = tr("Move the currently selected members up"); 1090 putValue(SMALL_ICON, ImageProvider.get("dialogs", "moveup")); 1091 Shortcut sc = Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Up"), 1092 KeyEvent.VK_UP, 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.moveUp(memberTable.getSelectedRows()); 1101 } 1102 1103 @Override 1104 public void valueChanged(ListSelectionEvent e) { 1105 setEnabled(memberTableModel.canMoveUp(memberTable.getSelectedRows())); 1106 } 1107 } 1108 1109 class MoveDownAction extends AbstractAction implements ListSelectionListener { 1110 public MoveDownAction() { 1111 String tooltip = tr("Move the currently selected members down"); 1112 putValue(SMALL_ICON, ImageProvider.get("dialogs", "movedown")); 1113 Shortcut sc = Shortcut.registerShortcut("relationeditor:movedown", tr("Relation Editor: Move Down"), 1114 KeyEvent.VK_DOWN, Shortcut.ALT); 1115 sc.setAccelerator(this); 1116 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1117 setEnabled(false); 1118 } 1119 1120 @Override 1121 public void actionPerformed(ActionEvent e) { 1122 memberTableModel.moveDown(memberTable.getSelectedRows()); 1123 } 1124 1125 @Override 1126 public void valueChanged(ListSelectionEvent e) { 1127 setEnabled(memberTableModel.canMoveDown(memberTable.getSelectedRows())); 1128 } 1129 } 1130 1131 class RemoveAction extends AbstractAction implements ListSelectionListener { 1132 public RemoveAction() { 1133 String tooltip = tr("Remove the currently selected members from this relation"); 1134 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 1135 putValue(NAME, tr("Remove")); 1136 Shortcut sc = Shortcut.registerShortcut("relationeditor:remove", tr("Relation Editor: Remove"), 1137 KeyEvent.VK_DELETE, Shortcut.ALT); 1138 sc.setAccelerator(this); 1139 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1140 setEnabled(false); 1141 } 1142 1143 @Override 1144 public void actionPerformed(ActionEvent e) { 1145 memberTableModel.remove(memberTable.getSelectedRows()); 1146 } 1147 1148 @Override 1149 public void valueChanged(ListSelectionEvent e) { 1150 setEnabled(memberTableModel.canRemove(memberTable.getSelectedRows())); 1151 } 1152 } 1153 1154 class DeleteCurrentRelationAction extends AbstractAction implements PropertyChangeListener{ 1155 public DeleteCurrentRelationAction() { 1156 putValue(SHORT_DESCRIPTION, tr("Delete the currently edited relation")); 1157 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 1158 putValue(NAME, tr("Delete")); 1159 updateEnabledState(); 1160 } 1161 1162 public void run() { 1163 Relation toDelete = getRelation(); 1164 if (toDelete == null) 1165 return; 1166 org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation( 1167 getLayer(), 1168 toDelete 1169 ); 1170 } 1171 1172 @Override 1173 public void actionPerformed(ActionEvent e) { 1174 run(); 1175 } 1176 1177 protected void updateEnabledState() { 1178 setEnabled(getRelationSnapshot() != null); 1179 } 1180 1181 @Override 1182 public void propertyChange(PropertyChangeEvent evt) { 1183 if (evt.getPropertyName().equals(RELATION_SNAPSHOT_PROP)) { 1184 updateEnabledState(); 1185 } 1186 } 1187 } 1188 1189 abstract class SavingAction extends AbstractAction { 1190 /** 1191 * apply updates to a new relation 1192 */ 1193 protected void applyNewRelation() { 1194 final Relation newRelation = new Relation(); 1195 tagEditorPanel.getModel().applyToPrimitive(newRelation); 1196 memberTableModel.applyToRelation(newRelation); 1197 List<RelationMember> newMembers = new ArrayList<>(); 1198 for (RelationMember rm: newRelation.getMembers()) { 1199 if (!rm.getMember().isDeleted()) { 1200 newMembers.add(rm); 1201 } 1202 } 1203 if (newRelation.getMembersCount() != newMembers.size()) { 1204 newRelation.setMembers(newMembers); 1205 String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" + 1206 "was open. They have been removed from the relation members list."); 1207 JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE); 1208 } 1209 // If the user wanted to create a new relation, but hasn't added any members or 1210 // tags, don't add an empty relation 1211 if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys()) 1212 return; 1213 Main.main.undoRedo.add(new AddCommand(getLayer(),newRelation)); 1214 1215 // make sure everybody is notified about the changes 1216 // 1217 getLayer().data.fireSelectionChanged(); 1218 GenericRelationEditor.this.setRelation(newRelation); 1219 RelationDialogManager.getRelationDialogManager().updateContext( 1220 getLayer(), 1221 getRelation(), 1222 GenericRelationEditor.this 1223 ); 1224 SwingUtilities.invokeLater(new Runnable() { 1225 @Override 1226 public void run() { 1227 // Relation list gets update in EDT so selecting my be postponed to following EDT run 1228 Main.map.relationListDialog.selectRelation(newRelation); 1229 } 1230 }); 1231 } 1232 1233 /** 1234 * Apply the updates for an existing relation which has been changed 1235 * outside of the relation editor. 1236 * 1237 */ 1238 protected void applyExistingConflictingRelation() { 1239 Relation editedRelation = new Relation(getRelation()); 1240 tagEditorPanel.getModel().applyToPrimitive(editedRelation); 1241 memberTableModel.applyToRelation(editedRelation); 1242 Conflict<Relation> conflict = new Conflict<>(getRelation(), editedRelation); 1243 Main.main.undoRedo.add(new ConflictAddCommand(getLayer(),conflict)); 1244 } 1245 1246 /** 1247 * Apply the updates for an existing relation which has not been changed 1248 * outside of the relation editor. 1249 * 1250 */ 1251 protected void applyExistingNonConflictingRelation() { 1252 Relation editedRelation = new Relation(getRelation()); 1253 tagEditorPanel.getModel().applyToPrimitive(editedRelation); 1254 memberTableModel.applyToRelation(editedRelation); 1255 Main.main.undoRedo.add(new ChangeCommand(getRelation(), editedRelation)); 1256 getLayer().data.fireSelectionChanged(); 1257 // this will refresh the snapshot and update the dialog title 1258 // 1259 setRelation(getRelation()); 1260 } 1261 1262 protected boolean confirmClosingBecauseOfDirtyState() { 1263 ButtonSpec [] options = new ButtonSpec[] { 1264 new ButtonSpec( 1265 tr("Yes, create a conflict and close"), 1266 ImageProvider.get("ok"), 1267 tr("Click to create a conflict and close this relation editor") , 1268 null /* no specific help topic */ 1269 ), 1270 new ButtonSpec( 1271 tr("No, continue editing"), 1272 ImageProvider.get("cancel"), 1273 tr("Click to return to the relation editor and to resume relation editing") , 1274 null /* no specific help topic */ 1275 ) 1276 }; 1277 1278 int ret = HelpAwareOptionPane.showOptionDialog( 1279 Main.parent, 1280 tr("<html>This relation has been changed outside of the editor.<br>" 1281 + "You cannot apply your changes and continue editing.<br>" 1282 + "<br>" 1283 + "Do you want to create a conflict and close the editor?</html>"), 1284 tr("Conflict in data"), 1285 JOptionPane.WARNING_MESSAGE, 1286 null, 1287 options, 1288 options[0], // OK is default 1289 "/Dialog/RelationEditor#RelationChangedOutsideOfEditor" 1290 ); 1291 return ret == 0; 1292 } 1293 1294 protected void warnDoubleConflict() { 1295 JOptionPane.showMessageDialog( 1296 Main.parent, 1297 tr("<html>Layer ''{0}'' already has a conflict for object<br>" 1298 + "''{1}''.<br>" 1299 + "Please resolve this conflict first, then try again.</html>", 1300 getLayer().getName(), 1301 getRelation().getDisplayName(DefaultNameFormatter.getInstance()) 1302 ), 1303 tr("Double conflict"), 1304 JOptionPane.WARNING_MESSAGE 1305 ); 1306 } 1307 } 1308 1309 class ApplyAction extends SavingAction { 1310 public ApplyAction() { 1311 putValue(SHORT_DESCRIPTION, tr("Apply the current updates")); 1312 putValue(SMALL_ICON, ImageProvider.get("save")); 1313 putValue(NAME, tr("Apply")); 1314 setEnabled(true); 1315 } 1316 1317 public void run() { 1318 if (getRelation() == null) { 1319 applyNewRelation(); 1320 } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) 1321 || tagEditorPanel.getModel().isDirty()) { 1322 if (isDirtyRelation()) { 1323 if (confirmClosingBecauseOfDirtyState()) { 1324 if (getLayer().getConflicts().hasConflictForMy(getRelation())) { 1325 warnDoubleConflict(); 1326 return; 1327 } 1328 applyExistingConflictingRelation(); 1329 setVisible(false); 1330 } 1331 } else { 1332 applyExistingNonConflictingRelation(); 1333 } 1334 } 1335 } 1336 1337 @Override 1338 public void actionPerformed(ActionEvent e) { 1339 run(); 1340 } 1341 } 1342 1343 class OKAction extends SavingAction { 1344 public OKAction() { 1345 putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog")); 1346 putValue(SMALL_ICON, ImageProvider.get("ok")); 1347 putValue(NAME, tr("OK")); 1348 setEnabled(true); 1349 } 1350 1351 public void run() { 1352 Main.pref.put("relation.editor.generic.lastrole", tfRole.getText()); 1353 memberTable.stopHighlighting(); 1354 if (getRelation() == null) { 1355 applyNewRelation(); 1356 } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) 1357 || tagEditorPanel.getModel().isDirty()) { 1358 if (isDirtyRelation()) { 1359 if (confirmClosingBecauseOfDirtyState()) { 1360 if (getLayer().getConflicts().hasConflictForMy(getRelation())) { 1361 warnDoubleConflict(); 1362 return; 1363 } 1364 applyExistingConflictingRelation(); 1365 } else 1366 return; 1367 } else { 1368 applyExistingNonConflictingRelation(); 1369 } 1370 } 1371 setVisible(false); 1372 } 1373 1374 @Override 1375 public void actionPerformed(ActionEvent e) { 1376 run(); 1377 } 1378 } 1379 1380 class CancelAction extends SavingAction { 1381 public CancelAction() { 1382 putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog")); 1383 putValue(SMALL_ICON, ImageProvider.get("cancel")); 1384 putValue(NAME, tr("Cancel")); 1385 1386 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) 1387 .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE"); 1388 getRootPane().getActionMap().put("ESCAPE", this); 1389 setEnabled(true); 1390 } 1391 1392 @Override 1393 public void actionPerformed(ActionEvent e) { 1394 memberTable.stopHighlighting(); 1395 TagEditorModel tagModel = tagEditorPanel.getModel(); 1396 Relation snapshot = getRelationSnapshot(); 1397 if ( (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty()) 1398 && !(snapshot == null && tagModel.getTags().isEmpty())) { 1399 //give the user a chance to save the changes 1400 int ret = confirmClosingByCancel(); 1401 if (ret == 0) { //Yes, save the changes 1402 //copied from OKAction.run() 1403 Main.pref.put("relation.editor.generic.lastrole", tfRole.getText()); 1404 if (getRelation() == null) { 1405 applyNewRelation(); 1406 } else if (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty()) { 1407 if (isDirtyRelation()) { 1408 if (confirmClosingBecauseOfDirtyState()) { 1409 if (getLayer().getConflicts().hasConflictForMy(getRelation())) { 1410 warnDoubleConflict(); 1411 return; 1412 } 1413 applyExistingConflictingRelation(); 1414 } else 1415 return; 1416 } else { 1417 applyExistingNonConflictingRelation(); 1418 } 1419 } 1420 } else if (ret == 2) //Cancel, continue editing 1421 return; 1422 //in case of "No, discard", there is no extra action to be performed here. 1423 } 1424 setVisible(false); 1425 } 1426 1427 protected int confirmClosingByCancel() { 1428 ButtonSpec [] options = new ButtonSpec[] { 1429 new ButtonSpec( 1430 tr("Yes, save the changes and close"), 1431 ImageProvider.get("ok"), 1432 tr("Click to save the changes and close this relation editor") , 1433 null /* no specific help topic */ 1434 ), 1435 new ButtonSpec( 1436 tr("No, discard the changes and close"), 1437 ImageProvider.get("cancel"), 1438 tr("Click to discard the changes and close this relation editor") , 1439 null /* no specific help topic */ 1440 ), 1441 new ButtonSpec( 1442 tr("Cancel, continue editing"), 1443 ImageProvider.get("cancel"), 1444 tr("Click to return to the relation editor and to resume relation editing") , 1445 null /* no specific help topic */ 1446 ) 1447 }; 1448 1449 return HelpAwareOptionPane.showOptionDialog( 1450 Main.parent, 1451 tr("<html>The relation has been changed.<br>" 1452 + "<br>" 1453 + "Do you want to save your changes?</html>"), 1454 tr("Unsaved changes"), 1455 JOptionPane.WARNING_MESSAGE, 1456 null, 1457 options, 1458 options[0], // OK is default, 1459 "/Dialog/RelationEditor#DiscardChanges" 1460 ); 1461 } 1462 } 1463 1464 class AddTagAction extends AbstractAction { 1465 public AddTagAction() { 1466 putValue(SHORT_DESCRIPTION, tr("Add an empty tag")); 1467 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); 1468 setEnabled(true); 1469 } 1470 1471 @Override 1472 public void actionPerformed(ActionEvent e) { 1473 tagEditorPanel.getModel().appendNewTag(); 1474 } 1475 } 1476 1477 class DownloadIncompleteMembersAction extends AbstractAction implements TableModelListener { 1478 public DownloadIncompleteMembersAction() { 1479 String tooltip = tr("Download all incomplete members"); 1480 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincomplete")); 1481 putValue(NAME, tr("Download Members")); 1482 Shortcut sc = Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"), 1483 KeyEvent.VK_HOME, Shortcut.ALT); 1484 sc.setAccelerator(this); 1485 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1486 updateEnabledState(); 1487 } 1488 1489 @Override 1490 public void actionPerformed(ActionEvent e) { 1491 if (!isEnabled()) 1492 return; 1493 Main.worker.submit(new DownloadRelationMemberTask( 1494 getRelation(), 1495 memberTableModel.getIncompleteMemberPrimitives(), 1496 getLayer(), 1497 GenericRelationEditor.this) 1498 ); 1499 } 1500 1501 protected void updateEnabledState() { 1502 setEnabled(memberTableModel.hasIncompleteMembers() && !Main.isOffline(OnlineResource.OSM_API)); 1503 } 1504 1505 @Override 1506 public void tableChanged(TableModelEvent e) { 1507 updateEnabledState(); 1508 } 1509 } 1510 1511 class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener, TableModelListener{ 1512 public DownloadSelectedIncompleteMembersAction() { 1513 putValue(SHORT_DESCRIPTION, tr("Download selected incomplete members")); 1514 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected")); 1515 putValue(NAME, tr("Download Members")); 1516 // Shortcut.register Shortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"), 1517 // KeyEvent.VK_K, Shortcut.ALT) 1518 updateEnabledState(); 1519 } 1520 1521 @Override 1522 public void actionPerformed(ActionEvent e) { 1523 if (!isEnabled()) 1524 return; 1525 Main.worker.submit(new DownloadRelationMemberTask( 1526 getRelation(), 1527 memberTableModel.getSelectedIncompleteMemberPrimitives(), 1528 getLayer(), 1529 GenericRelationEditor.this) 1530 ); 1531 } 1532 1533 protected void updateEnabledState() { 1534 setEnabled(memberTableModel.hasIncompleteSelectedMembers() && !Main.isOffline(OnlineResource.OSM_API)); 1535 } 1536 1537 @Override 1538 public void valueChanged(ListSelectionEvent e) { 1539 updateEnabledState(); 1540 } 1541 1542 @Override 1543 public void tableChanged(TableModelEvent e) { 1544 updateEnabledState(); 1545 } 1546 } 1547 1548 class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener { 1549 public SetRoleAction() { 1550 putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members")); 1551 putValue(SMALL_ICON, ImageProvider.get("apply")); 1552 putValue(NAME, tr("Apply Role")); 1553 refreshEnabled(); 1554 } 1555 1556 protected void refreshEnabled() { 1557 setEnabled(memberTable.getSelectedRowCount() > 0); 1558 } 1559 1560 protected boolean isEmptyRole() { 1561 return tfRole.getText() == null || tfRole.getText().trim().isEmpty(); 1562 } 1563 1564 protected boolean confirmSettingEmptyRole(int onNumMembers) { 1565 String message = "<html>" 1566 + trn("You are setting an empty role on {0} object.", 1567 "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers) 1568 + "<br>" 1569 + tr("This is equal to deleting the roles of these objects.") + 1570 "<br>" 1571 + tr("Do you really want to apply the new role?") + "</html>"; 1572 String [] options = new String[] { 1573 tr("Yes, apply it"), 1574 tr("No, do not apply") 1575 }; 1576 int ret = ConditionalOptionPaneUtil.showOptionDialog( 1577 "relation_editor.confirm_applying_empty_role", 1578 Main.parent, 1579 message, 1580 tr("Confirm empty role"), 1581 JOptionPane.YES_NO_OPTION, 1582 JOptionPane.WARNING_MESSAGE, 1583 options, 1584 options[0] 1585 ); 1586 switch(ret) { 1587 case JOptionPane.YES_OPTION: 1588 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 1589 return true; 1590 default: 1591 return false; 1592 } 1593 } 1594 1595 @Override 1596 public void actionPerformed(ActionEvent e) { 1597 if (isEmptyRole()) { 1598 if (! confirmSettingEmptyRole(memberTable.getSelectedRowCount())) 1599 return; 1600 } 1601 memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText()); 1602 } 1603 1604 @Override 1605 public void valueChanged(ListSelectionEvent e) { 1606 refreshEnabled(); 1607 } 1608 1609 @Override 1610 public void changedUpdate(DocumentEvent e) { 1611 refreshEnabled(); 1612 } 1613 1614 @Override 1615 public void insertUpdate(DocumentEvent e) { 1616 refreshEnabled(); 1617 } 1618 1619 @Override 1620 public void removeUpdate(DocumentEvent e) { 1621 refreshEnabled(); 1622 } 1623 } 1624 1625 /** 1626 * Creates a new relation with a copy of the current editor state. 1627 */ 1628 class DuplicateRelationAction extends AbstractAction { 1629 public DuplicateRelationAction() { 1630 putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window")); 1631 // FIXME provide an icon 1632 putValue(SMALL_ICON, ImageProvider.get("duplicate")); 1633 putValue(NAME, tr("Duplicate")); 1634 setEnabled(true); 1635 } 1636 1637 @Override 1638 public void actionPerformed(ActionEvent e) { 1639 Relation copy = new Relation(); 1640 tagEditorPanel.getModel().applyToPrimitive(copy); 1641 memberTableModel.applyToRelation(copy); 1642 RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers()); 1643 editor.setVisible(true); 1644 } 1645 } 1646 1647 /** 1648 * Action for editing the currently selected relation. 1649 */ 1650 class EditAction extends AbstractAction implements ListSelectionListener { 1651 public EditAction() { 1652 putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to")); 1653 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 1654 refreshEnabled(); 1655 } 1656 1657 protected void refreshEnabled() { 1658 setEnabled(memberTable.getSelectedRowCount() == 1 1659 && memberTableModel.isEditableRelation(memberTable.getSelectedRow())); 1660 } 1661 1662 protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) { 1663 Collection<RelationMember> members = new HashSet<>(); 1664 Collection<OsmPrimitive> selection = getLayer().data.getSelected(); 1665 for (RelationMember member: r.getMembers()) { 1666 if (selection.contains(member.getMember())) { 1667 members.add(member); 1668 } 1669 } 1670 return members; 1671 } 1672 1673 public void run() { 1674 int idx = memberTable.getSelectedRow(); 1675 if (idx < 0) 1676 return; 1677 OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx); 1678 if (!(primitive instanceof Relation)) 1679 return; 1680 Relation r = (Relation) primitive; 1681 if (r.isIncomplete()) 1682 return; 1683 1684 RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r)); 1685 editor.setVisible(true); 1686 } 1687 1688 @Override 1689 public void actionPerformed(ActionEvent e) { 1690 if (!isEnabled()) 1691 return; 1692 run(); 1693 } 1694 1695 @Override 1696 public void valueChanged(ListSelectionEvent e) { 1697 refreshEnabled(); 1698 } 1699 } 1700 1701 class PasteMembersAction extends AddFromSelectionAction { 1702 1703 @Override 1704 public void actionPerformed(ActionEvent e) { 1705 try { 1706 List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded(); 1707 DataSet ds = getLayer().data; 1708 List<OsmPrimitive> toAdd = new ArrayList<>(); 1709 boolean hasNewInOtherLayer = false; 1710 1711 for (PrimitiveData primitive: primitives) { 1712 OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive); 1713 if (primitiveInDs != null) { 1714 toAdd.add(primitiveInDs); 1715 } else if (!primitive.isNew()) { 1716 OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true); 1717 ds.addPrimitive(p); 1718 toAdd.add(p); 1719 } else { 1720 hasNewInOtherLayer = true; 1721 break; 1722 } 1723 } 1724 1725 if (hasNewInOtherLayer) { 1726 JOptionPane.showMessageDialog(Main.parent, tr("Members from paste buffer cannot be added because they are not included in current layer")); 1727 return; 1728 } 1729 1730 toAdd = filterConfirmedPrimitives(toAdd); 1731 int index = memberTableModel.getSelectionModel().getMaxSelectionIndex(); 1732 if (index == -1) { 1733 index = memberTableModel.getRowCount() - 1; 1734 } 1735 memberTableModel.addMembersAfterIdx(toAdd, index); 1736 1737 tfRole.requestFocusInWindow(); 1738 1739 } catch (AddAbortException ex) { 1740 // Do nothing 1741 } 1742 } 1743 } 1744 1745 class CopyMembersAction extends AbstractAction { 1746 @Override 1747 public void actionPerformed(ActionEvent e) { 1748 Set<OsmPrimitive> primitives = new HashSet<>(); 1749 for (RelationMember rm: memberTableModel.getSelectedMembers()) { 1750 primitives.add(rm.getMember()); 1751 } 1752 if (!primitives.isEmpty()) { 1753 CopyAction.copy(getLayer(), primitives); 1754 } 1755 } 1756 } 1757 1758 class MemberTableDblClickAdapter extends MouseAdapter { 1759 @Override 1760 public void mouseClicked(MouseEvent e) { 1761 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 1762 new EditAction().run(); 1763 } 1764 } 1765 } 1766}