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