001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.Font;
010import java.awt.GridBagLayout;
011import java.awt.event.ActionEvent;
012import java.awt.event.FocusAdapter;
013import java.awt.event.FocusEvent;
014import java.io.File;
015import java.util.EventObject;
016
017import javax.swing.AbstractAction;
018import javax.swing.BorderFactory;
019import javax.swing.JButton;
020import javax.swing.JLabel;
021import javax.swing.JPanel;
022import javax.swing.JTable;
023import javax.swing.event.CellEditorListener;
024import javax.swing.table.TableCellEditor;
025import javax.swing.table.TableCellRenderer;
026
027import org.openstreetmap.josm.actions.SaveActionBase;
028import org.openstreetmap.josm.gui.util.CellEditorSupport;
029import org.openstreetmap.josm.gui.widgets.JosmTextField;
030import org.openstreetmap.josm.tools.GBC;
031
032/**
033 * Display and edit layer name and file path in a <code>JTable</code>.
034 *
035 * Note: Do not use the same object both as <code>TableCellRenderer</code> and
036 * <code>TableCellEditor</code> - this can mess up the current editor component
037 * by subsequent calls to the renderer (#12462).
038 */
039class LayerNameAndFilePathTableCell extends JPanel implements TableCellRenderer, TableCellEditor {
040    private static final Color colorError = new Color(255, 197, 197);
041    private static final String ELLIPSIS = '…' + File.separator;
042
043    private final JLabel lblLayerName = new JLabel();
044    private final JLabel lblFilename = new JLabel("");
045    private final JosmTextField tfFilename = new JosmTextField();
046    private final JButton btnFileChooser = new JButton(new LaunchFileChooserAction());
047
048    private static final GBC defaultCellStyle = GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 0);
049
050    private final transient CellEditorSupport cellEditorSupport = new CellEditorSupport(this);
051    private File value;
052
053    /** constructor that sets the default on each element **/
054    LayerNameAndFilePathTableCell() {
055        setLayout(new GridBagLayout());
056
057        lblLayerName.setPreferredSize(new Dimension(lblLayerName.getPreferredSize().width, 19));
058        lblLayerName.setFont(lblLayerName.getFont().deriveFont(Font.BOLD));
059
060        lblFilename.setPreferredSize(new Dimension(lblFilename.getPreferredSize().width, 19));
061        lblFilename.setOpaque(true);
062        lblFilename.setLabelFor(btnFileChooser);
063
064        tfFilename.setToolTipText(tr("Either edit the path manually in the text field or click the \"...\" button to open a file chooser."));
065        tfFilename.setPreferredSize(new Dimension(tfFilename.getPreferredSize().width, 19));
066        tfFilename.addFocusListener(
067                new FocusAdapter() {
068                    @Override
069                    public void focusGained(FocusEvent e) {
070                        tfFilename.selectAll();
071                    }
072                }
073                );
074        // hide border
075        tfFilename.setBorder(BorderFactory.createLineBorder(getBackground()));
076
077        btnFileChooser.setPreferredSize(new Dimension(20, 19));
078        btnFileChooser.setOpaque(true);
079    }
080
081    /** renderer used while not editing the file path **/
082    @Override
083    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
084            boolean hasFocus, int row, int column) {
085        removeAll();
086        if (value == null) return this;
087        SaveLayerInfo info = (SaveLayerInfo) value;
088        StringBuilder sb = new StringBuilder();
089        sb.append("<html>")
090          .append(addLblLayerName(info));
091        if (info.isSavable()) {
092            add(btnFileChooser, GBC.std());
093            sb.append("<br>")
094              .append(addLblFilename(info));
095        }
096        sb.append("</html>");
097        setToolTipText(sb.toString());
098        return this;
099    }
100
101    @Override
102    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
103        removeAll();
104        SaveLayerInfo info = (SaveLayerInfo) value;
105        value = info.getFile();
106        tfFilename.setText(value == null ? "" : value.toString());
107
108        StringBuilder sb = new StringBuilder();
109        sb.append("<html>")
110          .append(addLblLayerName(info));
111
112        if (info.isSavable()) {
113            add(btnFileChooser, GBC.std());
114            add(tfFilename, GBC.eol().fill(GBC.HORIZONTAL).insets(1, 0, 0, 0));
115            tfFilename.selectAll();
116
117            sb.append("<br>")
118              .append(tfFilename.getToolTipText());
119        }
120        sb.append("</html>");
121        setToolTipText(sb.toString());
122        return this;
123    }
124
125    private static boolean canWrite(File f) {
126        if (f == null) return false;
127        if (f.isDirectory()) return false;
128        if (f.exists() && f.canWrite()) return true;
129        if (!f.exists() && f.getParentFile() != null && f.getParentFile().canWrite())
130            return true;
131        return false;
132    }
133
134    /**
135     * Adds layer name label to (this) using the given info. Returns tooltip that should be added to the panel
136     * @param info information, user preferences and save/upload states of the layer
137     * @return tooltip that should be added to the panel
138     */
139    private String addLblLayerName(SaveLayerInfo info) {
140        lblLayerName.setIcon(info.getLayer().getIcon());
141        lblLayerName.setText(info.getName());
142        add(lblLayerName, defaultCellStyle);
143        return tr("The bold text is the name of the layer.");
144    }
145
146    /**
147     * Adds filename label to (this) using the given info. Returns tooltip that should be added to the panel
148     * @param info information, user preferences and save/upload states of the layer
149     * @return tooltip that should be added to the panel
150     */
151    private String addLblFilename(SaveLayerInfo info) {
152        String tooltip;
153        boolean error = false;
154        if (info.getFile() == null) {
155            error = info.isDoSaveToFile();
156            lblFilename.setText(tr("Click here to choose save path"));
157            lblFilename.setFont(lblFilename.getFont().deriveFont(Font.ITALIC));
158            tooltip = tr("Layer ''{0}'' is not backed by a file", info.getName());
159        } else {
160            String t = info.getFile().getPath();
161            lblFilename.setText(makePathFit(t));
162            tooltip = info.getFile().getAbsolutePath();
163            if (info.isDoSaveToFile() && !canWrite(info.getFile())) {
164                error = true;
165                tooltip = tr("File ''{0}'' is not writable. Please enter another file name.", info.getFile().getPath());
166            }
167        }
168
169        lblFilename.setBackground(error ? colorError : getBackground());
170        btnFileChooser.setBackground(error ? colorError : getBackground());
171
172        add(lblFilename, defaultCellStyle);
173        return tr("Click cell to change the file path.") + "<br/>" + tooltip;
174    }
175
176    /**
177     * Makes the given path fit lblFilename, appends ellipsis on the left if it doesn’t fit.
178     * Idea: /home/user/josm → …/user/josm → …/josm; and take the first one that fits
179     * @param t complete path
180     * @return shorter path
181     */
182    private String makePathFit(String t) {
183        boolean hasEllipsis = false;
184        while (t != null && !t.isEmpty()) {
185            int txtwidth = lblFilename.getFontMetrics(lblFilename.getFont()).stringWidth(t);
186            if (txtwidth < lblFilename.getWidth() || t.lastIndexOf(File.separator) < ELLIPSIS.length()) {
187                break;
188            }
189            // remove ellipsis, if present
190            t = hasEllipsis ? t.substring(ELLIPSIS.length()) : t;
191            // cut next block, and re-add ellipsis
192            t = ELLIPSIS + t.substring(t.indexOf(File.separator) + 1);
193            hasEllipsis = true;
194        }
195        return t;
196    }
197
198    @Override
199    public void addCellEditorListener(CellEditorListener l) {
200        cellEditorSupport.addCellEditorListener(l);
201    }
202
203    @Override
204    public void cancelCellEditing() {
205        cellEditorSupport.fireEditingCanceled();
206    }
207
208    @Override
209    public Object getCellEditorValue() {
210        return value;
211    }
212
213    @Override
214    public boolean isCellEditable(EventObject anEvent) {
215        return true;
216    }
217
218    @Override
219    public void removeCellEditorListener(CellEditorListener l) {
220        cellEditorSupport.removeCellEditorListener(l);
221    }
222
223    @Override
224    public boolean shouldSelectCell(EventObject anEvent) {
225        return true;
226    }
227
228    @Override
229    public boolean stopCellEditing() {
230        if (tfFilename.getText() == null || tfFilename.getText().trim().isEmpty()) {
231            value = null;
232        } else {
233            value = new File(tfFilename.getText());
234        }
235        cellEditorSupport.fireEditingStopped();
236        return true;
237    }
238
239    private class LaunchFileChooserAction extends AbstractAction {
240        LaunchFileChooserAction() {
241            putValue(NAME, "...");
242            putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
243        }
244
245        @Override
246        public void actionPerformed(ActionEvent e) {
247            File f = SaveActionBase.createAndOpenSaveFileChooser(tr("Select filename"), "osm");
248            if (f != null) {
249                tfFilename.setText(f.toString());
250                stopCellEditing();
251            }
252        }
253    }
254}