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