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