org.jdesktop.swingx.plaf.basic.BasicDatePickerUI Maven / Gradle / Ivy
/*
* $Id: BasicDatePickerUI.java 4107 2012-01-19 14:24:15Z kleopatra $
*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx.plaf.basic;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.LayoutManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.border.Border;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.View;
import org.jdesktop.swingx.JXDatePicker;
import org.jdesktop.swingx.JXMonthView;
import org.jdesktop.swingx.SwingXUtilities;
import org.jdesktop.swingx.calendar.CalendarUtils;
import org.jdesktop.swingx.calendar.DatePickerFormatter;
import org.jdesktop.swingx.calendar.DateSelectionModel;
import org.jdesktop.swingx.calendar.DatePickerFormatter.DatePickerFormatterUIResource;
import org.jdesktop.swingx.event.DateSelectionEvent;
import org.jdesktop.swingx.event.DateSelectionListener;
import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
import org.jdesktop.swingx.plaf.DatePickerUI;
/**
* The basic implementation of a DatePickerUI
.
*
*
*
* @author Joshua Outwater
* @author Jeanette Winzenburg
*/
public class BasicDatePickerUI extends DatePickerUI {
@SuppressWarnings("all")
private static final Logger LOG = Logger.getLogger(BasicDatePickerUI.class
.getName());
protected JXDatePicker datePicker;
private JButton popupButton;
private BasicDatePickerPopup popup;
private Handler handler;
/*
* shared listeners
*/
protected PropertyChangeListener propertyChangeListener;
private FocusListener focusListener;
/*
* listener's for the arrow button
*/
protected MouseListener mouseListener;
protected MouseMotionListener mouseMotionListener;
/*
* listeners for the picker's editor
*/
private ActionListener editorActionListener;
private EditorCancelAction editorCancelAction;
private PropertyChangeListener editorPropertyListener;
/**
* listeners for the picker's monthview
*/
private DateSelectionListener monthViewSelectionListener;
private ActionListener monthViewActionListener;
private PropertyChangeListener monthViewPropertyListener;
private PopupRemover popupRemover;
private PopupMenuListener popupMenuListener;
@SuppressWarnings({"UnusedDeclaration"})
public static ComponentUI createUI(JComponent c) {
return new BasicDatePickerUI();
}
@Override
public void installUI(JComponent c) {
datePicker = (JXDatePicker)c;
datePicker.setLayout(createLayoutManager());
installComponents();
installDefaults();
installKeyboardActions();
installListeners();
}
@Override
public void uninstallUI(JComponent c) {
uninstallListeners();
uninstallKeyboardActions();
uninstallDefaults();
uninstallComponents();
datePicker.setLayout(null);
datePicker = null;
}
protected void installComponents() {
JFormattedTextField editor = datePicker.getEditor();
if (SwingXUtilities.isUIInstallable(editor)) {
DateFormat[] formats = getCustomFormats(editor);
// we are not yet listening ...
datePicker.setEditor(createEditor());
if (formats != null) {
datePicker.setFormats(formats);
}
}
updateFromEditorChanged(null, false);
popupButton = createPopupButton();
if (popupButton != null) {
// this is a trick to get hold of the client prop which
// prevents closing of the popup
JComboBox box = new JComboBox();
Object preventHide = box.getClientProperty("doNotCancelPopup");
popupButton.putClientProperty("doNotCancelPopup", preventHide);
datePicker.add(popupButton);
popupButton.setEnabled(datePicker.isEnabled());
popupButton.setInheritsPopupMenu(true);
}
updateChildLocale(datePicker.getLocale());
}
/**
* Checks and returns custom formats on the editor, if any.
*
* @param editor the editor to check
* @return the custom formats uses in the editor or null if it had
* used defaults as defined in the datepicker properties
*/
private DateFormat[] getCustomFormats(JFormattedTextField editor) {
DateFormat[] formats = null;
if (editor != null) {
AbstractFormatterFactory factory = editor.getFormatterFactory();
if (factory != null) {
AbstractFormatter formatter = factory.getFormatter(editor);
// fix for #1144: classCastException for custom formatters
// PENDING JW: revisit for #1138
if ((formatter instanceof DatePickerFormatter) && !(formatter instanceof UIResource)) {
// if (!(formatter instanceof DatePickerFormatterUIResource)) {
formats = ((DatePickerFormatter) formatter).getFormats();
}
}
}
return formats;
}
protected void uninstallComponents() {
JFormattedTextField editor = datePicker.getEditor();
if (editor != null) {
datePicker.remove(editor);
}
if (popupButton != null) {
datePicker.remove(popupButton);
popupButton = null;
}
}
/**
* Installs DatePicker default properties.
*/
protected void installDefaults() {
// PENDING JW: currently this is for testing only.
boolean zoomable = Boolean.TRUE.equals(UIManager.get("JXDatePicker.forceZoomable"));
if (zoomable) {
datePicker.getMonthView().setZoomable(true);
}
}
protected void uninstallDefaults() {
}
protected void installKeyboardActions() {
// install picker's actions
ActionMap pickerMap = datePicker.getActionMap();
pickerMap.put(JXDatePicker.CANCEL_KEY, createCancelAction());
pickerMap.put(JXDatePicker.COMMIT_KEY, createCommitAction());
pickerMap.put(JXDatePicker.HOME_NAVIGATE_KEY, createHomeAction(false));
pickerMap.put(JXDatePicker.HOME_COMMIT_KEY, createHomeAction(true));
TogglePopupAction popupAction = createTogglePopupAction();
pickerMap.put("TOGGLE_POPUP", popupAction);
InputMap pickerInputMap = datePicker.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
pickerInputMap.put(KeyStroke.getKeyStroke("ENTER"), JXDatePicker.COMMIT_KEY);
pickerInputMap.put(KeyStroke.getKeyStroke("ESCAPE"), JXDatePicker.CANCEL_KEY);
// PENDING: get from LF
pickerInputMap.put(KeyStroke.getKeyStroke("F5"), JXDatePicker.HOME_COMMIT_KEY);
pickerInputMap.put(KeyStroke.getKeyStroke("shift F5"), JXDatePicker.HOME_NAVIGATE_KEY);
pickerInputMap.put(KeyStroke.getKeyStroke("alt DOWN"), "TOGGLE_POPUP");
installLinkPanelKeyboardActions();
}
protected void uninstallKeyboardActions() {
uninstallLinkPanelKeyboardActions(datePicker.getLinkPanel());
}
/**
* Installs actions and key bindings on the datePicker's linkPanel. Does
* nothing if the linkPanel is null.
*
* PRE: keybindings installed on picker.
*/
protected void installLinkPanelKeyboardActions() {
if (datePicker.getLinkPanel() == null)
return;
ActionMap map = datePicker.getLinkPanel().getActionMap();
map.put(JXDatePicker.HOME_COMMIT_KEY, datePicker.getActionMap().get(
JXDatePicker.HOME_COMMIT_KEY));
map.put(JXDatePicker.HOME_NAVIGATE_KEY, datePicker.getActionMap().get(
JXDatePicker.HOME_NAVIGATE_KEY));
InputMap inputMap = datePicker.getLinkPanel().getInputMap(
JComponent.WHEN_IN_FOCUSED_WINDOW);
// PENDING: get from LF
inputMap.put(KeyStroke.getKeyStroke("F5"),
JXDatePicker.HOME_COMMIT_KEY);
inputMap.put(KeyStroke.getKeyStroke("shift F5"),
JXDatePicker.HOME_NAVIGATE_KEY);
}
/**
* Uninstalls actions and key bindings from linkPanel. Does nothing if the
* linkPanel is null.
*
* @param panel the component to uninstall
*
*/
protected void uninstallLinkPanelKeyboardActions(JComponent panel) {
if (panel == null) return;
ActionMap map = panel.getActionMap();
map.remove(JXDatePicker.HOME_COMMIT_KEY);
map.remove(JXDatePicker.HOME_NAVIGATE_KEY);
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
// PENDING: get from LF
inputMap.remove(KeyStroke.getKeyStroke("F5"));
inputMap.remove(KeyStroke.getKeyStroke("shift F5"));
}
/**
* Creates and installs all listeners to all components.
*
*/
protected void installListeners() {
/*
* create the listeners.
*/
// propertyListener for datePicker
propertyChangeListener = createPropertyChangeListener();
// mouseListener (for popup button only) ?
mouseListener = createMouseListener();
mouseMotionListener = createMouseMotionListener();
// shared focuslistener (installed to picker and editor)
focusListener = createFocusListener();
// editor related listeners
editorActionListener = createEditorActionListener();
editorPropertyListener = createEditorPropertyListener();
// montheView related listeners
monthViewSelectionListener = createMonthViewSelectionListener();
monthViewActionListener = createMonthViewActionListener();
monthViewPropertyListener = createMonthViewPropertyListener();
popupRemover = new PopupRemover();
/*
* install the listeners
*/
// picker
datePicker.addPropertyChangeListener(propertyChangeListener);
datePicker.addFocusListener(focusListener);
if (popupButton != null) {
// JW: which property do we want to monitor?
popupButton.addPropertyChangeListener(propertyChangeListener);
popupButton.addMouseListener(mouseListener);
popupButton.addMouseMotionListener(mouseMotionListener);
}
updateEditorListeners(null);
// JW the following does more than installing the listeners ..
// synchs properties of datepicker to monthView's
// prepares monthview for usage in popup
// synch the date
// Relies on being the last thing done in the install ..
//
updateFromMonthViewChanged(null);
}
/**
* Uninstalls and nulls all listeners which had been installed
* by this delegate.
*
*/
protected void uninstallListeners() {
// datePicker
datePicker.removePropertyChangeListener(propertyChangeListener);
datePicker.removeFocusListener(focusListener);
// monthView
datePicker.getMonthView().getSelectionModel().removeDateSelectionListener(monthViewSelectionListener);
datePicker.getMonthView().removeActionListener(monthViewActionListener);
datePicker.getMonthView().removePropertyChangeListener(propertyChangeListener);
// JW: when can that be null?
// maybe in the very beginning? if some code calls ui.uninstall
// before ui.install? The editor is created by the ui.
if (datePicker.getEditor() != null) {
uninstallEditorListeners(datePicker.getEditor());
}
if (popupButton != null) {
popupButton.removePropertyChangeListener(propertyChangeListener);
popupButton.removeMouseListener(mouseListener);
popupButton.removeMouseMotionListener(mouseMotionListener);
}
popupRemover.unload();
popupRemover = null;
propertyChangeListener = null;
mouseListener = null;
mouseMotionListener = null;
editorActionListener = null;
editorPropertyListener = null;
monthViewSelectionListener = null;
monthViewActionListener = null;
monthViewPropertyListener = null;
handler = null;
}
// --------------------- wiring listeners
/**
* Wires the picker's monthView related listening. Removes all
* listeners from the given old view and adds the listeners to
* the current monthView.
*
* @param oldMonthView
*/
protected void updateMonthViewListeners(JXMonthView oldMonthView) {
DateSelectionModel oldModel = null;
if (oldMonthView != null) {
oldMonthView.removePropertyChangeListener(monthViewPropertyListener);
oldMonthView.removeActionListener(monthViewActionListener);
oldModel = oldMonthView.getSelectionModel();
}
datePicker.getMonthView().addPropertyChangeListener(monthViewPropertyListener);
datePicker.getMonthView().addActionListener(monthViewActionListener);
updateSelectionModelListeners(oldModel);
}
/**
* Wires the picker's editor related listening and actions. Removes
* listeners/actions from the old editor and adds them to
* the new editor.
*
* @param oldEditor the pickers editor before the change
*/
protected void updateEditorListeners(JFormattedTextField oldEditor) {
if (oldEditor != null) {
uninstallEditorListeners(oldEditor);
}
datePicker.getEditor().addPropertyChangeListener(editorPropertyListener);
datePicker.getEditor().addActionListener(editorActionListener);
datePicker.getEditor().addFocusListener(focusListener);
editorCancelAction = new EditorCancelAction(datePicker.getEditor());
}
/**
* Uninstalls all listeners and actions which have been installed
* by this delegate from the given editor.
*
* @param oldEditor the editor to uninstall.
*/
private void uninstallEditorListeners(JFormattedTextField oldEditor) {
oldEditor.removePropertyChangeListener(editorPropertyListener);
oldEditor.removeActionListener(editorActionListener);
oldEditor.removeFocusListener(focusListener);
if (editorCancelAction != null) {
editorCancelAction.uninstall();
editorCancelAction = null;
}
}
/**
* Wires monthView's selection model listening. Removes the
* selection listener from the old model and add to the new model.
*
* @param oldModel the dateSelectionModel before the change, may be null.
*/
protected void updateSelectionModelListeners(DateSelectionModel oldModel) {
if (oldModel != null) {
oldModel.removeDateSelectionListener(monthViewSelectionListener);
}
datePicker.getMonthView().getSelectionModel()
.addDateSelectionListener(monthViewSelectionListener);
}
// ---------------- component creation
/**
* Creates the editor used to edit the date selection. The editor is
* configured with the default DatePickerFormatter marked as UIResource.
*
* @return an instance of a JFormattedTextField
*/
protected JFormattedTextField createEditor() {
JFormattedTextField f = new DefaultEditor(
new DatePickerFormatterUIResource(datePicker.getLocale()));
f.setName("dateField");
// this produces a fixed pref widths, looking a bit funny
// int columns = UIManagerExt.getInt("JXDatePicker.numColumns", null);
// if (columns > 0) {
// f.setColumns(columns);
// }
// that's always 0 as it comes from the resourcebundle
// f.setColumns(UIManager.getInt("JXDatePicker.numColumns"));
Border border = UIManager.getBorder("JXDatePicker.border");
if (border != null) {
f.setBorder(border);
}
return f;
}
protected JButton createPopupButton() {
JButton b = new JButton();
b.setName("popupButton");
b.setRolloverEnabled(false);
b.setMargin(new Insets(0, 3, 0, 3));
Icon icon = UIManager.getIcon("JXDatePicker.arrowIcon");
if (icon == null) {
icon = (Icon)UIManager.get("Tree.expandedIcon");
}
b.setIcon(icon);
b.setFocusable(false);
return b;
}
/**
*
* A subclass of JFormattedTextField which calculates a "reasonable"
* minimum preferred size, independent of value/text.
*
* Note: how to find the "reasonable" width is open to discussion.
* This implementation creates another datepicker, feeds it with
* the formats and asks its prefWidth.
*
* PENDING: there's a resource property JXDatePicker.numColumns - why
* don't we use it?
*/
private class DefaultEditor extends JFormattedTextField implements UIResource {
public DefaultEditor(AbstractFormatter formatter) {
super(formatter);
}
/**
* {@inheritDoc}
*
* Overridden to return a preferred size which has a reasonable lower bound.
*/
@Override
public Dimension getPreferredSize() {
Dimension preferredSize = super.getPreferredSize();
if (getColumns() <= 0) {
Dimension compare = getCompareMinimumSize();
if (preferredSize.width < compare.width) {
return compare;
}
}
return preferredSize;
}
/**
* {@inheritDoc}
*
* Overridden to return the preferred size.
*/
@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
private Dimension getCompareMinimumSize() {
JFormattedTextField field = new JFormattedTextField(getFormatter());
field.setMargin(getMargin());
field.setBorder(getBorder());
field.setFont(getFont());
field.setValue(new Date());
Dimension min = field.getPreferredSize();
field.setValue(null);
min.width += Math.max(field.getPreferredSize().width, 4);
return min;
}
}
// ---------------- Layout
/**
* {@inheritDoc}
*/
@Override
public Dimension getMinimumSize(JComponent c) {
return getPreferredSize(c);
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getPreferredSize(JComponent c) {
Dimension dim = datePicker.getEditor().getPreferredSize();
if (popupButton != null) {
dim.width += popupButton.getPreferredSize().width;
}
Insets insets = datePicker.getInsets();
dim.width += insets.left + insets.right;
dim.height += insets.top + insets.bottom;
return (Dimension)dim.clone();
}
@Override
public int getBaseline(int width, int height) {
JFormattedTextField editor = datePicker.getEditor();
View rootView = editor.getUI().getRootView(editor);
if (rootView.getViewCount() > 0) {
Insets insets = editor.getInsets();
Insets insetsOut = datePicker.getInsets();
int nh = height - insets.top - insets.bottom
- insetsOut.top - insetsOut.bottom;
int y = insets.top + insetsOut.top;
View fieldView = rootView.getView(0);
int vspan = (int) fieldView.getPreferredSpan(View.Y_AXIS);
if (nh != vspan) {
int slop = nh - vspan;
y += slop / 2;
}
FontMetrics fm = editor.getFontMetrics(editor.getFont());
y += fm.getAscent();
return y;
}
return -1;
}
//------------------------------- controller methods/classes
/**
* {@inheritDoc}
*/
@Override
public Date getSelectableDate(Date date) throws PropertyVetoException {
Date cleaned = date == null ? null :
datePicker.getMonthView().getSelectionModel().getNormalizedDate(date);
if (CalendarUtils.areEqual(cleaned, datePicker.getDate())) {
// one place to interrupt the update spiral
throw new PropertyVetoException("date not selectable", null);
}
if (cleaned == null) return cleaned;
if (datePicker.getMonthView().isUnselectableDate(cleaned)) {
throw new PropertyVetoException("date not selectable", null);
}
return cleaned;
}
//-------------------- update methods called from listeners
/**
* Updates internals after picker's date property changed.
*/
protected void updateFromDateChanged() {
Date visibleHook = datePicker.getDate() != null ?
datePicker.getDate() : datePicker.getLinkDay();
datePicker.getMonthView().ensureDateVisible(visibleHook);
datePicker.getEditor().setValue(datePicker.getDate());
}
/**
* Updates date related properties in picker/monthView
* after a change in the editor's value. Reverts the
* value if the new date is unselectable.
*
* @param oldDate the editor value before the change
* @param newDate the editor value after the change
*/
protected void updateFromValueChanged(Date oldDate, Date newDate) {
if ((newDate != null) && datePicker.getMonthView().isUnselectableDate(newDate)) {
revertValue(oldDate);
return;
}
// the other place to interrupt the update spiral
if (!CalendarUtils.areEqual(newDate, datePicker.getMonthView().getSelectionDate())) {
datePicker.getMonthView().setSelectionDate(newDate);
}
datePicker.setDate(newDate);
}
/**
* PENDING: currently this resets at once - but it's a no-no,
* because it happens during notification
*
* @param oldDate the old date to revert to
*/
private void revertValue(Date oldDate) {
datePicker.getEditor().setValue(oldDate);
}
/**
* Updates date related properties picker/editor
* after a change in the monthView's
* selection.
*
* Here: does nothing if the change is intermediate.
*
* PENDNG JW: shouldn't we listen to actionEvents then?
*
* @param eventType the type of the selection change
* @param adjusting flag to indicate whether the the selection change
* is intermediate
*/
protected void updateFromSelectionChanged(EventType eventType, boolean adjusting) {
if (adjusting) return;
updateEditorValue();
}
/**
* Updates internals after the picker's monthView has changed.
*
* Cleans to popup. Wires the listeners. Updates date.
* Updates formats' timezone.
*
* @param oldMonthView the picker's monthView before the change,
* may be null.
*/
protected void updateFromMonthViewChanged(JXMonthView oldMonthView) {
uninstallPopup();
updateMonthViewListeners(oldMonthView);
TimeZone oldTimeZone = null;
if (oldMonthView != null) {
oldMonthView.setComponentInputMapEnabled(false);
oldTimeZone = oldMonthView.getTimeZone();
}
datePicker.getMonthView().setComponentInputMapEnabled(true);
updateTimeZone(oldTimeZone);
updateEditorValue();
}
/**
* Updates internals after the picker's editor property
* has changed.
*
* Updates the picker's children. Removes the old editor and
* adds the new editor. Wires the editor listeners, it the flag
* set. Typically, this method is called during installing the
* componentUI with the flag set to false and true at all other
* moments.
*
*
* @param oldEditor the picker's editor before the change,
* may be null.
* @param updateListeners a flag to indicate whether the listeners
* are ready for usage.
*/
protected void updateFromEditorChanged(JFormattedTextField oldEditor,
boolean updateListeners) {
if (oldEditor != null) {
datePicker.remove(oldEditor);
oldEditor.putClientProperty("doNotCancelPopup", null);
}
datePicker.add(datePicker.getEditor());
// this is a trick to get hold of the client prop which
// prevents closing of the popup
JComboBox box = new JComboBox();
Object preventHide = box.getClientProperty("doNotCancelPopup");
datePicker.getEditor().putClientProperty("doNotCancelPopup", preventHide);
datePicker.getEditor().setInheritsPopupMenu(true);
updateEditorValue();
updateEditorProperties();
if (updateListeners) {
updateEditorListeners(oldEditor);
datePicker.revalidate();
}
}
/**
* Synchronizes the properties of the current editor to the properties of
* the JXDatePicker.
*/
private void updateEditorProperties() {
datePicker.getEditor().setEnabled(datePicker.isEnabled());
datePicker.getEditor().setEditable(datePicker.isEditable());
}
/**
* Updates internals after the selection model changed.
*
* @param oldModel the model before the change.
*/
protected void updateFromSelectionModelChanged(DateSelectionModel oldModel) {
updateSelectionModelListeners(oldModel);
updateEditorValue();
}
/**
* Sets the editor value to the model's selectedDate.
*/
private void updateEditorValue() {
datePicker.getEditor().setValue(datePicker.getMonthView().getSelectionDate());
}
//---------------------- updating other properties
/**
* Updates properties which depend on the picker's editable.
*
*/
protected void updateFromEditableChanged() {
boolean isEditable = datePicker.isEditable();
// PENDING JW: revisit - align with combo's editable?
datePicker.getMonthView().setEnabled(isEditable);
datePicker.getEditor().setEditable(isEditable);
/*
* PatrykRy: Commit today date is not allowed if datepicker is not editable!
*/
setActionEnabled(JXDatePicker.HOME_COMMIT_KEY, isEditable);
// for consistency, synch navigation as well
setActionEnabled(JXDatePicker.HOME_NAVIGATE_KEY, isEditable);
}
/**
* Update properties which depend on the picker's enabled.
*/
protected void updateFromEnabledChanged() {
boolean isEnabled = datePicker.isEnabled();
popupButton.setEnabled(isEnabled);
datePicker.getEditor().setEnabled(isEnabled);
}
/**
*
* @param key
* @param enabled
*/
private void setActionEnabled(String key, boolean enabled) {
Action action = datePicker.getActionMap().get(key);
if (action != null) {
action.setEnabled(enabled);
}
}
/**
* Updates the picker's formats to the given TimeZone.
* @param zone the timezone to set on the formats.
*/
protected void updateFormatsFromTimeZone(TimeZone zone) {
for (DateFormat format : datePicker.getFormats()) {
format.setTimeZone(zone);
}
}
/**
* Updates picker's timezone dependent properties on change notification
* from the associated monthView.
*
* PENDING JW: DatePicker needs to send notification on timezone change?
*
* @param old the timezone before the change.
*/
protected void updateTimeZone(TimeZone old) {
updateFormatsFromTimeZone(datePicker.getTimeZone());
updateLinkDate();
}
/**
* Updates the picker's linkDate to be in synch with monthView's today.
*/
protected void updateLinkDate() {
datePicker.setLinkDay(datePicker.getMonthView().getToday());
}
/**
* Called form property listener, updates all components locale, formats
* etc.
*
* @author PeS
*/
protected void updateLocale() {
Locale locale = datePicker.getLocale();
updateFormatLocale(locale);
updateChildLocale(locale);
}
private void updateFormatLocale(Locale locale) {
if (locale != null) {
// PENDING JW: timezone?
if (getCustomFormats(datePicker.getEditor()) == null) {
datePicker.getEditor().setFormatterFactory(
new DefaultFormatterFactory(
new DatePickerFormatterUIResource(locale)));
}
}
}
private void updateChildLocale(Locale locale) {
if (locale != null) {
datePicker.getEditor().setLocale(locale);
datePicker.getLinkPanel().setLocale(locale);
datePicker.getMonthView().setLocale(locale);
}
}
/**
* @param oldLinkPanel
*
*/
protected void updateLinkPanel(JComponent oldLinkPanel) {
if (oldLinkPanel != null) {
uninstallLinkPanelKeyboardActions(oldLinkPanel);
}
installLinkPanelKeyboardActions();
if (popup != null) {
popup.updateLinkPanel(oldLinkPanel);
}
}
//------------------- methods called by installed actions
/**
*
*/
protected void commit() {
hidePopup();
try {
datePicker.commitEdit();
} catch (ParseException ex) {
// can't help it
}
}
/**
*
*/
protected void cancel() {
if (isPopupVisible()) {
popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
}
hidePopup();
datePicker.cancelEdit();
}
/**
* PENDING: widened access for debugging - need api to
* control popup visibility?
*/
public void hidePopup() {
if (popup != null) popup.setVisible(false);
}
public boolean isPopupVisible() {
if (popup != null) {
return popup.isVisible();
}
return false;
}
/**
* Navigates to linkDate. If commit, the linkDate is selected
* and committed. If not commit, the linkDate is scrolled to visible, if the
* monthview is open, does nothing for invisible monthView.
*
* @param commit boolean to indicate whether the linkDate should be
* selected and committed
*/
protected void home(boolean commit) {
if (commit) {
Calendar cal = datePicker.getMonthView().getCalendar();
cal.setTime(datePicker.getLinkDay());
datePicker.getMonthView().setSelectionDate(cal.getTime());
datePicker.getMonthView().commitSelection();
} else {
datePicker.getMonthView().ensureDateVisible(datePicker.getLinkDay());
}
}
//---------------------- other stuff
/**
* Creates and returns the action for committing the picker's
* input.
*
* @return
*/
private Action createCommitAction() {
Action action = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
commit();
}
};
return action;
}
/**
* Creates and returns the action for cancel the picker's
* edit.
*
* @return
*/
private Action createCancelAction() {
Action action = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
cancel();
}
};
return action;
}
private Action createHomeAction(final boolean commit) {
Action action = new AbstractAction( ) {
@Override
public void actionPerformed(ActionEvent e) {
home(commit);
}
};
return action ;
}
/**
* The wrapper for the editor cancel action.
*
* PENDING: Need to extend TestAction?
*
*/
public class EditorCancelAction extends AbstractAction {
private JFormattedTextField editor;
private Action cancelAction;
public static final String TEXT_CANCEL_KEY = "reset-field-edit";
public EditorCancelAction(JFormattedTextField field) {
install(field);
}
/**
* Resets the contained editors actionMap to original and
* nulls all fields.
* NOTE: after calling this method the action must not be
* used! Create a new one for the same or another editor.
*
*/
public void uninstall() {
editor.getActionMap().remove(TEXT_CANCEL_KEY);
cancelAction = null;
editor = null;
}
/**
* @param editor
*/
private void install(JFormattedTextField editor) {
this.editor = editor;
cancelAction = editor.getActionMap().get(TEXT_CANCEL_KEY);
editor.getActionMap().put(TEXT_CANCEL_KEY, this);
}
@Override
public void actionPerformed(ActionEvent e) {
cancelAction.actionPerformed(null);
cancel();
}
}
/**
* Creates and returns the action which toggles the visibility of the popup.
*
* @return the action which toggles the visibility of the popup.
*/
protected TogglePopupAction createTogglePopupAction() {
return new TogglePopupAction();
}
/**
* Toggles the popups visibility after preparing internal state.
*
*
*/
public void toggleShowPopup() {
if (popup == null) {
installPopup();
}
if (popup.isVisible()) {
popup.setVisible(false);
} else {
// PENDING JW: Issue 757-swing - datePicker firing focusLost on
// opening
// not with following line - but need to run tests
datePicker.getEditor().requestFocusInWindow();
// datePicker.requestFocusInWindow();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// if (datePicker.getParent() == null) {
// // Tracking #1372-swingx - parent is null if used as
// // DatePickerCellEditor,
// // two different editors, clickCountToStart == 1 and
// // Metal
// // as a first hot fix, we back out
// LOG.info("couldn't show popup for: " + datePicker.getName());
// return;
// }
popup.show(datePicker, 0, datePicker.getHeight());
}
});
}
}
/**
* Creates the popup and registers the popup listener. All internal
* methods must use this method instead of calling createPopup directly.
*/
protected void installPopup() {
popup = createMonthViewPopup();
popup.addPopupMenuListener(getPopupMenuListener());
}
/**
* Removes the popup listener from the popup and null it, if
* it was not null. All internal popup removal/replacement must
* use this method instead of nulling directly.
*
*/
protected void uninstallPopup() {
if (popup != null) {
popup.removePopupMenuListener(getPopupMenuListener());
}
popup = null;
}
/**
* Returns the PopupMenuListener for the MonthView popup. Lazily created.
*
* @return the popupuMenuListener to install on the popup
*/
protected PopupMenuListener getPopupMenuListener() {
if (popupMenuListener == null) {
popupMenuListener = createPopupMenuListener();
}
return popupMenuListener;
}
/**
* Creates and returns a PopupMenuListener.
*
* PENDING JW: the listener management assumes a stateless implementation
* relative to the popup/picker. Custom implementations should take care
* to not keep any references.
*
* @return
*/
protected PopupMenuListener createPopupMenuListener() {
PopupMenuListener l= new PopupMenuListener() {
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
PopupMenuListener[] ls = datePicker.getPopupMenuListeners();
PopupMenuEvent retargeted = null;
for (PopupMenuListener listener : ls) {
if (retargeted == null) {
retargeted = new PopupMenuEvent(datePicker);
}
listener.popupMenuCanceled(retargeted);
}
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
PopupMenuListener[] ls = datePicker.getPopupMenuListeners();
PopupMenuEvent retargeted = null;
for (PopupMenuListener listener : ls) {
if (retargeted == null) {
retargeted = new PopupMenuEvent(datePicker);
}
listener.popupMenuWillBecomeInvisible(retargeted);
}
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
PopupMenuListener[] ls = datePicker.getPopupMenuListeners();
PopupMenuEvent retargeted = null;
for (PopupMenuListener listener : ls) {
if (retargeted == null) {
retargeted = new PopupMenuEvent(datePicker);
}
listener.popupMenuWillBecomeVisible(retargeted);
}
}
};
return l;
}
/**
*
*/
private BasicDatePickerPopup createMonthViewPopup() {
BasicDatePickerPopup popup = new BasicDatePickerPopup();
popup.setLightWeightPopupEnabled(datePicker.isLightWeightPopupEnabled());
return popup;
}
/**
* Action used to commit the current value in the JFormattedTextField.
* This action is used by the keyboard bindings.
*/
private class TogglePopupAction extends AbstractAction {
public TogglePopupAction() {
super("TogglePopup");
}
@Override
public void actionPerformed(ActionEvent ev) {
toggleShowPopup();
}
}
/**
* Popup component that shows a JXMonthView component along with controlling
* buttons to allow traversal of the months. Upon selection of a date the
* popup will automatically hide itself and enter the selection into the
* editable field of the JXDatePicker.
*
*/
protected class BasicDatePickerPopup extends JPopupMenu {
public BasicDatePickerPopup() {
setLayout(new BorderLayout());
add(datePicker.getMonthView(), BorderLayout.CENTER);
updateLinkPanel(null);
}
/**
* @param oldLinkPanel
*/
public void updateLinkPanel(JComponent oldLinkPanel) {
if (oldLinkPanel != null) {
remove(oldLinkPanel);
}
if (datePicker.getLinkPanel() != null) {
add(datePicker.getLinkPanel(), BorderLayout.SOUTH);
}
}
}
/**
* PENDING: JW - I really hate the one-in-all. Wont touch
* it for now, maybe later. As long as we have it, the new
* listeners (dateSelection) are here too, for consistency.
* Adding the Layout here as well is ... , IMO.
*/
private class Handler implements LayoutManager, MouseListener, MouseMotionListener,
PropertyChangeListener, DateSelectionListener, ActionListener, FocusListener {
//------------- implement Mouse/MotionListener
private boolean _forwardReleaseEvent = false;
@Override
public void mouseClicked(MouseEvent ev) {
}
@Override
public void mousePressed(MouseEvent ev) {
if (!datePicker.isEnabled() || !SwingUtilities.isLeftMouseButton(ev)) {
return;
}
// PENDING JW: why do we need a mouseListener? the
// arrowbutton should have the toggleAction installed?
// Hmm... maybe doesn't ... check!
// reason might be that we want to open on pressed
// typically (or LF-dependent?),
// the button's action is invoked on released.
// LOG.info("opening on mousePressed?");
toggleShowPopup();
}
@Override
public void mouseReleased(MouseEvent ev) {
if (!datePicker.isEnabled() || !datePicker.isEditable()) {
return;
}
// Retarget mouse event to the month view.
if (_forwardReleaseEvent) {
JXMonthView monthView = datePicker.getMonthView();
ev = SwingUtilities.convertMouseEvent(popupButton, ev,
monthView);
monthView.dispatchEvent(ev);
_forwardReleaseEvent = false;
}
}
@Override
public void mouseEntered(MouseEvent ev) {
}
@Override
public void mouseExited(MouseEvent ev) {
}
@Override
public void mouseDragged(MouseEvent ev) {
if (!datePicker.isEnabled() || !datePicker.isEditable()) {
return;
}
_forwardReleaseEvent = true;
if (!popup.isShowing()) {
return;
}
// Retarget mouse event to the month view.
JXMonthView monthView = datePicker.getMonthView();
ev = SwingUtilities.convertMouseEvent(popupButton, ev, monthView);
monthView.dispatchEvent(ev);
}
@Override
public void mouseMoved(MouseEvent ev) {
}
//------------------ implement DateSelectionListener
@Override
public void valueChanged(DateSelectionEvent ev) {
updateFromSelectionChanged(ev.getEventType(), ev.isAdjusting());
}
//------------------ implement propertyChangeListener
/**
* {@inheritDoc}
*/
@Override
public void propertyChange(PropertyChangeEvent e) {
if (e.getSource() == datePicker) {
datePickerPropertyChange(e);
} else
if (e.getSource() == datePicker.getEditor()) {
editorPropertyChange(e);
} else
if (e.getSource() == datePicker.getMonthView()) {
monthViewPropertyChange(e);
} else
if (e.getSource() == popupButton) {
buttonPropertyChange(e);
} else
// PENDING - move back, ...
if ("value".equals(e.getPropertyName())) {
throw new IllegalStateException(
"editor listening is moved to dedicated propertyChangeLisener");
}
}
/**
* Handles property changes from datepicker's editor.
*
* @param e the PropertyChangeEvent object describing the event source
* and the property that has changed
*/
private void editorPropertyChange(PropertyChangeEvent evt) {
if ("value".equals(evt.getPropertyName())) {
updateFromValueChanged((Date) evt.getOldValue(), (Date) evt
.getNewValue());
}
}
/**
* Handles property changes from DatePicker.
* @param e the PropertyChangeEvent object describing the
* event source and the property that has changed
*/
private void datePickerPropertyChange(PropertyChangeEvent e) {
String property = e.getPropertyName();
if ("date".equals(property)) {
updateFromDateChanged();
} else if ("enabled".equals(property)) {
updateFromEnabledChanged();
} else if ("editable".equals(property)) {
updateFromEditableChanged();
} else if (JComponent.TOOL_TIP_TEXT_KEY.equals(property)) {
String tip = datePicker.getToolTipText();
datePicker.getEditor().setToolTipText(tip);
popupButton.setToolTipText(tip);
} else if (JXDatePicker.MONTH_VIEW.equals(property)) {
updateFromMonthViewChanged((JXMonthView) e.getOldValue());
} else if (JXDatePicker.LINK_PANEL.equals(property)) {
updateLinkPanel((JComponent) e.getOldValue());
} else if (JXDatePicker.EDITOR.equals(property)) {
updateFromEditorChanged((JFormattedTextField) e.getOldValue(), true);
} else if ("componentOrientation".equals(property)) {
datePicker.revalidate();
} else if ("lightWeightPopupEnabled".equals(property)) {
// Force recreation of the popup when this property changes.
if (popup != null) {
popup.setVisible(false);
}
uninstallPopup();
} else if ("formats".equals(property)) {
updateFormatsFromTimeZone(datePicker.getTimeZone());
}
else if ("locale".equals(property)) {
updateLocale();
}
}
/**
* Handles propertyChanges from the picker's monthView.
*
* @param e the PropertyChangeEvent object describing the event source
* and the property that has changed
*/
private void monthViewPropertyChange(PropertyChangeEvent e) {
if ("selectionModel".equals(e.getPropertyName())) {
updateFromSelectionModelChanged((DateSelectionModel) e.getOldValue());
} else if ("timeZone".equals(e.getPropertyName())) {
updateTimeZone((TimeZone) e.getOldValue());
} else if ("today".equals(e.getPropertyName())) {
updateLinkDate();
}
}
/**
* Handles propertyChanges from the picker's popupButton.
*
* PENDING: does nothing, kept while refactoring .. which
* properties from the button do we want to handle?
*
* @param e the PropertyChangeEvent object describing the event source
* and the property that has changed.
*/
private void buttonPropertyChange(PropertyChangeEvent e) {
}
//-------------- implement LayoutManager
@Override
public void addLayoutComponent(String name, Component comp) { }
@Override
public void removeLayoutComponent(Component comp) { }
@Override
public Dimension preferredLayoutSize(Container parent) {
return parent.getPreferredSize();
}
@Override
public Dimension minimumLayoutSize(Container parent) {
return parent.getMinimumSize();
}
@Override
public void layoutContainer(Container parent) {
Insets insets = datePicker.getInsets();
int width = datePicker.getWidth() - insets.left - insets.right;
int height = datePicker.getHeight() - insets.top - insets.bottom;
int popupButtonWidth = popupButton != null ? popupButton.getPreferredSize().width : 0;
boolean ltr = datePicker.getComponentOrientation().isLeftToRight();
datePicker.getEditor().setBounds(ltr ? insets.left : insets.left + popupButtonWidth,
insets.top,
width - popupButtonWidth,
height);
if (popupButton != null) {
popupButton.setBounds(ltr ? width - popupButtonWidth + insets.left : insets.left,
insets.top,
popupButtonWidth,
height);
}
}
// ------------- implement actionListener (listening to monthView actionEvent)
@Override
public void actionPerformed(ActionEvent e) {
if (e == null) return;
if (e.getSource() == datePicker.getMonthView()) {
monthViewActionPerformed(e);
} else if (e.getSource() == datePicker.getEditor()) {
editorActionPerformed(e);
}
}
/**
* Listening to actionEvents fired by the picker's editor.
*
* @param e
*/
private void editorActionPerformed(ActionEvent e) {
// pass the commit on to the picker.
commit();
}
/**
* Listening to actionEvents fired by the picker's monthView.
*
* @param e
*/
private void monthViewActionPerformed(ActionEvent e) {
if (JXMonthView.CANCEL_KEY.equals(e.getActionCommand())) {
cancel();
} else if (JXMonthView.COMMIT_KEY.equals(e.getActionCommand())) {
commit();
}
}
//------------------- focusListener
/**
* Issue #573-swingx - F2 in table doesn't focus the editor.
*
* Do the same as combo: manually pass-on the focus to the editor.
*
*/
@Override
public void focusGained(FocusEvent e) {
if (e.isTemporary()) return;
popupRemover.load();
if (e.getSource() == datePicker) {
datePicker.getEditor().requestFocusInWindow();
}
}
/**
* #565-swingx: popup not hidden if clicked into combo.
* The problem is that the combo uses the same trick as
* this datepicker to prevent auto-closing of the popup
* if focus is transfered back to the picker's editor.
*
* The idea is to hide the popup manually when the
* permanentFocusOwner changes to somewhere else.
*
* JW: doesn't work - we only get the temporary lost,
* but no permanent loss if the focus is transfered from
* the focusOwner to a new permanentFocusOwner.
*
* OOOkaay ... looks like exclusively related to a combo:
* we do get the expected focusLost if the focus is
* transferred permanently from the temporary focusowner
* to a new "normal" permanentFocusOwner (like a textfield),
* we don't get it if transfered to a tricksing owner (like
* a combo or picker). So can't do anything here.
*
* listen to keyboardFocusManager?
*/
@Override
public void focusLost(FocusEvent e) {
}
}
public class PopupRemover implements PropertyChangeListener {
private KeyboardFocusManager manager;
private boolean loaded;
public void load() {
if (manager != KeyboardFocusManager.getCurrentKeyboardFocusManager()) {
unload();
manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
}
if (!loaded) {
manager.addPropertyChangeListener("permanentFocusOwner", this);
loaded = true;
}
}
/**
* @param b
*/
private void unload(boolean nullManager) {
if (manager != null) {
manager.removePropertyChangeListener("permanentFocusOwner", this);
if (nullManager) {
manager = null;
}
}
loaded = false;
}
public void unload() {
unload(true);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!isPopupVisible()) {
unload(false);
return;
}
Component comp = manager.getPermanentFocusOwner();
if ((comp != null) && !SwingXUtilities.isDescendingFrom(comp, datePicker)) {
unload(false);
// on hiding the popup the focusmanager transfers
// focus back to the old permanentFocusOwner
// before showing the popup, that is the picker
// or the editor. So we have to force it back ...
hidePopup();
comp.requestFocusInWindow();
// this has no effect as focus changes are asynchronous
// inHide = false;
}
}
}
// ------------------ listener creation
/**
* Creates and returns the property change listener for the
* picker's monthView
* @return the listener for monthView properties
*/
protected PropertyChangeListener createMonthViewPropertyListener() {
return getHandler();
}
/**
* Creates and returns the focuslistener for picker and editor.
* @return the focusListener
*/
protected FocusListener createFocusListener() {
return getHandler();
}
/**
* Creates and returns the ActionListener for the picker's editor.
* @return the Actionlistener for the editor.
*/
protected ActionListener createEditorActionListener() {
return getHandler();
}
/**
* Creates and returns the ActionListener for the picker's monthView.
*
* @return the Actionlistener for the monthView.
*/
protected ActionListener createMonthViewActionListener() {
return getHandler();
}
/**
* Returns the listener for the dateSelection.
*
* @return the date selection listener
*/
protected DateSelectionListener createMonthViewSelectionListener() {
return getHandler();
}
/**
* @return a propertyChangeListener listening to
* editor property changes
*/
protected PropertyChangeListener createEditorPropertyListener() {
return getHandler();
}
/**
* Lazily creates and returns the shared all-mighty listener of everything
*
* @return the shared listener.
*/
private Handler getHandler() {
if (handler == null) {
handler = new Handler();
}
return handler;
}
protected PropertyChangeListener createPropertyChangeListener() {
return getHandler();
}
protected LayoutManager createLayoutManager() {
return getHandler();
}
protected MouseListener createMouseListener() {
return getHandler();
}
protected MouseMotionListener createMouseMotionListener() {
return getHandler();
}
//------------ utility methods
}