1 /* |
|
2 * CDDL HEADER START |
|
3 * |
|
4 * The contents of this file are subject to the terms of the |
|
5 * Common Development and Distribution License (the "License"). |
|
6 * You may not use this file except in compliance with the License. |
|
7 * |
|
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE |
|
9 * or http://www.opensolaris.org/os/licensing. |
|
10 * See the License for the specific language governing permissions |
|
11 * and limitations under the License. |
|
12 * |
|
13 * When distributing Covered Code, include this CDDL HEADER in each |
|
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE. |
|
15 * If applicable, add the following below this CDDL HEADER, with the |
|
16 * fields enclosed by brackets "[]" replaced with your own identifying |
|
17 * information: Portions Copyright [yyyy] [name of copyright owner] |
|
18 * |
|
19 * CDDL HEADER END |
|
20 */ |
|
21 |
|
22 /* |
|
23 * Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. |
|
24 */ |
|
25 |
|
26 package com.oracle.solaris.vp.util.swing.time; |
|
27 |
|
28 import java.awt.*; |
|
29 import java.awt.event.*; |
|
30 import java.text.*; |
|
31 import java.util.*; |
|
32 import javax.swing.*; |
|
33 import javax.swing.event.*; |
|
34 import javax.swing.text.DefaultCaret; |
|
35 import com.oracle.solaris.vp.util.swing.GUIUtil; |
|
36 |
|
37 @SuppressWarnings({"serial"}) |
|
38 public class TimeSpinnerEditor extends JTextField { |
|
39 // |
|
40 // Instance data |
|
41 // |
|
42 |
|
43 private DateFormat format; |
|
44 private TimeSpinnerModel model; |
|
45 private Date time; |
|
46 private boolean ignoreCaretEvents; |
|
47 private FieldPosition selected; |
|
48 |
|
49 // Map Format.Fields to Calendar fields |
|
50 // These are all only fields that TimeSpinnerModel supports. |
|
51 private Map<Format.Field, Integer> fieldMap; |
|
52 { |
|
53 fieldMap = new HashMap<Format.Field, Integer>(); |
|
54 fieldMap.put(DateFormat.Field.AM_PM, Calendar.AM_PM); |
|
55 fieldMap.put(DateFormat.Field.DAY_OF_MONTH, Calendar.DAY_OF_MONTH); |
|
56 fieldMap.put(DateFormat.Field.DAY_OF_WEEK, Calendar.DAY_OF_WEEK); |
|
57 fieldMap.put(DateFormat.Field.DAY_OF_YEAR, Calendar.DAY_OF_YEAR); |
|
58 fieldMap.put(DateFormat.Field.ERA, Calendar.ERA); |
|
59 fieldMap.put(DateFormat.Field.HOUR0, Calendar.HOUR); |
|
60 fieldMap.put(DateFormat.Field.HOUR1, Calendar.HOUR); |
|
61 fieldMap.put(DateFormat.Field.HOUR_OF_DAY0, Calendar.HOUR_OF_DAY); |
|
62 fieldMap.put(DateFormat.Field.HOUR_OF_DAY1, Calendar.HOUR_OF_DAY); |
|
63 fieldMap.put(DateFormat.Field.MILLISECOND, Calendar.MILLISECOND); |
|
64 fieldMap.put(DateFormat.Field.MINUTE, Calendar.MINUTE); |
|
65 fieldMap.put(DateFormat.Field.MONTH, Calendar.MONTH); |
|
66 fieldMap.put(DateFormat.Field.SECOND, Calendar.SECOND); |
|
67 fieldMap.put(DateFormat.Field.TIME_ZONE, Calendar.ZONE_OFFSET); |
|
68 fieldMap.put(DateFormat.Field.WEEK_OF_MONTH, Calendar.WEEK_OF_MONTH); |
|
69 fieldMap.put(DateFormat.Field.WEEK_OF_YEAR, Calendar.WEEK_OF_YEAR); |
|
70 fieldMap.put(DateFormat.Field.YEAR, Calendar.YEAR); |
|
71 }; |
|
72 |
|
73 private FieldPosition[] positions; |
|
74 { |
|
75 Set<Format.Field> fields = fieldMap.keySet(); |
|
76 positions = new FieldPosition[fields.size()]; |
|
77 int i = 0; |
|
78 for (Format.Field field : fields) { |
|
79 positions[i++] = new FieldPosition(field); |
|
80 } |
|
81 } |
|
82 |
|
83 private Comparator<FieldPosition> posComparator = |
|
84 new Comparator<FieldPosition>() { |
|
85 @Override |
|
86 public int compare(FieldPosition o1, FieldPosition o2) { |
|
87 int begin1 = o1.getBeginIndex(); |
|
88 int begin2 = o2.getBeginIndex(); |
|
89 |
|
90 if (begin1 < begin2) { |
|
91 return -1; |
|
92 } else if (begin1 > begin2) { |
|
93 return 1; |
|
94 } |
|
95 |
|
96 int end1 = o1.getEndIndex(); |
|
97 int end2 = o2.getEndIndex(); |
|
98 |
|
99 if (end1 < end2) { |
|
100 return -1; |
|
101 } else if (end1 > end2) { |
|
102 return 1; |
|
103 } |
|
104 |
|
105 return 0; |
|
106 } |
|
107 }; |
|
108 |
|
109 // |
|
110 // Constructors |
|
111 // |
|
112 |
|
113 public TimeSpinnerEditor(TimeSpinnerModel model, DateFormat format) { |
|
114 this.model = model; |
|
115 this.format = format; |
|
116 |
|
117 setEditable(false); |
|
118 |
|
119 model.addChangeListener( |
|
120 new ChangeListener() { |
|
121 @Override |
|
122 public void stateChanged(ChangeEvent e) { |
|
123 updateFromModel(); |
|
124 } |
|
125 }); |
|
126 |
|
127 updateFromModel(); |
|
128 |
|
129 model.getTimeSelectionModel().addChangeListener( |
|
130 new ChangeListener() { |
|
131 @Override |
|
132 public void stateChanged(ChangeEvent e) { |
|
133 selectionChanged(); |
|
134 } |
|
135 }); |
|
136 |
|
137 addCaretListener( |
|
138 new CaretListener() { |
|
139 @Override |
|
140 public void caretUpdate(CaretEvent e) { |
|
141 if (!ignoreCaretEvents) { |
|
142 EventQueue.invokeLater( |
|
143 new Runnable() { |
|
144 @Override |
|
145 public void run() { |
|
146 selectFieldAtCaret(); |
|
147 } |
|
148 }); |
|
149 } |
|
150 } |
|
151 }); |
|
152 |
|
153 // Select first field |
|
154 selectFieldAtCaret(); |
|
155 |
|
156 addFocusListener( |
|
157 new FocusListener() { |
|
158 @Override |
|
159 public void focusGained(FocusEvent e) { |
|
160 if (getModel().getTimeSelectionModel().getSelectedField() == |
|
161 -1) { |
|
162 selectFieldAtCaret(); |
|
163 } |
|
164 } |
|
165 |
|
166 @Override |
|
167 public void focusLost(FocusEvent e) { |
|
168 if (!e.isTemporary()) { |
|
169 getModel().getTimeSelectionModel().setSelectedField(-1); |
|
170 } |
|
171 } |
|
172 }); |
|
173 |
|
174 KeyStroke right = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0); |
|
175 Action focusNextFieldAction = |
|
176 new AbstractAction() { |
|
177 @Override |
|
178 public void actionPerformed(ActionEvent e) { |
|
179 focusNextField(); |
|
180 } |
|
181 }; |
|
182 GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED, |
|
183 "next field", focusNextFieldAction, right); |
|
184 |
|
185 KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0); |
|
186 GUIUtil.removeFocusTraversalKeys(this, true, tab); |
|
187 Action focusNextFieldOrNextComponentAction = |
|
188 new AbstractAction() { |
|
189 @Override |
|
190 public void actionPerformed(ActionEvent e) { |
|
191 focusNextFieldOrNextComponent(); |
|
192 } |
|
193 }; |
|
194 GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED, |
|
195 "next field or next focus", focusNextFieldOrNextComponentAction, |
|
196 tab); |
|
197 |
|
198 KeyStroke left = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0); |
|
199 Action focusPreviousFieldAction = |
|
200 new AbstractAction() { |
|
201 @Override |
|
202 public void actionPerformed(ActionEvent e) { |
|
203 focusPreviousField(); |
|
204 } |
|
205 }; |
|
206 GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED, |
|
207 "previous field", focusPreviousFieldAction, left); |
|
208 |
|
209 KeyStroke shiftTab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, |
|
210 InputEvent.SHIFT_DOWN_MASK); |
|
211 GUIUtil.removeFocusTraversalKeys(this, false, shiftTab); |
|
212 Action focusPreviousFieldOrPreviousComponentAction = |
|
213 new AbstractAction() { |
|
214 @Override |
|
215 public void actionPerformed(ActionEvent e) { |
|
216 focusPreviousFieldOrPreviousComponent(); |
|
217 } |
|
218 }; |
|
219 GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED, |
|
220 "previous field or previous focus", |
|
221 focusPreviousFieldOrPreviousComponentAction, shiftTab); |
|
222 } |
|
223 |
|
224 public TimeSpinnerEditor(JSpinner spinner) { |
|
225 this((TimeSpinnerModel)spinner.getModel(), createDateFormat(spinner)); |
|
226 } |
|
227 |
|
228 // |
|
229 // Component methods |
|
230 // |
|
231 |
|
232 /* |
|
233 * Every once in a while a bug comes along that can only be worked around by |
|
234 * implementing a truly vile and repulsive hack. This hack violates several |
|
235 * tenets of good programming practice. |
|
236 * |
|
237 * My hand was forced, however, by the extremely limited implementation of |
|
238 * the javax.swing.text.DefaultCaret class. This class refuses to even |
|
239 * consider the possibility that a developer or user might not want every |
|
240 * single selection in a JTextComponent (in this class, numerous and |
|
241 * frequently changed) to be automatically copied to the system selection. |
|
242 * It further complicates the matter with excessively restrictive access |
|
243 * control on several methods which might otherwise be easily overridden to |
|
244 * change this default behavior. |
|
245 * |
|
246 * You might think that overriding copy() and paste() would be sufficient, |
|
247 * but those methods don't apply to the system selection. |
|
248 * |
|
249 * A proper fix might implement a Caret class to do exactly what |
|
250 * DefaultCaret does, minus the updateSystemSelection method. However, |
|
251 * DefaultCaret.java is ~2000 lines long, and our implementation would need |
|
252 * to keep pace with future developements in DefaultCaret. |
|
253 * |
|
254 * So we are left with this. Examining a stack trace to see who's calling |
|
255 * us is nauseatingly hideous, but surprisingly succinct. Throwing a |
|
256 * SecurityException whose handling we can know about only by examining the |
|
257 * DefaultCaret source code (not the API) is the cherry on the icing. |
|
258 */ |
|
259 @Override |
|
260 public Toolkit getToolkit() { |
|
261 StackTraceElement[] trace = Thread.currentThread().getStackTrace(); |
|
262 if (trace.length >= 4) { |
|
263 String className = DefaultCaret.class.getName(); |
|
264 if (trace[2].getClassName().equals(className) && |
|
265 trace[2].getMethodName().equals("getSystemSelection") && |
|
266 trace[3].getClassName().equals(className) && |
|
267 trace[3].getMethodName().equals("updateSystemSelection")) { |
|
268 throw new SecurityException(); |
|
269 } |
|
270 } |
|
271 return super.getToolkit(); |
|
272 } |
|
273 |
|
274 // |
|
275 // TimeSpinnerEditor methods |
|
276 // |
|
277 |
|
278 /** |
|
279 * Focusses/highlights the next field in the {@code DateFormat}, if the last |
|
280 * field is not already focussed. |
|
281 * |
|
282 * @return {@code true} if the next field was successfully focussed, |
|
283 * {@code false} if there is no next field |
|
284 */ |
|
285 public boolean focusNextField() { |
|
286 for (int i = 0; i < positions.length - 1; i++) { |
|
287 if (positions[i] == selected) { |
|
288 int calField = fieldPositionToCalendarField(positions[i + 1]); |
|
289 model.getTimeSelectionModel().setSelectedField(calField); |
|
290 return true; |
|
291 } |
|
292 } |
|
293 return false; |
|
294 } |
|
295 |
|
296 /** |
|
297 * Focusses/highlights the previous field in the {@code DateFormat}, if the |
|
298 * first field is not already focussed. |
|
299 * |
|
300 * @return {@code true} if the previous field was successfully |
|
301 * focussed, {@code false} if there is no previous field |
|
302 */ |
|
303 public boolean focusPreviousField() { |
|
304 for (int i = positions.length - 1; i > 0; i--) { |
|
305 if (positions[i] == selected) { |
|
306 for (i--; i >= 0; i--) { |
|
307 // Does this field actually appear in this format? |
|
308 if (positions[i].getBeginIndex() != |
|
309 positions[i].getEndIndex()) { |
|
310 |
|
311 int calField = fieldPositionToCalendarField( |
|
312 positions[i]); |
|
313 model.getTimeSelectionModel().setSelectedField( |
|
314 calField); |
|
315 return true; |
|
316 } |
|
317 } |
|
318 } |
|
319 } |
|
320 return false; |
|
321 } |
|
322 |
|
323 /** |
|
324 * Gets the {@link TimeSpinnerModel} used by this {@code TimeSpinnerEditor}. |
|
325 */ |
|
326 public TimeSpinnerModel getModel() { |
|
327 return model; |
|
328 } |
|
329 |
|
330 /** |
|
331 * Gets the last time that was set in this field. |
|
332 */ |
|
333 public Date getTime() { |
|
334 return time; |
|
335 } |
|
336 |
|
337 /** |
|
338 * Sets the time in this field. |
|
339 */ |
|
340 public void setTime(Date time) { |
|
341 this.time = time; |
|
342 |
|
343 // Don't react to caret events -- setSelectedField will set the |
|
344 // selection manually (and then reset this value) |
|
345 ignoreCaretEvents = true; |
|
346 |
|
347 setText(format.format(time)); |
|
348 |
|
349 // Set begin/end in each FieldPosition |
|
350 StringBuffer buffer = new StringBuffer(); |
|
351 for (FieldPosition position : positions) { |
|
352 buffer.setLength(0); |
|
353 format.format(time, buffer, position); |
|
354 } |
|
355 |
|
356 // Sort FieldPositions in the order in which they appear |
|
357 Arrays.sort(positions, posComparator); |
|
358 |
|
359 // Restore selection, if possible |
|
360 setSelectedField(this.selected); |
|
361 } |
|
362 |
|
363 // |
|
364 // Private methods |
|
365 // |
|
366 |
|
367 private int distance(int begin, int end, int pos) { |
|
368 assert begin <= end; |
|
369 if (pos <= begin) { |
|
370 return begin - pos; |
|
371 } |
|
372 return pos - end; |
|
373 } |
|
374 |
|
375 private void focusNextFieldOrNextComponent() { |
|
376 if (!focusNextField() && hasFocus()) { |
|
377 transferFocus(); |
|
378 } |
|
379 } |
|
380 |
|
381 private void focusPreviousFieldOrPreviousComponent() { |
|
382 if (!focusPreviousField() && hasFocus()) { |
|
383 transferFocusBackward(); |
|
384 } |
|
385 } |
|
386 |
|
387 private void selectFieldAtCaret() { |
|
388 int caret = getSelectionStart(); |
|
389 |
|
390 // Find FieldPosition closest to caret |
|
391 FieldPosition position = null; |
|
392 int minDistance = 0; |
|
393 for (int i = positions.length - 1; i >= 0; i--) { |
|
394 int begin = positions[i].getBeginIndex(); |
|
395 int end = positions[i].getEndIndex(); |
|
396 |
|
397 // Is this field used in this format? |
|
398 if (begin != end) { |
|
399 int distance = distance(begin, end, caret); |
|
400 if (position == null || distance < minDistance) { |
|
401 position = positions[i]; |
|
402 minDistance = distance; |
|
403 if (distance <= 0) { |
|
404 break; |
|
405 } |
|
406 } else { |
|
407 break; |
|
408 } |
|
409 } |
|
410 } |
|
411 |
|
412 int calField = fieldPositionToCalendarField(position); |
|
413 model.getTimeSelectionModel().setSelectedField(calField); |
|
414 } |
|
415 |
|
416 private FieldPosition calendarFieldToFieldPosition(int calField) { |
|
417 if (calField != -1) { |
|
418 for (int i = positions.length - 1; i >= 0; i--) { |
|
419 // Is this field used in this format? |
|
420 if (positions[i].getBeginIndex() != positions[i].getEndIndex()) |
|
421 { |
|
422 Format.Field fmtField = positions[i].getFieldAttribute(); |
|
423 if (fieldMap.get(fmtField) == calField) { |
|
424 return positions[i]; |
|
425 } |
|
426 } |
|
427 } |
|
428 } |
|
429 return null; |
|
430 } |
|
431 |
|
432 private int fieldPositionToCalendarField(FieldPosition position) { |
|
433 Format.Field fmtField = position.getFieldAttribute(); |
|
434 return fieldMap.get(fmtField); |
|
435 } |
|
436 |
|
437 private void selectionChanged() { |
|
438 int calendarField = model.getTimeSelectionModel().getSelectedField(); |
|
439 FieldPosition position = calendarFieldToFieldPosition(calendarField); |
|
440 setSelectedField(position); |
|
441 } |
|
442 |
|
443 private void setSelectedField(FieldPosition selected) { |
|
444 this.selected = selected; |
|
445 |
|
446 int newSelStart = 0; |
|
447 int newSelEnd = 0; |
|
448 |
|
449 if (selected != null) { |
|
450 newSelStart = selected.getBeginIndex(); |
|
451 newSelEnd = selected.getEndIndex(); |
|
452 } |
|
453 |
|
454 try { |
|
455 int selStart = getSelectionStart(); |
|
456 if (selStart != newSelStart) { |
|
457 // Don't recurse |
|
458 ignoreCaretEvents = true; |
|
459 setCaretPosition(newSelStart); |
|
460 } |
|
461 |
|
462 int selEnd = getSelectionEnd(); |
|
463 if (selEnd != newSelEnd) { |
|
464 // Don't recurse |
|
465 ignoreCaretEvents = true; |
|
466 moveCaretPosition(newSelEnd); |
|
467 } |
|
468 |
|
469 if (ignoreCaretEvents) { |
|
470 EventQueue.invokeLater( |
|
471 new Runnable() { |
|
472 @Override |
|
473 public void run() { |
|
474 ignoreCaretEvents = false; |
|
475 } |
|
476 }); |
|
477 } |
|
478 |
|
479 // Thrown by {set,move}CaretPosition -- see 10392 |
|
480 } catch (IllegalArgumentException ignore) { |
|
481 } |
|
482 } |
|
483 |
|
484 private void updateFromModel() { |
|
485 setTime(model.getTimeModel().getCalendar().getTime()); |
|
486 } |
|
487 |
|
488 // |
|
489 // Static methods |
|
490 // |
|
491 |
|
492 protected static DateFormat createDateFormat(JSpinner spinner) { |
|
493 Locale locale = spinner.getLocale(); |
|
494 |
|
495 DateFormat format = DateFormat.getTimeInstance(DateFormat.MEDIUM, |
|
496 locale == null ? Locale.getDefault() : locale); |
|
497 |
|
498 if (format instanceof SimpleDateFormat) { |
|
499 padUnits((SimpleDateFormat)format); |
|
500 } |
|
501 |
|
502 return format; |
|
503 } |
|
504 |
|
505 protected static void padUnits(SimpleDateFormat format) { |
|
506 String pattern = format.toPattern().replaceAll( |
|
507 "\\b([yMwWDdFHkKhmsS])\\b", "$1$1"); |
|
508 |
|
509 format.applyPattern(pattern); |
|
510 } |
|
511 } |
|