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