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