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