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