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