--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/time/TimeSpinnerEditor.java Thu May 24 04:16:47 2012 -0400
@@ -0,0 +1,511 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package com.oracle.solaris.vp.util.swing.time;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.text.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.DefaultCaret;
+import com.oracle.solaris.vp.util.swing.GUIUtil;
+
+@SuppressWarnings({"serial"})
+public class TimeSpinnerEditor extends JTextField {
+ //
+ // Instance data
+ //
+
+ private DateFormat format;
+ private TimeSpinnerModel model;
+ private Date time;
+ private boolean ignoreCaretEvents;
+ private FieldPosition selected;
+
+ // Map Format.Fields to Calendar fields
+ // These are all only fields that TimeSpinnerModel supports.
+ private Map<Format.Field, Integer> fieldMap;
+ {
+ fieldMap = new HashMap<Format.Field, Integer>();
+ fieldMap.put(DateFormat.Field.AM_PM, Calendar.AM_PM);
+ fieldMap.put(DateFormat.Field.DAY_OF_MONTH, Calendar.DAY_OF_MONTH);
+ fieldMap.put(DateFormat.Field.DAY_OF_WEEK, Calendar.DAY_OF_WEEK);
+ fieldMap.put(DateFormat.Field.DAY_OF_YEAR, Calendar.DAY_OF_YEAR);
+ fieldMap.put(DateFormat.Field.ERA, Calendar.ERA);
+ fieldMap.put(DateFormat.Field.HOUR0, Calendar.HOUR);
+ fieldMap.put(DateFormat.Field.HOUR1, Calendar.HOUR);
+ fieldMap.put(DateFormat.Field.HOUR_OF_DAY0, Calendar.HOUR_OF_DAY);
+ fieldMap.put(DateFormat.Field.HOUR_OF_DAY1, Calendar.HOUR_OF_DAY);
+ fieldMap.put(DateFormat.Field.MILLISECOND, Calendar.MILLISECOND);
+ fieldMap.put(DateFormat.Field.MINUTE, Calendar.MINUTE);
+ fieldMap.put(DateFormat.Field.MONTH, Calendar.MONTH);
+ fieldMap.put(DateFormat.Field.SECOND, Calendar.SECOND);
+ fieldMap.put(DateFormat.Field.TIME_ZONE, Calendar.ZONE_OFFSET);
+ fieldMap.put(DateFormat.Field.WEEK_OF_MONTH, Calendar.WEEK_OF_MONTH);
+ fieldMap.put(DateFormat.Field.WEEK_OF_YEAR, Calendar.WEEK_OF_YEAR);
+ fieldMap.put(DateFormat.Field.YEAR, Calendar.YEAR);
+ };
+
+ private FieldPosition[] positions;
+ {
+ Set<Format.Field> fields = fieldMap.keySet();
+ positions = new FieldPosition[fields.size()];
+ int i = 0;
+ for (Format.Field field : fields) {
+ positions[i++] = new FieldPosition(field);
+ }
+ }
+
+ private Comparator<FieldPosition> posComparator =
+ new Comparator<FieldPosition>() {
+ @Override
+ public int compare(FieldPosition o1, FieldPosition o2) {
+ int begin1 = o1.getBeginIndex();
+ int begin2 = o2.getBeginIndex();
+
+ if (begin1 < begin2) {
+ return -1;
+ } else if (begin1 > begin2) {
+ return 1;
+ }
+
+ int end1 = o1.getEndIndex();
+ int end2 = o2.getEndIndex();
+
+ if (end1 < end2) {
+ return -1;
+ } else if (end1 > end2) {
+ return 1;
+ }
+
+ return 0;
+ }
+ };
+
+ //
+ // Constructors
+ //
+
+ public TimeSpinnerEditor(TimeSpinnerModel model, DateFormat format) {
+ this.model = model;
+ this.format = format;
+
+ setEditable(false);
+
+ model.addChangeListener(
+ new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ updateFromModel();
+ }
+ });
+
+ updateFromModel();
+
+ model.getTimeSelectionModel().addChangeListener(
+ new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ selectionChanged();
+ }
+ });
+
+ addCaretListener(
+ new CaretListener() {
+ @Override
+ public void caretUpdate(CaretEvent e) {
+ if (!ignoreCaretEvents) {
+ EventQueue.invokeLater(
+ new Runnable() {
+ @Override
+ public void run() {
+ selectFieldAtCaret();
+ }
+ });
+ }
+ }
+ });
+
+ // Select first field
+ selectFieldAtCaret();
+
+ addFocusListener(
+ new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ if (getModel().getTimeSelectionModel().getSelectedField() ==
+ -1) {
+ selectFieldAtCaret();
+ }
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ if (!e.isTemporary()) {
+ getModel().getTimeSelectionModel().setSelectedField(-1);
+ }
+ }
+ });
+
+ KeyStroke right = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
+ Action focusNextFieldAction =
+ new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ focusNextField();
+ }
+ };
+ GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED,
+ "next field", focusNextFieldAction, right);
+
+ KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
+ GUIUtil.removeFocusTraversalKeys(this, true, tab);
+ Action focusNextFieldOrNextComponentAction =
+ new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ focusNextFieldOrNextComponent();
+ }
+ };
+ GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED,
+ "next field or next focus", focusNextFieldOrNextComponentAction,
+ tab);
+
+ KeyStroke left = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0);
+ Action focusPreviousFieldAction =
+ new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ focusPreviousField();
+ }
+ };
+ GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED,
+ "previous field", focusPreviousFieldAction, left);
+
+ KeyStroke shiftTab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
+ InputEvent.SHIFT_DOWN_MASK);
+ GUIUtil.removeFocusTraversalKeys(this, false, shiftTab);
+ Action focusPreviousFieldOrPreviousComponentAction =
+ new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ focusPreviousFieldOrPreviousComponent();
+ }
+ };
+ GUIUtil.installKeyBinding(this, JComponent.WHEN_FOCUSED,
+ "previous field or previous focus",
+ focusPreviousFieldOrPreviousComponentAction, shiftTab);
+ }
+
+ public TimeSpinnerEditor(JSpinner spinner) {
+ this((TimeSpinnerModel)spinner.getModel(), createDateFormat(spinner));
+ }
+
+ //
+ // Component methods
+ //
+
+ /*
+ * Every once in a while a bug comes along that can only be worked around by
+ * implementing a truly vile and repulsive hack. This hack violates several
+ * tenets of good programming practice.
+ *
+ * My hand was forced, however, by the extremely limited implementation of
+ * the javax.swing.text.DefaultCaret class. This class refuses to even
+ * consider the possibility that a developer or user might not want every
+ * single selection in a JTextComponent (in this class, numerous and
+ * frequently changed) to be automatically copied to the system selection.
+ * It further complicates the matter with excessively restrictive access
+ * control on several methods which might otherwise be easily overridden to
+ * change this default behavior.
+ *
+ * You might think that overriding copy() and paste() would be sufficient,
+ * but those methods don't apply to the system selection.
+ *
+ * A proper fix might implement a Caret class to do exactly what
+ * DefaultCaret does, minus the updateSystemSelection method. However,
+ * DefaultCaret.java is ~2000 lines long, and our implementation would need
+ * to keep pace with future developements in DefaultCaret.
+ *
+ * So we are left with this. Examining a stack trace to see who's calling
+ * us is nauseatingly hideous, but surprisingly succinct. Throwing a
+ * SecurityException whose handling we can know about only by examining the
+ * DefaultCaret source code (not the API) is the cherry on the icing.
+ */
+ @Override
+ public Toolkit getToolkit() {
+ StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+ if (trace.length >= 4) {
+ String className = DefaultCaret.class.getName();
+ if (trace[2].getClassName().equals(className) &&
+ trace[2].getMethodName().equals("getSystemSelection") &&
+ trace[3].getClassName().equals(className) &&
+ trace[3].getMethodName().equals("updateSystemSelection")) {
+ throw new SecurityException();
+ }
+ }
+ return super.getToolkit();
+ }
+
+ //
+ // TimeSpinnerEditor methods
+ //
+
+ /**
+ * Focusses/highlights the next field in the {@code DateFormat}, if the last
+ * field is not already focussed.
+ *
+ * @return {@code true} if the next field was successfully focussed,
+ * {@code false} if there is no next field
+ */
+ public boolean focusNextField() {
+ for (int i = 0; i < positions.length - 1; i++) {
+ if (positions[i] == selected) {
+ int calField = fieldPositionToCalendarField(positions[i + 1]);
+ model.getTimeSelectionModel().setSelectedField(calField);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Focusses/highlights the previous field in the {@code DateFormat}, if the
+ * first field is not already focussed.
+ *
+ * @return {@code true} if the previous field was successfully
+ * focussed, {@code false} if there is no previous field
+ */
+ public boolean focusPreviousField() {
+ for (int i = positions.length - 1; i > 0; i--) {
+ if (positions[i] == selected) {
+ for (i--; i >= 0; i--) {
+ // Does this field actually appear in this format?
+ if (positions[i].getBeginIndex() !=
+ positions[i].getEndIndex()) {
+
+ int calField = fieldPositionToCalendarField(
+ positions[i]);
+ model.getTimeSelectionModel().setSelectedField(
+ calField);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the {@link TimeSpinnerModel} used by this {@code TimeSpinnerEditor}.
+ */
+ public TimeSpinnerModel getModel() {
+ return model;
+ }
+
+ /**
+ * Gets the last time that was set in this field.
+ */
+ public Date getTime() {
+ return time;
+ }
+
+ /**
+ * Sets the time in this field.
+ */
+ public void setTime(Date time) {
+ this.time = time;
+
+ // Don't react to caret events -- setSelectedField will set the
+ // selection manually (and then reset this value)
+ ignoreCaretEvents = true;
+
+ setText(format.format(time));
+
+ // Set begin/end in each FieldPosition
+ StringBuffer buffer = new StringBuffer();
+ for (FieldPosition position : positions) {
+ buffer.setLength(0);
+ format.format(time, buffer, position);
+ }
+
+ // Sort FieldPositions in the order in which they appear
+ Arrays.sort(positions, posComparator);
+
+ // Restore selection, if possible
+ setSelectedField(this.selected);
+ }
+
+ //
+ // Private methods
+ //
+
+ private int distance(int begin, int end, int pos) {
+ assert begin <= end;
+ if (pos <= begin) {
+ return begin - pos;
+ }
+ return pos - end;
+ }
+
+ private void focusNextFieldOrNextComponent() {
+ if (!focusNextField() && hasFocus()) {
+ transferFocus();
+ }
+ }
+
+ private void focusPreviousFieldOrPreviousComponent() {
+ if (!focusPreviousField() && hasFocus()) {
+ transferFocusBackward();
+ }
+ }
+
+ private void selectFieldAtCaret() {
+ int caret = getSelectionStart();
+
+ // Find FieldPosition closest to caret
+ FieldPosition position = null;
+ int minDistance = 0;
+ for (int i = positions.length - 1; i >= 0; i--) {
+ int begin = positions[i].getBeginIndex();
+ int end = positions[i].getEndIndex();
+
+ // Is this field used in this format?
+ if (begin != end) {
+ int distance = distance(begin, end, caret);
+ if (position == null || distance < minDistance) {
+ position = positions[i];
+ minDistance = distance;
+ if (distance <= 0) {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ int calField = fieldPositionToCalendarField(position);
+ model.getTimeSelectionModel().setSelectedField(calField);
+ }
+
+ private FieldPosition calendarFieldToFieldPosition(int calField) {
+ if (calField != -1) {
+ for (int i = positions.length - 1; i >= 0; i--) {
+ // Is this field used in this format?
+ if (positions[i].getBeginIndex() != positions[i].getEndIndex())
+ {
+ Format.Field fmtField = positions[i].getFieldAttribute();
+ if (fieldMap.get(fmtField) == calField) {
+ return positions[i];
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private int fieldPositionToCalendarField(FieldPosition position) {
+ Format.Field fmtField = position.getFieldAttribute();
+ return fieldMap.get(fmtField);
+ }
+
+ private void selectionChanged() {
+ int calendarField = model.getTimeSelectionModel().getSelectedField();
+ FieldPosition position = calendarFieldToFieldPosition(calendarField);
+ setSelectedField(position);
+ }
+
+ private void setSelectedField(FieldPosition selected) {
+ this.selected = selected;
+
+ int newSelStart = 0;
+ int newSelEnd = 0;
+
+ if (selected != null) {
+ newSelStart = selected.getBeginIndex();
+ newSelEnd = selected.getEndIndex();
+ }
+
+ try {
+ int selStart = getSelectionStart();
+ if (selStart != newSelStart) {
+ // Don't recurse
+ ignoreCaretEvents = true;
+ setCaretPosition(newSelStart);
+ }
+
+ int selEnd = getSelectionEnd();
+ if (selEnd != newSelEnd) {
+ // Don't recurse
+ ignoreCaretEvents = true;
+ moveCaretPosition(newSelEnd);
+ }
+
+ if (ignoreCaretEvents) {
+ EventQueue.invokeLater(
+ new Runnable() {
+ @Override
+ public void run() {
+ ignoreCaretEvents = false;
+ }
+ });
+ }
+
+ // Thrown by {set,move}CaretPosition -- see 10392
+ } catch (IllegalArgumentException ignore) {
+ }
+ }
+
+ private void updateFromModel() {
+ setTime(model.getTimeModel().getCalendar().getTime());
+ }
+
+ //
+ // Static methods
+ //
+
+ protected static DateFormat createDateFormat(JSpinner spinner) {
+ Locale locale = spinner.getLocale();
+
+ DateFormat format = DateFormat.getTimeInstance(DateFormat.MEDIUM,
+ locale == null ? Locale.getDefault() : locale);
+
+ if (format instanceof SimpleDateFormat) {
+ padUnits((SimpleDateFormat)format);
+ }
+
+ return format;
+ }
+
+ protected static void padUnits(SimpleDateFormat format) {
+ String pattern = format.toPattern().replaceAll(
+ "\\b([yMwWDdFHkKhmsS])\\b", "$1$1");
+
+ format.applyPattern(pattern);
+ }
+}