001/*
002 * $Id: BasicDatePickerUI.java 4107 2012-01-19 14:24:15Z kleopatra $
003 * 
004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx.plaf.basic;
022
023import java.awt.BorderLayout;
024import java.awt.Component;
025import java.awt.Container;
026import java.awt.Dimension;
027import java.awt.FontMetrics;
028import java.awt.Insets;
029import java.awt.KeyboardFocusManager;
030import java.awt.LayoutManager;
031import java.awt.event.ActionEvent;
032import java.awt.event.ActionListener;
033import java.awt.event.FocusEvent;
034import java.awt.event.FocusListener;
035import java.awt.event.MouseEvent;
036import java.awt.event.MouseListener;
037import java.awt.event.MouseMotionListener;
038import java.beans.PropertyChangeEvent;
039import java.beans.PropertyChangeListener;
040import java.beans.PropertyVetoException;
041import java.text.DateFormat;
042import java.text.ParseException;
043import java.util.Calendar;
044import java.util.Date;
045import java.util.Locale;
046import java.util.TimeZone;
047import java.util.logging.Logger;
048
049import javax.swing.AbstractAction;
050import javax.swing.Action;
051import javax.swing.ActionMap;
052import javax.swing.Icon;
053import javax.swing.InputMap;
054import javax.swing.JButton;
055import javax.swing.JComboBox;
056import javax.swing.JComponent;
057import javax.swing.JFormattedTextField;
058import javax.swing.JPopupMenu;
059import javax.swing.KeyStroke;
060import javax.swing.SwingUtilities;
061import javax.swing.UIManager;
062import javax.swing.JFormattedTextField.AbstractFormatter;
063import javax.swing.JFormattedTextField.AbstractFormatterFactory;
064import javax.swing.border.Border;
065import javax.swing.event.PopupMenuEvent;
066import javax.swing.event.PopupMenuListener;
067import javax.swing.plaf.ComponentUI;
068import javax.swing.plaf.UIResource;
069import javax.swing.text.DefaultFormatterFactory;
070import javax.swing.text.View;
071
072import org.jdesktop.swingx.JXDatePicker;
073import org.jdesktop.swingx.JXMonthView;
074import org.jdesktop.swingx.SwingXUtilities;
075import org.jdesktop.swingx.calendar.CalendarUtils;
076import org.jdesktop.swingx.calendar.DatePickerFormatter;
077import org.jdesktop.swingx.calendar.DateSelectionModel;
078import org.jdesktop.swingx.calendar.DatePickerFormatter.DatePickerFormatterUIResource;
079import org.jdesktop.swingx.event.DateSelectionEvent;
080import org.jdesktop.swingx.event.DateSelectionListener;
081import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
082import org.jdesktop.swingx.plaf.DatePickerUI;
083
084/**
085 * The basic implementation of a <code>DatePickerUI</code>.
086 * <p>
087 * 
088 * 
089 * @author Joshua Outwater
090 * @author Jeanette Winzenburg
091 */
092public class BasicDatePickerUI extends DatePickerUI {
093
094    @SuppressWarnings("all")
095    private static final Logger LOG = Logger.getLogger(BasicDatePickerUI.class
096            .getName());
097    
098    protected JXDatePicker datePicker;
099    private JButton popupButton;
100    private BasicDatePickerPopup popup;
101    private Handler handler;
102    /* 
103     * shared listeners
104     */
105    protected PropertyChangeListener propertyChangeListener;
106    private FocusListener focusListener;
107    
108    /*
109     * listener's for the arrow button
110     */ 
111    protected MouseListener mouseListener;
112    protected MouseMotionListener mouseMotionListener;
113
114    /*
115     * listeners for the picker's editor
116     */
117    private ActionListener editorActionListener;
118    private EditorCancelAction editorCancelAction;
119    private PropertyChangeListener editorPropertyListener;
120    
121    /**
122     * listeners for the picker's monthview
123     */
124    private DateSelectionListener monthViewSelectionListener;
125    private ActionListener monthViewActionListener;
126    private PropertyChangeListener monthViewPropertyListener;
127
128    private PopupRemover popupRemover;
129
130    private PopupMenuListener popupMenuListener;
131
132
133    @SuppressWarnings({"UnusedDeclaration"})
134    public static ComponentUI createUI(JComponent c) {
135        return new BasicDatePickerUI();
136    }
137
138    @Override
139    public void installUI(JComponent c) {
140        datePicker = (JXDatePicker)c;
141        datePicker.setLayout(createLayoutManager());
142        installComponents();
143        installDefaults();
144        installKeyboardActions();
145        installListeners();
146    }
147
148    @Override
149    public void uninstallUI(JComponent c) {
150        uninstallListeners();
151        uninstallKeyboardActions();
152        uninstallDefaults();
153        uninstallComponents();
154        datePicker.setLayout(null);
155        datePicker = null;
156    }
157
158    protected void installComponents() {
159        
160        JFormattedTextField editor = datePicker.getEditor();
161        if (SwingXUtilities.isUIInstallable(editor)) {
162            DateFormat[] formats = getCustomFormats(editor);
163            // we are not yet listening ...
164            datePicker.setEditor(createEditor());
165            if (formats != null) {
166                datePicker.setFormats(formats);
167            }
168        }
169        updateFromEditorChanged(null, false);
170        
171        popupButton = createPopupButton();
172        if (popupButton != null) {
173            // this is a trick to get hold of the client prop which
174            // prevents closing of the popup
175            JComboBox box = new JComboBox();
176            Object preventHide = box.getClientProperty("doNotCancelPopup");
177            popupButton.putClientProperty("doNotCancelPopup", preventHide);
178            datePicker.add(popupButton);
179            popupButton.setEnabled(datePicker.isEnabled());
180            popupButton.setInheritsPopupMenu(true);
181        }
182            updateChildLocale(datePicker.getLocale());
183        
184    }
185
186    /**
187     * Checks and returns custom formats on the editor, if any.
188     * 
189     * @param editor the editor to check
190     * @return the custom formats uses in the editor or null if it had
191     *   used defaults as defined in the datepicker properties
192     */
193    private DateFormat[] getCustomFormats(JFormattedTextField editor) {
194        DateFormat[] formats = null;
195        if (editor != null) {
196            AbstractFormatterFactory factory = editor.getFormatterFactory();
197            if (factory != null) {
198                AbstractFormatter formatter = factory.getFormatter(editor);
199                // fix for #1144: classCastException for custom formatters
200                // PENDING JW: revisit for #1138
201                if ((formatter instanceof DatePickerFormatter) && !(formatter instanceof UIResource)) {
202//                if (!(formatter instanceof DatePickerFormatterUIResource))  {
203                    formats = ((DatePickerFormatter) formatter).getFormats();
204                }
205            }
206
207        }
208        return formats;
209    }
210
211    protected void uninstallComponents() {
212        JFormattedTextField editor = datePicker.getEditor();
213        if (editor != null) {
214            datePicker.remove(editor);
215        }
216
217        if (popupButton != null) {
218            datePicker.remove(popupButton);
219            popupButton = null;
220        }
221    }
222
223    /**
224     * Installs DatePicker default properties.
225     */
226    protected void installDefaults() {
227        // PENDING JW: currently this is for testing only. 
228        boolean zoomable = Boolean.TRUE.equals(UIManager.get("JXDatePicker.forceZoomable")); 
229        if (zoomable) {
230            datePicker.getMonthView().setZoomable(true);
231        }
232    }
233
234    protected void uninstallDefaults() {
235
236    }
237
238    protected void installKeyboardActions() {
239        // install picker's actions
240        ActionMap pickerMap = datePicker.getActionMap();
241        pickerMap.put(JXDatePicker.CANCEL_KEY, createCancelAction());
242        pickerMap.put(JXDatePicker.COMMIT_KEY, createCommitAction());
243        pickerMap.put(JXDatePicker.HOME_NAVIGATE_KEY, createHomeAction(false));
244        pickerMap.put(JXDatePicker.HOME_COMMIT_KEY, createHomeAction(true));
245        TogglePopupAction popupAction = createTogglePopupAction();
246        pickerMap.put("TOGGLE_POPUP", popupAction);
247        
248        InputMap pickerInputMap = datePicker.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
249        pickerInputMap.put(KeyStroke.getKeyStroke("ENTER"), JXDatePicker.COMMIT_KEY);
250        pickerInputMap.put(KeyStroke.getKeyStroke("ESCAPE"), JXDatePicker.CANCEL_KEY);
251        // PENDING: get from LF
252        pickerInputMap.put(KeyStroke.getKeyStroke("F5"), JXDatePicker.HOME_COMMIT_KEY);
253        pickerInputMap.put(KeyStroke.getKeyStroke("shift F5"), JXDatePicker.HOME_NAVIGATE_KEY);
254        pickerInputMap.put(KeyStroke.getKeyStroke("alt DOWN"), "TOGGLE_POPUP");
255        
256        installLinkPanelKeyboardActions();
257    }
258
259    protected void uninstallKeyboardActions() {
260        uninstallLinkPanelKeyboardActions(datePicker.getLinkPanel());
261    }
262
263    
264    /**
265     * Installs actions and key bindings on the datePicker's linkPanel. Does
266     * nothing if the linkPanel is null.
267     * 
268     * PRE: keybindings installed on picker.
269     */
270    protected void installLinkPanelKeyboardActions() {
271        if (datePicker.getLinkPanel() == null)
272            return;
273        ActionMap map = datePicker.getLinkPanel().getActionMap();
274        map.put(JXDatePicker.HOME_COMMIT_KEY, datePicker.getActionMap().get(
275                JXDatePicker.HOME_COMMIT_KEY));
276        map.put(JXDatePicker.HOME_NAVIGATE_KEY, datePicker.getActionMap().get(
277                JXDatePicker.HOME_NAVIGATE_KEY));
278        InputMap inputMap = datePicker.getLinkPanel().getInputMap(
279                JComponent.WHEN_IN_FOCUSED_WINDOW);
280        // PENDING: get from LF
281        inputMap.put(KeyStroke.getKeyStroke("F5"), 
282                JXDatePicker.HOME_COMMIT_KEY);
283        inputMap.put(KeyStroke.getKeyStroke("shift F5"),
284                JXDatePicker.HOME_NAVIGATE_KEY);
285    }
286
287
288    /**
289     * Uninstalls actions and key bindings from linkPanel. Does nothing if the
290     * linkPanel is null.
291     * 
292     * @param panel the component to uninstall
293     * 
294     */
295    protected void uninstallLinkPanelKeyboardActions(JComponent panel) {
296        if (panel == null) return;
297        ActionMap map = panel.getActionMap();
298        map.remove(JXDatePicker.HOME_COMMIT_KEY); 
299        map.remove(JXDatePicker.HOME_NAVIGATE_KEY); 
300        InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
301        // PENDING: get from LF
302        inputMap.remove(KeyStroke.getKeyStroke("F5"));
303        inputMap.remove(KeyStroke.getKeyStroke("shift F5"));
304        
305    }
306
307    /**
308     * Creates and installs all listeners to all components.
309     *
310     */
311    protected void installListeners() {
312        /*
313         * create the listeners. 
314         */
315        // propertyListener for datePicker
316        propertyChangeListener = createPropertyChangeListener();
317        
318        // mouseListener (for popup button only) ?
319        mouseListener = createMouseListener();
320        mouseMotionListener = createMouseMotionListener();
321        
322        // shared focuslistener (installed to picker and editor)
323        focusListener = createFocusListener();
324        
325        // editor related listeners
326        editorActionListener = createEditorActionListener();
327        editorPropertyListener = createEditorPropertyListener();
328        
329        // montheView related listeners
330        monthViewSelectionListener = createMonthViewSelectionListener();
331        monthViewActionListener = createMonthViewActionListener();
332        monthViewPropertyListener = createMonthViewPropertyListener();
333        
334        popupRemover = new PopupRemover();
335        /*
336         * install the listeners
337         */
338        // picker 
339        datePicker.addPropertyChangeListener(propertyChangeListener);
340        datePicker.addFocusListener(focusListener);
341        
342        if (popupButton != null) {
343            // JW: which property do we want to monitor?
344            popupButton.addPropertyChangeListener(propertyChangeListener);
345            popupButton.addMouseListener(mouseListener);
346            popupButton.addMouseMotionListener(mouseMotionListener);
347        }
348        
349        updateEditorListeners(null);
350        // JW the following does more than installing the listeners ..
351        // synchs properties of datepicker to monthView's
352        // prepares monthview for usage in popup
353        // synch the date
354        // Relies on being the last thing done in the install ..
355        //
356        updateFromMonthViewChanged(null);
357    }
358    /**
359     * Uninstalls and nulls all listeners which had been installed 
360     * by this delegate.
361     *
362     */
363    protected void uninstallListeners() {
364        // datePicker
365        datePicker.removePropertyChangeListener(propertyChangeListener);
366        datePicker.removeFocusListener(focusListener);
367        
368        // monthView
369        datePicker.getMonthView().getSelectionModel().removeDateSelectionListener(monthViewSelectionListener);
370        datePicker.getMonthView().removeActionListener(monthViewActionListener);
371        datePicker.getMonthView().removePropertyChangeListener(propertyChangeListener);
372        
373        // JW: when can that be null?
374        // maybe in the very beginning? if some code calls ui.uninstall
375        // before ui.install? The editor is created by the ui. 
376        if (datePicker.getEditor() != null) {
377            uninstallEditorListeners(datePicker.getEditor());
378        }
379        if (popupButton != null) {
380            popupButton.removePropertyChangeListener(propertyChangeListener);
381            popupButton.removeMouseListener(mouseListener);
382            popupButton.removeMouseMotionListener(mouseMotionListener);
383        }
384
385        popupRemover.unload();
386        
387        popupRemover = null;
388        propertyChangeListener = null;
389        mouseListener = null;
390        mouseMotionListener = null;
391        
392        editorActionListener = null;
393        editorPropertyListener = null;
394        
395        monthViewSelectionListener = null;
396        monthViewActionListener = null;
397        monthViewPropertyListener = null;
398        
399        handler = null;
400    }
401
402//  --------------------- wiring listeners    
403    /**
404     * Wires the picker's monthView related listening. Removes all
405     * listeners from the given old view and adds the listeners to 
406     * the current monthView. <p>
407     * 
408     * @param oldMonthView
409     */
410    protected void updateMonthViewListeners(JXMonthView oldMonthView) {
411        DateSelectionModel oldModel = null;
412        if (oldMonthView != null) {
413            oldMonthView.removePropertyChangeListener(monthViewPropertyListener);
414            oldMonthView.removeActionListener(monthViewActionListener);
415            oldModel = oldMonthView.getSelectionModel();
416        }
417        datePicker.getMonthView().addPropertyChangeListener(monthViewPropertyListener);
418        datePicker.getMonthView().addActionListener(monthViewActionListener);
419        updateSelectionModelListeners(oldModel);
420    }
421
422    
423    /**
424     * Wires the picker's editor related listening and actions. Removes 
425     * listeners/actions from the old editor and adds them to 
426     * the new editor. <p>
427     * 
428     * @param oldEditor the pickers editor before the change
429     */
430    protected void updateEditorListeners(JFormattedTextField oldEditor) {
431        if (oldEditor != null) {
432            uninstallEditorListeners(oldEditor);
433        }
434        datePicker.getEditor().addPropertyChangeListener(editorPropertyListener);
435        datePicker.getEditor().addActionListener(editorActionListener);
436        datePicker.getEditor().addFocusListener(focusListener);
437        editorCancelAction = new EditorCancelAction(datePicker.getEditor());
438    }
439
440    /**
441     * Uninstalls all listeners and actions which have been installed
442     * by this delegate from the given editor. 
443     * 
444     * @param oldEditor the editor to uninstall.
445     */
446    private void uninstallEditorListeners(JFormattedTextField oldEditor) {
447        oldEditor.removePropertyChangeListener(editorPropertyListener);
448        oldEditor.removeActionListener(editorActionListener);
449        oldEditor.removeFocusListener(focusListener);
450        if (editorCancelAction != null) {
451            editorCancelAction.uninstall();
452            editorCancelAction = null;
453        }
454    }
455
456    /**
457     * Wires monthView's selection model listening. Removes the
458     * selection listener from the old model and add to the new model.
459     * 
460     * @param oldModel the dateSelectionModel before the change, may be null.
461     */
462    protected void updateSelectionModelListeners(DateSelectionModel oldModel) {
463        if (oldModel != null) {
464            oldModel.removeDateSelectionListener(monthViewSelectionListener);
465        }
466        datePicker.getMonthView().getSelectionModel()
467            .addDateSelectionListener(monthViewSelectionListener);
468        
469    }
470
471
472    // ---------------- component creation
473    /**
474     * Creates the editor used to edit the date selection. The editor is
475     * configured with the default DatePickerFormatter marked as UIResource.
476     * 
477     * @return an instance of a JFormattedTextField
478     */
479    protected JFormattedTextField createEditor() {
480        JFormattedTextField f = new DefaultEditor(
481                new DatePickerFormatterUIResource(datePicker.getLocale()));
482        f.setName("dateField");
483        // this produces a fixed pref widths, looking a bit funny
484        // int columns = UIManagerExt.getInt("JXDatePicker.numColumns", null);
485        // if (columns > 0) {
486        // f.setColumns(columns);
487        // }
488        // that's always 0 as it comes from the resourcebundle
489        // f.setColumns(UIManager.getInt("JXDatePicker.numColumns"));
490        Border border = UIManager.getBorder("JXDatePicker.border");
491        if (border != null) {
492            f.setBorder(border);
493        }
494        return f;
495    }
496
497    protected JButton createPopupButton() {
498        JButton b = new JButton();
499        b.setName("popupButton");
500        b.setRolloverEnabled(false);
501        b.setMargin(new Insets(0, 3, 0, 3));
502
503        Icon icon = UIManager.getIcon("JXDatePicker.arrowIcon");
504        if (icon == null) {
505            icon = (Icon)UIManager.get("Tree.expandedIcon");
506        }
507        b.setIcon(icon);
508        b.setFocusable(false);
509        return b;
510    }
511
512    /**
513     * 
514     * A subclass of JFormattedTextField which calculates a "reasonable"
515     * minimum preferred size, independent of value/text.<p>
516     * 
517     * Note: how to find the "reasonable" width is open to discussion.
518     * This implementation creates another datepicker, feeds it with 
519     * the formats and asks its prefWidth. <p>
520     * 
521     * PENDING: there's a resource property JXDatePicker.numColumns - why 
522     *   don't we use it?
523     */
524    private class DefaultEditor extends JFormattedTextField implements UIResource {
525
526
527        public DefaultEditor(AbstractFormatter formatter) {
528            super(formatter);
529        }
530
531        /**
532         * {@inheritDoc} <p>
533         * 
534         * Overridden to return a preferred size which has a reasonable lower bound. 
535         */
536        @Override
537        public Dimension getPreferredSize() {
538            Dimension preferredSize = super.getPreferredSize();
539            if (getColumns() <= 0) {
540                Dimension compare = getCompareMinimumSize();
541                if (preferredSize.width < compare.width) {
542                    return compare;
543                }
544            }
545            return preferredSize;
546        }
547
548        /**
549         * {@inheritDoc} <p>
550         * 
551         * Overridden to return the preferred size.
552         */
553        @Override
554        public Dimension getMinimumSize() {
555            return getPreferredSize();
556        }
557
558        private Dimension getCompareMinimumSize() {
559            JFormattedTextField field = new JFormattedTextField(getFormatter());
560            field.setMargin(getMargin());
561            field.setBorder(getBorder());
562            field.setFont(getFont());
563            field.setValue(new Date());
564            Dimension min = field.getPreferredSize();
565            field.setValue(null);
566            min.width += Math.max(field.getPreferredSize().width, 4);
567            return min;
568        }
569
570
571    }
572
573// ---------------- Layout    
574    /**
575     * {@inheritDoc}
576     */
577    @Override
578    public Dimension getMinimumSize(JComponent c) {
579        return getPreferredSize(c);
580    }
581
582    /**
583     * {@inheritDoc}
584     */
585    @Override
586    public Dimension getPreferredSize(JComponent c) {
587        Dimension dim = datePicker.getEditor().getPreferredSize();
588        if (popupButton != null) {
589            dim.width += popupButton.getPreferredSize().width;
590        }
591        Insets insets = datePicker.getInsets();
592        dim.width += insets.left + insets.right;
593        dim.height += insets.top + insets.bottom;
594        return (Dimension)dim.clone();
595    }
596
597
598    @Override
599    public int getBaseline(int width, int height) {
600        JFormattedTextField editor = datePicker.getEditor();
601        View rootView = editor.getUI().getRootView(editor);
602        if (rootView.getViewCount() > 0) {
603            Insets insets = editor.getInsets();
604            Insets insetsOut = datePicker.getInsets();
605            int nh = height - insets.top - insets.bottom
606                    - insetsOut.top - insetsOut.bottom;
607            int y = insets.top + insetsOut.top;
608            View fieldView = rootView.getView(0);
609            int vspan = (int) fieldView.getPreferredSpan(View.Y_AXIS);
610            if (nh != vspan) {
611                int slop = nh - vspan;
612                y += slop / 2;
613            }
614            FontMetrics fm = editor.getFontMetrics(editor.getFont());
615            y += fm.getAscent();
616            return y;
617        }
618        return -1;
619    }
620
621
622//------------------------------- controller methods/classes 
623    
624    /**
625     * {@inheritDoc}
626     */
627    @Override
628    public Date getSelectableDate(Date date) throws PropertyVetoException {
629        Date cleaned = date == null ? null :
630            datePicker.getMonthView().getSelectionModel().getNormalizedDate(date);
631        if (CalendarUtils.areEqual(cleaned, datePicker.getDate())) { 
632            // one place to interrupt the update spiral
633            throw new PropertyVetoException("date not selectable", null);
634        }
635        if (cleaned == null) return cleaned;
636        if (datePicker.getMonthView().isUnselectableDate(cleaned)) {
637            throw new PropertyVetoException("date not selectable", null);
638         }
639        return cleaned;
640    }
641
642//-------------------- update methods called from listeners     
643    /**
644     * Updates internals after picker's date property changed.
645     */
646    protected void updateFromDateChanged() {
647        Date visibleHook = datePicker.getDate() != null ?
648                datePicker.getDate() : datePicker.getLinkDay();
649        datePicker.getMonthView().ensureDateVisible(visibleHook);        
650        datePicker.getEditor().setValue(datePicker.getDate());
651    }
652
653    /**
654     * Updates date related properties in picker/monthView 
655     * after a change in the editor's value. Reverts the 
656     * value if the new date is unselectable.
657     * 
658     * @param oldDate the editor value before the change
659     * @param newDate the editor value after the change
660     */
661    protected void updateFromValueChanged(Date oldDate, Date newDate) {
662        if ((newDate != null) && datePicker.getMonthView().isUnselectableDate(newDate)) {
663            revertValue(oldDate);
664            return;
665        }
666        // the other place to interrupt the update spiral
667        if (!CalendarUtils.areEqual(newDate, datePicker.getMonthView().getSelectionDate())) {
668            datePicker.getMonthView().setSelectionDate(newDate);
669        }
670        datePicker.setDate(newDate);
671    }
672
673    /**
674     * PENDING: currently this resets at once - but it's a no-no,
675     * because it happens during notification
676     * 
677     * @param oldDate the old date to revert to
678     */
679    private void revertValue(Date oldDate) {
680        datePicker.getEditor().setValue(oldDate);
681    }
682    /**
683     * Updates date related properties picker/editor 
684     * after a change in the monthView's
685     * selection.
686     * 
687     * Here: does nothing if the change is intermediate.
688     * 
689     * PENDNG JW: shouldn't we listen to actionEvents then?
690     * 
691     * @param eventType the type of the selection change
692     * @param adjusting flag to indicate whether the the selection change
693     *    is intermediate
694     */
695    protected void updateFromSelectionChanged(EventType eventType, boolean adjusting) {
696        if (adjusting) return;
697        updateEditorValue();
698    }
699
700    /**
701     * Updates internals after the picker's monthView has changed. <p>
702     * 
703     * Cleans to popup. Wires the listeners. Updates date. 
704     * Updates formats' timezone. 
705     * 
706     * @param oldMonthView the picker's monthView before the change,
707     *   may be null.
708     */
709    protected void updateFromMonthViewChanged(JXMonthView oldMonthView) {
710        uninstallPopup();
711        updateMonthViewListeners(oldMonthView);
712        TimeZone oldTimeZone = null;
713        if (oldMonthView != null) {
714            oldMonthView.setComponentInputMapEnabled(false);
715            oldTimeZone = oldMonthView.getTimeZone();
716        }
717        datePicker.getMonthView().setComponentInputMapEnabled(true);
718        updateTimeZone(oldTimeZone);
719        updateEditorValue();
720    }
721
722
723    /**
724     * Updates internals after the picker's editor property 
725     * has changed. <p>
726     * 
727     * Updates the picker's children. Removes the old editor and 
728     * adds the new editor. Wires the editor listeners, it the flag
729     *  set. Typically, this method is called during installing the
730     *  componentUI with the flag set to false and true at all other 
731     *  moments.
732     * 
733     * 
734     * @param oldEditor the picker's editor before the change,
735     *   may be null.
736     * @param updateListeners a flag to indicate whether the listeners
737     *   are ready for usage.   
738     */
739    protected void updateFromEditorChanged(JFormattedTextField oldEditor, 
740            boolean updateListeners) { 
741        if (oldEditor != null) {
742            datePicker.remove(oldEditor);
743            oldEditor.putClientProperty("doNotCancelPopup", null);
744        }
745        datePicker.add(datePicker.getEditor());
746        // this is a trick to get hold of the client prop which
747        // prevents closing of the popup
748        JComboBox box = new JComboBox();
749        Object preventHide = box.getClientProperty("doNotCancelPopup");
750        datePicker.getEditor().putClientProperty("doNotCancelPopup", preventHide);
751        datePicker.getEditor().setInheritsPopupMenu(true);
752
753        updateEditorValue();
754        updateEditorProperties();
755        if (updateListeners) {
756            updateEditorListeners(oldEditor);
757            datePicker.revalidate();
758        }
759    }
760
761
762    /**
763     * Synchronizes the properties of the current editor to the properties of
764     * the JXDatePicker. 
765     */
766    private void updateEditorProperties() {
767        datePicker.getEditor().setEnabled(datePicker.isEnabled());
768        datePicker.getEditor().setEditable(datePicker.isEditable());
769    }
770
771    /**
772     * Updates internals after the selection model changed.
773     * 
774     * @param oldModel the model before the change.
775     */
776    protected void updateFromSelectionModelChanged(DateSelectionModel oldModel) {
777        updateSelectionModelListeners(oldModel);
778        updateEditorValue();
779    }
780
781    /**
782     * Sets the editor value to the model's selectedDate.
783     */
784    private void updateEditorValue() {
785        datePicker.getEditor().setValue(datePicker.getMonthView().getSelectionDate());
786    }
787
788    //---------------------- updating other properties
789
790    
791    /**
792     * Updates properties which depend on the picker's editable. <p>
793     * 
794     */
795    protected void updateFromEditableChanged() {
796        boolean isEditable = datePicker.isEditable();
797        // PENDING JW: revisit - align with combo's editable?
798        datePicker.getMonthView().setEnabled(isEditable);
799        datePicker.getEditor().setEditable(isEditable);
800        /*
801         * PatrykRy: Commit today date is not allowed if datepicker is not editable!
802         */
803        setActionEnabled(JXDatePicker.HOME_COMMIT_KEY, isEditable);
804        // for consistency, synch navigation as well 
805        setActionEnabled(JXDatePicker.HOME_NAVIGATE_KEY, isEditable);
806    }
807
808    /**
809     * Update properties which depend on the picker's enabled.
810     */
811    protected void updateFromEnabledChanged() {
812        boolean isEnabled = datePicker.isEnabled();
813        popupButton.setEnabled(isEnabled);
814        datePicker.getEditor().setEnabled(isEnabled);
815    }
816
817
818    /**
819     * 
820     * @param key
821     * @param enabled
822     */
823    private void setActionEnabled(String key, boolean enabled) {
824        Action action = datePicker.getActionMap().get(key);
825        if (action != null) {
826            action.setEnabled(enabled);
827        }
828    }
829
830    /**
831     * Updates the picker's formats to the given TimeZone.
832     * @param zone the timezone to set on the formats.
833     */
834    protected void updateFormatsFromTimeZone(TimeZone zone) {
835        for (DateFormat format : datePicker.getFormats()) {
836            format.setTimeZone(zone);
837        }
838    }
839    
840    /**
841     * Updates picker's timezone dependent properties on change notification
842     * from the associated monthView.
843     * 
844     * PENDING JW: DatePicker needs to send notification on timezone change? 
845     * 
846     * @param old the timezone before the change.
847     */
848    protected void updateTimeZone(TimeZone old) {
849        updateFormatsFromTimeZone(datePicker.getTimeZone());
850        updateLinkDate();
851    }
852
853    /**
854     * Updates the picker's linkDate to be in synch with monthView's today.
855     */
856    protected void updateLinkDate() {
857        datePicker.setLinkDay(datePicker.getMonthView().getToday());
858    }
859
860    /**
861     * Called form property listener, updates all components locale, formats
862     * etc.
863     * 
864     * @author PeS
865     */
866    protected void updateLocale() {
867        Locale locale = datePicker.getLocale();
868        updateFormatLocale(locale);
869        updateChildLocale(locale);
870    }
871
872    private void updateFormatLocale(Locale locale) {
873        if (locale != null) {
874            // PENDING JW: timezone?
875            if (getCustomFormats(datePicker.getEditor()) == null) {
876                datePicker.getEditor().setFormatterFactory(
877                        new DefaultFormatterFactory(
878                                new DatePickerFormatterUIResource(locale)));
879            }
880        }
881    }
882
883    private void updateChildLocale(Locale locale) {
884        if (locale != null) {
885            datePicker.getEditor().setLocale(locale);
886            datePicker.getLinkPanel().setLocale(locale);
887            datePicker.getMonthView().setLocale(locale);
888        }
889    }
890    
891    /**
892     * @param oldLinkPanel 
893     * 
894     */
895    protected void updateLinkPanel(JComponent oldLinkPanel) {
896        if (oldLinkPanel != null) {
897            uninstallLinkPanelKeyboardActions(oldLinkPanel);
898        }
899        installLinkPanelKeyboardActions();
900        if (popup != null) {
901            popup.updateLinkPanel(oldLinkPanel);
902        }
903    }
904
905
906//------------------- methods called by installed actions
907    
908    /**
909     * 
910     */
911    protected void commit() {
912        hidePopup();
913        try {
914            datePicker.commitEdit();
915        } catch (ParseException ex) {
916            // can't help it
917        }
918    }
919
920    /**
921     * 
922     */
923    protected void cancel() {
924        if (isPopupVisible()) {
925            popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
926        }
927        hidePopup();
928        datePicker.cancelEdit();
929    }
930
931    /**
932     * PENDING: widened access for debugging - need api to
933     * control popup visibility?
934     */
935    public void hidePopup() {
936        if (popup != null) popup.setVisible(false);
937    }
938
939    public boolean isPopupVisible() {
940        if (popup != null) {
941            return popup.isVisible();
942        }
943        return false;
944    }
945    /**
946     * Navigates to linkDate. If commit, the linkDate is selected
947     * and committed. If not commit, the linkDate is scrolled to visible, if the 
948     * monthview is open, does nothing for invisible monthView.  
949     * 
950     * @param commit boolean to indicate whether the linkDate should be
951     *   selected and committed
952     */
953    protected void home(boolean commit) {
954        if (commit) {
955            Calendar cal = datePicker.getMonthView().getCalendar();
956            cal.setTime(datePicker.getLinkDay());
957            datePicker.getMonthView().setSelectionDate(cal.getTime());
958            datePicker.getMonthView().commitSelection();
959        } else {
960            datePicker.getMonthView().ensureDateVisible(datePicker.getLinkDay());
961        }
962    }
963
964//---------------------- other stuff    
965    
966    /**
967     * Creates and returns the action for committing the picker's 
968     * input.
969     * 
970     * @return
971     */
972    private Action createCommitAction() {
973        Action action = new AbstractAction() {
974
975            @Override
976            public void actionPerformed(ActionEvent e) {
977                commit();
978            }
979            
980        };
981        return action;
982    }
983
984    /**
985     * Creates and returns the action for cancel the picker's 
986     * edit.
987     * 
988     * @return
989     */
990    private Action createCancelAction() {
991        Action action = new AbstractAction() {
992
993            @Override
994            public void actionPerformed(ActionEvent e) {
995                cancel();
996            }
997            
998        };
999        return action;
1000    }
1001
1002    private Action createHomeAction(final boolean commit) {
1003        Action action = new AbstractAction( ) {
1004
1005            @Override
1006            public void actionPerformed(ActionEvent e) {
1007                home(commit);
1008                
1009            }
1010            
1011        };
1012        return action ;
1013    }
1014    /**
1015     * The wrapper for the editor cancel action. 
1016     * 
1017     * PENDING: Need to extend TestAction?
1018     * 
1019     */
1020    public class EditorCancelAction extends AbstractAction {
1021        private JFormattedTextField editor;
1022        private Action cancelAction;
1023        public static final String TEXT_CANCEL_KEY = "reset-field-edit";
1024       
1025        public EditorCancelAction(JFormattedTextField field) {
1026            install(field);
1027        }
1028        
1029        /**
1030         * Resets the contained editors actionMap to original and
1031         * nulls all fields. <p>
1032         * NOTE: after calling this method the action must not be
1033         * used! Create a new one for the same or another editor.
1034         *
1035         */
1036        public void uninstall() {
1037            editor.getActionMap().remove(TEXT_CANCEL_KEY);
1038            cancelAction = null;
1039            editor = null;
1040        }
1041        
1042        /**
1043         * @param editor
1044         */
1045        private void install(JFormattedTextField editor) {
1046            this.editor = editor;
1047            cancelAction = editor.getActionMap().get(TEXT_CANCEL_KEY);
1048            editor.getActionMap().put(TEXT_CANCEL_KEY, this);
1049        }
1050        
1051        @Override
1052        public void actionPerformed(ActionEvent e) {
1053            cancelAction.actionPerformed(null);
1054            cancel();
1055        }
1056
1057    }
1058
1059    /**
1060     * Creates and returns the action which toggles the visibility of the popup.
1061     * 
1062     * @return the action which toggles the visibility of the popup.
1063     */
1064    protected TogglePopupAction createTogglePopupAction() {
1065        return new TogglePopupAction();
1066    }
1067
1068    /**
1069     * Toggles the popups visibility after preparing internal state.
1070     * 
1071     * 
1072     */
1073    public void toggleShowPopup() {
1074        if (popup == null) {
1075            installPopup();
1076        }
1077        if (popup.isVisible()) {
1078            popup.setVisible(false);
1079        } else {
1080            // PENDING JW: Issue 757-swing - datePicker firing focusLost on
1081            // opening
1082            // not with following line - but need to run tests
1083            datePicker.getEditor().requestFocusInWindow();
1084//            datePicker.requestFocusInWindow();
1085            SwingUtilities.invokeLater(new Runnable() {
1086                @Override
1087                public void run() {
1088//                    if (datePicker.getParent() == null) {
1089//                        // Tracking #1372-swingx - parent is null if used as
1090//                        // DatePickerCellEditor,
1091//                        // two different editors, clickCountToStart == 1 and
1092//                        // Metal
1093//                        // as a first hot fix, we back out
1094//                         LOG.info("couldn't show popup for: " + datePicker.getName());
1095//                        return;
1096//                    }
1097                    popup.show(datePicker, 0, datePicker.getHeight());
1098                }
1099            });
1100        }
1101    }
1102
1103    /**
1104     * Creates the popup and registers the popup listener. All internal 
1105     * methods must use this method instead of calling createPopup directly.
1106     */
1107    protected void installPopup() {
1108        popup = createMonthViewPopup();
1109        popup.addPopupMenuListener(getPopupMenuListener());
1110    }
1111
1112    /**
1113     * Removes the popup listener from the popup and null it, if
1114     * it was not null. All internal popup removal/replacement must 
1115     * use this method instead of nulling directly.
1116     * 
1117     */
1118    protected void uninstallPopup() {
1119        if (popup != null) {
1120            popup.removePopupMenuListener(getPopupMenuListener());
1121        }
1122       popup = null;
1123    }
1124
1125    /**
1126     * Returns the PopupMenuListener for the MonthView popup. Lazily created.
1127     * 
1128     * @return the popupuMenuListener to install on the popup
1129     */
1130    protected PopupMenuListener getPopupMenuListener() {
1131        if (popupMenuListener == null) {
1132            popupMenuListener = createPopupMenuListener();
1133        }
1134        return popupMenuListener;
1135    }
1136
1137    /**
1138     * Creates and returns a PopupMenuListener.
1139     * 
1140     * PENDING JW: the listener management assumes a stateless implementation
1141     * relative to the popup/picker. Custom implementations should take care
1142     * to not keep any references. 
1143     * 
1144     * @return
1145     */
1146    protected PopupMenuListener createPopupMenuListener() {
1147        PopupMenuListener l= new PopupMenuListener() {
1148            
1149            @Override
1150            public void popupMenuCanceled(PopupMenuEvent e) {
1151                PopupMenuListener[] ls = datePicker.getPopupMenuListeners();
1152                PopupMenuEvent retargeted = null;
1153                for (PopupMenuListener listener : ls) {
1154                    if (retargeted == null) {
1155                        retargeted = new PopupMenuEvent(datePicker);
1156                    }
1157                    listener.popupMenuCanceled(retargeted);
1158                }
1159            }
1160
1161            @Override
1162            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
1163                PopupMenuListener[] ls = datePicker.getPopupMenuListeners();
1164                PopupMenuEvent retargeted = null;
1165                for (PopupMenuListener listener : ls) {
1166                    if (retargeted == null) {
1167                        retargeted = new PopupMenuEvent(datePicker);
1168                    }
1169                    listener.popupMenuWillBecomeInvisible(retargeted);
1170                }
1171            }
1172
1173            @Override
1174            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
1175                PopupMenuListener[] ls = datePicker.getPopupMenuListeners();
1176                PopupMenuEvent retargeted = null;
1177                for (PopupMenuListener listener : ls) {
1178                    if (retargeted == null) {
1179                        retargeted = new PopupMenuEvent(datePicker);
1180                    }
1181                    listener.popupMenuWillBecomeVisible(retargeted);
1182                }
1183            }
1184            
1185        };
1186        return l;
1187    }
1188
1189
1190    /**
1191     * 
1192     */
1193    private BasicDatePickerPopup createMonthViewPopup() {
1194        BasicDatePickerPopup popup = new BasicDatePickerPopup();
1195        popup.setLightWeightPopupEnabled(datePicker.isLightWeightPopupEnabled());
1196        return popup;
1197    }
1198    /**
1199     * Action used to commit the current value in the JFormattedTextField.
1200     * This action is used by the keyboard bindings.
1201     */
1202    private class TogglePopupAction extends AbstractAction {
1203        public TogglePopupAction() {
1204            super("TogglePopup");
1205        }
1206
1207        @Override
1208        public void actionPerformed(ActionEvent ev) {
1209            toggleShowPopup();
1210        }
1211    }
1212
1213
1214    /**
1215     * Popup component that shows a JXMonthView component along with controlling
1216     * buttons to allow traversal of the months.  Upon selection of a date the
1217     * popup will automatically hide itself and enter the selection into the
1218     * editable field of the JXDatePicker.
1219     * 
1220     */
1221    protected class BasicDatePickerPopup extends JPopupMenu {
1222
1223        public BasicDatePickerPopup() {
1224            setLayout(new BorderLayout());
1225            add(datePicker.getMonthView(), BorderLayout.CENTER);
1226            updateLinkPanel(null);
1227        }
1228
1229        /**
1230         * @param oldLinkPanel
1231         */
1232        public void updateLinkPanel(JComponent oldLinkPanel) {
1233            if (oldLinkPanel != null) {
1234                remove(oldLinkPanel);
1235            }
1236            if (datePicker.getLinkPanel() != null) {
1237                add(datePicker.getLinkPanel(), BorderLayout.SOUTH);
1238            }
1239            
1240        }
1241    }
1242
1243    /**
1244     * PENDING: JW - I <b>really</b> hate the one-in-all. Wont touch
1245     *   it for now, maybe later. As long as we have it, the new
1246     *   listeners (dateSelection) are here too, for consistency.
1247     *   Adding the Layout here as well is ... , IMO.
1248     */
1249    private class Handler implements LayoutManager, MouseListener, MouseMotionListener,
1250            PropertyChangeListener, DateSelectionListener, ActionListener, FocusListener {
1251
1252//------------- implement Mouse/MotionListener        
1253        private boolean _forwardReleaseEvent = false;
1254
1255        @Override
1256        public void mouseClicked(MouseEvent ev) {
1257        }
1258
1259        @Override
1260        public void mousePressed(MouseEvent ev) {
1261            if (!datePicker.isEnabled() || !SwingUtilities.isLeftMouseButton(ev)) {
1262                return;
1263            }
1264            // PENDING JW: why do we need a mouseListener? the
1265            // arrowbutton should have the toggleAction installed?
1266            // Hmm... maybe doesn't ... check!
1267            // reason might be that we want to open on pressed
1268            // typically (or LF-dependent?),
1269            // the button's action is invoked on released.
1270//            LOG.info("opening on mousePressed?");
1271            toggleShowPopup();
1272        }
1273
1274        @Override
1275        public void mouseReleased(MouseEvent ev) {
1276            if (!datePicker.isEnabled() || !datePicker.isEditable()) {
1277                return;
1278            }
1279
1280            // Retarget mouse event to the month view.
1281            if (_forwardReleaseEvent) {
1282                JXMonthView monthView = datePicker.getMonthView();
1283                ev = SwingUtilities.convertMouseEvent(popupButton, ev,
1284                        monthView);
1285                monthView.dispatchEvent(ev);
1286                _forwardReleaseEvent = false;
1287            }
1288        }
1289
1290        @Override
1291        public void mouseEntered(MouseEvent ev) {
1292        }
1293
1294        @Override
1295        public void mouseExited(MouseEvent ev) {
1296        }
1297
1298        @Override
1299        public void mouseDragged(MouseEvent ev) {
1300            if (!datePicker.isEnabled() || !datePicker.isEditable()) {
1301                return;
1302            }
1303
1304            _forwardReleaseEvent = true;
1305
1306            if (!popup.isShowing()) {
1307                return;
1308            }
1309
1310            // Retarget mouse event to the month view.
1311            JXMonthView monthView = datePicker.getMonthView();
1312            ev = SwingUtilities.convertMouseEvent(popupButton, ev, monthView);
1313            monthView.dispatchEvent(ev);
1314        }
1315
1316        @Override
1317        public void mouseMoved(MouseEvent ev) {
1318        }
1319//------------------ implement DateSelectionListener
1320        
1321        @Override
1322        public void valueChanged(DateSelectionEvent ev) {
1323            updateFromSelectionChanged(ev.getEventType(), ev.isAdjusting());
1324        }
1325
1326//------------------ implement propertyChangeListener        
1327        /**
1328         * {@inheritDoc}
1329         */
1330        @Override
1331        public void propertyChange(PropertyChangeEvent e) {
1332            if (e.getSource() == datePicker) {
1333                datePickerPropertyChange(e);
1334            } else
1335            if (e.getSource() == datePicker.getEditor()) {
1336                editorPropertyChange(e);
1337            } else
1338            if (e.getSource() == datePicker.getMonthView()) {
1339                monthViewPropertyChange(e);
1340            } else
1341            if (e.getSource() == popupButton) {
1342                buttonPropertyChange(e);
1343            } else
1344            // PENDING - move back, ...
1345            if ("value".equals(e.getPropertyName())) {
1346                throw new IllegalStateException(
1347                        "editor listening is moved to dedicated propertyChangeLisener");
1348            }
1349        }
1350        
1351        /**
1352         * Handles property changes from datepicker's editor.
1353         * 
1354         * @param e the PropertyChangeEvent object describing the event source
1355         *        and the property that has changed
1356         */
1357        private void editorPropertyChange(PropertyChangeEvent evt) {
1358            if ("value".equals(evt.getPropertyName())) {
1359                updateFromValueChanged((Date) evt.getOldValue(), (Date) evt
1360                        .getNewValue());
1361            }
1362
1363        }
1364
1365        /**
1366         * Handles property changes from DatePicker.
1367         * @param e the PropertyChangeEvent object describing the 
1368         *     event source and the property that has changed
1369         */
1370        private void datePickerPropertyChange(PropertyChangeEvent e) {
1371            String property = e.getPropertyName();
1372            if ("date".equals(property)) {
1373                updateFromDateChanged();
1374            } else if ("enabled".equals(property)) {
1375                updateFromEnabledChanged();
1376            } else if ("editable".equals(property)) {
1377                updateFromEditableChanged();
1378            } else if (JComponent.TOOL_TIP_TEXT_KEY.equals(property)) {
1379                String tip = datePicker.getToolTipText();
1380                datePicker.getEditor().setToolTipText(tip);
1381                popupButton.setToolTipText(tip);
1382            } else if (JXDatePicker.MONTH_VIEW.equals(property)) {
1383                updateFromMonthViewChanged((JXMonthView) e.getOldValue());
1384            } else if (JXDatePicker.LINK_PANEL.equals(property)) {
1385                updateLinkPanel((JComponent) e.getOldValue());
1386            } else if (JXDatePicker.EDITOR.equals(property)) {
1387                updateFromEditorChanged((JFormattedTextField) e.getOldValue(), true);
1388            } else if ("componentOrientation".equals(property)) {
1389                datePicker.revalidate();
1390            } else if ("lightWeightPopupEnabled".equals(property)) {
1391                // Force recreation of the popup when this property changes.
1392                if (popup != null) {
1393                    popup.setVisible(false);
1394                }
1395                uninstallPopup();
1396            } else if ("formats".equals(property)) {
1397                updateFormatsFromTimeZone(datePicker.getTimeZone());
1398            }
1399            else if ("locale".equals(property)) {
1400                updateLocale();
1401            }            
1402        }
1403
1404        /**
1405         * Handles propertyChanges from the picker's monthView.
1406         * 
1407         * @param e the PropertyChangeEvent object describing the event source
1408         *        and the property that has changed
1409         */
1410        private void monthViewPropertyChange(PropertyChangeEvent e) {
1411            if ("selectionModel".equals(e.getPropertyName())) {
1412                updateFromSelectionModelChanged((DateSelectionModel) e.getOldValue());
1413            } else if ("timeZone".equals(e.getPropertyName())) {
1414                updateTimeZone((TimeZone) e.getOldValue());
1415            } else if ("today".equals(e.getPropertyName())) {
1416                updateLinkDate();
1417            }
1418        }
1419
1420        /**
1421         * Handles propertyChanges from the picker's popupButton.
1422         * 
1423         * PENDING: does nothing, kept while refactoring .. which
1424         *   properties from the button do we want to handle?
1425         * 
1426         * @param e the PropertyChangeEvent object describing the event source
1427         *        and the property that has changed.
1428         */
1429        private void buttonPropertyChange(PropertyChangeEvent e) {
1430        }
1431
1432//-------------- implement LayoutManager
1433        
1434        @Override
1435        public void addLayoutComponent(String name, Component comp) { }
1436
1437        @Override
1438        public void removeLayoutComponent(Component comp) { }
1439
1440        @Override
1441        public Dimension preferredLayoutSize(Container parent) {
1442            return parent.getPreferredSize();
1443        }
1444
1445        @Override
1446        public Dimension minimumLayoutSize(Container parent) {
1447            return parent.getMinimumSize();
1448        }
1449
1450        @Override
1451        public void layoutContainer(Container parent) {
1452            Insets insets = datePicker.getInsets();
1453            int width = datePicker.getWidth() - insets.left - insets.right;
1454            int height = datePicker.getHeight() - insets.top - insets.bottom;
1455
1456            int popupButtonWidth = popupButton != null ? popupButton.getPreferredSize().width : 0;
1457
1458            boolean ltr = datePicker.getComponentOrientation().isLeftToRight();
1459
1460            datePicker.getEditor().setBounds(ltr ? insets.left : insets.left + popupButtonWidth,
1461                    insets.top,
1462                    width - popupButtonWidth,
1463                    height);
1464
1465            if (popupButton != null) {
1466                popupButton.setBounds(ltr ? width - popupButtonWidth + insets.left : insets.left,
1467                        insets.top,
1468                        popupButtonWidth,
1469                        height);
1470            }
1471        }
1472
1473// ------------- implement actionListener (listening to monthView actionEvent)
1474        
1475        @Override
1476        public void actionPerformed(ActionEvent e) {
1477            if (e == null) return;
1478            if (e.getSource() == datePicker.getMonthView()) {
1479                monthViewActionPerformed(e);
1480            } else if (e.getSource() == datePicker.getEditor()) {
1481                editorActionPerformed(e);
1482            }
1483        }
1484
1485        /**
1486         * Listening to actionEvents fired by the picker's editor.
1487         * 
1488         * @param e
1489         */
1490        private void editorActionPerformed(ActionEvent e) {
1491            // pass the commit on to the picker.
1492            commit();
1493        }
1494
1495        /**
1496         * Listening to actionEvents fired by the picker's monthView.
1497         * 
1498         * @param e
1499         */
1500        private void monthViewActionPerformed(ActionEvent e) {
1501            if (JXMonthView.CANCEL_KEY.equals(e.getActionCommand())) {
1502                cancel();
1503            } else if (JXMonthView.COMMIT_KEY.equals(e.getActionCommand())) {
1504                commit();
1505            }
1506        }
1507
1508//------------------- focusListener
1509        
1510        /**
1511         * Issue #573-swingx - F2 in table doesn't focus the editor.
1512         * 
1513         * Do the same as combo: manually pass-on the focus to the editor.
1514         * 
1515         */
1516        @Override
1517        public void focusGained(FocusEvent e) {
1518            if (e.isTemporary()) return;
1519            popupRemover.load();
1520            if (e.getSource() == datePicker) {
1521               datePicker.getEditor().requestFocusInWindow(); 
1522            }
1523        }
1524
1525        /**
1526         * #565-swingx: popup not hidden if clicked into combo.
1527         * The problem is that the combo uses the same trick as
1528         * this datepicker to prevent auto-closing of the popup
1529         * if focus is transfered back to the picker's editor.
1530         * 
1531         * The idea is to hide the popup manually when the
1532         * permanentFocusOwner changes to somewhere else.
1533         * 
1534         * JW: doesn't work - we only get the temporary lost,
1535         * but no permanent loss if the focus is transfered from 
1536         * the focusOwner to a new permanentFocusOwner.
1537         * 
1538         * OOOkaay ... looks like exclusively related to a combo:
1539         * we do get the expected focusLost if the focus is
1540         * transferred permanently from the temporary focusowner
1541         * to a new "normal" permanentFocusOwner (like a textfield),
1542         * we don't get it if transfered to a tricksing owner (like
1543         * a combo or picker). So can't do anything here. 
1544         * 
1545         * listen to keyboardFocusManager?
1546         */
1547        @Override
1548        public void focusLost(FocusEvent e) {
1549            
1550        }
1551    }
1552
1553    public class PopupRemover implements PropertyChangeListener {
1554
1555        private KeyboardFocusManager manager;
1556        private boolean loaded;
1557        
1558        public void load() {
1559            if (manager != KeyboardFocusManager.getCurrentKeyboardFocusManager()) {
1560                unload();
1561                manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
1562            }
1563            if (!loaded) {
1564                manager.addPropertyChangeListener("permanentFocusOwner", this);
1565                loaded = true;
1566            }
1567        }
1568        
1569        /**
1570         * @param b
1571         */
1572        private void unload(boolean nullManager) {
1573            if (manager != null) {
1574                manager.removePropertyChangeListener("permanentFocusOwner", this);
1575                if (nullManager) {
1576                    manager = null;
1577                }
1578            }
1579            loaded = false;
1580         }
1581
1582        public void unload() {
1583            unload(true);
1584        }
1585        
1586        @Override
1587        public void propertyChange(PropertyChangeEvent evt) {
1588            if (!isPopupVisible()) {
1589                unload(false);
1590                return;
1591            }
1592            Component comp = manager.getPermanentFocusOwner();
1593            if ((comp != null) && !SwingXUtilities.isDescendingFrom(comp, datePicker)) {
1594                 unload(false);
1595                // on hiding the popup the focusmanager transfers 
1596                // focus back to the old permanentFocusOwner
1597                // before showing the popup, that is the picker
1598                // or the editor. So we have to force it back ... 
1599                hidePopup();
1600                comp.requestFocusInWindow();
1601                // this has no effect as focus changes are asynchronous
1602//                inHide = false;
1603            }
1604        }
1605        
1606        
1607    }
1608
1609    
1610//  ------------------ listener creation
1611
1612    /**
1613     * Creates and returns the property change listener for the 
1614     * picker's monthView
1615     * @return the listener for monthView properties
1616     */
1617    protected PropertyChangeListener createMonthViewPropertyListener() {
1618        return getHandler();
1619    }
1620
1621    /**
1622     * Creates and returns the focuslistener for picker and editor.
1623     * @return the focusListener
1624     */
1625    protected FocusListener createFocusListener() {
1626        return getHandler();
1627    }
1628
1629
1630    /**
1631     * Creates and returns the ActionListener for the picker's editor.
1632     * @return the Actionlistener for the editor.
1633     */
1634    protected ActionListener createEditorActionListener() {
1635        return getHandler();
1636    }
1637   
1638    /**
1639     * Creates and returns the ActionListener for the picker's monthView.
1640     * 
1641     * @return the Actionlistener for the monthView.
1642     */
1643    protected ActionListener createMonthViewActionListener() {
1644        return getHandler();
1645    }
1646
1647/**
1648     * Returns the listener for the dateSelection.
1649     * 
1650     * @return the date selection listener
1651     */
1652    protected DateSelectionListener createMonthViewSelectionListener() {
1653        return getHandler();
1654    }
1655
1656    /**
1657     * @return a propertyChangeListener listening to 
1658     *    editor property changes
1659     */
1660    protected PropertyChangeListener createEditorPropertyListener() {
1661        return getHandler();
1662    }
1663
1664    /**
1665     * Lazily creates and returns the shared all-mighty listener of everything
1666     *
1667     * @return the shared listener.
1668     */
1669    private Handler getHandler() {
1670        if (handler == null) {
1671            handler = new Handler();
1672        }
1673        return handler;
1674    }
1675
1676    protected PropertyChangeListener createPropertyChangeListener() {
1677        return getHandler();
1678    }
1679
1680    protected LayoutManager createLayoutManager() {
1681        return getHandler();
1682    }
1683
1684    protected MouseListener createMouseListener() {
1685        return getHandler();
1686    }
1687
1688    protected MouseMotionListener createMouseMotionListener() {
1689        return getHandler();
1690    }
1691
1692    
1693//------------ utility methods
1694
1695
1696}