001    package market.swing;
002    
003    import java.awt.Dimension;
004    import java.awt.event.ItemEvent;
005    import java.awt.event.ItemListener;
006    import java.io.Serializable;
007    
008    import javax.swing.ComboBoxModel;
009    import javax.swing.DefaultComboBoxModel;
010    import javax.swing.JComboBox;
011    
012    import market.SMarket;
013    import market.event.MarketEventAdapter;
014    import market.statistics.Statistics;
015    
016    /**
017     * A set of {@link JComboBox JComboBoxes} for specifying a range of time for which statistics should
018     * be displayed.
019     */
020    public class JCTimeRangeBoxes implements Serializable {
021    
022        /**
023         * The JComboBox for the start date's month.
024         */
025        private JComboBox jcbFromMonth = new JComboBox();
026    
027        /**
028         * The JComboBox for the start date's year.
029         */
030        private JComboBox jcbFromYear = new JComboBox();
031    
032        /**
033         * The JComboBox for the finish date's month.
034         */
035        private JComboBox jcbToMonth = new JComboBox();
036    
037        /**
038         * The JComboBox for the finish date's year.
039         */
040        private JComboBox jcbToYear = new JComboBox();
041        private Object[] oFromMonth, oFromYear, oToMonth, oToYear;
042        private Dimension monthBoxDimension = new Dimension(100,25);
043        private Dimension yearBoxDimension = new Dimension(65,25);
044    
045        public JCTimeRangeBoxes() {
046            //both preferredSize and minimumSize have to be set:
047            //preferredSize for the size the JComboBox is actually displayed with its borders
048            //minimumSize for the size the BoxLayout manager reserves for displaying the JComboBox
049            jcbFromYear.setPreferredSize(yearBoxDimension);
050            jcbFromYear.setMinimumSize(yearBoxDimension);
051            jcbToYear.setPreferredSize(yearBoxDimension);
052            jcbToYear.setMinimumSize(yearBoxDimension);
053            jcbFromMonth.setPreferredSize(monthBoxDimension);
054            jcbFromMonth.setMinimumSize(monthBoxDimension);
055            jcbToMonth.setPreferredSize(monthBoxDimension);
056            jcbToMonth.setMinimumSize(monthBoxDimension);
057            init();
058            SMarket.addEventListener(new MarketEventAdapter() {
059                public void timeAdvanced() {
060                    init();
061                }
062            });
063            jcbFromYear.addItemListener(new FromItemListener());
064            jcbToYear.addItemListener(new ToItemListener());
065        }
066    
067        /**
068         * Initializes the ComboBoxes' models according to the range of time from which
069         * statistics can be displayed.
070         */
071        public void init() {
072            jcbFromYear.setModel(new DefaultComboBoxModel(Statistics.createArticleStatisticsYears()));
073            jcbToYear.setModel(new DefaultComboBoxModel(Statistics.createArticleStatisticsYears()));
074            jcbFromMonth.setModel(new DefaultComboBoxModel(Statistics.createArticleStatisticsMonths(
075                    jcbFromYear.getSelectedItem())));
076            jcbToMonth.setModel(new DefaultComboBoxModel(Statistics.createArticleStatisticsMonths(
077                    jcbFromYear.getSelectedItem())));
078        }
079    
080        /**
081         * @return the index of the month displayed by {@link #jcbFromMonth}.
082         */
083        public int getFromMonth() {
084            return getMonth((String)jcbFromMonth.getSelectedItem());
085        }
086    
087        /**
088         * @return the index of the month displayed by {@link #jcbToMonth}.
089         */
090        public int getToMonth() {
091            return getMonth((String)jcbToMonth.getSelectedItem());
092        }
093    
094        /**
095         * @return the index of the month displayed by {@link #jcbFromYear}.
096         */
097        public int getFromYear() {
098            return new Integer((String)jcbFromYear.getSelectedItem()).intValue();
099        }
100    
101        /**
102         * @return the index of the month displayed by {@link #jcbToYear}.
103         */
104        public int getToYear() {
105            return new Integer((String)jcbToYear.getSelectedItem()).intValue();
106        }
107    
108        /**
109         * @param s the name of a month (e.g. Januar or März).
110         * @return the index of the searched month according to the
111         * {@linkplain market.statistics.Statistics#MONTHS months array}.
112         */
113        public int getMonth(String s) {
114            Object[] months = Statistics.MONTHS;
115            for (int i = 0; i <= 11; i++) {
116                if (months[i].equals(s)) return i;
117            }
118            return -1;
119        }
120    
121        /**
122         * Checks if the start of the desired range of time is not after the end of the time range.
123         * @return <code>true</code> if a valid time range was selected, otherwise <code>false</code>.
124         */
125        public boolean isValidTimeRange() {
126            return (getFromYear() < getToYear()) ||
127                    (getFromYear() == getToYear() && getFromMonth() <= getToMonth());
128        }
129    
130        /**
131         * Checks if two {@link ComboBoxModel ComboBoxModels} are equal.<br>
132         * This method is used to check if the available months of a box changed. This can happen when
133         * the year's box is changed.
134         *
135         * @see FromItemListener
136         * @see ToItemListener
137         * @param cbm1 first ComboBoxModel to be compared.
138         * @param cbm2 second ComboBoxModel to be compared.
139         * @return <code>true</code> if both models are equal, otherwise <code>false</code>.
140         */
141        private boolean areModelsEqual(ComboBoxModel cbm1, ComboBoxModel cbm2) {
142            String firstOld = (String)cbm1.getElementAt(0);
143            String lastOld = (String)cbm1.getElementAt(cbm1.getSize() - 1);
144            String firstNew = (String)cbm2.getElementAt(0);
145            String lastNew = (String)cbm2.getElementAt(cbm2.getSize() - 1);
146            return firstOld.equals(firstNew) && lastOld.equals(lastNew);
147        }
148    
149        /**
150         * Checks if a month is contained in a JComboBox.<br>
151         * @see FromItemListener
152         * @see ToItemListener
153         * @param jcb the JComboBox to be examined.
154         * @param month the searched month.
155         * @return <code>true</code> if the month is part of the JComboBox, otherwise <code>false</code>.
156         */
157        private boolean containsMonth(JComboBox jcb, int month) {
158            //searched month later than last month in box?
159            if (month > jcb.getItemCount() - 1) return false;
160            //searched month earlier than first month in box?
161            int firstMonthOfBox = getMonth((String)jcb.getItemAt(0));
162            return !(firstMonthOfBox > month);
163        }
164    
165        /**
166         * @return {@link #jcbFromMonth}.
167         */
168        public JComboBox getFromMonthBox() {
169            return jcbFromMonth;
170        }
171    
172        /**
173         * @return {@link #jcbFromYear}.
174         */
175        public JComboBox getFromYearBox() {
176            return jcbFromYear;
177        }
178    
179        /**
180         * @return {@link #jcbToMonth}.
181         */
182        public JComboBox getToMonthBox() {
183            return jcbToMonth;
184        }
185    
186        /**
187         * @return {@link #jcbToYear}.
188         */
189        public JComboBox getToYearBox() {
190            return jcbToYear;
191        }
192    
193       /**
194        * Listens to state changes on the {@link #jcbFromYear}.<br><br>
195        * Whenever a new year is selected, the {@linkplain #jcbFromMonth month's values} will be
196        * {@linkplain Statistics#createArticleStatisticsMonths recomputed}.<br>
197        * If the JComboBoxes' models {@linkplain #areModelsEqual(ComboBoxModel, ComboBoxModel) are not equal},
198        * i.e. months were added or removed, the new model is set. But that causes the JComboBox to be set
199        * to its first value, even if that's not necessary.<br>
200        * That's why there is another check, namely if the originally displayed month
201        * {@linkplain #containsMonth(JComboBox, int) is contained} in the new JComboBox. If so, the
202        * JComboBox will be set to this month.
203        */
204        private class FromItemListener implements ItemListener, Serializable {
205            public void itemStateChanged(ItemEvent e) {
206                if (e.getStateChange() == e.SELECTED) {
207                    ComboBoxModel cbm = new DefaultComboBoxModel(
208                            Statistics.createArticleStatisticsMonths(
209                            jcbFromYear.getSelectedItem()));               //update list of available months
210                    if (!areModelsEqual(jcbFromMonth.getModel(), cbm)) {   //if number of available months changed
211                        int oldMonthSet = getFromMonth();                  //save currently displayed month
212                        String oldMonthString = (String)jcbFromMonth.getSelectedItem();
213                        jcbFromMonth.setModel(cbm);                        //set new model
214                        if (containsMonth(jcbFromMonth, oldMonthSet)) {    //if saved month contained in new model
215                            jcbFromMonth.setSelectedItem(oldMonthString);  //set box to that month
216                        }
217                    }
218                }
219            }
220        }
221    
222        /**
223         * Listens to state changes on the {@link #jcbToYear}.<br><br>
224         * Whenever a new year is selected, the {@linkplain #jcbToMonth month's values} will be
225         * {@linkplain Statistics#createArticleStatisticsMonths recomputed}.<br>
226         * If the JComboBoxes' models {@linkplain #areModelsEqual(ComboBoxModel, ComboBoxModel) are not equal},
227         * i.e. months were added or removed, the new model is set. But that causes the JComboBox to be set
228         * to its first value, even if that's not necessary.<br>
229         * That's why there is another check, namely if the originally displayed month
230         * {@linkplain #containsMonth(JComboBox, int) is contained} in the new JComboBox. If so, the
231         * JComboBox will be set to this month.
232         */
233        private class ToItemListener implements ItemListener, Serializable {
234            public void itemStateChanged(ItemEvent e) {
235                if (e.getStateChange() == e.SELECTED) {
236                    ComboBoxModel cbm = new DefaultComboBoxModel(
237                            Statistics.createArticleStatisticsMonths(
238                            jcbToYear.getSelectedItem()));                //update list of available months
239                    if (!areModelsEqual(jcbToMonth.getModel(), cbm)) {    //if number of available months changed
240                        int oldMonthSet = getToMonth();                   //save currently displayed month
241                        String oldMonthString = (String)jcbToMonth.getSelectedItem();
242                        jcbToMonth.setModel(cbm);                         //set new model
243                        if (containsMonth(jcbToMonth, oldMonthSet)) {     //if saved month contained in new model
244                            jcbToMonth.setSelectedItem(oldMonthString);   //set box to that month
245                        }
246                    }
247                }
248            }
249        }
250    
251    }