// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.dialogs; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableColumn; import org.openstreetmap.josm.data.osm.DefaultNameFormatter; import org.openstreetmap.josm.data.osm.NameFormatter; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationToChildReference; import org.openstreetmap.josm.gui.MainApplication; import org.openstreetmap.josm.gui.PrimitiveRenderer; import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.gui.util.WindowGeometry; import org.openstreetmap.josm.gui.widgets.HtmlPanel; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.I18n; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.tools.Utils; /** * This dialog is used to get a user confirmation that a collection of primitives can be removed * from their parent relations. * @since 2308 */ public class DeleteFromRelationConfirmationDialog extends JDialog implements TableModelListener { /** the unique instance of this dialog */ private static DeleteFromRelationConfirmationDialog instance; /** * Replies the unique instance of this dialog * * @return The unique instance of this dialog */ public static synchronized DeleteFromRelationConfirmationDialog getInstance() { if (instance == null) { instance = new DeleteFromRelationConfirmationDialog(); } return instance; } /** the data model */ private RelationMemberTableModel model; /** The data model for deleting relations */ private RelationDeleteModel deletedRelationsModel; /** The table to hide/show if the relations to delete are not empty*/ private final HtmlPanel htmlPanel = new HtmlPanel(); private boolean canceled; private final JButton btnOK = new JButton(new OKAction()); protected JPanel buildRelationMemberTablePanel() { JTable table = new JTable(model, new RelationMemberTableColumnModel()); JPanel pnl = new JPanel(new GridBagLayout()); pnl.add(new JScrollPane(table), GBC.eol().fill()); JTable deletedRelationsTable = new JTable(this.deletedRelationsModel, new RelationDeleteTableColumnModel()); JScrollPane deletedRelationsModelTableScrollPane = new JScrollPane(deletedRelationsTable); this.deletedRelationsModel.addTableModelListener( e -> deletedRelationsModelTableScrollPane.setVisible(this.deletedRelationsModel.getRowCount() > 0)); // Default to not visible deletedRelationsModelTableScrollPane.setVisible(false); pnl.add(deletedRelationsModelTableScrollPane, GBC.eol().fill()); return pnl; } protected JPanel buildButtonPanel() { JPanel pnl = new JPanel(new FlowLayout()); pnl.add(btnOK); btnOK.setFocusable(true); pnl.add(new JButton(new CancelAction())); pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Action/Delete#DeleteFromRelations")))); return pnl; } protected final void build() { model = new RelationMemberTableModel(); model.addTableModelListener(this); this.deletedRelationsModel = new RelationDeleteModel(); this.deletedRelationsModel.addTableModelListener(this); getContentPane().setLayout(new BorderLayout()); getContentPane().add(htmlPanel, BorderLayout.NORTH); getContentPane().add(buildRelationMemberTablePanel(), BorderLayout.CENTER); getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH); HelpUtil.setHelpContext(this.getRootPane(), ht("/Action/Delete#DeleteFromRelations")); addWindowListener(new WindowEventHandler()); } protected void updateMessage() { int numObjectsToDelete = this.model.getNumObjectsToDelete() + this.deletedRelationsModel.getNumObjectsToDelete(); int numParentRelations = this.model.getNumParentRelations() + this.deletedRelationsModel.getNumParentRelations(); final String msg1 = trn( "Please confirm to remove {0} object.", "Please confirm to remove {0} objects.", numObjectsToDelete, numObjectsToDelete); final String msg2 = trn( "{0} relation is affected.", "{0} relations are affected.", numParentRelations, numParentRelations); @I18n.QuirkyPluralString final String msg = "" + msg1 + ' ' + msg2 + ""; htmlPanel.getEditorPane().setText(msg); invalidate(); } protected void updateTitle() { int numObjectsToDelete = this.model.getNumObjectsToDelete() + this.deletedRelationsModel.getNumObjectsToDelete(); if (numObjectsToDelete > 0) { setTitle(trn("Deleting {0} object", "Deleting {0} objects", numObjectsToDelete, numObjectsToDelete)); } else { setTitle(tr("Delete objects")); } } /** * Constructs a new {@code DeleteFromRelationConfirmationDialog}. */ public DeleteFromRelationConfirmationDialog() { super(GuiHelper.getFrameForComponent(MainApplication.getMainFrame()), "", ModalityType.DOCUMENT_MODAL); build(); } /** * Replies the data model used in this dialog * * @return the data model */ public RelationMemberTableModel getModel() { return model; } /** * Replies the data model used for relations that should probably be deleted. * @return the data model * @since 18395 */ public RelationDeleteModel getDeletedRelationsModel() { return this.deletedRelationsModel; } /** * Replies true if the dialog was canceled * * @return true if the dialog was canceled */ public boolean isCanceled() { return canceled; } protected void setCanceled(boolean canceled) { this.canceled = canceled; } @Override public void setVisible(boolean visible) { if (visible) { new WindowGeometry( getClass().getName() + ".geometry", WindowGeometry.centerInWindow( MainApplication.getMainFrame(), new Dimension(400, 200) ) ).applySafe(this); setCanceled(false); } else { if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); } model.data.clear(); this.deletedRelationsModel.data.clear(); } super.setVisible(visible); } @Override public void tableChanged(TableModelEvent e) { updateMessage(); updateTitle(); } /** * The table model which manages the list of relation-to-child references */ public static class RelationMemberTableModel extends DefaultTableModel { private static final class RelationToChildReferenceComparator implements Comparator, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(RelationToChildReference o1, RelationToChildReference o2) { NameFormatter nf = DefaultNameFormatter.getInstance(); int cmp = o1.getChild().getDisplayName(nf).compareTo(o2.getChild().getDisplayName(nf)); if (cmp != 0) return cmp; cmp = o1.getParent().getDisplayName(nf).compareTo(o2.getParent().getDisplayName(nf)); if (cmp != 0) return cmp; return Integer.compare(o1.getPosition(), o2.getPosition()); } } private final transient List data; /** * Constructs a new {@code RelationMemberTableModel}. */ public RelationMemberTableModel() { data = new ArrayList<>(); } @Override public int getRowCount() { if (data == null) return 0; return data.size(); } /** * Sets the data that should be displayed in the list. * @param references A list of references to display */ public void populate(Collection references) { data.clear(); if (references != null) { data.addAll(references); } data.sort(new RelationToChildReferenceComparator()); fireTableDataChanged(); } /** * Gets the list of children that are currently displayed. * @return The children. */ public Set getObjectsToDelete() { return data.stream().map(RelationToChildReference::getChild).collect(Collectors.toSet()); } /** * Gets the number of elements {@link #getObjectsToDelete()} would return. * @return That number. */ public int getNumObjectsToDelete() { return getObjectsToDelete().size(); } /** * Gets the set of parent relations * @return All parent relations of the references */ public Set getParentRelations() { return data.stream().map(RelationToChildReference::getParent).collect(Collectors.toSet()); } /** * Gets the number of elements {@link #getParentRelations()} would return. * @return That number. */ public int getNumParentRelations() { return getParentRelations().size(); } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (data == null) return null; RelationToChildReference ref = data.get(rowIndex); switch (columnIndex) { case 0: return ref.getChild(); case 1: return ref.getParent(); case 2: return ref.getPosition()+1; case 3: return ref.getRole(); default: assert false : "Illegal column index"; } return null; } @Override public boolean isCellEditable(int row, int column) { return false; } } private static class RelationMemberTableColumnModel extends DefaultTableColumnModel { protected final void createColumns() { // column 0 - To Delete TableColumn col = new TableColumn(0); col.setHeaderValue(tr("To delete")); col.setResizable(true); col.setWidth(100); col.setPreferredWidth(100); col.setCellRenderer(new PrimitiveRenderer()); addColumn(col); // column 0 - From Relation col = new TableColumn(1); col.setHeaderValue(tr("From Relation")); col.setResizable(true); col.setWidth(100); col.setPreferredWidth(100); col.setCellRenderer(new PrimitiveRenderer()); addColumn(col); // column 1 - Pos. col = new TableColumn(2); col.setHeaderValue(tr("Pos.")); col.setResizable(true); col.setWidth(30); col.setPreferredWidth(30); addColumn(col); // column 2 - Role col = new TableColumn(3); col.setHeaderValue(tr("Role")); col.setResizable(true); col.setWidth(50); col.setPreferredWidth(50); addColumn(col); } RelationMemberTableColumnModel() { createColumns(); } } /** * The table model which manages relations that will be deleted, if their children are deleted. * @since 18395 */ public static class RelationDeleteModel extends DefaultTableModel { private final transient List> data = new ArrayList<>(); @Override public int getRowCount() { // This is called in the super constructor. Before we have instantiated the list. Removing the null check // WILL LEAD TO A SILENT NPE! if (this.data == null) { return 0; } return this.data.size(); } /** * Sets the data that should be displayed in the list. * @param references A list of references to display */ public void populate(Collection> references) { this.data.clear(); if (references != null) { this.data.addAll(references); } this.data.sort(Comparator.comparing(pair -> pair.a)); fireTableDataChanged(); } /** * Gets the list of children that are currently displayed. * @return The children. */ public Set getObjectsToDelete() { return this.data.stream().filter(relation -> relation.b).map(relation -> relation.a).collect(Collectors.toSet()); } /** * Gets the number of elements {@link #getObjectsToDelete()} would return. * @return That number. */ public int getNumObjectsToDelete() { return getObjectsToDelete().size(); } /** * Gets the set of parent relations * @return All parent relations of the references */ public Set getParentRelations() { return this.data.stream() .flatMap(pair -> Utils.filteredCollection(pair.a.getReferrers(), Relation.class).stream()) .collect(Collectors.toSet()); } /** * Gets the number of elements {@link #getParentRelations()} would return. * @return That number. */ public int getNumParentRelations() { return getParentRelations().size(); } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (this.data.isEmpty()) { return null; } Pair ref = this.data.get(rowIndex); switch (columnIndex) { case 0: return ref.a; case 1: return ref.b; default: assert false : "Illegal column index"; } return null; } @Override public boolean isCellEditable(int row, int column) { return !this.data.isEmpty() && column == 1; } @Override public void setValueAt(Object aValue, int row, int column) { if (this.data.size() > row && column == 1 && aValue instanceof Boolean) { this.data.get(row).b = ((Boolean) aValue); } } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Relation.class; case 1: return Boolean.class; default: return super.getColumnClass(columnIndex); } } } private static class RelationDeleteTableColumnModel extends DefaultTableColumnModel { protected final void createColumns() { // column 0 - To Delete TableColumn col = new TableColumn(0); col.setHeaderValue(tr("Relation")); col.setResizable(true); col.setWidth(100); col.setPreferredWidth(100); col.setCellRenderer(new PrimitiveRenderer()); addColumn(col); // column 0 - From Relation col = new TableColumn(1); final String toDelete = tr("To delete"); col.setHeaderValue(toDelete); col.setResizable(true); col.setPreferredWidth(toDelete.length()); addColumn(col); } RelationDeleteTableColumnModel() { createColumns(); } } class OKAction extends AbstractAction { OKAction() { putValue(NAME, tr("OK")); new ImageProvider("ok").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and remove the object from the relations")); } @Override public void actionPerformed(ActionEvent e) { setCanceled(false); setVisible(false); } } class CancelAction extends AbstractAction { CancelAction() { putValue(NAME, tr("Cancel")); new ImageProvider("cancel").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort deleting the objects")); } @Override public void actionPerformed(ActionEvent e) { setCanceled(true); setVisible(false); } } class WindowEventHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { setCanceled(true); } @Override public void windowOpened(WindowEvent e) { btnOK.requestFocusInWindow(); } } }