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