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}