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 CellEditorSupport cellEditorSupport = new CellEditorSupport(this); 045 private File value; 046 047 /** constructor that sets the default on each element **/ 048 public 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 057 tfFilename.setToolTipText(tr("Either edit the path manually in the text field or click the \"...\" button to open a file chooser.")); 058 tfFilename.setPreferredSize(new Dimension(tfFilename.getPreferredSize().width, 19)); 059 tfFilename.addFocusListener( 060 new FocusAdapter() { 061 @Override 062 public void focusGained(FocusEvent e) { 063 tfFilename.selectAll(); 064 } 065 } 066 ); 067 // hide border 068 tfFilename.setBorder(BorderFactory.createLineBorder(getBackground())); 069 070 btnFileChooser.setPreferredSize(new Dimension(20, 19)); 071 btnFileChooser.setOpaque(true); 072 } 073 074 /** renderer used while not editing the file path **/ 075 @Override 076 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, 077 boolean hasFocus, int row, int column) { 078 removeAll(); 079 SaveLayerInfo info = (SaveLayerInfo)value; 080 StringBuilder sb = new StringBuilder(); 081 sb.append("<html>"); 082 sb.append(addLblLayerName(info)); 083 sb.append("<br>"); 084 add(btnFileChooser, GBC.std()); 085 sb.append(addLblFilename(info)); 086 087 sb.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 sb.append(addLblLayerName(info)); 102 sb.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 sb.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 /** adds layer name label to (this) using the given info. Returns tooltip that 124 * should be added to the panel **/ 125 private String addLblLayerName(SaveLayerInfo info) { 126 lblLayerName.setIcon(info.getLayer().getIcon()); 127 lblLayerName.setText(info.getName()); 128 add(lblLayerName, defaultCellStyle); 129 return tr("The bold text is the name of the layer."); 130 } 131 132 /** adds filename label to (this) using the given info. Returns tooltip that 133 * should be added to the panel */ 134 private String addLblFilename(SaveLayerInfo info) { 135 String tooltip = ""; 136 boolean error = false; 137 if (info.getFile() == null) { 138 error = info.isDoSaveToFile(); 139 lblFilename.setText(tr("Click here to choose save path")); 140 lblFilename.setFont(lblFilename.getFont().deriveFont(Font.ITALIC)); 141 tooltip = tr("Layer ''{0}'' is not backed by a file", info.getName()); 142 } else { 143 String t = info.getFile().getPath(); 144 lblFilename.setText(makePathFit(t)); 145 tooltip = info.getFile().getAbsolutePath(); 146 if (info.isDoSaveToFile() && !canWrite(info.getFile())) { 147 error = true; 148 tooltip = tr("File ''{0}'' is not writable. Please enter another file name.", info.getFile().getPath()); 149 } 150 } 151 152 lblFilename.setBackground(error ? colorError : getBackground()); 153 btnFileChooser.setBackground(error ? colorError : getBackground()); 154 155 add(lblFilename, defaultCellStyle); 156 return tr("Click cell to change the file path.") + "<br/>" + tooltip; 157 } 158 159 /** makes the given path fit lblFilename, appends ellipsis on the left if it doesn’t fit. 160 * Idea: /home/user/josm → …/user/josm → …/josm; and take the first one that fits */ 161 private String makePathFit(String t) { 162 boolean hasEllipsis = false; 163 while(t != null && !t.isEmpty()) { 164 int txtwidth = lblFilename.getFontMetrics(lblFilename.getFont()).stringWidth(t); 165 if(txtwidth < lblFilename.getWidth() || t.lastIndexOf(separator) < ellipsis.length()) { 166 break; 167 } 168 // remove ellipsis, if present 169 t = hasEllipsis ? t.substring(ellipsis.length()) : t; 170 // cut next block, and re-add ellipsis 171 t = ellipsis + t.substring(t.indexOf(separator) + 1); 172 hasEllipsis = true; 173 } 174 return t; 175 } 176 177 @Override 178 public void addCellEditorListener(CellEditorListener l) { 179 cellEditorSupport.addCellEditorListener(l); 180 } 181 182 @Override 183 public void cancelCellEditing() { 184 cellEditorSupport.fireEditingCanceled(); 185 } 186 187 @Override 188 public Object getCellEditorValue() { 189 return value; 190 } 191 192 @Override 193 public boolean isCellEditable(EventObject anEvent) { 194 return true; 195 } 196 197 @Override 198 public void removeCellEditorListener(CellEditorListener l) { 199 cellEditorSupport.removeCellEditorListener(l); 200 } 201 202 @Override 203 public boolean shouldSelectCell(EventObject anEvent) { 204 return true; 205 } 206 207 @Override 208 public boolean stopCellEditing() { 209 if (tfFilename.getText() == null || tfFilename.getText().trim().isEmpty()) { 210 value = null; 211 } else { 212 value = new File(tfFilename.getText()); 213 } 214 cellEditorSupport.fireEditingStopped(); 215 return true; 216 } 217 218 private class LaunchFileChooserAction extends AbstractAction { 219 public LaunchFileChooserAction() { 220 putValue(NAME, "..."); 221 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file")); 222 } 223 224 @Override 225 public void actionPerformed(ActionEvent e) { 226 File f = SaveActionBase.createAndOpenSaveFileChooser(tr("Select filename"), "osm"); 227 if (f != null) { 228 tfFilename.setText(f.toString()); 229 stopCellEditing(); 230 } 231 } 232 } 233}