001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs.relation.actions; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.util.ArrayList; 008import java.util.List; 009 010import javax.swing.JOptionPane; 011import javax.swing.SwingUtilities; 012 013import org.openstreetmap.josm.Main; 014import org.openstreetmap.josm.command.AddCommand; 015import org.openstreetmap.josm.command.ChangeCommand; 016import org.openstreetmap.josm.command.conflict.ConflictAddCommand; 017import org.openstreetmap.josm.data.conflict.Conflict; 018import org.openstreetmap.josm.data.osm.Relation; 019import org.openstreetmap.josm.data.osm.RelationMember; 020import org.openstreetmap.josm.gui.DefaultNameFormatter; 021import org.openstreetmap.josm.gui.HelpAwareOptionPane; 022import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 023import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor; 024import org.openstreetmap.josm.gui.dialogs.relation.MemberTable; 025import org.openstreetmap.josm.gui.dialogs.relation.MemberTableModel; 026import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager; 027import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; 028import org.openstreetmap.josm.gui.layer.OsmDataLayer; 029import org.openstreetmap.josm.gui.tagging.TagEditorModel; 030import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField; 031import org.openstreetmap.josm.tools.ImageProvider; 032 033/** 034 * Abstract superclass of relation saving actions (OK, Apply, Cancel). 035 * @since 9496 036 */ 037abstract class SavingAction extends AbstractRelationEditorAction { 038 039 protected final TagEditorModel tagModel; 040 protected final AutoCompletingTextField tfRole; 041 042 protected SavingAction(MemberTable memberTable, MemberTableModel memberTableModel, TagEditorModel tagModel, OsmDataLayer layer, 043 IRelationEditor editor, AutoCompletingTextField tfRole) { 044 super(memberTable, memberTableModel, null, layer, editor); 045 this.tagModel = tagModel; 046 this.tfRole = tfRole; 047 } 048 049 /** 050 * apply updates to a new relation 051 * @param tagEditorModel tag editor model 052 */ 053 protected void applyNewRelation(TagEditorModel tagEditorModel) { 054 final Relation newRelation = new Relation(); 055 tagEditorModel.applyToPrimitive(newRelation); 056 memberTableModel.applyToRelation(newRelation); 057 List<RelationMember> newMembers = new ArrayList<>(); 058 for (RelationMember rm: newRelation.getMembers()) { 059 if (!rm.getMember().isDeleted()) { 060 newMembers.add(rm); 061 } 062 } 063 if (newRelation.getMembersCount() != newMembers.size()) { 064 newRelation.setMembers(newMembers); 065 String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" + 066 "was open. They have been removed from the relation members list."); 067 JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE); 068 } 069 // If the user wanted to create a new relation, but hasn't added any members or 070 // tags, don't add an empty relation 071 if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys()) 072 return; 073 Main.main.undoRedo.add(new AddCommand(layer, newRelation)); 074 075 // make sure everybody is notified about the changes 076 // 077 layer.data.fireSelectionChanged(); 078 editor.setRelation(newRelation); 079 if (editor instanceof RelationEditor) { 080 RelationDialogManager.getRelationDialogManager().updateContext( 081 layer, editor.getRelation(), (RelationEditor) editor); 082 } 083 // Relation list gets update in EDT so selecting my be postponed to following EDT run 084 SwingUtilities.invokeLater(() -> Main.map.relationListDialog.selectRelation(newRelation)); 085 } 086 087 /** 088 * Apply the updates for an existing relation which has been changed outside of the relation editor. 089 * @param tagEditorModel tag editor model 090 */ 091 protected void applyExistingConflictingRelation(TagEditorModel tagEditorModel) { 092 Relation editedRelation = new Relation(editor.getRelation()); 093 tagEditorModel.applyToPrimitive(editedRelation); 094 memberTableModel.applyToRelation(editedRelation); 095 Conflict<Relation> conflict = new Conflict<>(editor.getRelation(), editedRelation); 096 Main.main.undoRedo.add(new ConflictAddCommand(layer, conflict)); 097 } 098 099 /** 100 * Apply the updates for an existing relation which has not been changed outside of the relation editor. 101 * @param tagEditorModel tag editor model 102 */ 103 protected void applyExistingNonConflictingRelation(TagEditorModel tagEditorModel) { 104 Relation editedRelation = new Relation(editor.getRelation()); 105 tagEditorModel.applyToPrimitive(editedRelation); 106 memberTableModel.applyToRelation(editedRelation); 107 Main.main.undoRedo.add(new ChangeCommand(editor.getRelation(), editedRelation)); 108 layer.data.fireSelectionChanged(); 109 } 110 111 protected boolean confirmClosingBecauseOfDirtyState() { 112 ButtonSpec[] options = new ButtonSpec[] { 113 new ButtonSpec( 114 tr("Yes, create a conflict and close"), 115 ImageProvider.get("ok"), 116 tr("Click to create a conflict and close this relation editor"), 117 null /* no specific help topic */ 118 ), 119 new ButtonSpec( 120 tr("No, continue editing"), 121 ImageProvider.get("cancel"), 122 tr("Click to return to the relation editor and to resume relation editing"), 123 null /* no specific help topic */ 124 ) 125 }; 126 127 int ret = HelpAwareOptionPane.showOptionDialog( 128 Main.parent, 129 tr("<html>This relation has been changed outside of the editor.<br>" 130 + "You cannot apply your changes and continue editing.<br>" 131 + "<br>" 132 + "Do you want to create a conflict and close the editor?</html>"), 133 tr("Conflict in data"), 134 JOptionPane.WARNING_MESSAGE, 135 null, 136 options, 137 options[0], // OK is default 138 "/Dialog/RelationEditor#RelationChangedOutsideOfEditor" 139 ); 140 if (ret == 0) { 141 Main.map.conflictDialog.unfurlDialog(); 142 } 143 return ret == 0; 144 } 145 146 protected void warnDoubleConflict() { 147 JOptionPane.showMessageDialog( 148 Main.parent, 149 tr("<html>Layer ''{0}'' already has a conflict for object<br>" 150 + "''{1}''.<br>" 151 + "Please resolve this conflict first, then try again.</html>", 152 layer.getName(), 153 editor.getRelation().getDisplayName(DefaultNameFormatter.getInstance()) 154 ), 155 tr("Double conflict"), 156 JOptionPane.WARNING_MESSAGE 157 ); 158 } 159 160 @Override 161 protected void updateEnabledState() { 162 // Do nothing 163 } 164 165 protected boolean applyChanges() { 166 if (editor.getRelation() == null) { 167 applyNewRelation(tagModel); 168 } else if (isEditorDirty()) { 169 if (editor.isDirtyRelation()) { 170 if (confirmClosingBecauseOfDirtyState()) { 171 if (layer.getConflicts().hasConflictForMy(editor.getRelation())) { 172 warnDoubleConflict(); 173 return false; 174 } 175 applyExistingConflictingRelation(tagModel); 176 hideEditor(); 177 } else 178 return false; 179 } else { 180 applyExistingNonConflictingRelation(tagModel); 181 } 182 } 183 editor.setRelation(editor.getRelation()); 184 return true; 185 } 186 187 protected void hideEditor() { 188 if (editor instanceof Component) { 189 ((Component) editor).setVisible(false); 190 } 191 } 192 193 protected boolean isEditorDirty() { 194 Relation snapshot = editor.getRelationSnapshot(); 195 return (snapshot != null && !memberTableModel.hasSameMembersAs(snapshot)) || tagModel.isDirty(); 196 } 197}