001    /*
002     * $Id: SingleDaySelectionModel.java 3100 2008-10-14 22:33:10Z rah003 $
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     */
021    package org.jdesktop.swingx.calendar;
022    
023    import java.util.Date;
024    import java.util.Locale;
025    import java.util.SortedSet;
026    import java.util.TreeSet;
027    
028    import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
029    import org.jdesktop.swingx.util.Contract;
030    
031    /**
032     * DateSelectionModel which allows a single selection only. <p>
033     * 
034     * Temporary quick & dirty class to explore requirements as needed by
035     * a DatePicker. Need to define the states more exactly. Currently 
036     * 
037     * <li> takes all Dates as-are (that is the normalized is the same as the given):
038     * selected, unselectable, lower/upper bounds
039     * <li> interprets any Date between the start/end of day of the selected as selected
040     * <li> interprets any Date between the start/end of an unselectable date as unselectable
041     * <li> interprets the lower/upper bounds as being the start/end of the given
042     * dates, that is any Date after the start of day of the lower and before the end of
043     * day of the upper is selectable.
044     * 
045     * 
046     * @author Jeanette Winzenburg
047     */
048    public class SingleDaySelectionModel extends AbstractDateSelectionModel {
049    
050        private SortedSet<Date> selectedDates;
051        private SortedSet<Date> unselectableDates;
052        
053        /**
054         * Instantiates a SingleDaySelectionModel with default locale.
055         */
056        public SingleDaySelectionModel() {
057            this(null);
058        }
059    
060        /**
061         * Instantiates a SingleSelectionModel with the given locale. If the locale is
062         * null, the Locale's default is used.
063         * 
064         * PENDING JW: fall back to JComponent.getDefaultLocale instead? We use this
065         *   with components anyway?
066         * 
067         * @param locale the Locale to use with this model, defaults to Locale.default()
068         *    if null.
069         */
070        public SingleDaySelectionModel(Locale locale) {
071            super(locale);
072            this.selectedDates = new TreeSet<Date>();
073            this.unselectableDates = new TreeSet<Date>();
074        }
075    
076        /**
077         * {@inheritDoc}
078         */
079        public SelectionMode getSelectionMode() {
080            return SelectionMode.SINGLE_SELECTION;
081        }
082    
083        /**
084         * {@inheritDoc}<p>
085         * 
086         * Implemented to do nothing.
087         * 
088         */
089        public void setSelectionMode(final SelectionMode selectionMode) {
090        }
091    
092    
093        //---------------------- selection ops    
094        /**
095         * {@inheritDoc} <p>
096         * 
097         * Implemented to call setSelectionInterval with startDate for both 
098         * parameters.
099         */
100        public void addSelectionInterval(Date startDate, Date endDate) {
101            setSelection(startDate);
102        }
103    
104        /**
105         * {@inheritDoc}<p>
106         * 
107         * PENDING JW: define what happens if we have a selection but the interval
108         * isn't selectable. 
109         */
110        public void setSelectionInterval(Date startDate, Date endDate) {
111            setSelection(startDate);
112        }
113    
114        /**
115         * {@inheritDoc}
116         */
117        public void removeSelectionInterval(Date startDate, Date endDate) {
118            Contract.asNotNull(startDate, "date must not be null");
119            if (isSelectionEmpty()) return;
120            if (isSelectionInInterval(startDate, endDate)) {
121                selectedDates.clear();
122                fireValueChanged(EventType.DATES_REMOVED);
123            }
124        }
125        
126        /**
127         * Checks and returns whether the selected date is contained in the interval
128         * given by startDate/endDate. The selection must not be empty when 
129         * calling this method. <p>
130         * 
131         * This implementation interprets the interval between the start of the day
132         * of startDay to the end of the day of endDate. 
133         * 
134         * @param startDate the start of the interval, must not be null
135         * @param endDate  the end of the interval, must not be null
136         * @return true if the selected date is contained in the interval
137         */
138        protected boolean isSelectionInInterval(Date startDate, Date endDate) {
139            if (selectedDates.first().before(startOfDay(startDate)) 
140                    || (selectedDates.first().after(endOfDay(endDate)))) return false;
141            return true;
142        }
143    
144        /**
145         * Selects the given date if it is selectable and not yet selected. 
146         * Does nothing otherwise.
147         * If this operation changes the current selection, it will fire a 
148         * DateSelectionEvent of type DATES_SET.
149         * 
150         * @param date the Date to select, must not be null. 
151         */
152        protected void setSelection(Date date) {
153            Contract.asNotNull(date, "date must not be null");
154            if (isSelectedStrict(date)) return;
155            if (isSelectable(date)) {
156                selectedDates.clear();
157                // PENDING JW: use normalized
158                selectedDates.add(date);
159                fireValueChanged(EventType.DATES_SET);
160            }
161        }
162        
163        /**
164         * Checks and returns whether the given date is contained in the selection.
165         * This differs from isSelected in that it tests for the exact (normalized)
166         * Date instead of for the same day.
167         * 
168         * @param date the Date to test.
169         * @return true if the given date is contained in the selection, 
170         *    false otherwise
171         * 
172         */
173        private boolean isSelectedStrict(Date date) {
174            if (!isSelectionEmpty()) {
175                // PENDING JW: use normalized
176                return selectedDates.first().equals(date);
177            }
178            return false;
179        }
180    
181        /**
182         * {@inheritDoc}
183         */
184        public Date getFirstSelectionDate() {
185            return isSelectionEmpty() ? null : selectedDates.first();
186        }
187    
188        /**
189         * {@inheritDoc}
190         */
191        public Date getLastSelectionDate() {
192            return isSelectionEmpty() ? null : selectedDates.last();
193        }
194    
195        /**
196         * Returns a boolean indicating whether the given date is selectable.
197         * 
198         * @param date the date to check for selectable, must not be null.
199         * @return true if the given date is selectable, false if not.
200         */
201        public  boolean isSelectable(Date date) {
202            if (outOfBounds(date)) return false;
203            return !inUnselectables(date);
204        }
205    
206        /**
207         * @param date
208         * @return
209         */
210        private boolean inUnselectables(Date date) {
211            for (Date unselectable : unselectableDates) {
212                if (isSameDay(unselectable, date)) return true;
213            }
214            return false;
215        }
216    
217        /**
218         * Returns a boolean indication whether the given date is below
219         * the lower or above the upper bound. 
220         * 
221         * @param date
222         * @return
223         */
224        private boolean outOfBounds(Date date) {
225            if (belowLowerBound(date)) return true;
226            if (aboveUpperBound(date)) return true;
227            return false;
228        }
229    
230        /**
231         * @param date
232         * @return
233         */
234        private boolean aboveUpperBound(Date date) {
235            if (upperBound != null) {
236                return endOfDay(upperBound).before(date);
237            }
238            return false;
239        }
240    
241        /**
242         * @param date
243         * @return
244         */
245        private boolean belowLowerBound(Date date) {
246            if (lowerBound != null) {
247               return startOfDay(lowerBound).after(date);
248            }
249            return false;
250        }
251    
252    
253        /**
254         * {@inheritDoc}
255         */
256        public void clearSelection() {
257            if (isSelectionEmpty()) return;
258            selectedDates.clear();
259            fireValueChanged(EventType.SELECTION_CLEARED);
260        }
261    
262    
263        /**
264         * {@inheritDoc}
265         */
266        public SortedSet<Date> getSelection() {
267            return new TreeSet<Date>(selectedDates);
268        }
269    
270        /**
271         * {@inheritDoc}
272         */
273        public boolean isSelected(Date date) {
274            Contract.asNotNull(date, "date must not be null");
275            if (isSelectionEmpty()) return false;
276            return isSameDay(selectedDates.first(), date);
277        }
278    
279        
280    
281        /**
282         * {@inheritDoc}<p>
283         * 
284         * Implemented to return the date itself.
285         */
286        public Date getNormalizedDate(Date date) {
287            return new Date(date.getTime());
288        }
289    
290    
291        /**
292         * {@inheritDoc}
293         */
294        public boolean isSelectionEmpty() {
295            return selectedDates.isEmpty();
296        }
297    
298    
299        /**
300         * {@inheritDoc}
301         */
302        public SortedSet<Date> getUnselectableDates() {
303            return new TreeSet<Date>(unselectableDates);
304        }
305    
306        /**
307         * {@inheritDoc}
308         */
309        public void setUnselectableDates(SortedSet<Date> unselectables) {
310            Contract.asNotNull(unselectables, "unselectable dates must not be null");
311            this.unselectableDates.clear();
312            for (Date unselectableDate : unselectables) {
313                removeSelectionInterval(unselectableDate, unselectableDate);
314                unselectableDates.add(unselectableDate);
315            }
316            fireValueChanged(EventType.UNSELECTED_DATES_CHANGED);
317        }
318    
319        /**
320         * {@inheritDoc}
321         */
322        public boolean isUnselectableDate(Date date) {
323            return !isSelectable(date);
324        }
325    
326    
327    
328    
329    
330    }