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