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; 006 007import java.awt.BorderLayout; 008import java.awt.Component; 009import java.awt.Dimension; 010import java.awt.FlowLayout; 011import java.awt.GridBagConstraints; 012import java.awt.GridBagLayout; 013import java.awt.Window; 014import java.awt.datatransfer.Clipboard; 015import java.awt.datatransfer.FlavorListener; 016import java.awt.event.ActionEvent; 017import java.awt.event.FocusAdapter; 018import java.awt.event.FocusEvent; 019import java.awt.event.InputEvent; 020import java.awt.event.KeyEvent; 021import java.awt.event.MouseAdapter; 022import java.awt.event.MouseEvent; 023import java.awt.event.WindowAdapter; 024import java.awt.event.WindowEvent; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.EnumSet; 030import java.util.List; 031import java.util.Set; 032import java.util.stream.Collectors; 033 034import javax.swing.AbstractAction; 035import javax.swing.BorderFactory; 036import javax.swing.InputMap; 037import javax.swing.JButton; 038import javax.swing.JComponent; 039import javax.swing.JLabel; 040import javax.swing.JMenuItem; 041import javax.swing.JOptionPane; 042import javax.swing.JPanel; 043import javax.swing.JRootPane; 044import javax.swing.JScrollPane; 045import javax.swing.JSplitPane; 046import javax.swing.JTabbedPane; 047import javax.swing.JTable; 048import javax.swing.JToolBar; 049import javax.swing.KeyStroke; 050 051import org.openstreetmap.josm.actions.JosmAction; 052import org.openstreetmap.josm.command.ChangeMembersCommand; 053import org.openstreetmap.josm.command.Command; 054import org.openstreetmap.josm.data.UndoRedoHandler; 055import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueListener; 056import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 057import org.openstreetmap.josm.data.osm.OsmPrimitive; 058import org.openstreetmap.josm.data.osm.Relation; 059import org.openstreetmap.josm.data.osm.RelationMember; 060import org.openstreetmap.josm.data.osm.Tag; 061import org.openstreetmap.josm.data.validation.tests.RelationChecker; 062import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 063import org.openstreetmap.josm.gui.MainApplication; 064import org.openstreetmap.josm.gui.MainMenu; 065import org.openstreetmap.josm.gui.ScrollViewport; 066import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 067import org.openstreetmap.josm.gui.dialogs.relation.actions.AbstractRelationEditorAction; 068import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAfterSelection; 069import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtEndAction; 070import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtStartAction; 071import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedBeforeSelection; 072import org.openstreetmap.josm.gui.dialogs.relation.actions.ApplyAction; 073import org.openstreetmap.josm.gui.dialogs.relation.actions.CancelAction; 074import org.openstreetmap.josm.gui.dialogs.relation.actions.CopyMembersAction; 075import org.openstreetmap.josm.gui.dialogs.relation.actions.DeleteCurrentRelationAction; 076import org.openstreetmap.josm.gui.dialogs.relation.actions.DownloadIncompleteMembersAction; 077import org.openstreetmap.josm.gui.dialogs.relation.actions.DownloadSelectedIncompleteMembersAction; 078import org.openstreetmap.josm.gui.dialogs.relation.actions.DuplicateRelationAction; 079import org.openstreetmap.josm.gui.dialogs.relation.actions.EditAction; 080import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionAccess; 081import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionGroup; 082import org.openstreetmap.josm.gui.dialogs.relation.actions.MoveDownAction; 083import org.openstreetmap.josm.gui.dialogs.relation.actions.MoveUpAction; 084import org.openstreetmap.josm.gui.dialogs.relation.actions.OKAction; 085import org.openstreetmap.josm.gui.dialogs.relation.actions.PasteMembersAction; 086import org.openstreetmap.josm.gui.dialogs.relation.actions.RefreshAction; 087import org.openstreetmap.josm.gui.dialogs.relation.actions.RemoveAction; 088import org.openstreetmap.josm.gui.dialogs.relation.actions.RemoveSelectedAction; 089import org.openstreetmap.josm.gui.dialogs.relation.actions.ReverseAction; 090import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectAction; 091import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectPrimitivesForSelectedMembersAction; 092import org.openstreetmap.josm.gui.dialogs.relation.actions.SelectedMembersForSelectionAction; 093import org.openstreetmap.josm.gui.dialogs.relation.actions.SetRoleAction; 094import org.openstreetmap.josm.gui.dialogs.relation.actions.SortAction; 095import org.openstreetmap.josm.gui.dialogs.relation.actions.SortBelowAction; 096import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 097import org.openstreetmap.josm.gui.help.HelpUtil; 098import org.openstreetmap.josm.gui.layer.OsmDataLayer; 099import org.openstreetmap.josm.gui.tagging.TagEditorModel; 100import org.openstreetmap.josm.gui.tagging.TagEditorPanel; 101import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField; 102import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; 103import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; 104import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; 105import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler; 106import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType; 107import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets; 108import org.openstreetmap.josm.gui.util.WindowGeometry; 109import org.openstreetmap.josm.spi.preferences.Config; 110import org.openstreetmap.josm.tools.CheckParameterUtil; 111import org.openstreetmap.josm.tools.InputMapUtils; 112import org.openstreetmap.josm.tools.Logging; 113import org.openstreetmap.josm.tools.Shortcut; 114import org.openstreetmap.josm.tools.Utils; 115 116/** 117 * This dialog is for editing relations. 118 * @since 343 119 */ 120public class GenericRelationEditor extends RelationEditor implements CommandQueueListener { 121 /** the tag table and its model */ 122 private final TagEditorPanel tagEditorPanel; 123 private final ReferringRelationsBrowser referrerBrowser; 124 private final ReferringRelationsBrowserModel referrerModel; 125 126 /** the member table and its model */ 127 private final MemberTable memberTable; 128 private final MemberTableModel memberTableModel; 129 130 /** the selection table and its model */ 131 private final SelectionTable selectionTable; 132 private final SelectionTableModel selectionTableModel; 133 134 private final AutoCompletingTextField tfRole; 135 136 /** 137 * the menu item in the windows menu. Required to properly hide on dialog close. 138 */ 139 private JMenuItem windowMenuItem; 140 /** 141 * Action for performing the {@link RefreshAction} 142 */ 143 private final RefreshAction refreshAction; 144 /** 145 * Action for performing the {@link ApplyAction} 146 */ 147 private final ApplyAction applyAction; 148 /** 149 * Action for performing the {@link SelectAction} 150 */ 151 private final SelectAction selectAction; 152 /** 153 * Action for performing the {@link DuplicateRelationAction} 154 */ 155 private final DuplicateRelationAction duplicateAction; 156 /** 157 * Action for performing the {@link DeleteCurrentRelationAction} 158 */ 159 private final DeleteCurrentRelationAction deleteAction; 160 /** 161 * Action for performing the {@link OKAction} 162 */ 163 private final OKAction okAction; 164 /** 165 * Action for performing the {@link CancelAction} 166 */ 167 private final CancelAction cancelAction; 168 /** 169 * A list of listeners that need to be notified on clipboard content changes. 170 */ 171 private final ArrayList<FlavorListener> clipboardListeners = new ArrayList<>(); 172 173 private Component selectedTabPane; 174 private JTabbedPane tabbedPane; 175 176 /** 177 * Creates a new relation editor for the given relation. The relation will be saved if the user 178 * selects "ok" in the editor. 179 * 180 * If no relation is given, will create an editor for a new relation. 181 * 182 * @param layer the {@link OsmDataLayer} the new or edited relation belongs to 183 * @param relation relation to edit, or null to create a new one. 184 * @param selectedMembers a collection of members which shall be selected initially 185 */ 186 public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) { 187 super(layer, relation); 188 189 setRememberWindowGeometry(getClass().getName() + ".geometry", 190 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(700, 650))); 191 192 final TaggingPresetHandler presetHandler = new TaggingPresetHandler() { 193 194 @Override 195 public void updateTags(List<Tag> tags) { 196 tagEditorPanel.getModel().updateTags(tags); 197 } 198 199 @Override 200 public Collection<OsmPrimitive> getSelection() { 201 Relation relation = new Relation(); 202 tagEditorPanel.getModel().applyToPrimitive(relation); 203 return Collections.<OsmPrimitive>singletonList(relation); 204 } 205 }; 206 207 // init the various models 208 // 209 memberTableModel = new MemberTableModel(relation, getLayer(), presetHandler); 210 memberTableModel.register(); 211 selectionTableModel = new SelectionTableModel(getLayer()); 212 selectionTableModel.register(); 213 referrerModel = new ReferringRelationsBrowserModel(relation); 214 215 tagEditorPanel = new TagEditorPanel(relation, presetHandler); 216 populateModels(relation); 217 tagEditorPanel.getModel().ensureOneTag(); 218 219 // setting up the member table 220 memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel); 221 memberTable.addMouseListener(new MemberTableDblClickAdapter()); 222 memberTableModel.addMemberModelListener(memberTable); 223 224 MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor(); 225 selectionTable = new SelectionTable(selectionTableModel, memberTableModel); 226 selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height); 227 228 LeftButtonToolbar leftButtonToolbar = new LeftButtonToolbar(new RelationEditorActionAccess()); 229 tfRole = buildRoleTextField(this); 230 231 JSplitPane pane = buildSplitPane( 232 buildTagEditorPanel(tagEditorPanel), 233 buildMemberEditorPanel(leftButtonToolbar, new RelationEditorActionAccess()), 234 this); 235 pane.setPreferredSize(new Dimension(100, 100)); 236 237 JPanel pnl = new JPanel(new BorderLayout()); 238 pnl.add(pane, BorderLayout.CENTER); 239 pnl.setBorder(BorderFactory.createRaisedBevelBorder()); 240 241 getContentPane().setLayout(new BorderLayout()); 242 tabbedPane = new JTabbedPane(); 243 tabbedPane.add(tr("Tags and Members"), pnl); 244 referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel); 245 tabbedPane.add(tr("Parent Relations"), referrerBrowser); 246 tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation)); 247 selectedTabPane = tabbedPane.getSelectedComponent(); 248 tabbedPane.addChangeListener(e -> { 249 JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource(); 250 int index = sourceTabbedPane.getSelectedIndex(); 251 String title = sourceTabbedPane.getTitleAt(index); 252 if (title.equals(tr("Parent Relations"))) { 253 referrerBrowser.init(); 254 } 255 // see #20228 256 boolean selIsTagsAndMembers = sourceTabbedPane.getSelectedComponent() == pnl; 257 if (selectedTabPane == pnl && !selIsTagsAndMembers) { 258 unregisterMain(); 259 } else if (selectedTabPane != pnl && selIsTagsAndMembers) { 260 registerMain(); 261 } 262 selectedTabPane = sourceTabbedPane.getSelectedComponent(); 263 }); 264 265 IRelationEditorActionAccess actionAccess = new RelationEditorActionAccess(); 266 267 refreshAction = new RefreshAction(actionAccess); 268 applyAction = new ApplyAction(actionAccess); 269 selectAction = new SelectAction(actionAccess); 270 duplicateAction = new DuplicateRelationAction(actionAccess); 271 deleteAction = new DeleteCurrentRelationAction(actionAccess); 272 addPropertyChangeListener(deleteAction); 273 274 okAction = new OKAction(actionAccess); 275 cancelAction = new CancelAction(actionAccess); 276 277 getContentPane().add(buildToolBar(refreshAction, applyAction, selectAction, duplicateAction, deleteAction), BorderLayout.NORTH); 278 getContentPane().add(tabbedPane, BorderLayout.CENTER); 279 getContentPane().add(buildOkCancelButtonPanel(okAction, cancelAction), BorderLayout.SOUTH); 280 281 setSize(findMaxDialogSize()); 282 283 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 284 addWindowListener( 285 new WindowAdapter() { 286 @Override 287 public void windowOpened(WindowEvent e) { 288 cleanSelfReferences(memberTableModel, getRelation()); 289 } 290 291 @Override 292 public void windowClosing(WindowEvent e) { 293 cancel(); 294 } 295 } 296 ); 297 InputMapUtils.addCtrlEnterAction(getRootPane(), okAction); 298 // CHECKSTYLE.OFF: LineLength 299 registerCopyPasteAction(tagEditorPanel.getPasteAction(), "PASTE_TAGS", 300 Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke(), 301 getRootPane(), memberTable, selectionTable); 302 // CHECKSTYLE.ON: LineLength 303 304 KeyStroke key = Shortcut.getPasteKeyStroke(); 305 if (key != null) { 306 // handle uncommon situation, that user has no keystroke assigned to paste 307 registerCopyPasteAction(new PasteMembersAction(actionAccess) { 308 private static final long serialVersionUID = 1L; 309 310 @Override 311 public void actionPerformed(ActionEvent e) { 312 super.actionPerformed(e); 313 tfRole.requestFocusInWindow(); 314 } 315 }, "PASTE_MEMBERS", key, getRootPane(), memberTable, selectionTable); 316 } 317 key = Shortcut.getCopyKeyStroke(); 318 if (key != null) { 319 // handle uncommon situation, that user has no keystroke assigned to copy 320 registerCopyPasteAction(new CopyMembersAction(actionAccess), 321 "COPY_MEMBERS", key, getRootPane(), memberTable, selectionTable); 322 } 323 tagEditorPanel.setNextFocusComponent(memberTable); 324 selectionTable.setFocusable(false); 325 memberTableModel.setSelectedMembers(selectedMembers); 326 HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/RelationEditor")); 327 UndoRedoHandler.getInstance().addCommandQueueListener(this); 328 } 329 330 private void registerMain() { 331 selectionTableModel.register(); 332 memberTableModel.register(); 333 memberTable.registerListeners(); 334 } 335 336 private void unregisterMain() { 337 selectionTableModel.unregister(); 338 memberTableModel.unregister(); 339 memberTable.unregisterListeners(); 340 } 341 342 @Override 343 public void reloadDataFromRelation() { 344 setRelation(getRelation()); 345 populateModels(getRelation()); 346 refreshAction.updateEnabledState(); 347 } 348 349 private void populateModels(Relation relation) { 350 if (relation != null) { 351 tagEditorPanel.getModel().initFromPrimitive(relation); 352 memberTableModel.populate(relation); 353 if (!getLayer().data.getRelations().contains(relation)) { 354 // treat it as a new relation if it doesn't exist in the data set yet. 355 setRelation(null); 356 } 357 } else { 358 tagEditorPanel.getModel().clear(); 359 memberTableModel.populate(null); 360 } 361 } 362 363 /** 364 * Apply changes. 365 * @see ApplyAction 366 */ 367 public void apply() { 368 applyAction.actionPerformed(null); 369 } 370 371 /** 372 * Select relation. 373 * @see SelectAction 374 * @since 12933 375 */ 376 public void select() { 377 selectAction.actionPerformed(null); 378 } 379 380 /** 381 * Cancel changes. 382 * @see CancelAction 383 */ 384 public void cancel() { 385 cancelAction.actionPerformed(null); 386 } 387 388 /** 389 * Creates the toolbar 390 * @param actions relation toolbar actions 391 * @return the toolbar 392 * @since 12933 393 */ 394 protected static JToolBar buildToolBar(AbstractRelationEditorAction... actions) { 395 JToolBar tb = new JToolBar(); 396 tb.setFloatable(false); 397 for (AbstractRelationEditorAction action : actions) { 398 tb.add(action); 399 } 400 return tb; 401 } 402 403 /** 404 * builds the panel with the OK and the Cancel button 405 * @param okAction OK action 406 * @param cancelAction Cancel action 407 * 408 * @return the panel with the OK and the Cancel button 409 */ 410 protected static JPanel buildOkCancelButtonPanel(OKAction okAction, CancelAction cancelAction) { 411 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 412 pnl.add(new JButton(okAction)); 413 pnl.add(new JButton(cancelAction)); 414 pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor")))); 415 return pnl; 416 } 417 418 /** 419 * builds the panel with the tag editor 420 * @param tagEditorPanel tag editor panel 421 * 422 * @return the panel with the tag editor 423 */ 424 protected static JPanel buildTagEditorPanel(TagEditorPanel tagEditorPanel) { 425 JPanel pnl = new JPanel(new GridBagLayout()); 426 427 GridBagConstraints gc = new GridBagConstraints(); 428 gc.gridx = 0; 429 gc.gridy = 0; 430 gc.gridheight = 1; 431 gc.gridwidth = 1; 432 gc.fill = GridBagConstraints.HORIZONTAL; 433 gc.anchor = GridBagConstraints.FIRST_LINE_START; 434 gc.weightx = 1.0; 435 gc.weighty = 0.0; 436 pnl.add(new JLabel(tr("Tags")), gc); 437 438 gc.gridx = 0; 439 gc.gridy = 1; 440 gc.fill = GridBagConstraints.BOTH; 441 gc.anchor = GridBagConstraints.CENTER; 442 gc.weightx = 1.0; 443 gc.weighty = 1.0; 444 pnl.add(tagEditorPanel, gc); 445 return pnl; 446 } 447 448 /** 449 * builds the role text field 450 * @param re relation editor 451 * @return the role text field 452 */ 453 protected static AutoCompletingTextField buildRoleTextField(final IRelationEditor re) { 454 final AutoCompletingTextField tfRole = new AutoCompletingTextField(10); 455 tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members")); 456 tfRole.addFocusListener(new FocusAdapter() { 457 @Override 458 public void focusGained(FocusEvent e) { 459 tfRole.selectAll(); 460 } 461 }); 462 tfRole.setAutoCompletionList(new AutoCompletionList()); 463 tfRole.addFocusListener( 464 new FocusAdapter() { 465 @Override 466 public void focusGained(FocusEvent e) { 467 AutoCompletionList list = tfRole.getAutoCompletionList(); 468 if (list != null) { 469 list.clear(); 470 AutoCompletionManager.of(re.getLayer().data).populateWithMemberRoles(list, re.getRelation()); 471 } 472 } 473 } 474 ); 475 tfRole.setText(Config.getPref().get("relation.editor.generic.lastrole", "")); 476 return tfRole; 477 } 478 479 /** 480 * builds the panel for the relation member editor 481 * @param leftButtonToolbar left button toolbar 482 * @param editorAccess The relation editor 483 * 484 * @return the panel for the relation member editor 485 */ 486 protected static JPanel buildMemberEditorPanel( 487 LeftButtonToolbar leftButtonToolbar, IRelationEditorActionAccess editorAccess) { 488 final JPanel pnl = new JPanel(new GridBagLayout()); 489 final JScrollPane scrollPane = new JScrollPane(editorAccess.getMemberTable()); 490 491 GridBagConstraints gc = new GridBagConstraints(); 492 gc.gridx = 0; 493 gc.gridy = 0; 494 gc.gridwidth = 2; 495 gc.fill = GridBagConstraints.HORIZONTAL; 496 gc.anchor = GridBagConstraints.FIRST_LINE_START; 497 gc.weightx = 1.0; 498 gc.weighty = 0.0; 499 pnl.add(new JLabel(tr("Members")), gc); 500 501 gc.gridx = 0; 502 gc.gridy = 1; 503 gc.gridheight = 2; 504 gc.gridwidth = 1; 505 gc.fill = GridBagConstraints.VERTICAL; 506 gc.anchor = GridBagConstraints.NORTHWEST; 507 gc.weightx = 0.0; 508 gc.weighty = 1.0; 509 pnl.add(new ScrollViewport(leftButtonToolbar, ScrollViewport.VERTICAL_DIRECTION), gc); 510 511 gc.gridx = 1; 512 gc.gridy = 1; 513 gc.gridheight = 1; 514 gc.fill = GridBagConstraints.BOTH; 515 gc.anchor = GridBagConstraints.CENTER; 516 gc.weightx = 0.6; 517 gc.weighty = 1.0; 518 pnl.add(scrollPane, gc); 519 520 // --- role editing 521 JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 522 p3.add(new JLabel(tr("Apply Role:"))); 523 p3.add(editorAccess.getTextFieldRole()); 524 SetRoleAction setRoleAction = new SetRoleAction(editorAccess); 525 editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(setRoleAction); 526 editorAccess.getTextFieldRole().getDocument().addDocumentListener(setRoleAction); 527 editorAccess.getTextFieldRole().addActionListener(setRoleAction); 528 editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener( 529 e -> editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0) 530 ); 531 editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0); 532 JButton btnApply = new JButton(setRoleAction); 533 btnApply.setPreferredSize(new Dimension(20, 20)); 534 btnApply.setText(""); 535 p3.add(btnApply); 536 537 gc.gridx = 1; 538 gc.gridy = 2; 539 gc.fill = GridBagConstraints.HORIZONTAL; 540 gc.anchor = GridBagConstraints.LAST_LINE_START; 541 gc.weightx = 1.0; 542 gc.weighty = 0.0; 543 pnl.add(p3, gc); 544 545 JPanel pnl2 = new JPanel(new GridBagLayout()); 546 547 gc.gridx = 0; 548 gc.gridy = 0; 549 gc.gridheight = 1; 550 gc.gridwidth = 3; 551 gc.fill = GridBagConstraints.HORIZONTAL; 552 gc.anchor = GridBagConstraints.FIRST_LINE_START; 553 gc.weightx = 1.0; 554 gc.weighty = 0.0; 555 pnl2.add(new JLabel(tr("Selection")), gc); 556 557 gc.gridx = 0; 558 gc.gridy = 1; 559 gc.gridheight = 1; 560 gc.gridwidth = 1; 561 gc.fill = GridBagConstraints.VERTICAL; 562 gc.anchor = GridBagConstraints.NORTHWEST; 563 gc.weightx = 0.0; 564 gc.weighty = 1.0; 565 pnl2.add(new ScrollViewport(buildSelectionControlButtonToolbar(editorAccess), 566 ScrollViewport.VERTICAL_DIRECTION), gc); 567 568 gc.gridx = 1; 569 gc.gridy = 1; 570 gc.weightx = 1.0; 571 gc.weighty = 1.0; 572 gc.fill = GridBagConstraints.BOTH; 573 pnl2.add(buildSelectionTablePanel(editorAccess.getSelectionTable()), gc); 574 575 final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); 576 splitPane.setLeftComponent(pnl); 577 splitPane.setRightComponent(pnl2); 578 splitPane.setOneTouchExpandable(false); 579 if (editorAccess.getEditor() instanceof Window) { 580 ((Window) editorAccess.getEditor()).addWindowListener(new WindowAdapter() { 581 @Override 582 public void windowOpened(WindowEvent e) { 583 // has to be called when the window is visible, otherwise no effect 584 splitPane.setDividerLocation(0.6); 585 } 586 }); 587 } 588 589 JPanel pnl3 = new JPanel(new BorderLayout()); 590 pnl3.add(splitPane, BorderLayout.CENTER); 591 592 return pnl3; 593 } 594 595 /** 596 * builds the panel with the table displaying the currently selected primitives 597 * @param selectionTable selection table 598 * 599 * @return panel with current selection 600 */ 601 protected static JPanel buildSelectionTablePanel(SelectionTable selectionTable) { 602 JPanel pnl = new JPanel(new BorderLayout()); 603 pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER); 604 return pnl; 605 } 606 607 /** 608 * builds the {@link JSplitPane} which divides the editor in an upper and a lower half 609 * @param top top panel 610 * @param bottom bottom panel 611 * @param re relation editor 612 * 613 * @return the split panel 614 */ 615 protected static JSplitPane buildSplitPane(JPanel top, JPanel bottom, IRelationEditor re) { 616 final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); 617 pane.setTopComponent(top); 618 pane.setBottomComponent(bottom); 619 pane.setOneTouchExpandable(true); 620 if (re instanceof Window) { 621 ((Window) re).addWindowListener(new WindowAdapter() { 622 @Override 623 public void windowOpened(WindowEvent e) { 624 // has to be called when the window is visible, otherwise no effect 625 pane.setDividerLocation(0.3); 626 } 627 }); 628 } 629 return pane; 630 } 631 632 /** 633 * The toolbar with the buttons on the left 634 */ 635 static class LeftButtonToolbar extends JToolBar { 636 private static final long serialVersionUID = 1L; 637 638 /** 639 * Constructs a new {@code LeftButtonToolbar}. 640 * @param editorAccess relation editor 641 */ 642 LeftButtonToolbar(IRelationEditorActionAccess editorAccess) { 643 setOrientation(JToolBar.VERTICAL); 644 setFloatable(false); 645 646 List<IRelationEditorActionGroup> groups = new ArrayList<>(); 647 // Move 648 groups.add(buildNativeGroup(10, 649 new MoveUpAction(editorAccess, "moveUp"), 650 new MoveDownAction(editorAccess, "moveDown") 651 )); 652 // Edit 653 groups.add(buildNativeGroup(20, 654 new EditAction(editorAccess), 655 new RemoveAction(editorAccess, "removeSelected") 656 )); 657 // Sort 658 groups.add(buildNativeGroup(30, 659 new SortAction(editorAccess), 660 new SortBelowAction(editorAccess) 661 )); 662 // Reverse 663 groups.add(buildNativeGroup(40, 664 new ReverseAction(editorAccess) 665 )); 666 // Download 667 groups.add(buildNativeGroup(50, 668 new DownloadIncompleteMembersAction(editorAccess, "downloadIncomplete"), 669 new DownloadSelectedIncompleteMembersAction(editorAccess) 670 )); 671 groups.addAll(RelationEditorHooks.getMemberActions()); 672 673 IRelationEditorActionGroup.fillToolbar(this, groups, editorAccess); 674 675 676 InputMap inputMap = editorAccess.getMemberTable().getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 677 inputMap.put((KeyStroke) new RemoveAction(editorAccess, "removeSelected") 678 .getValue(AbstractAction.ACCELERATOR_KEY), "removeSelected"); 679 inputMap.put((KeyStroke) new MoveUpAction(editorAccess, "moveUp") 680 .getValue(AbstractAction.ACCELERATOR_KEY), "moveUp"); 681 inputMap.put((KeyStroke) new MoveDownAction(editorAccess, "moveDown") 682 .getValue(AbstractAction.ACCELERATOR_KEY), "moveDown"); 683 inputMap.put((KeyStroke) new DownloadIncompleteMembersAction( 684 editorAccess, "downloadIncomplete").getValue(AbstractAction.ACCELERATOR_KEY), "downloadIncomplete"); 685 } 686 } 687 688 /** 689 * build the toolbar with the buttons for adding or removing the current selection 690 * @param editorAccess relation editor 691 * 692 * @return control buttons panel for selection/members 693 */ 694 protected static JToolBar buildSelectionControlButtonToolbar(IRelationEditorActionAccess editorAccess) { 695 JToolBar tb = new JToolBar(JToolBar.VERTICAL); 696 tb.setFloatable(false); 697 698 List<IRelationEditorActionGroup> groups = new ArrayList<>(); 699 groups.add(buildNativeGroup(10, 700 new AddSelectedAtStartAction(editorAccess), 701 new AddSelectedBeforeSelection(editorAccess), 702 new AddSelectedAfterSelection(editorAccess), 703 new AddSelectedAtEndAction(editorAccess) 704 )); 705 groups.add(buildNativeGroup(20, 706 new SelectedMembersForSelectionAction(editorAccess), 707 new SelectPrimitivesForSelectedMembersAction(editorAccess) 708 )); 709 groups.add(buildNativeGroup(30, 710 new RemoveSelectedAction(editorAccess) 711 )); 712 groups.addAll(RelationEditorHooks.getSelectActions()); 713 714 IRelationEditorActionGroup.fillToolbar(tb, groups, editorAccess); 715 return tb; 716 } 717 718 private static IRelationEditorActionGroup buildNativeGroup(int order, AbstractRelationEditorAction... actions) { 719 return new IRelationEditorActionGroup() { 720 @Override 721 public int order() { 722 return order; 723 } 724 725 @Override 726 public List<AbstractRelationEditorAction> getActions(IRelationEditorActionAccess editorAccess) { 727 return Arrays.asList(actions); 728 } 729 }; 730 } 731 732 @Override 733 protected Dimension findMaxDialogSize() { 734 return new Dimension(700, 650); 735 } 736 737 @Override 738 public void setVisible(boolean visible) { 739 if (isVisible() == visible) { 740 return; 741 } 742 if (visible) { 743 tagEditorPanel.initAutoCompletion(getLayer()); 744 } 745 super.setVisible(visible); 746 Clipboard clipboard = ClipboardUtils.getClipboard(); 747 if (visible) { 748 RelationDialogManager.getRelationDialogManager().positionOnScreen(this); 749 if (windowMenuItem == null) { 750 windowMenuItem = addToWindowMenu(this, getLayer().getName()); 751 } 752 tagEditorPanel.requestFocusInWindow(); 753 for (FlavorListener listener : clipboardListeners) { 754 clipboard.addFlavorListener(listener); 755 } 756 } else { 757 // make sure all registered listeners are unregistered 758 // 759 memberTable.stopHighlighting(); 760 if (tabbedPane != null && tr("Tags and Members").equals(tabbedPane.getTitleAt(tabbedPane.getSelectedIndex()))) { 761 unregisterMain(); 762 } 763 if (windowMenuItem != null) { 764 MainApplication.getMenu().windowMenu.remove(windowMenuItem); 765 windowMenuItem = null; 766 } 767 for (FlavorListener listener : clipboardListeners) { 768 clipboard.removeFlavorListener(listener); 769 } 770 dispose(); 771 } 772 } 773 774 /** 775 * Adds current relation editor to the windows menu (in the "volatile" group) 776 * @param re relation editor 777 * @param layerName layer name 778 * @return created menu item 779 */ 780 protected static JMenuItem addToWindowMenu(IRelationEditor re, String layerName) { 781 Relation r = re.getRelation(); 782 String name = r == null ? tr("New relation") : r.getLocalName(); 783 JosmAction focusAction = new JosmAction( 784 tr("Relation Editor: {0}", name == null && r != null ? r.getId() : name), 785 "dialogs/relationlist", 786 tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''", name, layerName), 787 null, false, false) { 788 private static final long serialVersionUID = 1L; 789 790 @Override 791 public void actionPerformed(ActionEvent e) { 792 ((RelationEditor) getValue("relationEditor")).setVisible(true); 793 } 794 }; 795 focusAction.putValue("relationEditor", re); 796 return MainMenu.add(MainApplication.getMenu().windowMenu, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE); 797 } 798 799 /** 800 * checks whether the current relation has members referring to itself. If so, 801 * warns the users and provides an option for removing these members. 802 * @param memberTableModel member table model 803 * @param relation relation 804 */ 805 protected static void cleanSelfReferences(MemberTableModel memberTableModel, Relation relation) { 806 List<OsmPrimitive> toCheck = new ArrayList<>(); 807 toCheck.add(relation); 808 if (memberTableModel.hasMembersReferringTo(toCheck)) { 809 int ret = ConditionalOptionPaneUtil.showOptionDialog( 810 "clean_relation_self_references", 811 MainApplication.getMainFrame(), 812 tr("<html>There is at least one member in this relation referring<br>" 813 + "to the relation itself.<br>" 814 + "This creates circular dependencies and is discouraged.<br>" 815 + "How do you want to proceed with circular dependencies?</html>"), 816 tr("Warning"), 817 JOptionPane.YES_NO_OPTION, 818 JOptionPane.WARNING_MESSAGE, 819 new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")}, 820 tr("Remove them, clean up relation") 821 ); 822 switch(ret) { 823 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 824 case JOptionPane.CLOSED_OPTION: 825 case JOptionPane.NO_OPTION: 826 return; 827 case JOptionPane.YES_OPTION: 828 memberTableModel.removeMembersReferringTo(toCheck); 829 break; 830 default: // Do nothing 831 } 832 } 833 } 834 835 private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut, 836 JRootPane rootPane, JTable... tables) { 837 if (shortcut == null) { 838 Logging.warn("No shortcut provided for the Paste action in Relation editor dialog"); 839 } else { 840 int mods = shortcut.getModifiers(); 841 int code = shortcut.getKeyCode(); 842 if (code != KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) { 843 Logging.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut); 844 return; 845 } 846 } 847 rootPane.getActionMap().put(actionName, action); 848 if (shortcut != null) { 849 rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 850 // Assign also to JTables because they have their own Copy&Paste implementation 851 // (which is disabled in this case but eats key shortcuts anyway) 852 for (JTable table : tables) { 853 table.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName); 854 table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName); 855 table.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName); 856 } 857 } 858 if (action instanceof FlavorListener) { 859 clipboardListeners.add((FlavorListener) action); 860 } 861 } 862 863 @Override 864 public void dispose() { 865 refreshAction.destroy(); 866 UndoRedoHandler.getInstance().removeCommandQueueListener(this); 867 super.dispose(); // call before setting relation to null, see #20304 868 setRelation(null); 869 selectedTabPane = null; 870 } 871 872 /** 873 * Exception thrown when user aborts add operation. 874 */ 875 public static class AddAbortException extends Exception { 876 } 877 878 /** 879 * Asks confirmationbefore adding a primitive. 880 * @param primitive primitive to add 881 * @return {@code true} is user confirms the operation, {@code false} otherwise 882 * @throws AddAbortException if user aborts operation 883 */ 884 public static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException { 885 String msg = tr("<html>This relation already has one or more members referring to<br>" 886 + "the object ''{0}''<br>" 887 + "<br>" 888 + "Do you really want to add another relation member?</html>", 889 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance())) 890 ); 891 int ret = ConditionalOptionPaneUtil.showOptionDialog( 892 "add_primitive_to_relation", 893 MainApplication.getMainFrame(), 894 msg, 895 tr("Multiple members referring to same object."), 896 JOptionPane.YES_NO_CANCEL_OPTION, 897 JOptionPane.WARNING_MESSAGE, 898 null, 899 null 900 ); 901 switch(ret) { 902 case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: 903 case JOptionPane.YES_OPTION: 904 return true; 905 case JOptionPane.NO_OPTION: 906 case JOptionPane.CLOSED_OPTION: 907 return false; 908 case JOptionPane.CANCEL_OPTION: 909 default: 910 throw new AddAbortException(); 911 } 912 } 913 914 /** 915 * Warn about circular references. 916 * @param primitive the concerned primitive 917 */ 918 public static void warnOfCircularReferences(OsmPrimitive primitive) { 919 warnOfCircularReferences(primitive, Collections.emptyList()); 920 } 921 922 /** 923 * Warn about circular references. 924 * @param primitive the concerned primitive 925 * @param loop list of relation that form the circular dependencies. 926 * Only used to report the loop if more than one relation is involved. 927 * @since 16651 928 */ 929 public static void warnOfCircularReferences(OsmPrimitive primitive, List<Relation> loop) { 930 final String msg; 931 DefaultNameFormatter df = DefaultNameFormatter.getInstance(); 932 if (loop.size() <= 2) { 933 msg = tr("<html>You are trying to add a relation to itself.<br>" 934 + "<br>" 935 + "This generates a circular dependency of parent/child elements and is therefore discouraged.<br>" 936 + "Skipping relation ''{0}''.</html>", 937 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(df))); 938 } else { 939 msg = tr("<html>You are trying to add a child relation which refers to the parent relation.<br>" 940 + "<br>" 941 + "This generates a circular dependency of parent/child elements and is therefore discouraged.<br>" 942 + "Skipping relation ''{0}''." + "<br>" 943 + "Relations that would generate the circular dependency:<br>{1}</html>", 944 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(df)), 945 loop.stream().map(p -> Utils.escapeReservedCharactersHTML(p.getDisplayName(df))) 946 .collect(Collectors.joining(" -> <br>"))); 947 } 948 JOptionPane.showMessageDialog( 949 MainApplication.getMainFrame(), 950 msg, 951 tr("Warning"), 952 JOptionPane.WARNING_MESSAGE); 953 } 954 955 /** 956 * Adds primitives to a given relation. 957 * @param orig The relation to modify 958 * @param primitivesToAdd The primitives to add as relation members 959 * @return The resulting command 960 * @throws IllegalArgumentException if orig is null 961 */ 962 public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) { 963 CheckParameterUtil.ensureParameterNotNull(orig, "orig"); 964 try { 965 final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets( 966 EnumSet.of(TaggingPresetType.forPrimitive(orig)), orig.getKeys(), false); 967 Relation target = new Relation(orig); 968 boolean modified = false; 969 for (OsmPrimitive p : primitivesToAdd) { 970 if (p instanceof Relation) { 971 List<Relation> loop = RelationChecker.checkAddMember(target, (Relation) p); 972 if (!loop.isEmpty() && loop.get(0).equals(loop.get(loop.size() - 1))) { 973 warnOfCircularReferences(p, loop); 974 continue; 975 } 976 } else if (MemberTableModel.hasMembersReferringTo(target.getMembers(), Collections.singleton(p)) 977 && !confirmAddingPrimitive(p)) { 978 continue; 979 } 980 final Set<String> roles = findSuggestedRoles(presets, p); 981 target.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p)); 982 modified = true; 983 } 984 List<RelationMember> members = new ArrayList<>(target.getMembers()); 985 target.setMembers(null); // see #19885 986 return modified ? new ChangeMembersCommand(orig, members) : null; 987 } catch (AddAbortException ign) { 988 Logging.trace(ign); 989 return null; 990 } 991 } 992 993 protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) { 994 return presets.stream() 995 .map(preset -> preset.suggestRoleForOsmPrimitive(p)) 996 .filter(role -> !Utils.isEmpty(role)) 997 .collect(Collectors.toSet()); 998 } 999 1000 class MemberTableDblClickAdapter extends MouseAdapter { 1001 @Override 1002 public void mouseClicked(MouseEvent e) { 1003 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 1004 new EditAction(new RelationEditorActionAccess()).actionPerformed(null); 1005 } 1006 } 1007 } 1008 1009 private class RelationEditorActionAccess implements IRelationEditorActionAccess { 1010 1011 @Override 1012 public MemberTable getMemberTable() { 1013 return memberTable; 1014 } 1015 1016 @Override 1017 public MemberTableModel getMemberTableModel() { 1018 return memberTableModel; 1019 } 1020 1021 @Override 1022 public SelectionTable getSelectionTable() { 1023 return selectionTable; 1024 } 1025 1026 @Override 1027 public SelectionTableModel getSelectionTableModel() { 1028 return selectionTableModel; 1029 } 1030 1031 @Override 1032 public IRelationEditor getEditor() { 1033 return GenericRelationEditor.this; 1034 } 1035 1036 @Override 1037 public TagEditorModel getTagModel() { 1038 return tagEditorPanel.getModel(); 1039 } 1040 1041 @Override 1042 public AutoCompletingTextField getTextFieldRole() { 1043 return tfRole; 1044 } 1045 1046 } 1047 1048 @Override 1049 public void commandChanged(int queueSize, int redoSize) { 1050 Relation r = getRelation(); 1051 if (r != null && r.getDataSet() == null) { 1052 // see #19915 1053 setRelation(null); 1054 applyAction.updateEnabledState(); 1055 } 1056 } 1057}