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.layer.NoteLayer; 029import org.openstreetmap.josm.gui.util.CellEditorSupport; 030import org.openstreetmap.josm.gui.widgets.JosmTextField; 031import org.openstreetmap.josm.tools.GBC; 032 033/** 034 * Display and edit layer name and file path in a <code>JTable</code>. 035 * 036 * Note: Do not use the same object both as <code>TableCellRenderer</code> and 037 * <code>TableCellEditor</code> - this can mess up the current editor component 038 * by subsequent calls to the renderer (#12462). 039 */ 040class LayerNameAndFilePathTableCell extends JPanel implements TableCellRenderer, TableCellEditor { 041 private static final Color COLOR_ERROR = new Color(255, 197, 197); 042 private static final String ELLIPSIS = '…' + File.separator; 043 044 private final JLabel lblLayerName = new JLabel(); 045 private final JLabel lblFilename = new JLabel(""); 046 private final JosmTextField tfFilename = new JosmTextField(); 047 private final JButton btnFileChooser = new JButton(new LaunchFileChooserAction()); 048 049 private static final GBC DEFAULT_CELL_STYLE = GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 0); 050 051 private final transient CellEditorSupport cellEditorSupport = new CellEditorSupport(this); 052 private String extension = "osm"; 053 private File value; 054 055 /** constructor that sets the default on each element **/ 056 LayerNameAndFilePathTableCell() { 057 setLayout(new GridBagLayout()); 058 059 lblLayerName.setPreferredSize(new Dimension(lblLayerName.getPreferredSize().width, 19)); 060 lblLayerName.setFont(lblLayerName.getFont().deriveFont(Font.BOLD)); 061 062 lblFilename.setPreferredSize(new Dimension(lblFilename.getPreferredSize().width, 19)); 063 lblFilename.setOpaque(true); 064 lblFilename.setLabelFor(btnFileChooser); 065 066 tfFilename.setToolTipText(tr("Either edit the path manually in the text field or click the \"...\" button to open a file chooser.")); 067 tfFilename.setPreferredSize(new Dimension(tfFilename.getPreferredSize().width, 19)); 068 tfFilename.addFocusListener( 069 new FocusAdapter() { 070 @Override 071 public void focusGained(FocusEvent e) { 072 tfFilename.selectAll(); 073 } 074 } 075 ); 076 // hide border 077 tfFilename.setBorder(BorderFactory.createLineBorder(getBackground())); 078 079 btnFileChooser.setPreferredSize(new Dimension(20, 19)); 080 btnFileChooser.setOpaque(true); 081 } 082 083 /** renderer used while not editing the file path **/ 084 @Override 085 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, 086 boolean hasFocus, int row, int column) { 087 removeAll(); 088 if (value == null) return this; 089 SaveLayerInfo info = (SaveLayerInfo) value; 090 StringBuilder sb = new StringBuilder(); 091 sb.append("<html>") 092 .append(addLblLayerName(info)); 093 if (info.isSavable()) { 094 extension = info.getLayer() instanceof NoteLayer ? "osn" : "osm"; 095 add(btnFileChooser, GBC.std()); 096 sb.append("<br>") 097 .append(addLblFilename(info)); 098 } 099 sb.append("</html>"); 100 setToolTipText(sb.toString()); 101 return this; 102 } 103 104 @Override 105 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 106 removeAll(); 107 SaveLayerInfo info = (SaveLayerInfo) value; 108 value = info.getFile(); 109 tfFilename.setText(value == null ? "" : value.toString()); 110 111 StringBuilder sb = new StringBuilder(); 112 sb.append("<html>") 113 .append(addLblLayerName(info)); 114 115 if (info.isSavable()) { 116 extension = info.getLayer() instanceof NoteLayer ? "osn" : "osm"; 117 add(btnFileChooser, GBC.std()); 118 add(tfFilename, GBC.eol().fill(GBC.HORIZONTAL).insets(1, 0, 0, 0)); 119 tfFilename.selectAll(); 120 121 sb.append("<br>") 122 .append(tfFilename.getToolTipText()); 123 } 124 sb.append("</html>"); 125 setToolTipText(sb.toString()); 126 return this; 127 } 128 129 private static boolean canWrite(File f) { 130 if (f == null || f.isDirectory()) return false; 131 if (f.exists() && f.canWrite()) return true; 132 return !f.exists() && f.getParentFile() != null && f.getParentFile().canWrite(); 133 } 134 135 /** 136 * Adds layer name 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 addLblLayerName(SaveLayerInfo info) { 141 lblLayerName.setIcon(info.getLayer().getIcon()); 142 lblLayerName.setText(info.getName()); 143 add(lblLayerName, DEFAULT_CELL_STYLE); 144 return tr("The bold text is the name of the layer."); 145 } 146 147 /** 148 * Adds filename label to (this) using the given info. Returns tooltip that should be added to the panel 149 * @param info information, user preferences and save/upload states of the layer 150 * @return tooltip that should be added to the panel 151 */ 152 private String addLblFilename(SaveLayerInfo info) { 153 String tooltip; 154 boolean error = false; 155 if (info.getFile() == null) { 156 error = info.isDoSaveToFile(); 157 lblFilename.setText(tr("Click here to choose save path")); 158 lblFilename.setFont(lblFilename.getFont().deriveFont(Font.ITALIC)); 159 tooltip = tr("Layer ''{0}'' is not backed by a file", info.getName()); 160 } else { 161 String t = info.getFile().getPath(); 162 lblFilename.setText(makePathFit(t)); 163 tooltip = info.getFile().getAbsolutePath(); 164 if (info.isDoSaveToFile() && !canWrite(info.getFile())) { 165 error = true; 166 tooltip = tr("File ''{0}'' is not writable. Please enter another file name.", info.getFile().getPath()); 167 } 168 } 169 170 lblFilename.setBackground(error ? COLOR_ERROR : getBackground()); 171 btnFileChooser.setBackground(error ? COLOR_ERROR : getBackground()); 172 173 add(lblFilename, DEFAULT_CELL_STYLE); 174 return tr("Click cell to change the file path.") + "<br/>" + tooltip; 175 } 176 177 /** 178 * Makes the given path fit lblFilename, appends ellipsis on the left if it doesn't fit. 179 * Idea: /home/user/josm → …/user/josm → …/josm; and take the first one that fits 180 * @param t complete path 181 * @return shorter path 182 */ 183 private String makePathFit(String t) { 184 boolean hasEllipsis = false; 185 while (t != null && !t.isEmpty()) { 186 int txtwidth = lblFilename.getFontMetrics(lblFilename.getFont()).stringWidth(t); 187 if (txtwidth < lblFilename.getWidth() || t.lastIndexOf(File.separator) < ELLIPSIS.length()) { 188 break; 189 } 190 // remove ellipsis, if present 191 t = hasEllipsis ? t.substring(ELLIPSIS.length()) : t; 192 // cut next block, and re-add ellipsis 193 t = ELLIPSIS + t.substring(t.indexOf(File.separator) + 1); 194 hasEllipsis = true; 195 } 196 return t; 197 } 198 199 @Override 200 public void addCellEditorListener(CellEditorListener l) { 201 cellEditorSupport.addCellEditorListener(l); 202 } 203 204 @Override 205 public void cancelCellEditing() { 206 cellEditorSupport.fireEditingCanceled(); 207 } 208 209 @Override 210 public Object getCellEditorValue() { 211 return value; 212 } 213 214 @Override 215 public boolean isCellEditable(EventObject anEvent) { 216 return true; 217 } 218 219 @Override 220 public void removeCellEditorListener(CellEditorListener l) { 221 cellEditorSupport.removeCellEditorListener(l); 222 } 223 224 @Override 225 public boolean shouldSelectCell(EventObject anEvent) { 226 return true; 227 } 228 229 @Override 230 public boolean stopCellEditing() { 231 if (tfFilename.getText() == null || tfFilename.getText().trim().isEmpty()) { 232 value = null; 233 } else { 234 value = new File(tfFilename.getText()); 235 } 236 cellEditorSupport.fireEditingStopped(); 237 return true; 238 } 239 240 private class LaunchFileChooserAction extends AbstractAction { 241 LaunchFileChooserAction() { 242 putValue(NAME, "..."); 243 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file")); 244 } 245 246 @Override 247 public void actionPerformed(ActionEvent e) { 248 File f = SaveActionBase.createAndOpenSaveFileChooser(tr("Select filename"), extension); 249 if (f != null) { 250 tfFilename.setText(f.toString()); 251 stopCellEditing(); 252 } 253 } 254 } 255}