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.io.OnlineResource; 089import org.openstreetmap.josm.tools.CheckParameterUtil; 090import org.openstreetmap.josm.tools.ImageProvider; 091import org.openstreetmap.josm.tools.Shortcut; 092import org.openstreetmap.josm.tools.WindowGeometry; 093 094/** 095 * This dialog is for editing relations. 096 * @since 343 097 */ 098public class GenericRelationEditor extends RelationEditor { 099 /** the tag table and its model */ 100 private TagEditorPanel tagEditorPanel; 101 private ReferringRelationsBrowser referrerBrowser; 102 private ReferringRelationsBrowserModel referrerModel; 103 104 /** the member table */ 105 private MemberTable memberTable; 106 private MemberTableModel memberTableModel; 107 108 /** the model for the selection table */ 109 private SelectionTable selectionTable; 110 private SelectionTableModel selectionTableModel; 111 112 private AutoCompletingTextField tfRole; 113 114 /** the menu item in the windows menu. Required to properly 115 * hide on dialog close. 116 */ 117 private JMenuItem windowMenuItem; 118 /** 119 * Button for performing the {@link org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor.SortBelowAction}. 120 */ 121 private JButton sortBelowButton; 122 123 /** 124 * Creates a new relation editor for the given relation. The relation will be saved if the user 125 * selects "ok" in the editor. 126 * 127 * If no relation is given, will create an editor for a new relation. 128 * 129 * @param layer the {@link OsmDataLayer} the new or edited relation belongs to 130 * @param relation relation to edit, or null to create a new one. 131 * @param selectedMembers a collection of members which shall be selected initially 132 */ 133 public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) { 134 super(layer, relation, selectedMembers); 135 136 setRememberWindowGeometry(getClass().getName() + ".geometry", 137 WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650))); 138 139 final TaggingPresetHandler presetHandler = new TaggingPresetHandler() { 140 141 @Override 142 public void updateTags(List<Tag> tags) { 143 tagEditorPanel.getModel().updateTags(tags); 144 } 145 146 @Override 147 public Collection<OsmPrimitive> getSelection() { 148 Relation relation = new Relation(); 149 tagEditorPanel.getModel().applyToPrimitive(relation); 150 return Collections.<OsmPrimitive>singletonList(relation); 151 } 152 }; 153 154 // init the various models 155 // 156 memberTableModel = new MemberTableModel(getLayer(), presetHandler); 157 memberTableModel.register(); 158 selectionTableModel = new SelectionTableModel(getLayer()); 159 selectionTableModel.register(); 160 referrerModel = new ReferringRelationsBrowserModel(relation); 161 162 tagEditorPanel = new TagEditorPanel(presetHandler); 163 164 // populate the models 165 // 166 if (relation != null) { 167 tagEditorPanel.getModel().initFromPrimitive(relation); 168 this.memberTableModel.populate(relation); 169 if (!getLayer().data.getRelations().contains(relation)) { 170 // treat it as a new relation if it doesn't exist in the 171 // data set yet. 172 setRelation(null); 173 } 174 } else { 175 tagEditorPanel.getModel().clear(); 176 this.memberTableModel.populate(null); 177 } 178 tagEditorPanel.getModel().ensureOneTag(); 179 180 JSplitPane pane = buildSplitPane(); 181 pane.setPreferredSize(new Dimension(100, 100)); 182 183 JPanel pnl = new JPanel(); 184 pnl.setLayout(new BorderLayout()); 185 pnl.add(pane, BorderLayout.CENTER); 186 pnl.setBorder(BorderFactory.createRaisedBevelBorder()); 187 188 getContentPane().setLayout(new BorderLayout()); 189 JTabbedPane tabbedPane = new JTabbedPane(); 190 tabbedPane.add(tr("Tags and Members"), pnl); 191 referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel); 192 tabbedPane.add(tr("Parent Relations"), referrerBrowser); 193 tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation)); 194 tabbedPane.addChangeListener( 195 new ChangeListener() { 196 @Override 197 public void stateChanged(ChangeEvent e) { 198 JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource(); 199 int index = sourceTabbedPane.getSelectedIndex(); 200 String title = sourceTabbedPane.getTitleAt(index); 201 if (title.equals(tr("Parent Relations"))) { 202 referrerBrowser.init(); 203 } 204 } 205 } 206 ); 207 208 getContentPane().add(buildToolBar(), BorderLayout.NORTH); 209 getContentPane().add(tabbedPane, BorderLayout.CENTER); 210 getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH); 211 212 setSize(findMaxDialogSize()); 213 214 addWindowListener( 215 new WindowAdapter() { 216 @Override 217 public void windowOpened(WindowEvent e) { 218 cleanSelfReferences(); 219 } 220 } 221 ); 222 registerCopyPasteAction(tagEditorPanel.getPasteAction(), 223 "PASTE_TAGS", 224 // CHECKSTYLE.OFF: LineLength 225 Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke()); 226 // CHECKSTYLE.ON: LineLength 227 registerCopyPasteAction(new PasteMembersAction(), "PASTE_MEMBERS", Shortcut.getPasteKeyStroke()); 228 registerCopyPasteAction(new CopyMembersAction(), "COPY_MEMBERS", Shortcut.getCopyKeyStroke()); 229 230 tagEditorPanel.setNextFocusComponent(memberTable); 231 selectionTable.setFocusable(false); 232 memberTableModel.setSelectedMembers(selectedMembers); 233 HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/RelationEditor")); 234 } 235 236 /** 237 * Creates the toolbar 238 * 239 * @return the toolbar 240 */ 241 protected JToolBar buildToolBar() { 242 JToolBar tb = new JToolBar(); 243 tb.setFloatable(false); 244 tb.add(new ApplyAction()); 245 tb.add(new DuplicateRelationAction()); 246 DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction(); 247 addPropertyChangeListener(deleteAction); 248 tb.add(deleteAction); 249 return tb; 250 } 251 252 /** 253 * builds the panel with the OK and the Cancel button 254 * 255 * @return the panel with the OK and the Cancel button 256 */ 257 protected JPanel buildOkCancelButtonPanel() { 258 JPanel pnl = new JPanel(); 259 pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); 260 261 pnl.add(new SideButton(new OKAction())); 262 pnl.add(new SideButton(new CancelAction())); 263 pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor")))); 264 return pnl; 265 } 266 267 /** 268 * builds the panel with the tag editor 269 * 270 * @return the panel with the tag editor 271 */ 272 protected JPanel buildTagEditorPanel() { 273 JPanel pnl = new JPanel(); 274 pnl.setLayout(new GridBagLayout()); 275 276 GridBagConstraints gc = new GridBagConstraints(); 277 gc.gridx = 0; 278 gc.gridy = 0; 279 gc.gridheight = 1; 280 gc.gridwidth = 1; 281 gc.fill = GridBagConstraints.HORIZONTAL; 282 gc.anchor = GridBagConstraints.FIRST_LINE_START; 283 gc.weightx = 1.0; 284 gc.weighty = 0.0; 285 pnl.add(new JLabel(tr("Tags")), gc); 286 287 gc.gridx = 0; 288 gc.gridy = 1; 289 gc.fill = GridBagConstraints.BOTH; 290 gc.anchor = GridBagConstraints.CENTER; 291 gc.weightx = 1.0; 292 gc.weighty = 1.0; 293 pnl.add(tagEditorPanel, gc); 294 return pnl; 295 } 296 297 /** 298 * builds the panel for the relation member editor 299 * 300 * @return the panel for the relation member editor 301 */ 302 protected JPanel buildMemberEditorPanel() { 303 final JPanel pnl = new JPanel(new GridBagLayout()); 304 // setting up the member table 305 memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel); 306 memberTable.addMouseListener(new MemberTableDblClickAdapter()); 307 memberTableModel.addMemberModelListener(memberTable); 308 309 final JScrollPane scrollPane = new JScrollPane(memberTable); 310 311 GridBagConstraints gc = new GridBagConstraints(); 312 gc.gridx = 0; 313 gc.gridy = 0; 314 gc.gridwidth = 2; 315 gc.fill = GridBagConstraints.HORIZONTAL; 316 gc.anchor = GridBagConstraints.FIRST_LINE_START; 317 gc.weightx = 1.0; 318 gc.weighty = 0.0; 319 pnl.add(new JLabel(tr("Members")), gc); 320 321 gc.gridx = 0; 322 gc.gridy = 1; 323 gc.gridheight = 2; 324 gc.gridwidth = 1; 325 gc.fill = GridBagConstraints.VERTICAL; 326 gc.anchor = GridBagConstraints.NORTHWEST; 327 gc.weightx = 0.0; 328 gc.weighty = 1.0; 329 pnl.add(buildLeftButtonPanel(), gc); 330 331 gc.gridx = 1; 332 gc.gridy = 1; 333 gc.gridheight = 1; 334 gc.fill = GridBagConstraints.BOTH; 335 gc.anchor = GridBagConstraints.CENTER; 336 gc.weightx = 0.6; 337 gc.weighty = 1.0; 338 pnl.add(scrollPane, gc); 339 340 // --- role editing 341 JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 342 p3.add(new JLabel(tr("Apply Role:"))); 343 tfRole = new AutoCompletingTextField(10); 344 tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members")); 345 tfRole.addFocusListener(new FocusAdapter() { 346 @Override 347 public void focusGained(FocusEvent e) { 348 tfRole.selectAll(); 349 } 350 }); 351 tfRole.setAutoCompletionList(new AutoCompletionList()); 352 tfRole.addFocusListener( 353 new FocusAdapter() { 354 @Override 355 public void focusGained(FocusEvent e) { 356 AutoCompletionList list = tfRole.getAutoCompletionList(); 357 if (list != null) { 358 list.clear(); 359 getLayer().data.getAutoCompletionManager().populateWithMemberRoles(list, getRelation()); 360 } 361 } 362 } 363 ); 364 tfRole.setText(Main.pref.get("relation.editor.generic.lastrole", "")); 365 p3.add(tfRole); 366 SetRoleAction setRoleAction = new SetRoleAction(); 367 memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction); 368 tfRole.getDocument().addDocumentListener(setRoleAction); 369 tfRole.addActionListener(setRoleAction); 370 memberTableModel.getSelectionModel().addListSelectionListener( 371 new ListSelectionListener() { 372 @Override 373 public void valueChanged(ListSelectionEvent e) { 374 tfRole.setEnabled(memberTable.getSelectedRowCount() > 0); 375 } 376 } 377 ); 378 tfRole.setEnabled(memberTable.getSelectedRowCount() > 0); 379 SideButton btnApply = new SideButton(setRoleAction); 380 btnApply.setPreferredSize(new Dimension(20, 20)); 381 btnApply.setText(""); 382 p3.add(btnApply); 383 384 gc.gridx = 1; 385 gc.gridy = 2; 386 gc.fill = GridBagConstraints.HORIZONTAL; 387 gc.anchor = GridBagConstraints.LAST_LINE_START; 388 gc.weightx = 1.0; 389 gc.weighty = 0.0; 390 pnl.add(p3, gc); 391 392 JPanel pnl2 = new JPanel(); 393 pnl2.setLayout(new GridBagLayout()); 394 395 gc.gridx = 0; 396 gc.gridy = 0; 397 gc.gridheight = 1; 398 gc.gridwidth = 3; 399 gc.fill = GridBagConstraints.HORIZONTAL; 400 gc.anchor = GridBagConstraints.FIRST_LINE_START; 401 gc.weightx = 1.0; 402 gc.weighty = 0.0; 403 pnl2.add(new JLabel(tr("Selection")), gc); 404 405 gc.gridx = 0; 406 gc.gridy = 1; 407 gc.gridheight = 1; 408 gc.gridwidth = 1; 409 gc.fill = GridBagConstraints.VERTICAL; 410 gc.anchor = GridBagConstraints.NORTHWEST; 411 gc.weightx = 0.0; 412 gc.weighty = 1.0; 413 pnl2.add(buildSelectionControlButtonPanel(), gc); 414 415 gc.gridx = 1; 416 gc.gridy = 1; 417 gc.weightx = 1.0; 418 gc.weighty = 1.0; 419 gc.fill = GridBagConstraints.BOTH; 420 pnl2.add(buildSelectionTablePanel(), gc); 421 422 final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); 423 splitPane.setLeftComponent(pnl); 424 splitPane.setRightComponent(pnl2); 425 splitPane.setOneTouchExpandable(false); 426 addWindowListener(new WindowAdapter() { 427 @Override 428 public void windowOpened(WindowEvent e) { 429 // has to be called when the window is visible, otherwise 430 // no effect 431 splitPane.setDividerLocation(0.6); 432 } 433 }); 434 435 JPanel pnl3 = new JPanel(); 436 pnl3.setLayout(new BorderLayout()); 437 pnl3.add(splitPane, BorderLayout.CENTER); 438 439 return pnl3; 440 } 441 442 /** 443 * builds the panel with the table displaying the currently selected primitives 444 * 445 * @return panel with current selection 446 */ 447 protected JPanel buildSelectionTablePanel() { 448 JPanel pnl = new JPanel(new BorderLayout()); 449 MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor(); 450 selectionTable = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel)); 451 selectionTable.setMemberTableModel(memberTableModel); 452 selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height); 453 pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER); 454 return pnl; 455 } 456 457 /** 458 * builds the {@link JSplitPane} which divides the editor in an upper and a lower half 459 * 460 * @return the split panel 461 */ 462 protected JSplitPane buildSplitPane() { 463 final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); 464 pane.setTopComponent(buildTagEditorPanel()); 465 pane.setBottomComponent(buildMemberEditorPanel()); 466 pane.setOneTouchExpandable(true); 467 addWindowListener(new WindowAdapter() { 468 @Override 469 public void windowOpened(WindowEvent e) { 470 // has to be called when the window is visible, otherwise no effect 471 pane.setDividerLocation(0.3); 472 } 473 }); 474 return pane; 475 } 476 477 /** 478 * build the panel with the buttons on the left 479 * 480 * @return left button panel 481 */ 482 protected JToolBar buildLeftButtonPanel() { 483 JToolBar tb = new JToolBar(); 484 tb.setOrientation(JToolBar.VERTICAL); 485 tb.setFloatable(false); 486 487 // -- move up action 488 MoveUpAction moveUpAction = new MoveUpAction(); 489 memberTableModel.getSelectionModel().addListSelectionListener(moveUpAction); 490 tb.add(moveUpAction); 491 memberTable.getActionMap().put("moveUp", moveUpAction); 492 493 // -- move down action 494 MoveDownAction moveDownAction = new MoveDownAction(); 495 memberTableModel.getSelectionModel().addListSelectionListener(moveDownAction); 496 tb.add(moveDownAction); 497 memberTable.getActionMap().put("moveDown", moveDownAction); 498 499 tb.addSeparator(); 500 501 // -- edit action 502 EditAction editAction = new EditAction(); 503 memberTableModel.getSelectionModel().addListSelectionListener(editAction); 504 tb.add(editAction); 505 506 // -- delete action 507 RemoveAction removeSelectedAction = new RemoveAction(); 508 memberTable.getSelectionModel().addListSelectionListener(removeSelectedAction); 509 tb.add(removeSelectedAction); 510 memberTable.getActionMap().put("removeSelected", removeSelectedAction); 511 512 tb.addSeparator(); 513 // -- sort action 514 SortAction sortAction = new SortAction(); 515 memberTableModel.addTableModelListener(sortAction); 516 tb.add(sortAction); 517 final SortBelowAction sortBelowAction = new SortBelowAction(); 518 memberTableModel.addTableModelListener(sortBelowAction); 519 memberTableModel.getSelectionModel().addListSelectionListener(sortBelowAction); 520 sortBelowButton = tb.add(sortBelowAction); 521 522 // -- reverse action 523 ReverseAction reverseAction = new ReverseAction(); 524 memberTableModel.addTableModelListener(reverseAction); 525 tb.add(reverseAction); 526 527 tb.addSeparator(); 528 529 // -- download action 530 DownloadIncompleteMembersAction downloadIncompleteMembersAction = new DownloadIncompleteMembersAction(); 531 memberTable.getModel().addTableModelListener(downloadIncompleteMembersAction); 532 tb.add(downloadIncompleteMembersAction); 533 memberTable.getActionMap().put("downloadIncomplete", downloadIncompleteMembersAction); 534 535 // -- download selected action 536 DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction(); 537 memberTable.getModel().addTableModelListener(downloadSelectedIncompleteMembersAction); 538 memberTable.getSelectionModel().addListSelectionListener(downloadSelectedIncompleteMembersAction); 539 tb.add(downloadSelectedIncompleteMembersAction); 540 541 InputMap inputMap = memberTable.getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 542 inputMap.put((KeyStroke) removeSelectedAction.getValue(AbstractAction.ACCELERATOR_KEY), "removeSelected"); 543 inputMap.put((KeyStroke) moveUpAction.getValue(AbstractAction.ACCELERATOR_KEY), "moveUp"); 544 inputMap.put((KeyStroke) moveDownAction.getValue(AbstractAction.ACCELERATOR_KEY), "moveDown"); 545 inputMap.put((KeyStroke) downloadIncompleteMembersAction.getValue(AbstractAction.ACCELERATOR_KEY), "downloadIncomplete"); 546 547 return tb; 548 } 549 550 /** 551 * build the panel with the buttons for adding or removing the current selection 552 * 553 * @return control buttons panel for selection/members 554 */ 555 protected JToolBar buildSelectionControlButtonPanel() { 556 JToolBar tb = new JToolBar(JToolBar.VERTICAL); 557 tb.setFloatable(false); 558 559 // -- add at start action 560 AddSelectedAtStartAction addSelectionAction = new AddSelectedAtStartAction(); 561 selectionTableModel.addTableModelListener(addSelectionAction); 562 tb.add(addSelectionAction); 563 564 // -- add before selected action 565 AddSelectedBeforeSelection addSelectedBeforeSelectionAction = new AddSelectedBeforeSelection(); 566 selectionTableModel.addTableModelListener(addSelectedBeforeSelectionAction); 567 memberTableModel.getSelectionModel().addListSelectionListener(addSelectedBeforeSelectionAction); 568 tb.add(addSelectedBeforeSelectionAction); 569 570 // -- add after selected action 571 AddSelectedAfterSelection addSelectedAfterSelectionAction = new AddSelectedAfterSelection(); 572 selectionTableModel.addTableModelListener(addSelectedAfterSelectionAction); 573 memberTableModel.getSelectionModel().addListSelectionListener(addSelectedAfterSelectionAction); 574 tb.add(addSelectedAfterSelectionAction); 575 576 // -- add at end action 577 AddSelectedAtEndAction addSelectedAtEndAction = new AddSelectedAtEndAction(); 578 selectionTableModel.addTableModelListener(addSelectedAtEndAction); 579 tb.add(addSelectedAtEndAction); 580 581 tb.addSeparator(); 582 583 // -- select members action 584 SelectedMembersForSelectionAction selectMembersForSelectionAction = new SelectedMembersForSelectionAction(); 585 selectionTableModel.addTableModelListener(selectMembersForSelectionAction); 586 memberTableModel.addTableModelListener(selectMembersForSelectionAction); 587 tb.add(selectMembersForSelectionAction); 588 589 // -- select action 590 SelectPrimitivesForSelectedMembersAction selectAction = new SelectPrimitivesForSelectedMembersAction(); 591 memberTable.getSelectionModel().addListSelectionListener(selectAction); 592 tb.add(selectAction); 593 594 tb.addSeparator(); 595 596 // -- remove selected action 597 RemoveSelectedAction removeSelectedAction = new RemoveSelectedAction(); 598 selectionTableModel.addTableModelListener(removeSelectedAction); 599 tb.add(removeSelectedAction); 600 601 return tb; 602 } 603 604 @Override 605 protected Dimension findMaxDialogSize() { 606 return new Dimension(700, 650); 607 } 608 609 @Override 610 public void setVisible(boolean visible) { 611 if (visible) { 612 tagEditorPanel.initAutoCompletion(getLayer()); 613 } 614 super.setVisible(visible); 615 if (visible) { 616 sortBelowButton.setVisible(ExpertToggleAction.isExpert()); 617 RelationDialogManager.getRelationDialogManager().positionOnScreen(this); 618 if (windowMenuItem == null) { 619 addToWindowMenu(); 620 } 621 tagEditorPanel.requestFocusInWindow(); 622 } else { 623 // make sure all registered listeners are unregistered 624 // 625 memberTable.stopHighlighting(); 626 selectionTableModel.unregister(); 627 memberTableModel.unregister(); 628 memberTable.unlinkAsListener(); 629 if (windowMenuItem != null) { 630 Main.main.menu.windowMenu.remove(windowMenuItem); 631 windowMenuItem = null; 632 } 633 dispose(); 634 } 635 } 636 637 /** adds current relation editor to the windows menu (in the "volatile" group) o*/ 638 protected void addToWindowMenu() { 639 String name = getRelation() == null ? tr("New Relation") : getRelation().getLocalName(); 640 final String tt = tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''", 641 name, getLayer().getName()); 642 name = tr("Relation Editor: {0}", name == null ? getRelation().getId() : name); 643 final JMenu wm = Main.main.menu.windowMenu; 644 final JosmAction focusAction = new JosmAction(name, "dialogs/relationlist", tt, null, false, false) { 645 @Override 646 public void actionPerformed(ActionEvent e) { 647 final RelationEditor r = (RelationEditor) getValue("relationEditor"); 648 r.setVisible(true); 649 } 650 }; 651 focusAction.putValue("relationEditor", this); 652 windowMenuItem = MainMenu.add(wm, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE); 653 } 654 655 /** 656 * checks whether the current relation has members referring to itself. If so, 657 * warns the users and provides an option for removing these members. 658 * 659 */ 660 protected void cleanSelfReferences() { 661 List<OsmPrimitive> toCheck = new ArrayList<>(); 662 toCheck.add(getRelation()); 663 if (memberTableModel.hasMembersReferringTo(toCheck)) { 664 int ret = ConditionalOptionPaneUtil.showOptionDialog( 665 "clean_relation_self_references", 666 Main.parent, 667 tr("<html>There is at least one member in this relation referring<br>" 668 + "to the relation itself.<br>" 669 + "This creates circular dependencies and is discouraged.<br>" 670 + "How do you want to proceed with circular dependencies?</html>"), 671 tr("Warning"), 672 JOptionPane.YES_NO_OPTION, 673 JOptionPane.WARNING_MESSAGE, 674 new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")}, 675 tr("Remove them, clean up relation") 676 ); 677 switch(ret) { 678 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 679 case JOptionPane.CLOSED_OPTION: 680 case JOptionPane.NO_OPTION: 681 return; 682 case JOptionPane.YES_OPTION: 683 memberTableModel.removeMembersReferringTo(toCheck); 684 break; 685 } 686 } 687 } 688 689 private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut) { 690 int mods = shortcut.getModifiers(); 691 int code = shortcut.getKeyCode(); 692 if (code != KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) { 693 Main.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut); 694 return; 695 } 696 getRootPane().getActionMap().put(actionName, action); 697 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 698 // Assign also to JTables because they have their own Copy&Paste implementation 699 // (which is disabled in this case but eats key shortcuts anyway) 700 memberTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName); 701 memberTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName); 702 memberTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 703 selectionTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName); 704 selectionTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName); 705 selectionTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 706 } 707 708 static class AddAbortException extends Exception { 709 } 710 711 static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException { 712 String msg = tr("<html>This relation already has one or more members referring to<br>" 713 + "the object ''{0}''<br>" 714 + "<br>" 715 + "Do you really want to add another relation member?</html>", 716 primitive.getDisplayName(DefaultNameFormatter.getInstance()) 717 ); 718 int ret = ConditionalOptionPaneUtil.showOptionDialog( 719 "add_primitive_to_relation", 720 Main.parent, 721 msg, 722 tr("Multiple members referring to same object."), 723 JOptionPane.YES_NO_CANCEL_OPTION, 724 JOptionPane.WARNING_MESSAGE, 725 null, 726 null 727 ); 728 switch(ret) { 729 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 730 case JOptionPane.YES_OPTION: 731 return true; 732 case JOptionPane.NO_OPTION: 733 case JOptionPane.CLOSED_OPTION: 734 return false; 735 case JOptionPane.CANCEL_OPTION: 736 throw new AddAbortException(); 737 } 738 // should not happen 739 return false; 740 } 741 742 static void warnOfCircularReferences(OsmPrimitive primitive) { 743 String msg = tr("<html>You are trying to add a relation to itself.<br>" 744 + "<br>" 745 + "This creates circular references and is therefore discouraged.<br>" 746 + "Skipping relation ''{0}''.</html>", 747 primitive.getDisplayName(DefaultNameFormatter.getInstance())); 748 JOptionPane.showMessageDialog( 749 Main.parent, 750 msg, 751 tr("Warning"), 752 JOptionPane.WARNING_MESSAGE); 753 } 754 755 /** 756 * Adds primitives to a given relation. 757 * @param orig The relation to modify 758 * @param primitivesToAdd The primitives to add as relation members 759 * @return The resulting command 760 * @throws IllegalArgumentException if orig is null 761 */ 762 public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) { 763 CheckParameterUtil.ensureParameterNotNull(orig, "orig"); 764 try { 765 final Collection<TaggingPreset> presets = TaggingPreset.getMatchingPresets( 766 EnumSet.of(TaggingPresetType.RELATION), orig.getKeys(), false); 767 Relation relation = new Relation(orig); 768 boolean modified = false; 769 for (OsmPrimitive p : primitivesToAdd) { 770 if (p instanceof Relation && orig.equals(p)) { 771 warnOfCircularReferences(p); 772 continue; 773 } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p)) 774 && !confirmAddingPrimitive(p)) { 775 continue; 776 } 777 final Set<String> roles = findSuggestedRoles(presets, p); 778 relation.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p)); 779 modified = true; 780 } 781 return modified ? new ChangeCommand(orig, relation) : null; 782 } catch (AddAbortException ign) { 783 return null; 784 } 785 } 786 787 protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) { 788 final Set<String> roles = new HashSet<>(); 789 for (TaggingPreset preset : presets) { 790 String role = preset.suggestRoleForOsmPrimitive(p); 791 if (role != null && !role.isEmpty()) { 792 roles.add(role); 793 } 794 } 795 return roles; 796 } 797 798 abstract class AddFromSelectionAction extends AbstractAction { 799 protected boolean isPotentialDuplicate(OsmPrimitive primitive) { 800 return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive)); 801 } 802 803 protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException { 804 if (primitives == null || primitives.isEmpty()) 805 return primitives; 806 List<OsmPrimitive> ret = new ArrayList<>(); 807 ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation"); 808 for (OsmPrimitive primitive : primitives) { 809 if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) { 810 warnOfCircularReferences(primitive); 811 continue; 812 } 813 if (isPotentialDuplicate(primitive)) { 814 if (confirmAddingPrimitive(primitive)) { 815 ret.add(primitive); 816 } 817 continue; 818 } else { 819 ret.add(primitive); 820 } 821 } 822 ConditionalOptionPaneUtil.endBulkOperation("add_primitive_to_relation"); 823 return ret; 824 } 825 } 826 827 class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener { 828 AddSelectedAtStartAction() { 829 putValue(SHORT_DESCRIPTION, 830 tr("Add all objects selected in the current dataset before the first member")); 831 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright")); 832 refreshEnabled(); 833 } 834 835 protected void refreshEnabled() { 836 setEnabled(selectionTableModel.getRowCount() > 0); 837 } 838 839 @Override 840 public void actionPerformed(ActionEvent e) { 841 try { 842 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 843 memberTableModel.addMembersAtBeginning(toAdd); 844 } catch (AddAbortException ex) { 845 // do nothing 846 if (Main.isTraceEnabled()) { 847 Main.trace(ex.getMessage()); 848 } 849 } 850 } 851 852 @Override 853 public void tableChanged(TableModelEvent e) { 854 refreshEnabled(); 855 } 856 } 857 858 class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener { 859 AddSelectedAtEndAction() { 860 putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member")); 861 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright")); 862 refreshEnabled(); 863 } 864 865 protected void refreshEnabled() { 866 setEnabled(selectionTableModel.getRowCount() > 0); 867 } 868 869 @Override 870 public void actionPerformed(ActionEvent e) { 871 try { 872 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 873 memberTableModel.addMembersAtEnd(toAdd); 874 } catch (AddAbortException ex) { 875 // do nothing 876 if (Main.isTraceEnabled()) { 877 Main.trace(ex.getMessage()); 878 } 879 } 880 } 881 882 @Override 883 public void tableChanged(TableModelEvent e) { 884 refreshEnabled(); 885 } 886 } 887 888 class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { 889 /** 890 * Constructs a new {@code AddSelectedBeforeSelection}. 891 */ 892 AddSelectedBeforeSelection() { 893 putValue(SHORT_DESCRIPTION, 894 tr("Add all objects selected in the current dataset before the first selected member")); 895 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright")); 896 refreshEnabled(); 897 } 898 899 protected void refreshEnabled() { 900 setEnabled(selectionTableModel.getRowCount() > 0 901 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); 902 } 903 904 @Override 905 public void actionPerformed(ActionEvent e) { 906 try { 907 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 908 memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel 909 .getSelectionModel().getMinSelectionIndex()); 910 } catch (AddAbortException ex) { 911 // do nothing 912 if (Main.isTraceEnabled()) { 913 Main.trace(ex.getMessage()); 914 } 915 } 916 } 917 918 @Override 919 public void tableChanged(TableModelEvent e) { 920 refreshEnabled(); 921 } 922 923 @Override 924 public void valueChanged(ListSelectionEvent e) { 925 refreshEnabled(); 926 } 927 } 928 929 class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { 930 AddSelectedAfterSelection() { 931 putValue(SHORT_DESCRIPTION, 932 tr("Add all objects selected in the current dataset after the last selected member")); 933 putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright")); 934 refreshEnabled(); 935 } 936 937 protected void refreshEnabled() { 938 setEnabled(selectionTableModel.getRowCount() > 0 939 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); 940 } 941 942 @Override 943 public void actionPerformed(ActionEvent e) { 944 try { 945 List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); 946 memberTableModel.addMembersAfterIdx(toAdd, memberTableModel 947 .getSelectionModel().getMaxSelectionIndex()); 948 } catch (AddAbortException ex) { 949 // do nothing 950 if (Main.isTraceEnabled()) { 951 Main.trace(ex.getMessage()); 952 } 953 } 954 } 955 956 @Override 957 public void tableChanged(TableModelEvent e) { 958 refreshEnabled(); 959 } 960 961 @Override 962 public void valueChanged(ListSelectionEvent e) { 963 refreshEnabled(); 964 } 965 } 966 967 class RemoveSelectedAction extends AbstractAction implements TableModelListener { 968 /** 969 * Constructs a new {@code RemoveSelectedAction}. 970 */ 971 RemoveSelectedAction() { 972 putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects")); 973 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers")); 974 updateEnabledState(); 975 } 976 977 protected void updateEnabledState() { 978 DataSet ds = getLayer().data; 979 if (ds == null || ds.getSelected().isEmpty()) { 980 setEnabled(false); 981 return; 982 } 983 // only enable the action if we have members referring to the 984 // selected primitives 985 // 986 setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected())); 987 } 988 989 @Override 990 public void actionPerformed(ActionEvent e) { 991 memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection()); 992 } 993 994 @Override 995 public void tableChanged(TableModelEvent e) { 996 updateEnabledState(); 997 } 998 } 999 1000 /** 1001 * Selects members in the relation editor which refer to primitives in the current 1002 * selection of the context layer. 1003 * 1004 */ 1005 class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener { 1006 SelectedMembersForSelectionAction() { 1007 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection")); 1008 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers")); 1009 updateEnabledState(); 1010 } 1011 1012 protected void updateEnabledState() { 1013 boolean enabled = selectionTableModel.getRowCount() > 0 1014 && !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty(); 1015 1016 if (enabled) { 1017 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection", 1018 memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size())); 1019 } else { 1020 putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection")); 1021 } 1022 setEnabled(enabled); 1023 } 1024 1025 @Override 1026 public void actionPerformed(ActionEvent e) { 1027 memberTableModel.selectMembersReferringTo(getLayer().data.getSelected()); 1028 } 1029 1030 @Override 1031 public void tableChanged(TableModelEvent e) { 1032 updateEnabledState(); 1033 } 1034 } 1035 1036 /** 1037 * Selects primitives in the layer this editor belongs to. The selected primitives are 1038 * equal to the set of primitives the currently selected relation members refer to. 1039 * 1040 */ 1041 class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener { 1042 SelectPrimitivesForSelectedMembersAction() { 1043 putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members")); 1044 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives")); 1045 updateEnabledState(); 1046 } 1047 1048 protected void updateEnabledState() { 1049 setEnabled(memberTable.getSelectedRowCount() > 0); 1050 } 1051 1052 @Override 1053 public void actionPerformed(ActionEvent e) { 1054 getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives()); 1055 } 1056 1057 @Override 1058 public void valueChanged(ListSelectionEvent e) { 1059 updateEnabledState(); 1060 } 1061 } 1062 1063 class SortAction extends AbstractAction implements TableModelListener { 1064 SortAction() { 1065 String tooltip = tr("Sort the relation members"); 1066 putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort")); 1067 putValue(NAME, tr("Sort")); 1068 Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"), 1069 KeyEvent.VK_END, Shortcut.ALT); 1070 sc.setAccelerator(this); 1071 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 1072 updateEnabledState(); 1073 } 1074 1075 @Override 1076 public void actionPerformed(ActionEvent e) { 1077 memberTableModel.sort(); 1078 } 1079 1080 protected void updateEnabledState() { 1081 setEnabled(memberTableModel.getRowCount() > 0); 1082 } 1083 1084 @Override 1085 public void tableChanged(TableModelEvent e) { 1086 updateEnabledState(); 1087 } 1088 } 1089 1090 class SortBelowAction extends AbstractAction implements TableModelListener, ListSelectionListener { 1091 SortBelowAction() { 1092 putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort_below")); 1093 putValue(NAME, tr("Sort below")); 1094 putValue(SHORT_DESCRIPTION, tr("Sort the selected relation members and all members below")); 1095 updateEnabledState(); 1096 } 1097 1098 @Override 1099 public void actionPerformed(ActionEvent e) { 1100 memberTableModel.sortBelow(); 1101 } 1102 1103 protected void updateEnabledState() { 1104 setEnabled(memberTableModel.getRowCount() > 0 && !memberTableModel.getSelectionModel().isSelectionEmpty()); 1105 } 1106 1107 @Override 1108 public void tableChanged(TableModelEvent e) { 1109 updateEnabledState(); 1110 } 1111 1112 @Override 1113 public void valueChanged(ListSelectionEvent e) { 1114 updateEnabledState(); 1115 } 1116 } 1117 1118 class ReverseAction extends AbstractAction implements TableModelListener { 1119 ReverseAction() { 1120 putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members")); 1121 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse")); 1122 putValue(NAME, tr("Reverse")); 1123 // Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"), 1124 // 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"), 1573 // KeyEvent.VK_K, Shortcut.ALT) 1574 updateEnabledState(); 1575 } 1576 1577 @Override 1578 public void actionPerformed(ActionEvent e) { 1579 if (!isEnabled()) 1580 return; 1581 Main.worker.submit(new DownloadRelationMemberTask( 1582 getRelation(), 1583 memberTableModel.getSelectedIncompleteMemberPrimitives(), 1584 getLayer(), 1585 GenericRelationEditor.this) 1586 ); 1587 } 1588 1589 protected void updateEnabledState() { 1590 setEnabled(memberTableModel.hasIncompleteSelectedMembers() && !Main.isOffline(OnlineResource.OSM_API)); 1591 } 1592 1593 @Override 1594 public void valueChanged(ListSelectionEvent e) { 1595 updateEnabledState(); 1596 } 1597 1598 @Override 1599 public void tableChanged(TableModelEvent e) { 1600 updateEnabledState(); 1601 } 1602 } 1603 1604 class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener { 1605 SetRoleAction() { 1606 putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members")); 1607 putValue(SMALL_ICON, ImageProvider.get("apply")); 1608 putValue(NAME, tr("Apply Role")); 1609 refreshEnabled(); 1610 } 1611 1612 protected void refreshEnabled() { 1613 setEnabled(memberTable.getSelectedRowCount() > 0); 1614 } 1615 1616 protected boolean isEmptyRole() { 1617 return tfRole.getText() == null || tfRole.getText().trim().isEmpty(); 1618 } 1619 1620 protected boolean confirmSettingEmptyRole(int onNumMembers) { 1621 String message = "<html>" 1622 + trn("You are setting an empty role on {0} object.", 1623 "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers) 1624 + "<br>" 1625 + tr("This is equal to deleting the roles of these objects.") + 1626 "<br>" 1627 + tr("Do you really want to apply the new role?") + "</html>"; 1628 String[] options = new String[] { 1629 tr("Yes, apply it"), 1630 tr("No, do not apply") 1631 }; 1632 int ret = ConditionalOptionPaneUtil.showOptionDialog( 1633 "relation_editor.confirm_applying_empty_role", 1634 Main.parent, 1635 message, 1636 tr("Confirm empty role"), 1637 JOptionPane.YES_NO_OPTION, 1638 JOptionPane.WARNING_MESSAGE, 1639 options, 1640 options[0] 1641 ); 1642 switch(ret) { 1643 case JOptionPane.YES_OPTION: 1644 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 1645 return true; 1646 default: 1647 return false; 1648 } 1649 } 1650 1651 @Override 1652 public void actionPerformed(ActionEvent e) { 1653 if (isEmptyRole()) { 1654 if (!confirmSettingEmptyRole(memberTable.getSelectedRowCount())) 1655 return; 1656 } 1657 memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText()); 1658 } 1659 1660 @Override 1661 public void valueChanged(ListSelectionEvent e) { 1662 refreshEnabled(); 1663 } 1664 1665 @Override 1666 public void changedUpdate(DocumentEvent e) { 1667 refreshEnabled(); 1668 } 1669 1670 @Override 1671 public void insertUpdate(DocumentEvent e) { 1672 refreshEnabled(); 1673 } 1674 1675 @Override 1676 public void removeUpdate(DocumentEvent e) { 1677 refreshEnabled(); 1678 } 1679 } 1680 1681 /** 1682 * Creates a new relation with a copy of the current editor state. 1683 */ 1684 class DuplicateRelationAction extends AbstractAction { 1685 DuplicateRelationAction() { 1686 putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window")); 1687 // FIXME provide an icon 1688 putValue(SMALL_ICON, ImageProvider.get("duplicate")); 1689 putValue(NAME, tr("Duplicate")); 1690 setEnabled(true); 1691 } 1692 1693 @Override 1694 public void actionPerformed(ActionEvent e) { 1695 Relation copy = new Relation(); 1696 tagEditorPanel.getModel().applyToPrimitive(copy); 1697 memberTableModel.applyToRelation(copy); 1698 RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers()); 1699 editor.setVisible(true); 1700 } 1701 } 1702 1703 /** 1704 * Action for editing the currently selected relation. 1705 */ 1706 class EditAction extends AbstractAction implements ListSelectionListener { 1707 EditAction() { 1708 putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to")); 1709 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 1710 refreshEnabled(); 1711 } 1712 1713 protected void refreshEnabled() { 1714 setEnabled(memberTable.getSelectedRowCount() == 1 1715 && memberTableModel.isEditableRelation(memberTable.getSelectedRow())); 1716 } 1717 1718 protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) { 1719 Collection<RelationMember> members = new HashSet<>(); 1720 Collection<OsmPrimitive> selection = getLayer().data.getSelected(); 1721 for (RelationMember member: r.getMembers()) { 1722 if (selection.contains(member.getMember())) { 1723 members.add(member); 1724 } 1725 } 1726 return members; 1727 } 1728 1729 public void run() { 1730 int idx = memberTable.getSelectedRow(); 1731 if (idx < 0) 1732 return; 1733 OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx); 1734 if (!(primitive instanceof Relation)) 1735 return; 1736 Relation r = (Relation) primitive; 1737 if (r.isIncomplete()) 1738 return; 1739 1740 RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r)); 1741 editor.setVisible(true); 1742 } 1743 1744 @Override 1745 public void actionPerformed(ActionEvent e) { 1746 if (!isEnabled()) 1747 return; 1748 run(); 1749 } 1750 1751 @Override 1752 public void valueChanged(ListSelectionEvent e) { 1753 refreshEnabled(); 1754 } 1755 } 1756 1757 class PasteMembersAction extends AddFromSelectionAction { 1758 1759 @Override 1760 public void actionPerformed(ActionEvent e) { 1761 try { 1762 List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded(); 1763 DataSet ds = getLayer().data; 1764 List<OsmPrimitive> toAdd = new ArrayList<>(); 1765 boolean hasNewInOtherLayer = false; 1766 1767 for (PrimitiveData primitive: primitives) { 1768 OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive); 1769 if (primitiveInDs != null) { 1770 toAdd.add(primitiveInDs); 1771 } else if (!primitive.isNew()) { 1772 OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true); 1773 ds.addPrimitive(p); 1774 toAdd.add(p); 1775 } else { 1776 hasNewInOtherLayer = true; 1777 break; 1778 } 1779 } 1780 1781 if (hasNewInOtherLayer) { 1782 JOptionPane.showMessageDialog(Main.parent, 1783 tr("Members from paste buffer cannot be added because they are not included in current layer")); 1784 return; 1785 } 1786 1787 toAdd = filterConfirmedPrimitives(toAdd); 1788 int index = memberTableModel.getSelectionModel().getMaxSelectionIndex(); 1789 if (index == -1) { 1790 index = memberTableModel.getRowCount() - 1; 1791 } 1792 memberTableModel.addMembersAfterIdx(toAdd, index); 1793 1794 tfRole.requestFocusInWindow(); 1795 1796 } catch (AddAbortException ex) { 1797 // Do nothing 1798 if (Main.isTraceEnabled()) { 1799 Main.trace(ex.getMessage()); 1800 } 1801 } 1802 } 1803 } 1804 1805 class CopyMembersAction extends AbstractAction { 1806 @Override 1807 public void actionPerformed(ActionEvent e) { 1808 Set<OsmPrimitive> primitives = new HashSet<>(); 1809 for (RelationMember rm: memberTableModel.getSelectedMembers()) { 1810 primitives.add(rm.getMember()); 1811 } 1812 if (!primitives.isEmpty()) { 1813 CopyAction.copy(getLayer(), primitives); 1814 } 1815 } 1816 } 1817 1818 class MemberTableDblClickAdapter extends MouseAdapter { 1819 @Override 1820 public void mouseClicked(MouseEvent e) { 1821 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 1822 new EditAction().run(); 1823 } 1824 } 1825 } 1826}