001/*
002 * $Id: SingleDaySelectionModel.java 3927 2011-02-22 16:34:11Z 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.calendar;
022
023import java.util.Date;
024import java.util.Locale;
025import java.util.SortedSet;
026import java.util.TreeSet;
027
028import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
029import 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 */
048public 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    @Override
080    public SelectionMode getSelectionMode() {
081        return SelectionMode.SINGLE_SELECTION;
082    }
083
084    /**
085     * {@inheritDoc}<p>
086     * 
087     * Implemented to do nothing.
088     * 
089     */
090    @Override
091    public void setSelectionMode(final SelectionMode selectionMode) {
092    }
093
094
095    //---------------------- selection ops    
096    /**
097     * {@inheritDoc} <p>
098     * 
099     * Implemented to call setSelectionInterval with startDate for both 
100     * parameters.
101     */
102    @Override
103    public void addSelectionInterval(Date startDate, Date endDate) {
104        setSelection(startDate);
105    }
106
107    /**
108     * {@inheritDoc}<p>
109     * 
110     * PENDING JW: define what happens if we have a selection but the interval
111     * isn't selectable. 
112     */
113    @Override
114    public void setSelectionInterval(Date startDate, Date endDate) {
115        setSelection(startDate);
116    }
117
118    /**
119     * {@inheritDoc}
120     */
121    @Override
122    public void removeSelectionInterval(Date startDate, Date endDate) {
123        Contract.asNotNull(startDate, "date must not be null");
124        if (isSelectionEmpty()) return;
125        if (isSelectionInInterval(startDate, endDate)) {
126            selectedDates.clear();
127            fireValueChanged(EventType.DATES_REMOVED);
128        }
129    }
130    
131    /**
132     * Checks and returns whether the selected date is contained in the interval
133     * given by startDate/endDate. The selection must not be empty when 
134     * calling this method. <p>
135     * 
136     * This implementation interprets the interval between the start of the day
137     * of startDay to the end of the day of endDate. 
138     * 
139     * @param startDate the start of the interval, must not be null
140     * @param endDate  the end of the interval, must not be null
141     * @return true if the selected date is contained in the interval
142     */
143    protected boolean isSelectionInInterval(Date startDate, Date endDate) {
144        if (selectedDates.first().before(startOfDay(startDate)) 
145                || (selectedDates.first().after(endOfDay(endDate)))) return false;
146        return true;
147    }
148
149    /**
150     * Selects the given date if it is selectable and not yet selected. 
151     * Does nothing otherwise.
152     * If this operation changes the current selection, it will fire a 
153     * DateSelectionEvent of type DATES_SET.
154     * 
155     * @param date the Date to select, must not be null. 
156     */
157    protected void setSelection(Date date) {
158        Contract.asNotNull(date, "date must not be null");
159        if (isSelectedStrict(date)) return;
160        if (isSelectable(date)) {
161            selectedDates.clear();
162            // PENDING JW: use normalized
163            selectedDates.add(date);
164            fireValueChanged(EventType.DATES_SET);
165        }
166    }
167    
168    /**
169     * Checks and returns whether the given date is contained in the selection.
170     * This differs from isSelected in that it tests for the exact (normalized)
171     * Date instead of for the same day.
172     * 
173     * @param date the Date to test.
174     * @return true if the given date is contained in the selection, 
175     *    false otherwise
176     * 
177     */
178    private boolean isSelectedStrict(Date date) {
179        if (!isSelectionEmpty()) {
180            // PENDING JW: use normalized
181            return selectedDates.first().equals(date);
182        }
183        return false;
184    }
185
186    /**
187     * {@inheritDoc}
188     */
189    @Override
190    public Date getFirstSelectionDate() {
191        return isSelectionEmpty() ? null : selectedDates.first();
192    }
193
194    /**
195     * {@inheritDoc}
196     */
197    @Override
198    public Date getLastSelectionDate() {
199        return isSelectionEmpty() ? null : selectedDates.last();
200    }
201
202    /**
203     * Returns a boolean indicating whether the given date is selectable.
204     * 
205     * @param date the date to check for selectable, must not be null.
206     * @return true if the given date is selectable, false if not.
207     */
208    public  boolean isSelectable(Date date) {
209        if (outOfBounds(date)) return false;
210        return !inUnselectables(date);
211    }
212
213    /**
214     * @param date
215     * @return
216     */
217    private boolean inUnselectables(Date date) {
218        for (Date unselectable : unselectableDates) {
219            if (isSameDay(unselectable, date)) return true;
220        }
221        return false;
222    }
223
224    /**
225     * Returns a boolean indication whether the given date is below
226     * the lower or above the upper bound. 
227     * 
228     * @param date
229     * @return
230     */
231    private boolean outOfBounds(Date date) {
232        if (belowLowerBound(date)) return true;
233        if (aboveUpperBound(date)) return true;
234        return false;
235    }
236
237    /**
238     * @param date
239     * @return
240     */
241    private boolean aboveUpperBound(Date date) {
242        if (upperBound != null) {
243            return endOfDay(upperBound).before(date);
244        }
245        return false;
246    }
247
248    /**
249     * @param date
250     * @return
251     */
252    private boolean belowLowerBound(Date date) {
253        if (lowerBound != null) {
254           return startOfDay(lowerBound).after(date);
255        }
256        return false;
257    }
258
259
260    /**
261     * {@inheritDoc}
262     */
263    @Override
264    public void clearSelection() {
265        if (isSelectionEmpty()) return;
266        selectedDates.clear();
267        fireValueChanged(EventType.SELECTION_CLEARED);
268    }
269
270
271    /**
272     * {@inheritDoc}
273     */
274    @Override
275    public SortedSet<Date> getSelection() {
276        return new TreeSet<Date>(selectedDates);
277    }
278
279    /**
280     * {@inheritDoc}
281     */
282    @Override
283    public boolean isSelected(Date date) {
284        Contract.asNotNull(date, "date must not be null");
285        if (isSelectionEmpty()) return false;
286        return isSameDay(selectedDates.first(), date);
287    }
288
289    
290
291    /**
292     * {@inheritDoc}<p>
293     * 
294     * Implemented to return the date itself.
295     */
296    @Override
297    public Date getNormalizedDate(Date date) {
298        return new Date(date.getTime());
299    }
300
301
302    /**
303     * {@inheritDoc}
304     */
305    @Override
306    public boolean isSelectionEmpty() {
307        return selectedDates.isEmpty();
308    }
309
310
311    /**
312     * {@inheritDoc}
313     */
314    @Override
315    public SortedSet<Date> getUnselectableDates() {
316        return new TreeSet<Date>(unselectableDates);
317    }
318
319    /**
320     * {@inheritDoc}
321     */
322    @Override
323    public void setUnselectableDates(SortedSet<Date> unselectables) {
324        Contract.asNotNull(unselectables, "unselectable dates must not be null");
325        this.unselectableDates.clear();
326        for (Date unselectableDate : unselectables) {
327            removeSelectionInterval(unselectableDate, unselectableDate);
328            unselectableDates.add(unselectableDate);
329        }
330        fireValueChanged(EventType.UNSELECTED_DATES_CHANGED);
331    }
332
333    /**
334     * {@inheritDoc}
335     */
336    @Override
337    public boolean isUnselectableDate(Date date) {
338        return !isSelectable(date);
339    }
340
341
342
343
344
345}