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