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