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