package org.openstreetmap.josm.gui.dialogs.relation; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JToolBar; import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.ConflictAddCommand; import org.openstreetmap.josm.data.conflict.Conflict; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; import org.openstreetmap.josm.gui.DefaultNameFormatter; import org.openstreetmap.josm.gui.HelpAwareOptionPane; import org.openstreetmap.josm.gui.SideButton; import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.tagging.AutoCompletingTextField; import org.openstreetmap.josm.gui.tagging.TagEditorPanel; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionCache; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; import org.openstreetmap.josm.tools.WindowGeometry; /** * This dialog is for editing relations. * */ public class GenericRelationEditor extends RelationEditor { @SuppressWarnings("unused") static private final Logger logger = Logger.getLogger(GenericRelationEditor.class.getName()); /** the tag table and its model */ private TagEditorPanel tagEditorPanel; private ReferringRelationsBrowser referrerBrowser; private ReferringRelationsBrowserModel referrerModel; /** the member table */ private MemberTable memberTable; private MemberTableModel memberTableModel; /** the model for the selection table */ private SelectionTableModel selectionTableModel; private AutoCompletingTextField tfRole; /** * Creates a new relation editor for the given relation. The relation will be saved if the user * selects "ok" in the editor. * * If no relation is given, will create an editor for a new relation. * * @param layer the {@see OsmDataLayer} the new or edited relation belongs to * @param relation relation to edit, or null to create a new one. * @param selectedMembers a collection of members which shall be selected initially */ public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) { super(layer, relation, selectedMembers); setRememberWindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650))); // initialize the autocompletion infrastructure // AutoCompletionCache.getCacheForLayer(getLayer()).initFromDataSet(); // init the various models // memberTableModel = new MemberTableModel(getLayer()); memberTableModel.register(); selectionTableModel = new SelectionTableModel(getLayer()); selectionTableModel.register(); referrerModel = new ReferringRelationsBrowserModel(relation); tagEditorPanel = new TagEditorPanel(); // populate the models // if (relation != null) { tagEditorPanel.getModel().initFromPrimitive(relation); //this.tagEditorModel.initFromPrimitive(relation); this.memberTableModel.populate(relation); if (!getLayer().data.getRelations().contains(relation)) { // treat it as a new relation if it doesn't exist in the // data set yet. setRelation(null); } } else { tagEditorPanel.getModel().clear(); this.memberTableModel.populate(null); } tagEditorPanel.getModel().ensureOneTag(); JSplitPane pane = buildSplitPane(); pane.setPreferredSize(new Dimension(100, 100)); JPanel pnl = new JPanel(); pnl.setLayout(new BorderLayout()); pnl.add(pane, BorderLayout.CENTER); pnl.setBorder(BorderFactory.createRaisedBevelBorder()); getContentPane().setLayout(new BorderLayout()); JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.add(tr("Tags and Members"), pnl); referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel, this); tabbedPane.add(tr("Parent Relations"), referrerBrowser); tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation)); tabbedPane.addChangeListener( new ChangeListener() { public void stateChanged(ChangeEvent e) { JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource(); int index = sourceTabbedPane.getSelectedIndex(); String title = sourceTabbedPane.getTitleAt(index); if (title.equals(tr("Parent Relations"))) { referrerBrowser.init(); } } } ); getContentPane().add(buildToolBar(), BorderLayout.NORTH); getContentPane().add(tabbedPane, BorderLayout.CENTER); getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH); setSize(findMaxDialogSize()); addWindowListener( new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { cleanSelfReferences(); } } ); memberTableModel.setSelectedMembers(selectedMembers); HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/RelationEditor")); } /** * Creates the toolbar * * @return the toolbar */ protected JToolBar buildToolBar() { JToolBar tb = new JToolBar(); tb.setFloatable(false); tb.add(new ApplyAction()); tb.add(new DuplicateRelationAction()); DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction(); addPropertyChangeListener(deleteAction); tb.add(deleteAction); return tb; } /** * builds the panel with the OK and the Cancel button * * @return the panel with the OK and the Cancel button */ protected JPanel buildOkCancelButtonPanel() { JPanel pnl = new JPanel(); pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); pnl.add(new SideButton(new OKAction())); pnl.add(new SideButton(new CancelAction())); pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor")))); return pnl; } /** * builds the panel with the tag editor * * @return the panel with the tag editor */ protected JPanel buildTagEditorPanel() { JPanel pnl = new JPanel(); pnl.setLayout(new GridBagLayout()); GridBagConstraints gc = new GridBagConstraints(); gc.gridx = 0; gc.gridy = 0; gc.gridheight = 1; gc.gridwidth = 1; gc.fill = GridBagConstraints.HORIZONTAL; gc.anchor = GridBagConstraints.FIRST_LINE_START; gc.weightx = 1.0; gc.weighty = 0.0; pnl.add(new JLabel(tr("Tags")), gc); gc.gridx = 0; gc.gridy = 1; gc.fill = GridBagConstraints.BOTH; gc.anchor = GridBagConstraints.CENTER; gc.weightx = 1.0; gc.weighty = 1.0; pnl.add(tagEditorPanel, gc); return pnl; } /** * builds the panel for the relation member editor * * @return the panel for the relation member editor */ protected JPanel buildMemberEditorPanel() { final JPanel pnl = new JPanel(); pnl.setLayout(new GridBagLayout()); // setting up the member table memberTable = new MemberTable(getLayer(),memberTableModel); memberTable.addMouseListener(new MemberTableDblClickAdapter()); memberTableModel.addMemberModelListener(memberTable); final JScrollPane scrollPane = new JScrollPane(memberTable); GridBagConstraints gc = new GridBagConstraints(); gc.gridx = 0; gc.gridy = 0; gc.gridheight = 1; gc.gridwidth = 3; gc.fill = GridBagConstraints.HORIZONTAL; gc.anchor = GridBagConstraints.FIRST_LINE_START; gc.weightx = 1.0; gc.weighty = 0.0; pnl.add(new JLabel(tr("Members")), gc); gc.gridx = 0; gc.gridy = 1; gc.gridheight = 1; gc.gridwidth = 1; gc.fill = GridBagConstraints.VERTICAL; gc.anchor = GridBagConstraints.NORTHWEST; gc.weightx = 0.0; gc.weighty = 1.0; pnl.add(buildLeftButtonPanel(), gc); gc.gridx = 1; gc.gridy = 1; gc.fill = GridBagConstraints.BOTH; gc.anchor = GridBagConstraints.CENTER; gc.weightx = 0.6; gc.weighty = 1.0; pnl.add(scrollPane, gc); // --- role editing JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT)); p3.add(new JLabel(tr("Apply Role:"))); tfRole = new AutoCompletingTextField(10); tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members")); tfRole.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { tfRole.selectAll(); } }); tfRole.setAutoCompletionList(new AutoCompletionList()); tfRole.addFocusListener( new FocusAdapter() { @Override public void focusGained(FocusEvent e) { AutoCompletionList list = tfRole.getAutoCompletionList(); AutoCompletionCache.getCacheForLayer(Main.main.getEditLayer()).populateWithMemberRoles(list); } } ); p3.add(tfRole); SetRoleAction setRoleAction = new SetRoleAction(); memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction); tfRole.getDocument().addDocumentListener(setRoleAction); tfRole.addActionListener(setRoleAction); memberTableModel.getSelectionModel().addListSelectionListener( new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { tfRole.setEnabled(memberTable.getSelectedRowCount() > 0); } } ); tfRole.setEnabled(memberTable.getSelectedRowCount() > 0); SideButton btnApply = new SideButton(setRoleAction); btnApply.setPreferredSize(new Dimension(20,20)); btnApply.setText(""); p3.add(btnApply); gc.gridx = 1; gc.gridy = 2; gc.fill = GridBagConstraints.BOTH; gc.anchor = GridBagConstraints.CENTER; gc.weightx = 1.0; gc.weighty = 0.0; pnl.add(p3, gc); JPanel pnl2 = new JPanel(); pnl2.setLayout(new GridBagLayout()); gc.gridx = 0; gc.gridy = 0; gc.gridheight = 1; gc.gridwidth = 3; gc.fill = GridBagConstraints.HORIZONTAL; gc.anchor = GridBagConstraints.FIRST_LINE_START; gc.weightx = 1.0; gc.weighty = 0.0; pnl2.add(new JLabel(tr("Selection")), gc); gc.gridx = 0; gc.gridy = 1; gc.gridheight = 1; gc.gridwidth = 1; gc.fill = GridBagConstraints.VERTICAL; gc.anchor = GridBagConstraints.NORTHWEST; gc.weightx = 0.0; gc.weighty = 1.0; pnl2.add(buildSelectionControlButtonPanel(), gc); gc.gridx = 1; gc.gridy = 1; gc.weightx = 1.0; gc.weighty = 1.0; gc.fill = GridBagConstraints.BOTH; pnl2.add(buildSelectionTablePanel(), gc); final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitPane.setLeftComponent(pnl); splitPane.setRightComponent(pnl2); splitPane.setOneTouchExpandable(false); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { // has to be called when the window is visible, otherwise // no effect splitPane.setDividerLocation(0.6); } }); JPanel pnl3 = new JPanel(); pnl3.setLayout(new BorderLayout()); pnl3.add(splitPane, BorderLayout.CENTER); return pnl3; } /** * builds the panel with the table displaying the currently selected primitives * * @return */ protected JPanel buildSelectionTablePanel() { JPanel pnl = new JPanel(); pnl.setLayout(new BorderLayout()); SelectionTable tbl = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel)); tbl.setMemberTableModel(memberTableModel); JScrollPane pane = new JScrollPane(tbl); pnl.add(pane, BorderLayout.CENTER); return pnl; } /** * builds the {@see JSplitPane} which divides the editor in an upper and a lower half * * @return the split panel */ protected JSplitPane buildSplitPane() { final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); pane.setTopComponent(buildTagEditorPanel()); pane.setBottomComponent(buildMemberEditorPanel()); pane.setOneTouchExpandable(true); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { // has to be called when the window is visible, otherwise // no effect pane.setDividerLocation(0.3); } }); return pane; } /** * build the panel with the buttons on the left * * @return */ protected JToolBar buildLeftButtonPanel() { JToolBar tb = new JToolBar(); tb.setOrientation(JToolBar.VERTICAL); tb.setFloatable(false); // -- move up action MoveUpAction moveUpAction = new MoveUpAction(); memberTableModel.getSelectionModel().addListSelectionListener(moveUpAction); tb.add(moveUpAction); // -- move down action MoveDownAction moveDownAction = new MoveDownAction(); memberTableModel.getSelectionModel().addListSelectionListener(moveDownAction); tb.add(moveDownAction); tb.addSeparator(); // -- edit action EditAction editAction = new EditAction(); memberTableModel.getSelectionModel().addListSelectionListener(editAction); tb.add(editAction); // -- delete action RemoveAction removeSelectedAction = new RemoveAction(); memberTable.getSelectionModel().addListSelectionListener(removeSelectedAction); tb.add(removeSelectedAction); tb.addSeparator(); // -- sort action SortAction sortAction = new SortAction(); memberTableModel.addTableModelListener(sortAction); tb.add(sortAction); // -- reverse action ReverseAction reverseAction = new ReverseAction(); memberTableModel.addTableModelListener(reverseAction); tb.add(reverseAction); tb.addSeparator(); // -- download action DownloadIncompleteMembersAction downloadIncompleteMembersAction = new DownloadIncompleteMembersAction(); memberTable.getModel().addTableModelListener(downloadIncompleteMembersAction); tb.add(downloadIncompleteMembersAction); // -- download selected action DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction(); memberTable.getModel().addTableModelListener(downloadSelectedIncompleteMembersAction); memberTable.getSelectionModel().addListSelectionListener(downloadSelectedIncompleteMembersAction); tb.add(downloadSelectedIncompleteMembersAction); return tb; } /** * build the panel with the buttons for adding or removing the current selection * * @return */ protected JToolBar buildSelectionControlButtonPanel() { JToolBar tb = new JToolBar(JToolBar.VERTICAL); tb.setFloatable(false); // -- add at end action AddSelectedAtEndAction addSelectedAtEndAction = new AddSelectedAtEndAction(); selectionTableModel.addTableModelListener(addSelectedAtEndAction); tb.add(addSelectedAtEndAction); // -- select members action SelectedMembersForSelectionAction selectMembersForSelectionAction = new SelectedMembersForSelectionAction(); selectionTableModel.addTableModelListener(selectMembersForSelectionAction); memberTableModel.addTableModelListener(selectMembersForSelectionAction); tb.add(selectMembersForSelectionAction); tb.addSeparator(); // -- remove selected action RemoveSelectedAction removeSelectedAction = new RemoveSelectedAction(); selectionTableModel.addTableModelListener(removeSelectedAction); tb.add(removeSelectedAction); // -- select action SelectPrimitivesForSelectedMembersAction selectAction = new SelectPrimitivesForSelectedMembersAction(); memberTable.getSelectionModel().addListSelectionListener(selectAction); tb.add(selectAction); tb.addSeparator(); // -- add at start action AddSelectedAtStartAction addSelectionAction = new AddSelectedAtStartAction(); selectionTableModel.addTableModelListener(addSelectionAction); tb.add(addSelectionAction); // -- add before selected action AddSelectedBeforeSelection addSelectedBeforeSelectionAction = new AddSelectedBeforeSelection(); selectionTableModel.addTableModelListener(addSelectedBeforeSelectionAction); memberTableModel.getSelectionModel().addListSelectionListener(addSelectedBeforeSelectionAction); tb.add(addSelectedBeforeSelectionAction); // -- add after selected action AddSelectedAfterSelection addSelectedAfterSelectionAction = new AddSelectedAfterSelection(); selectionTableModel.addTableModelListener(addSelectedAfterSelectionAction); memberTableModel.getSelectionModel().addListSelectionListener(addSelectedAfterSelectionAction); tb.add(addSelectedAfterSelectionAction); return tb; } @Override protected Dimension findMaxDialogSize() { return new Dimension(700, 650); } @Override public void setVisible(boolean visible) { if (visible) { tagEditorPanel.initAutoCompletion(getLayer()); } super.setVisible(visible); if (visible) { RelationDialogManager.getRelationDialogManager().positionOnScreen(this); } else { // make sure all registered listeners are unregistered // selectionTableModel.unregister(); memberTableModel.unregister(); memberTable.unlinkAsListener(); dispose(); } } /** * checks whether the current relation has members referring to itself. If so, * warns the users and provides an option for removing these members. * */ protected void cleanSelfReferences() { ArrayList<OsmPrimitive> toCheck = new ArrayList<OsmPrimitive>(); toCheck.add(getRelation()); if (memberTableModel.hasMembersReferringTo(toCheck)) { int ret = ConditionalOptionPaneUtil.showOptionDialog( "clean_relation_self_references", Main.parent, tr("<html>There is at least one member in this relation referring<br>" + "to the relation itself.<br>" + "This creates circular dependencies and is discouraged.<br>" + "How do you want to proceed with circular dependencies?</html>"), tr("Warning"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")}, tr("Remove them, clean up relation") ); switch(ret) { case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return; case JOptionPane.CLOSED_OPTION: return; case JOptionPane.NO_OPTION: return; case JOptionPane.YES_OPTION: memberTableModel.removeMembersReferringTo(toCheck); break; } } } static class AddAbortException extends Exception { } abstract class AddFromSelectionAction extends AbstractAction { protected boolean isPotentialDuplicate(OsmPrimitive primitive) { return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive)); } protected boolean confirmAddingPrimtive(OsmPrimitive primitive) throws AddAbortException { String msg = tr("<html>This relation already has one or more members referring to<br>" + "the primitive ''{0}''<br>" + "<br>" + "Do you really want to add another relation member?</html>", primitive.getDisplayName(DefaultNameFormatter.getInstance()) ); int ret = ConditionalOptionPaneUtil.showOptionDialog( "add_primitive_to_relation", Main.parent, msg, tr("Multiple members referring to same primitive"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, null ); switch(ret) { case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION : return true; case JOptionPane.YES_OPTION: return true; case JOptionPane.NO_OPTION: return false; case JOptionPane.CLOSED_OPTION: return false; case JOptionPane.CANCEL_OPTION: throw new AddAbortException(); } // should not happen return false; } protected void warnOfCircularReferences(OsmPrimitive primitive) { String msg = tr("<html>You are trying to add a relation to itself.<br>" + "<br>" + "This creates circular references and is therefore discouraged.<br>" + "Skipping relation ''{0}''.</html>", primitive.getDisplayName(DefaultNameFormatter.getInstance()) ); JOptionPane.showMessageDialog( Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE ); } protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException { if (primitives == null || primitives.isEmpty()) return primitives; ArrayList<OsmPrimitive> ret = new ArrayList<OsmPrimitive>(); Iterator<OsmPrimitive> it = primitives.iterator(); while(it.hasNext()) { OsmPrimitive primitive = it.next(); if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) { warnOfCircularReferences(primitive); continue; } if (isPotentialDuplicate(primitive)) { if (confirmAddingPrimtive(primitive)) { ret.add(primitive); } continue; } else { ret.add(primitive); } } return ret; } } class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener { public AddSelectedAtStartAction() { putValue(SHORT_DESCRIPTION, tr("Add all primitives selected in the current dataset before the first member")); putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright")); // putValue(NAME, tr("Add Selected")); refreshEnabled(); } protected void refreshEnabled() { setEnabled(selectionTableModel.getRowCount() > 0 && memberTableModel.getRowCount() > 0); } public void actionPerformed(ActionEvent e) { try { List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); memberTableModel.addMembersAtBeginning(toAdd); } catch(AddAbortException ex) { // do nothing } } public void tableChanged(TableModelEvent e) { refreshEnabled(); } } class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener { public AddSelectedAtEndAction() { putValue(SHORT_DESCRIPTION, tr("Add all primitives selected in the current dataset after the last member")); putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright")); // putValue(NAME, tr("Add Selected")); refreshEnabled(); } protected void refreshEnabled() { setEnabled(selectionTableModel.getRowCount() > 0); } public void actionPerformed(ActionEvent e) { try { List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); memberTableModel.addMembersAtEnd(toAdd); } catch(AddAbortException ex) { // do nothing } } public void tableChanged(TableModelEvent e) { refreshEnabled(); } } class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { public AddSelectedBeforeSelection() { putValue(SHORT_DESCRIPTION, tr("Add all primitives selected in the current dataset before the first selected member")); putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright")); // putValue(NAME, tr("Add Selected")); refreshEnabled(); } protected void refreshEnabled() { setEnabled(selectionTableModel.getRowCount() > 0 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); } public void actionPerformed(ActionEvent e) { try { List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel .getSelectionModel().getMinSelectionIndex()); } catch(AddAbortException ex) { // do nothing } } public void tableChanged(TableModelEvent e) { refreshEnabled(); } public void valueChanged(ListSelectionEvent e) { refreshEnabled(); } } class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener { public AddSelectedAfterSelection() { putValue(SHORT_DESCRIPTION, tr("Add all primitives selected in the current dataset after the last selected member")); putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright")); // putValue(NAME, tr("Add Selected")); refreshEnabled(); } protected void refreshEnabled() { setEnabled(selectionTableModel.getRowCount() > 0 && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0); } public void actionPerformed(ActionEvent e) { try { List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection()); memberTableModel.addMembersAfterIdx(toAdd, memberTableModel .getSelectionModel().getMaxSelectionIndex()); } catch(AddAbortException ex) { // do nothing } } public void tableChanged(TableModelEvent e) { refreshEnabled(); } public void valueChanged(ListSelectionEvent e) { refreshEnabled(); } } class RemoveSelectedAction extends AbstractAction implements TableModelListener { public RemoveSelectedAction() { putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected primitives")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers")); // putValue(NAME, tr("Remove Selected")); Shortcut.registerShortcut("relationeditor:removeselected", tr("Relation Editor: Remove Selected"), KeyEvent.VK_S, Shortcut.GROUP_MNEMONIC); updateEnabledState(); } protected void updateEnabledState() { DataSet ds = getLayer().data; if (ds == null || ds.getSelected().isEmpty()) { setEnabled(false); return; } // only enable the action if we have members referring to the // selected primitives // setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected())); } public void actionPerformed(ActionEvent e) { memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection()); } public void tableChanged(TableModelEvent e) { updateEnabledState(); } } /** * Selects members in the relation editor which refer to primitives in the current * selection of the context layer. * */ class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener { public SelectedMembersForSelectionAction() { putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to primitives in the current selection")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers")); updateEnabledState(); } protected void updateEnabledState() { boolean enabled = selectionTableModel.getRowCount() > 0 && !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty(); if (enabled) { putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} primitives in the current selection",memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size())); } else { putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to primitives in the current selection")); } setEnabled(enabled); } public void actionPerformed(ActionEvent e) { memberTableModel.selectMembersReferringTo(getLayer().data.getSelected()); } public void tableChanged(TableModelEvent e) { updateEnabledState(); } } /** * Selects primitives in the layer this editor belongs to. The selected primitives are * equal to the set of primitives the currently selected relation members refer to. * */ class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener { public SelectPrimitivesForSelectedMembersAction() { putValue(SHORT_DESCRIPTION, tr("Select primitives for selected relation members")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives")); updateEnabledState(); } protected void updateEnabledState() { setEnabled(memberTable.getSelectedRowCount() > 0); } public void actionPerformed(ActionEvent e) { getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives()); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } class SortAction extends AbstractAction implements TableModelListener { public SortAction() { putValue(SHORT_DESCRIPTION, tr("Sort the relation members")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort")); putValue(NAME, tr("Sort")); Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"), KeyEvent.VK_T, Shortcut.GROUP_MNEMONIC); updateEnabledState(); } public void actionPerformed(ActionEvent e) { memberTableModel.sort(); } protected void updateEnabledState() { setEnabled(memberTableModel.getRowCount() > 0); } public void tableChanged(TableModelEvent e) { updateEnabledState(); } } class ReverseAction extends AbstractAction implements TableModelListener { public ReverseAction() { putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse")); putValue(NAME, tr("Reverse")); Shortcut.registerShortcut("relationeditor:reverse", tr("Relation Editor: Reverse"), KeyEvent.VK_R, Shortcut.GROUP_MNEMONIC); updateEnabledState(); } public void actionPerformed(ActionEvent e) { memberTableModel.reverse(); } protected void updateEnabledState() { setEnabled(memberTableModel.getRowCount() > 0); } public void tableChanged(TableModelEvent e) { updateEnabledState(); } } class MoveUpAction extends AbstractAction implements ListSelectionListener { public MoveUpAction() { putValue(SHORT_DESCRIPTION, tr("Move the currently selected members up")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "moveup")); // putValue(NAME, tr("Move Up")); Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Up"), KeyEvent.VK_N, Shortcut.GROUP_MNEMONIC); setEnabled(false); } public void actionPerformed(ActionEvent e) { memberTableModel.moveUp(memberTable.getSelectedRows()); } public void valueChanged(ListSelectionEvent e) { setEnabled(memberTableModel.canMoveUp(memberTable.getSelectedRows())); } } class MoveDownAction extends AbstractAction implements ListSelectionListener { public MoveDownAction() { putValue(SHORT_DESCRIPTION, tr("Move the currently selected members down")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "movedown")); // putValue(NAME, tr("Move Down")); Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Down"), KeyEvent.VK_J, Shortcut.GROUP_MNEMONIC); setEnabled(false); } public void actionPerformed(ActionEvent e) { memberTableModel.moveDown(memberTable.getSelectedRows()); } public void valueChanged(ListSelectionEvent e) { setEnabled(memberTableModel.canMoveDown(memberTable.getSelectedRows())); } } class RemoveAction extends AbstractAction implements ListSelectionListener { public RemoveAction() { putValue(SHORT_DESCRIPTION, tr("Remove the currently selected members from this relation")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "remove")); // putValue(NAME, tr("Remove")); Shortcut.registerShortcut("relationeditor:remove", tr("Relation Editor: Remove"), KeyEvent.VK_J, Shortcut.GROUP_MNEMONIC); setEnabled(false); } public void actionPerformed(ActionEvent e) { memberTableModel.remove(memberTable.getSelectedRows()); } public void valueChanged(ListSelectionEvent e) { setEnabled(memberTableModel.canRemove(memberTable.getSelectedRows())); } } class DeleteCurrentRelationAction extends AbstractAction implements PropertyChangeListener{ public DeleteCurrentRelationAction() { putValue(SHORT_DESCRIPTION, tr("Delete the currently edited relation")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); putValue(NAME, tr("Delete")); updateEnabledState(); } public void run() { Relation toDelete = getRelation(); if (toDelete == null) return; org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation( getLayer(), toDelete ); } public void actionPerformed(ActionEvent e) { run(); } protected void updateEnabledState() { setEnabled(getRelationSnapshot() != null); } public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(RELATION_SNAPSHOT_PROP)) { updateEnabledState(); } } } abstract class SavingAction extends AbstractAction { /** * apply updates to a new relation */ protected void applyNewRelation() { Relation newRelation = new Relation(); tagEditorPanel.getModel().applyToPrimitive(newRelation); memberTableModel.applyToRelation(newRelation); List<RelationMember> newMembers = new ArrayList<RelationMember>(); for (RelationMember rm: newRelation.getMembers()) { if (!rm.getMember().isDeleted()) { newMembers.add(rm); } } if (newRelation.getMembersCount() != newMembers.size()) { newRelation.setMembers(newMembers); String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" + "was open. They have been removed from the relation members list."); JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE); } // If the user wanted to create a new relation, but hasn't added any members or // tags, don't add an empty relation if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys()) return; Main.main.undoRedo.add(new AddCommand(getLayer(),newRelation)); // make sure everybody is notified about the changes // getLayer().data.fireSelectionChanged(); GenericRelationEditor.this.setRelation(newRelation); RelationDialogManager.getRelationDialogManager().updateContext( getLayer(), getRelation(), GenericRelationEditor.this ); } /** * Apply the updates for an existing relation which has been changed * outside of the relation editor. * */ protected void applyExistingConflictingRelation() { Relation editedRelation = new Relation(getRelation()); tagEditorPanel.getModel().applyToPrimitive(editedRelation); memberTableModel.applyToRelation(editedRelation); Conflict<Relation> conflict = new Conflict<Relation>(getRelation(), editedRelation); Main.main.undoRedo.add(new ConflictAddCommand(getLayer(),conflict)); } /** * Apply the updates for an existing relation which has not been changed * outside of the relation editor. * */ protected void applyExistingNonConflictingRelation() { Relation editedRelation = new Relation(getRelation()); tagEditorPanel.getModel().applyToPrimitive(editedRelation); memberTableModel.applyToRelation(editedRelation); Main.main.undoRedo.add(new ChangeCommand(getRelation(), editedRelation)); getLayer().data.fireSelectionChanged(); getLayer().fireDataChange(); // this will refresh the snapshot and update the dialog title // setRelation(getRelation()); } protected boolean confirmClosingBecauseOfDirtyState() { ButtonSpec [] options = new ButtonSpec[] { new ButtonSpec( tr("Yes, create a conflict and close"), ImageProvider.get("ok"), tr("Click to create a conflict and close this relation editor") , null /* no specific help topic */ ), new ButtonSpec( tr("No, continue editing"), ImageProvider.get("cancel"), tr("Click to return to the relation editor and to resume relation editing") , null /* no specific help topic */ ) }; int ret = HelpAwareOptionPane.showOptionDialog( Main.parent, tr("<html>This relation has been changed outside of the editor.<br>" + "You cannot apply your changes and continue editing.<br>" + "<br>" + "Do you want to create a conflict and close the editor?</html>"), tr("Conflict in data"), JOptionPane.WARNING_MESSAGE, null, options, options[0], // OK is default "/Dialog/RelationEditor#RelationChangedOutsideOfEditor" ); return ret == 0; } protected void warnDoubleConflict() { JOptionPane.showMessageDialog( Main.parent, tr("<html>Layer ''{0}'' already has a conflict for primitive<br>" + "''{1}''.<br>" + "Please resolve this conflict first, then try again.</html>", getLayer().getName(), getRelation().getDisplayName(DefaultNameFormatter.getInstance()) ), tr("Double conflict"), JOptionPane.WARNING_MESSAGE ); } } class ApplyAction extends SavingAction { public ApplyAction() { putValue(SHORT_DESCRIPTION, tr("Apply the current updates")); putValue(SMALL_ICON, ImageProvider.get("save")); putValue(NAME, tr("Apply")); setEnabled(true); } public void run() { if (getRelation() == null) { applyNewRelation(); } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) || tagEditorPanel.getModel().isDirty()) { if (isDirtyRelation()) { if (confirmClosingBecauseOfDirtyState()) { if (getLayer().getConflicts().hasConflictForMy(getRelation())) { warnDoubleConflict(); return; } applyExistingConflictingRelation(); setVisible(false); } } else { applyExistingNonConflictingRelation(); } } } public void actionPerformed(ActionEvent e) { run(); } } class OKAction extends SavingAction { public OKAction() { putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog")); putValue(SMALL_ICON, ImageProvider.get("ok")); putValue(NAME, tr("OK")); setEnabled(true); } public void run() { if (getRelation() == null) { applyNewRelation(); } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) || tagEditorPanel.getModel().isDirty()) { if (isDirtyRelation()) { if (confirmClosingBecauseOfDirtyState()) { if (getLayer().getConflicts().hasConflictForMy(getRelation())) { warnDoubleConflict(); return; } applyExistingConflictingRelation(); } else return; } else { applyExistingNonConflictingRelation(); } } setVisible(false); } public void actionPerformed(ActionEvent e) { run(); } } class CancelAction extends AbstractAction { public CancelAction() { putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog")); putValue(SMALL_ICON, ImageProvider.get("cancel")); putValue(NAME, tr("Cancel")); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE"); getRootPane().getActionMap().put("ESCAPE", this); setEnabled(true); } public void actionPerformed(ActionEvent e) { setVisible(false); } } class AddTagAction extends AbstractAction { public AddTagAction() { putValue(SHORT_DESCRIPTION, tr("Add an empty tag")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); // putValue(NAME, tr("Cancel")); setEnabled(true); } public void actionPerformed(ActionEvent e) { tagEditorPanel.getModel().appendNewTag(); } } class DownloadIncompleteMembersAction extends AbstractAction implements TableModelListener { public DownloadIncompleteMembersAction() { putValue(SHORT_DESCRIPTION, tr("Download all incomplete members")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincomplete")); putValue(NAME, tr("Download Members")); Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"), KeyEvent.VK_K, Shortcut.GROUP_MNEMONIC); updateEnabledState(); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; Main.worker.submit(new DownloadRelationMemberTask( getRelation(), memberTableModel.getIncompleteMemberPrimitives(), getLayer(), GenericRelationEditor.this) ); } protected void updateEnabledState() { setEnabled( getRelation() != null && memberTableModel.hasIncompleteMembers() ); } public void tableChanged(TableModelEvent e) { updateEnabledState(); } } class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener, TableModelListener{ public DownloadSelectedIncompleteMembersAction() { putValue(SHORT_DESCRIPTION, tr("Download selected incomplete members")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected")); putValue(NAME, tr("Download Members")); Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"), KeyEvent.VK_K, Shortcut.GROUP_MNEMONIC); updateEnabledState(); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; Main.worker.submit(new DownloadRelationMemberTask( getRelation(), memberTableModel.getSelectedIncompleteMemberPrimitives(), getLayer(), GenericRelationEditor.this) ); } protected void updateEnabledState() { setEnabled( getRelation() != null && memberTableModel.hasIncompleteSelectedMembers() ); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } public void tableChanged(TableModelEvent e) { updateEnabledState(); } } class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener { public SetRoleAction() { putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members")); putValue(SMALL_ICON, ImageProvider.get("apply")); putValue(NAME, tr("Apply Role")); refreshEnabled(); } protected void refreshEnabled() { setEnabled(memberTable.getSelectedRowCount() > 0); } protected boolean isEmptyRole() { return tfRole.getText() == null || tfRole.getText().trim().equals(""); } protected boolean confirmSettingEmptyRole(int onNumMembers) { String message = "<html>" + trn("You are setting an empty role on {0} primitive.", "You are setting an empty role on {0} primitives.", onNumMembers, onNumMembers) + "<br>" + tr("This is equal to deleting the roles of these primitives.") + "<br>" + tr("Do you really want to apply the new role?") + "</html>"; String [] options = new String[] { tr("Yes, apply it"), tr("No, do not apply") }; int ret = ConditionalOptionPaneUtil.showOptionDialog( "relation_editor.confirm_applying_empty_role", Main.parent, message, tr("Confirm empty role"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, options, options[0] ); switch(ret) { case JOptionPane.YES_OPTION: return true; case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return true; default: return false; } } public void actionPerformed(ActionEvent e) { if (isEmptyRole()) { if (! confirmSettingEmptyRole(memberTable.getSelectedRowCount())) return; } memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText()); } public void valueChanged(ListSelectionEvent e) { refreshEnabled(); } public void changedUpdate(DocumentEvent e) { refreshEnabled(); } public void insertUpdate(DocumentEvent e) { refreshEnabled(); } public void removeUpdate(DocumentEvent e) { refreshEnabled(); } } /** * Creates a new relation with a copy of the current editor state * */ class DuplicateRelationAction extends AbstractAction { public DuplicateRelationAction() { putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window")); // FIXME provide an icon putValue(SMALL_ICON, ImageProvider.get("duplicate")); putValue(NAME, tr("Duplicate")); setEnabled(true); } public void actionPerformed(ActionEvent e) { Relation copy = new Relation(); tagEditorPanel.getModel().applyToPrimitive(copy); memberTableModel.applyToRelation(copy); RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers()); editor.setVisible(true); } } /** * Action for editing the currently selected relation * * */ class EditAction extends AbstractAction implements ListSelectionListener { public EditAction() { putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); //putValue(NAME, tr("Edit")); refreshEnabled(); } protected void refreshEnabled() { setEnabled(memberTable.getSelectedRowCount() == 1 && memberTableModel.isEditableRelation(memberTable.getSelectedRow())); } protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) { Collection<RelationMember> members = new HashSet<RelationMember>(); Collection<OsmPrimitive> selection = getLayer().data.getSelected(); for (RelationMember member: r.getMembers()) { if (selection.contains(member.getMember())) { members.add(member); } } return members; } public void run() { int idx = memberTable.getSelectedRow(); if (idx < 0) return; OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx); if (!(primitive instanceof Relation)) return; Relation r = (Relation) primitive; if (r.isIncomplete()) return; RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r)); editor.setVisible(true); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; run(); } public void valueChanged(ListSelectionEvent e) { refreshEnabled(); } } class MemberTableDblClickAdapter extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { new EditAction().run(); } } } }