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 }