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