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