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 }