001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.BorderLayout;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.event.ActionEvent;
012import java.awt.event.WindowAdapter;
013import java.awt.event.WindowEvent;
014import java.util.ArrayList;
015import java.util.Collection;
016import java.util.Comparator;
017import java.util.HashSet;
018import java.util.List;
019import java.util.Set;
020
021import javax.swing.AbstractAction;
022import javax.swing.JButton;
023import javax.swing.JDialog;
024import javax.swing.JPanel;
025import javax.swing.JScrollPane;
026import javax.swing.JTable;
027import javax.swing.event.TableModelEvent;
028import javax.swing.event.TableModelListener;
029import javax.swing.table.DefaultTableColumnModel;
030import javax.swing.table.DefaultTableModel;
031import javax.swing.table.TableColumn;
032
033import org.openstreetmap.josm.Main;
034import org.openstreetmap.josm.data.osm.NameFormatter;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.data.osm.RelationToChildReference;
037import org.openstreetmap.josm.gui.DefaultNameFormatter;
038import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
039import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
040import org.openstreetmap.josm.gui.help.HelpUtil;
041import org.openstreetmap.josm.gui.util.GuiHelper;
042import org.openstreetmap.josm.gui.widgets.HtmlPanel;
043import org.openstreetmap.josm.tools.I18n;
044import org.openstreetmap.josm.tools.ImageProvider;
045import org.openstreetmap.josm.tools.WindowGeometry;
046
047/**
048 * This dialog is used to get a user confirmation that a collection of primitives can be removed
049 * from their parent relations.
050 * @since 2308
051 */
052public class DeleteFromRelationConfirmationDialog extends JDialog implements TableModelListener {
053    /** the unique instance of this dialog */
054    private static DeleteFromRelationConfirmationDialog instance;
055
056    /**
057     * Replies the unique instance of this dialog
058     *
059     * @return The unique instance of this dialog
060     */
061    public static synchronized DeleteFromRelationConfirmationDialog getInstance() {
062        if (instance == null) {
063            instance = new DeleteFromRelationConfirmationDialog();
064        }
065        return instance;
066    }
067
068    /** the data model */
069    private RelationMemberTableModel model;
070    private final HtmlPanel htmlPanel = new HtmlPanel();
071    private boolean canceled;
072    private final JButton btnOK = new JButton(new OKAction());
073
074    protected JPanel buildRelationMemberTablePanel() {
075        JTable table = new JTable(model, new RelationMemberTableColumnModel());
076        JPanel pnl = new JPanel(new BorderLayout());
077        pnl.add(new JScrollPane(table));
078        return pnl;
079    }
080
081    protected JPanel buildButtonPanel() {
082        JPanel pnl = new JPanel(new FlowLayout());
083        pnl.add(btnOK);
084        btnOK.setFocusable(true);
085        pnl.add(new JButton(new CancelAction()));
086        pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Action/Delete#DeleteFromRelations"))));
087        return pnl;
088    }
089
090    protected final void build() {
091        model = new RelationMemberTableModel();
092        model.addTableModelListener(this);
093        getContentPane().setLayout(new BorderLayout());
094        getContentPane().add(htmlPanel, BorderLayout.NORTH);
095        getContentPane().add(buildRelationMemberTablePanel(), BorderLayout.CENTER);
096        getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH);
097
098        HelpUtil.setHelpContext(this.getRootPane(), ht("/Action/Delete#DeleteFromRelations"));
099
100        addWindowListener(new WindowEventHandler());
101    }
102
103    protected void updateMessage() {
104        int numObjectsToDelete = model.getNumObjectsToDelete();
105        int numParentRelations = model.getNumParentRelations();
106        final String msg1 = trn(
107                "Please confirm to remove <strong>{0} object</strong>.",
108                "Please confirm to remove <strong>{0} objects</strong>.",
109                numObjectsToDelete, numObjectsToDelete);
110        final String msg2 = trn(
111                "{0} relation is affected.",
112                "{0} relations are affected.",
113                numParentRelations, numParentRelations);
114        @I18n.QuirkyPluralString
115        final String msg = "<html>" + msg1 + ' ' + msg2 + "</html>";
116        htmlPanel.getEditorPane().setText(msg);
117        invalidate();
118    }
119
120    protected void updateTitle() {
121        int numObjectsToDelete = model.getNumObjectsToDelete();
122        if (numObjectsToDelete > 0) {
123            setTitle(trn("Deleting {0} object", "Deleting {0} objects", numObjectsToDelete, numObjectsToDelete));
124        } else {
125            setTitle(tr("Delete objects"));
126        }
127    }
128
129    /**
130     * Constructs a new {@code DeleteFromRelationConfirmationDialog}.
131     */
132    public DeleteFromRelationConfirmationDialog() {
133        super(GuiHelper.getFrameForComponent(Main.parent), "", ModalityType.DOCUMENT_MODAL);
134        build();
135    }
136
137    /**
138     * Replies the data model used in this dialog
139     *
140     * @return the data model
141     */
142    public RelationMemberTableModel getModel() {
143        return model;
144    }
145
146    /**
147     * Replies true if the dialog was canceled
148     *
149     * @return true if the dialog was canceled
150     */
151    public boolean isCanceled() {
152        return canceled;
153    }
154
155    protected void setCanceled(boolean canceled) {
156        this.canceled = canceled;
157    }
158
159    @Override
160    public void setVisible(boolean visible) {
161        if (visible) {
162            new WindowGeometry(
163                    getClass().getName() + ".geometry",
164                    WindowGeometry.centerInWindow(
165                            Main.parent,
166                            new Dimension(400, 200)
167                    )
168            ).applySafe(this);
169            setCanceled(false);
170        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
171            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
172        }
173        super.setVisible(visible);
174    }
175
176    @Override
177    public void tableChanged(TableModelEvent e) {
178        updateMessage();
179        updateTitle();
180    }
181
182    /**
183     * The table model which manages the list of relation-to-child references
184     *
185     */
186    public static class RelationMemberTableModel extends DefaultTableModel {
187        private final transient List<RelationToChildReference> data;
188
189        /**
190         * Constructs a new {@code RelationMemberTableModel}.
191         */
192        public RelationMemberTableModel() {
193            data = new ArrayList<>();
194        }
195
196        @Override
197        public int getRowCount() {
198            if (data == null) return 0;
199            return data.size();
200        }
201
202        protected void sort() {
203            data.sort(new Comparator<RelationToChildReference>() {
204                    private NameFormatter nf = DefaultNameFormatter.getInstance();
205                    @Override
206                    public int compare(RelationToChildReference o1, RelationToChildReference o2) {
207                        int cmp = o1.getChild().getDisplayName(nf).compareTo(o2.getChild().getDisplayName(nf));
208                        if (cmp != 0) return cmp;
209                        cmp = o1.getParent().getDisplayName(nf).compareTo(o2.getParent().getDisplayName(nf));
210                        if (cmp != 0) return cmp;
211                        return Integer.compare(o1.getPosition(), o2.getPosition());
212                    }
213                }
214            );
215        }
216
217        public void populate(Collection<RelationToChildReference> references) {
218            data.clear();
219            if (references != null) {
220                data.addAll(references);
221            }
222            sort();
223            fireTableDataChanged();
224        }
225
226        public Set<OsmPrimitive> getObjectsToDelete() {
227            Set<OsmPrimitive> ret = new HashSet<>();
228            for (RelationToChildReference ref: data) {
229                ret.add(ref.getChild());
230            }
231            return ret;
232        }
233
234        public int getNumObjectsToDelete() {
235            return getObjectsToDelete().size();
236        }
237
238        public Set<OsmPrimitive> getParentRelations() {
239            Set<OsmPrimitive> ret = new HashSet<>();
240            for (RelationToChildReference ref: data) {
241                ret.add(ref.getParent());
242            }
243            return ret;
244        }
245
246        public int getNumParentRelations() {
247            return getParentRelations().size();
248        }
249
250        @Override
251        public Object getValueAt(int rowIndex, int columnIndex) {
252            if (data == null) return null;
253            RelationToChildReference ref = data.get(rowIndex);
254            switch(columnIndex) {
255            case 0: return ref.getChild();
256            case 1: return ref.getParent();
257            case 2: return ref.getPosition()+1;
258            case 3: return ref.getRole();
259            default:
260                assert false : "Illegal column index";
261            }
262            return null;
263        }
264
265        @Override
266        public boolean isCellEditable(int row, int column) {
267            return false;
268        }
269    }
270
271    private static class RelationMemberTableColumnModel extends DefaultTableColumnModel {
272
273        protected final void createColumns() {
274
275            // column 0 - To Delete
276            TableColumn col = new TableColumn(0);
277            col.setHeaderValue(tr("To delete"));
278            col.setResizable(true);
279            col.setWidth(100);
280            col.setPreferredWidth(100);
281            col.setCellRenderer(new OsmPrimitivRenderer());
282            addColumn(col);
283
284            // column 0 - From Relation
285            col = new TableColumn(1);
286            col.setHeaderValue(tr("From Relation"));
287            col.setResizable(true);
288            col.setWidth(100);
289            col.setPreferredWidth(100);
290            col.setCellRenderer(new OsmPrimitivRenderer());
291            addColumn(col);
292
293            // column 1 - Pos.
294            col = new TableColumn(2);
295            col.setHeaderValue(tr("Pos."));
296            col.setResizable(true);
297            col.setWidth(30);
298            col.setPreferredWidth(30);
299            addColumn(col);
300
301            // column 2 - Role
302            col = new TableColumn(3);
303            col.setHeaderValue(tr("Role"));
304            col.setResizable(true);
305            col.setWidth(50);
306            col.setPreferredWidth(50);
307            addColumn(col);
308        }
309
310        RelationMemberTableColumnModel() {
311            createColumns();
312        }
313    }
314
315    class OKAction extends AbstractAction {
316        OKAction() {
317            putValue(NAME, tr("OK"));
318            new ImageProvider("ok").getResource().attachImageIcon(this);
319            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and remove the object from the relations"));
320        }
321
322        @Override
323        public void actionPerformed(ActionEvent e) {
324            setCanceled(false);
325            setVisible(false);
326        }
327    }
328
329    class CancelAction extends AbstractAction {
330        CancelAction() {
331            putValue(NAME, tr("Cancel"));
332            new ImageProvider("cancel").getResource().attachImageIcon(this);
333            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort deleting the objects"));
334        }
335
336        @Override
337        public void actionPerformed(ActionEvent e) {
338            setCanceled(true);
339            setVisible(false);
340        }
341    }
342
343    class WindowEventHandler extends WindowAdapter {
344
345        @Override
346        public void windowClosing(WindowEvent e) {
347            setCanceled(true);
348        }
349
350        @Override
351        public void windowOpened(WindowEvent e) {
352            btnOK.requestFocusInWindow();
353        }
354    }
355}