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