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