source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagTable.java@ 1916

Last change on this file since 1916 was 1884, checked in by jttt, 15 years ago

Fix ArrayIndexOutOfBoundsException 0:-2 in relation editor when Shift-Tab is pressed in table without selected cell

File size: 14.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.relation;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.AWTException;
7import java.awt.MouseInfo;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.Robot;
11import java.awt.event.ActionEvent;
12import java.awt.event.InputEvent;
13import java.awt.event.KeyEvent;
14import java.awt.event.KeyListener;
15import java.util.logging.Level;
16import java.util.logging.Logger;
17
18import javax.swing.AbstractAction;
19import javax.swing.Action;
20import javax.swing.JComponent;
21import javax.swing.JTable;
22import javax.swing.KeyStroke;
23import javax.swing.ListSelectionModel;
24import javax.swing.SwingUtilities;
25import javax.swing.event.ListSelectionEvent;
26import javax.swing.event.ListSelectionListener;
27import javax.swing.table.DefaultTableColumnModel;
28import javax.swing.table.TableColumn;
29import javax.swing.table.TableColumnModel;
30import javax.swing.table.TableModel;
31
32import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionCache;
33import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionList;
34
35/**
36 * This is the tabular editor component for OSM tags.
37 *
38 */
39@SuppressWarnings("serial")
40public class TagTable extends JTable {
41
42 private static Logger logger = Logger.getLogger(TagTable.class.getName());
43
44 /** the table cell editor used by this table */
45 private TagCellEditor editor = null;
46
47 /**
48 * The table has two columns. The first column is used for editing rendering and
49 * editing tag keys, the second for rendering and editing tag values.
50 *
51 */
52 static class TagTableColumnModel extends DefaultTableColumnModel {
53
54 public TagTableColumnModel() {
55 TableColumn col = null;
56 TagCellRenderer renderer = new TagCellRenderer();
57
58
59 // column 0 - tag key
60 col = new TableColumn(0);
61 col.setHeaderValue(tr("Key"));
62 col.setResizable(true);
63 col.setCellRenderer(renderer);
64 addColumn(col);
65
66 // column 1 - tag value
67 col = new TableColumn(1);
68 col.setHeaderValue(tr("Value"));
69 col.setResizable(true);
70 col.setCellRenderer(renderer);
71 addColumn(col);
72
73 }
74 }
75
76 /**
77 * Action to be run when the user navigates to the next cell in the table,
78 * for instance by pressing TAB or ENTER. The action alters the standard
79 * navigation path from cell to cell:
80 * <ul>
81 * <li>it jumps over cells in the first column</li>
82 * <li>it automatically add a new empty row when the user leaves the
83 * last cell in the table</li>
84 * <ul>
85 *
86 * @author gubaer
87 *
88 */
89 class SelectNextColumnCellAction extends AbstractAction {
90 public void actionPerformed(ActionEvent e) {
91 run();
92 }
93
94 public void run() {
95 int col = getSelectedColumn();
96 int row = getSelectedRow();
97 if (getCellEditor() != null) {
98 getCellEditor().stopCellEditing();
99 }
100
101 if (col == 0) {
102 col++;
103 } else if (col == 1 && row < getRowCount()-1) {
104 col=0;
105 row++;
106 } else if (col == 1 && row == getRowCount()-1){
107 // we are at the end. Append an empty row and move the focus
108 // to its second column
109 TagEditorModel model = (TagEditorModel)getModel();
110 model.appendNewTag();
111 col=0;
112 row++;
113 }
114 changeSelection(row, col, false, false);
115 }
116 }
117
118
119 /**
120 * Action to be run when the user navigates to the previous cell in the table,
121 * for instance by pressing Shift-TAB
122 *
123 */
124 class SelectPreviousColumnCellAction extends AbstractAction {
125
126 public void actionPerformed(ActionEvent e) {
127 int col = getSelectedColumn();
128 int row = getSelectedRow();
129 if (getCellEditor() != null) {
130 getCellEditor().stopCellEditing();
131 }
132
133
134 if (col <= 0 && row <= 0) {
135 // change nothing
136 } else if (col == 1) {
137 col--;
138 } else {
139 col = 1;
140 row--;
141 }
142 changeSelection(row, col, false, false);
143 }
144 }
145
146 /**
147 * Action to be run when the user invokes a delete action on the table, for
148 * instance by pressing DEL.
149 *
150 * Depending on the shape on the current selection the action deletes individual
151 * values or entire tags from the model.
152 *
153 * If the current selection consists of cells in the second column only, the keys of
154 * the selected tags are set to the empty string.
155 *
156 * If the current selection consists of cell in the third column only, the values of the
157 * selected tags are set to the empty string.
158 *
159 * If the current selection consists of cells in the second and the third column,
160 * the selected tags are removed from the model.
161 *
162 * This action listens to the table selection. It becomes enabled when the selection
163 * is non-empty, otherwise it is disabled.
164 *
165 *
166 */
167 class DeleteAction extends RunnableAction implements ListSelectionListener {
168
169 /**
170 * delete a selection of tag names
171 */
172 protected void deleteTagNames() {
173 int[] rows = getSelectedRows();
174 TagEditorModel model = (TagEditorModel)getModel();
175 model.deleteTagNames(rows);
176 }
177
178 /**
179 * delete a selection of tag values
180 */
181 protected void deleteTagValues() {
182 int[] rows = getSelectedRows();
183 TagEditorModel model = (TagEditorModel)getModel();
184 model.deleteTagValues(rows);
185 }
186
187 /**
188 * delete a selection of tags
189 */
190 protected void deleteTags() {
191 int[] rows = getSelectedRows();
192 TagEditorModel model = (TagEditorModel)getModel();
193 model.deleteTags(rows);
194 }
195
196 /**
197 * constructor
198 */
199 public DeleteAction() {
200 putValue(Action.NAME, tr("Delete"));
201 getSelectionModel().addListSelectionListener(this);
202 getColumnModel().getSelectionModel().addListSelectionListener(this);
203 }
204
205
206
207 @Override
208 public void run() {
209 if (!isEnabled())
210 return;
211 getCellEditor().stopCellEditing();
212 if (getSelectedColumnCount() == 1) {
213 if (getSelectedColumn() == 0) {
214 deleteTagNames();
215 } else if (getSelectedColumn() == 1) {
216 deleteTagValues();
217 } else
218 // should not happen
219 //
220 throw new IllegalStateException("unexpected selected clolumn: getSelectedColumn() is " + getSelectedColumn());
221 } else if (getSelectedColumnCount() == 2) {
222 deleteTags();
223 }
224 TagEditorModel model = (TagEditorModel)getModel();
225 if (model.getRowCount() == 0) {
226 model.ensureOneTag();
227 requestFocusInCell(0, 0);
228 }
229 }
230
231 /**
232 * listens to the table selection model
233 */
234 public void valueChanged(ListSelectionEvent e) {
235 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
236 setEnabled(false);
237 } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
238 setEnabled(true);
239 } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
240 setEnabled(true);
241 } else {
242 setEnabled(false);
243 }
244
245 }
246 }
247
248 /**
249 * Action to be run when the user adds a new tag.
250 *
251 *
252 */
253 class AddAction extends RunnableAction {
254
255 public AddAction() {
256 putValue(Action.NAME, tr("Add"));
257 }
258
259 @Override
260 public void run() {
261 getCellEditor().stopCellEditing();
262 ((TagEditorModel)getModel()).appendNewTag();
263 final int rowIdx = getModel().getRowCount()-1;
264 requestFocusInCell(rowIdx, 0);
265 }
266 }
267
268
269 /** the delete action */
270 private RunnableAction deleteAction = null;
271
272 /** the add action */
273 private RunnableAction addAction = null;
274
275 /**
276 *
277 * @return the delete action used by this table
278 */
279 public RunnableAction getDeleteAction() {
280 return deleteAction;
281 }
282
283 public RunnableAction getAddAction() {
284 return addAction;
285 }
286
287 /**
288 * initialize the table
289 */
290 protected void init() {
291 setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
292 setCellSelectionEnabled(true);
293 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
294 putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
295
296 // make ENTER behave like TAB
297 //
298 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
299 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
300
301 // install custom navigation actions
302 //
303 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
304 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
305
306 // create a delete action. Installing this action in the input and action map
307 // didn't work. We therefore handle delete requests in processKeyBindings(...)
308 //
309 deleteAction = new DeleteAction();
310
311 // create the add action
312 //
313 addAction = new AddAction();
314 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
315 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
316 getActionMap().put("addTag", addAction);
317
318 // create the table cell editor and set it to key and value columns
319 //
320 editor = new TagCellEditor();
321 editor.setTagEditorModel((TagEditorModel)getModel());
322 getColumnModel().getColumn(0).setCellEditor(editor);
323 getColumnModel().getColumn(1).setCellEditor(editor);
324 }
325
326 /**
327 * constructor
328 *
329 * @param model
330 * @param columnModel
331 */
332 public TagTable(TableModel model) {
333 super(model, new TagTableColumnModel());
334 init();
335 }
336
337
338
339 /**
340 * adjusts the width of the columns for the tag name and the tag value
341 * to the width of the scroll panes viewport.
342 *
343 * Note: {@see #getPreferredScrollableViewportSize()} did not work as expected
344 *
345 * @param scrollPaneWidth the width of the scroll panes viewport
346 */
347 public void adjustColumnWidth(int scrollPaneWidth) {
348 TableColumnModel tcm = getColumnModel();
349 int width = scrollPaneWidth;
350 width = width / 2;
351 if (width > 0) {
352 tcm.getColumn(0).setMinWidth(width);
353 tcm.getColumn(0).setMaxWidth(width);
354 tcm.getColumn(1).setMinWidth(width);
355 tcm.getColumn(1).setMaxWidth(width);
356 }
357 }
358
359
360 @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
361 int condition, boolean pressed) {
362
363 // handle delete key
364 //
365 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
366 getDeleteAction().run();
367 }
368 return super.processKeyBinding(ks, e, condition, pressed);
369 }
370
371
372 /**
373 * @param autoCompletionList
374 */
375 public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
376 if (autoCompletionList == null)
377 return;
378 if (editor != null) {
379 editor.setAutoCompletionList(autoCompletionList);
380 }
381 }
382
383 public void setAutoCompletionCache(AutoCompletionCache acCache) {
384 if (acCache == null) {
385 logger.warning("argument acCache should not be null. Aborting.");
386 return;
387 }
388 if (editor != null) {
389 editor.setAutoCompletionCache(acCache);
390 }
391 }
392
393 public AutoCompletionList getAutoCompletionList() {
394 if (editor != null)
395 return editor.getAutoCompletionList();
396 else
397 return null;
398 }
399
400 public TagCellEditor getTableCellEditor() {
401 return editor;
402 }
403
404 public void addOKAccelatorListener(KeyListener l) {
405 addKeyListener(l);
406 if (editor == null) {
407 logger.warning("editor is null. cannot register OK accelator listener.");
408 }
409 editor.getEditor().addKeyListener(l);
410 }
411
412 public void requestFocusInCell(final int row, final int col) {
413
414 // the following code doesn't work reliably. If a table cell
415 // gains focus using editCellAt() and requestFocusInWindow()
416 // it isn't possible to tab to the next table cell using TAB or
417 // ENTER. Don't know why.
418 //
419 // tblTagEditor.editCellAt(row, col);
420 // if (tblTagEditor.getEditorComponent() != null) {
421 // tblTagEditor.getEditorComponent().requestFocusInWindow();
422 // }
423
424 // this is a workaround. We move the focus to the respective cell
425 // using a simulated mouse click. In this case one can tab out of
426 // the cell using TAB and ENTER.
427 //
428 Rectangle r = getCellRect(row,col, false);
429 Point p = new Point(r.x + r.width/2, r.y + r.height/2);
430 SwingUtilities.convertPointToScreen(p, this);
431 Point before = MouseInfo.getPointerInfo().getLocation();
432
433 try {
434 Robot robot = new Robot();
435 robot.mouseMove(p.x,p.y);
436 robot.mousePress(InputEvent.BUTTON1_MASK);
437 robot.mouseRelease(InputEvent.BUTTON1_MASK);
438 robot.mouseMove(before.x, before.y);
439 } catch(AWTException e) {
440 logger.log(Level.SEVERE, "failed to simulate mouse click event at (" + r.x + "," + r.y + "). Exception: " + e.toString());
441 return;
442 }
443 }
444}
Note: See TracBrowser for help on using the repository browser.