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 }