001/*
002 * $Id: DaySelectionModel.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.ArrayList;
024import java.util.Calendar;
025import java.util.Date;
026import java.util.Locale;
027import java.util.SortedSet;
028import java.util.TreeSet;
029
030import org.jdesktop.swingx.event.EventListenerMap;
031import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
032import org.jdesktop.swingx.util.Contract;
033
034/**
035 * 
036 * DaySelectionModel is a (temporary?) implementation of DateSelectionModel 
037 * which normalizes all dates to the start of the day, that is zeroes all 
038 * time fields. Responsibility extracted from JXMonthView (which must
039 * respect rules of model instead of trying to be clever itself).
040 * 
041 * @author Joshua Outwater
042 */
043public class DaySelectionModel extends AbstractDateSelectionModel {
044    private SelectionMode selectionMode;
045    private SortedSet<Date> selectedDates;
046    private SortedSet<Date> unselectableDates;
047
048    /**
049     * 
050     */
051    public DaySelectionModel() {
052        this(null);
053    }
054
055    /**
056     * 
057     */
058    public DaySelectionModel(Locale locale) {
059        super(locale);
060        this.listenerMap = new EventListenerMap();
061        this.selectionMode = SelectionMode.SINGLE_SELECTION;
062        this.selectedDates = new TreeSet<Date>();
063        this.unselectableDates = new TreeSet<Date>();
064        
065    }
066    /**
067     * 
068     */
069    @Override
070    public SelectionMode getSelectionMode() {
071        return selectionMode;
072    }
073
074    /**
075     * 
076     */
077    @Override
078    public void setSelectionMode(final SelectionMode selectionMode) {
079        this.selectionMode = selectionMode;
080        clearSelection();
081    }
082
083    //---------------------- selection ops    
084    /**
085     * {@inheritDoc}
086     */
087    @Override
088    public void addSelectionInterval(Date startDate, Date endDate) {
089        if (startDate.after(endDate)) {
090            return;
091        }
092        startDate = startOfDay(startDate);
093        endDate = startOfDay(endDate);
094        boolean added = false;
095        switch (selectionMode) {
096            case SINGLE_SELECTION:
097                if (isSelected(startDate)) return;
098                clearSelectionImpl();
099                added = addSelectionImpl(startDate, startDate);
100                break;
101            case SINGLE_INTERVAL_SELECTION:
102                if (isIntervalSelected(startDate, endDate)) return;
103                clearSelectionImpl();
104                added = addSelectionImpl(startDate, endDate);
105                break;
106            case MULTIPLE_INTERVAL_SELECTION:
107                if (isIntervalSelected(startDate, endDate)) return;
108                added = addSelectionImpl(startDate, endDate);
109                break;
110            default:
111                break;
112        }
113        if (added) {
114            fireValueChanged(EventType.DATES_ADDED);
115        }
116    }
117
118    /**
119     * {@inheritDoc}
120     */
121    @Override
122    public void setSelectionInterval(Date startDate, Date endDate) {
123        startDate = startOfDay(startDate);
124        endDate = startOfDay(endDate);
125        if (SelectionMode.SINGLE_SELECTION.equals(selectionMode)) {
126           if (isSelected(startDate)) return;
127           endDate = startDate;
128        } else {
129            if (isIntervalSelected(startDate, endDate)) return;
130        }
131        clearSelectionImpl();
132        if (addSelectionImpl(startDate, endDate)) {
133            fireValueChanged(EventType.DATES_SET);
134        }
135    }
136
137    /**
138     * Checks and returns if the single date interval bounded by startDate and endDate
139     * is selected. This is useful only for SingleInterval mode.
140     * 
141     * @param startDate the start of the interval
142     * @param endDate the end of the interval, must be >= startDate
143     * @return true the interval is selected, false otherwise.
144     */
145    private boolean isIntervalSelected(Date startDate, Date endDate) {
146        if (isSelectionEmpty()) return false;
147        startDate = startOfDay(startDate);
148        endDate = startOfDay(endDate);
149        return selectedDates.first().equals(startDate) 
150           && selectedDates.last().equals(endDate);
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    public void removeSelectionInterval(Date startDate, Date endDate) {
158        if (startDate.after(endDate)) {
159            return;
160        }
161
162        startDate = startOfDay(startDate);
163        endDate = startOfDay(endDate);
164        long startDateMs = startDate.getTime();
165        long endDateMs = endDate.getTime();
166        ArrayList<Date> datesToRemove = new ArrayList<Date>();
167        for (Date selectedDate : selectedDates) {
168            long selectedDateMs = selectedDate.getTime();
169            if (selectedDateMs >= startDateMs && selectedDateMs <= endDateMs) {
170                datesToRemove.add(selectedDate);
171            }
172        }
173
174        if (!datesToRemove.isEmpty()) {
175            selectedDates.removeAll(datesToRemove);
176            fireValueChanged(EventType.DATES_REMOVED);
177        }
178    }
179
180    /**
181     * {@inheritDoc}
182     */
183    @Override
184    public void clearSelection() {
185        if (isSelectionEmpty()) return;
186        clearSelectionImpl();
187        fireValueChanged(EventType.SELECTION_CLEARED);
188    }
189
190    private void clearSelectionImpl() {
191        selectedDates.clear();
192    }
193
194    /**
195     * {@inheritDoc}
196     */
197    @Override
198    public SortedSet<Date> getSelection() {
199        return new TreeSet<Date>(selectedDates);
200    }
201
202    /**
203     * {@inheritDoc}
204     */
205    @Override
206    public Date getFirstSelectionDate() {
207        return isSelectionEmpty() ? null : selectedDates.first();
208    }
209
210    /**
211     * {@inheritDoc}
212     */
213    @Override
214    public Date getLastSelectionDate() {
215        return isSelectionEmpty() ? null : selectedDates.last();
216    }
217
218    /**
219     * {@inheritDoc}
220     */
221    @Override
222    public boolean isSelected(Date date) {
223        // JW: don't need Contract ... startOfDay will throw NPE if null
224        return selectedDates.contains(startOfDay(date));
225    }
226
227    /**
228     * {@inheritDoc}
229     */
230    @Override
231    public boolean isSelectionEmpty() {
232        return selectedDates.isEmpty();
233    }
234
235    /**
236     * {@inheritDoc}
237     */
238    @Override
239    public SortedSet<Date> getUnselectableDates() {
240        return new TreeSet<Date>(unselectableDates);
241    }
242
243    /**
244     * {@inheritDoc}
245     */
246    @Override
247    public void setUnselectableDates(SortedSet<Date> unselectables) {
248        this.unselectableDates.clear();
249        for (Date date : unselectables) {
250            unselectableDates.add(startOfDay(date));
251        }
252        for (Date unselectableDate : this.unselectableDates) {
253            removeSelectionInterval(unselectableDate, unselectableDate);
254        }
255        fireValueChanged(EventType.UNSELECTED_DATES_CHANGED);
256    }
257
258    /**
259     * {@inheritDoc}
260     */
261    @Override
262    public boolean isUnselectableDate(Date date) {
263        date = startOfDay(date);
264        return upperBound != null && upperBound.getTime() < date.getTime() ||
265                lowerBound != null && lowerBound.getTime() > date.getTime() ||
266                unselectableDates != null && unselectableDates.contains(date);
267    }
268
269
270    private boolean addSelectionImpl(final Date startDate, final Date endDate) {
271        boolean hasAdded = false;
272        calendar.setTime(startDate);
273        Date date = calendar.getTime();
274        while (date.before(endDate) || date.equals(endDate)) {
275            if (!isUnselectableDate(date)) {
276                hasAdded = true;
277                selectedDates.add(date);
278            }
279            calendar.add(Calendar.DATE, 1);
280            date = calendar.getTime();
281        }
282        return hasAdded;
283    }
284
285
286    /**
287     * {@inheritDoc}
288     */
289
290    /**
291     * {@inheritDoc} <p>
292     * 
293     * Implemented to return the start of the day which contains the date.
294     */
295    @Override
296    public Date getNormalizedDate(Date date) {
297        Contract.asNotNull(date, "date must not be null");
298        return startOfDay(date);
299    }
300
301}