001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.gui.io;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005
006 import java.awt.Color;
007 import java.awt.Component;
008 import java.awt.Dimension;
009 import java.awt.Font;
010 import java.awt.GridBagLayout;
011 import java.awt.event.ActionEvent;
012 import java.awt.event.FocusAdapter;
013 import java.awt.event.FocusEvent;
014 import java.io.File;
015 import java.util.EventObject;
016 import java.util.concurrent.CopyOnWriteArrayList;
017
018 import javax.swing.AbstractAction;
019 import javax.swing.BorderFactory;
020 import javax.swing.JButton;
021 import javax.swing.JLabel;
022 import javax.swing.JPanel;
023 import javax.swing.JTable;
024 import javax.swing.JTextField;
025 import javax.swing.event.CellEditorListener;
026 import javax.swing.event.ChangeEvent;
027 import javax.swing.table.TableCellEditor;
028 import javax.swing.table.TableCellRenderer;
029
030 import org.openstreetmap.josm.actions.SaveActionBase;
031 import org.openstreetmap.josm.tools.GBC;
032
033
034 class LayerNameAndFilePathTableCell extends JPanel implements TableCellRenderer, TableCellEditor {
035 private final static Color colorError = new Color(255,197,197);
036 private final static String separator = System.getProperty("file.separator");
037 private final static String ellipsis = "???" + separator;
038
039 private final JLabel lblLayerName = new JLabel();
040 private final JLabel lblFilename = new JLabel("");
041 private final JTextField tfFilename = new JTextField();
042 private final JButton btnFileChooser = new JButton(new LaunchFileChooserAction());
043
044 private final static GBC defaultCellStyle = GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 0);
045
046 private CopyOnWriteArrayList<CellEditorListener> listeners;
047 private File value;
048
049
050 /** constructor that sets the default on each element **/
051 public LayerNameAndFilePathTableCell() {
052 setLayout(new GridBagLayout());
053
054 lblLayerName.setPreferredSize(new Dimension(lblLayerName.getPreferredSize().width, 19));
055 lblLayerName.setFont(lblLayerName.getFont().deriveFont(Font.BOLD));
056
057 lblFilename.setPreferredSize(new Dimension(lblFilename.getPreferredSize().width, 19));
058 lblFilename.setOpaque(true);
059
060 tfFilename.setToolTipText(tr("Either edit the path manually in the text field or click the \"...\" button to open a file chooser."));
061 tfFilename.setPreferredSize(new Dimension(tfFilename.getPreferredSize().width, 19));
062 tfFilename.addFocusListener(
063 new FocusAdapter() {
064 @Override public void focusGained(FocusEvent e) {
065 tfFilename.selectAll();
066 }
067 }
068 );
069 // hide border
070 tfFilename.setBorder(BorderFactory.createLineBorder(getBackground()));
071
072 btnFileChooser.setPreferredSize(new Dimension(20, 19));
073 btnFileChooser.setOpaque(true);
074
075 listeners = new CopyOnWriteArrayList<CellEditorListener>();
076 }
077
078 /** renderer used while not editing the file path **/
079 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
080 boolean hasFocus, int row, int column) {
081 removeAll();
082 SaveLayerInfo info = (SaveLayerInfo)value;
083 StringBuilder sb = new StringBuilder();
084 sb.append("<html>");
085 sb.append(addLblLayerName(info));
086 sb.append("<br>");
087 add(btnFileChooser, GBC.std());
088 sb.append(addLblFilename(info));
089
090 sb.append("</html>");
091 setToolTipText(sb.toString());
092 return this;
093 }
094
095 /** renderer used while the file path is being edited **/
096 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected,
097 int row, int column) {
098 removeAll();
099 SaveLayerInfo info = (SaveLayerInfo)value;
100 value = info.getFile();
101 tfFilename.setText(value == null ? "" : value.toString());
102
103 StringBuilder sb = new StringBuilder();
104 sb.append("<html>");
105 sb.append(addLblLayerName(info));
106 sb.append("<br/>");
107
108 add(btnFileChooser, GBC.std());
109 add(tfFilename, GBC.eol().fill(GBC.HORIZONTAL).insets(1, 0, 0, 0));
110 tfFilename.selectAll();
111
112 sb.append(tfFilename.getToolTipText());
113 sb.append("</html>");
114 setToolTipText(sb.toString());
115 return this;
116 }
117
118 private static boolean canWrite(File f) {
119 if (f == null) return false;
120 if (f.isDirectory()) return false;
121 if (f.exists() && f.canWrite()) return true;
122 if (!f.exists() && f.getParentFile() != null && f.getParentFile().canWrite())
123 return true;
124 return false;
125 }
126
127 /** adds layer name label to (this) using the given info. Returns tooltip that
128 * should be added to the panel **/
129 private String addLblLayerName(SaveLayerInfo info) {
130 lblLayerName.setIcon(info.getLayer().getIcon());
131 lblLayerName.setText(info.getName());
132 add(lblLayerName, defaultCellStyle);
133 return tr("The bold text is the name of the layer.");
134 }
135
136 /** adds filename label to (this) using the given info. Returns tooltip that
137 * should be added to the panel */
138 private String addLblFilename(SaveLayerInfo info) {
139 String tooltip = "";
140 boolean error = false;
141 if (info.getFile() == null) {
142 error = info.isDoSaveToFile();
143 lblFilename.setText(tr("Click here to choose save path"));
144 lblFilename.setFont(lblFilename.getFont().deriveFont(Font.ITALIC));
145 tooltip = tr("Layer ''{0}'' is not backed by a file", info.getName());
146 } else {
147 String t = info.getFile().getPath();
148 lblFilename.setText(makePathFit(t));
149 tooltip = info.getFile().getAbsolutePath();
150 if (info.isDoSaveToFile() && !canWrite(info.getFile())) {
151 error = true;
152 tooltip = tr("File ''{0}'' is not writable. Please enter another file name.", info.getFile().getPath());
153 }
154 }
155
156 lblFilename.setBackground(error ? colorError : getBackground());
157 btnFileChooser.setBackground(error ? colorError : getBackground());
158
159 add(lblFilename, defaultCellStyle);
160 return tr("Click cell to change the file path.") + "<br/>" + tooltip;
161 }
162
163 /** makes the given path fit lblFilename, appends ellipsis on the left if it doesn???t fit.
164 * Idea: /home/user/josm ??? ???/user/josm ??? ???/josm; and take the first one that fits */
165 private String makePathFit(String t) {
166 boolean hasEllipsis = false;
167 while(t != null && !t.isEmpty()) {
168 int txtwidth = lblFilename.getFontMetrics(lblFilename.getFont()).stringWidth(t);
169 if(txtwidth < lblFilename.getWidth() || t.lastIndexOf(separator) < ellipsis.length()) {
170 break;
171 }
172 // remove ellipsis, if present
173 t = hasEllipsis ? t.substring(ellipsis.length()) : t;
174 // cut next block, and re-add ellipsis
175 t = ellipsis + t.substring(t.indexOf(separator) + 1);
176 hasEllipsis = true;
177 }
178 return t;
179 }
180
181 public void addCellEditorListener(CellEditorListener l) {
182 if (l != null) {
183 listeners.addIfAbsent(l);
184 }
185 }
186
187 protected void fireEditingCanceled() {
188 for (CellEditorListener l: listeners) {
189 l.editingCanceled(new ChangeEvent(this));
190 }
191 }
192
193 protected void fireEditingStopped() {
194 for (CellEditorListener l: listeners) {
195 l.editingStopped(new ChangeEvent(this));
196 }
197 }
198
199 public void cancelCellEditing() {
200 fireEditingCanceled();
201 }
202
203 public Object getCellEditorValue() {
204 return value;
205 }
206
207 public boolean isCellEditable(EventObject anEvent) {
208 return true;
209 }
210
211 public void removeCellEditorListener(CellEditorListener l) {
212 listeners.remove(l);
213 }
214
215 public boolean shouldSelectCell(EventObject anEvent) {
216 return true;
217 }
218
219 public boolean stopCellEditing() {
220 if (tfFilename.getText() == null || tfFilename.getText().trim().equals("")) {
221 value = null;
222 } else {
223 value = new File(tfFilename.getText());
224 }
225 fireEditingStopped();
226 return true;
227 }
228
229 private class LaunchFileChooserAction extends AbstractAction {
230 public LaunchFileChooserAction() {
231 putValue(NAME, "...");
232 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
233 }
234
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 }