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