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