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