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}