components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/time/TimeSpinnerEditor.java
changeset 3553 f1d133b09a8c
parent 3552 077ebe3d0d24
child 3554 ef58713bafc4
equal deleted inserted replaced
3552:077ebe3d0d24 3553:f1d133b09a8c
     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 }