1 /*
2  *  timeperiod.h  -  time period data entry widget
3  *  Program:  kalarm
4  *  SPDX-FileCopyrightText: 2003-2021 David Jarvie <djarvie@kde.org>
5  *
6  *  SPDX-License-Identifier: GPL-2.0-or-later
7  */
8 
9 #include "timeperiod.h"
10 
11 #include "combobox.h"
12 #include "spinbox.h"
13 #include "timespinbox.h"
14 #include "lib/stackedwidgets.h"
15 
16 #include <KLocalizedString>
17 
18 #include <QHBoxLayout>
19 
20 using namespace KCalendarCore;
21 
22 // Collect these widget labels together to ensure consistent wording and
23 // translations across different modules.
i18n_minutes()24 QString TimePeriod::i18n_minutes()      { return i18nc("@item:inlistbox Time units", "minutes"); }
i18n_hours_mins()25 QString TimePeriod::i18n_hours_mins()   { return i18nc("@item:inlistbox Time units", "hours/minutes"); }
i18n_days()26 QString TimePeriod::i18n_days()         { return i18nc("@item:inlistbox Time units", "days"); }
i18n_weeks()27 QString TimePeriod::i18n_weeks()        { return i18nc("@item:inlistbox Time units", "weeks"); }
28 
29 static const int maxMinutes = 1000*60-1;   // absolute maximum value for hours:minutes = 999H59M
30 
31 /*=============================================================================
32 = Class TimePeriod
33 = Contains a time unit combo box, plus a time spinbox, to select a time period.
34 =============================================================================*/
35 
TimePeriod(bool allowHourMinute,QWidget * parent)36 TimePeriod::TimePeriod(bool allowHourMinute, QWidget* parent)
37     : QWidget(parent)
38     , mNoHourMinute(!allowHourMinute)
39 {
40     auto layout = new QHBoxLayout;
41     setLayout(layout);
42     layout->setContentsMargins(0, 0, 0, 0);
43 
44     mSpinStack = new StackedWidget(this);
45     mSpinBox = new SpinBox(mSpinStack);
46     mSpinBox->setSingleStep(1);
47     mSpinBox->setSingleShiftStep(10);
48     mSpinBox->setRange(1, mMaxDays);
49     connect(mSpinBox, &SpinBox::valueChanged, this, &TimePeriod::slotDaysChanged);
50     mSpinStack->addWidget(mSpinBox);
51 
52     mTimeSpinBox = new TimeSpinBox(0, 99999, mSpinStack);
53     mTimeSpinBox->setRange(1, maxMinutes);    // max 999H59M
54     connect(mTimeSpinBox, &TimeSpinBox::valueChanged, this, &TimePeriod::slotTimeChanged);
55     mSpinStack->addWidget(mTimeSpinBox);
56 
57     mHourMinuteRaised = mNoHourMinute;
58     showHourMin(!mNoHourMinute);
59     layout->addWidget(mSpinStack);
60 
61     mUnitsCombo = new ComboBox(this);
62     mUnitsCombo->setEditable(false);
63     if (mNoHourMinute)
64         mDateOnlyOffset = 2;
65     else
66     {
67         mDateOnlyOffset = 0;
68         mUnitsCombo->addItem(i18n_minutes());
69         mUnitsCombo->addItem(i18n_hours_mins());
70     }
71     mUnitsCombo->addItem(i18n_days());
72     mUnitsCombo->addItem(i18n_weeks());
73     mMaxUnitShown = Weeks;
74     connect(mUnitsCombo, &ComboBox::activated, this, &TimePeriod::slotUnitsSelected);
75     layout->addWidget(mUnitsCombo);
76 
77     setFocusProxy(mUnitsCombo);
78     setTabOrder(mUnitsCombo, mSpinStack);
79 }
80 
setReadOnly(bool ro)81 void TimePeriod::setReadOnly(bool ro)
82 {
83     if (ro != mReadOnly)
84     {
85         mReadOnly = ro;
86         mSpinBox->setReadOnly(ro);
87         mTimeSpinBox->setReadOnly(ro);
88         mUnitsCombo->setReadOnly(ro);
89     }
90 }
91 
92 /******************************************************************************
93 *  Set whether the editor text is to be selected whenever spin buttons are
94 *  clicked. Default is to select them.
95 */
setSelectOnStep(bool sel)96 void TimePeriod::setSelectOnStep(bool sel)
97 {
98     mSpinBox->setSelectOnStep(sel);
99     mTimeSpinBox->setSelectOnStep(sel);
100 }
101 
102 /******************************************************************************
103 *  Set the input focus on the count field.
104 */
setFocusOnCount()105 void TimePeriod::setFocusOnCount()
106 {
107     mSpinStack->setFocus();
108 }
109 
110 /******************************************************************************
111 *  Set the maximum values for the hours:minutes and days/weeks spinboxes.
112 *  If 'hourmin' = 0, the hours:minutes maximum is left unchanged.
113 */
setMaximum(int hourmin,int days)114 void TimePeriod::setMaximum(int hourmin, int days)
115 {
116     const Duration oldmins = period();
117     if (hourmin > 0)
118     {
119         if (hourmin > maxMinutes)
120             hourmin = maxMinutes;
121         mTimeSpinBox->setRange(1, hourmin);
122     }
123     mMaxDays = (days >= 0) ? days : 0;
124     adjustDayWeekShown();
125     setUnitRange();
126     const Duration mins = period();
127     if (mins != oldmins)
128         Q_EMIT valueChanged(mins);
129 }
130 
131 /******************************************************************************
132  * Get the specified time period.
133  * Reply = 0 if error.
134  */
period() const135 Duration TimePeriod::period() const
136 {
137     int factor = 1;
138     switch (mUnitsCombo->currentIndex() + mDateOnlyOffset)
139     {
140         case HoursMinutes:
141             return Duration(mTimeSpinBox->value() * 60, Duration::Seconds);
142         case Minutes:
143             return Duration(mSpinBox->value() * 60, Duration::Seconds);
144         case Weeks:
145             factor = 7;
146             // fall through to DAYS
147             Q_FALLTHROUGH();
148         case Days:
149             return Duration(mSpinBox->value() * factor, Duration::Days);
150     }
151     return 0;
152 }
153 
154 /******************************************************************************
155 *  Initialise the controls with a specified time period.
156 *  The time unit combo-box is initialised to 'defaultUnits', but if 'dateOnly'
157 *  is true, it will never be initialised to minutes or hours/minutes.
158 */
setPeriod(const Duration & perod,bool dateOnly,TimePeriod::Units defaultUnits)159 void TimePeriod::setPeriod(const Duration& perod, bool dateOnly, TimePeriod::Units defaultUnits)
160 {
161     const Duration oldinterval = period();
162     if (!dateOnly  &&  mNoHourMinute)
163         dateOnly = true;
164     int item;
165     if (!perod.isNull())
166     {
167         int count = perod.value();
168         if (perod.isDaily())
169         {
170             if (count % 7)
171                 item = Days;
172             else
173             {
174                 item = Weeks;
175                 count /= 7;
176             }
177         }
178         else
179         {
180             count /= 60;   // minutes
181             item = (defaultUnits == Minutes && count <= mSpinBox->maximum()) ? Minutes : HoursMinutes;
182         }
183         if (item < mDateOnlyOffset)
184             item = mDateOnlyOffset;
185         else if (item > mMaxUnitShown)
186             item = mMaxUnitShown;
187         mUnitsCombo->setCurrentIndex(item - mDateOnlyOffset);
188         if (item == HoursMinutes)
189             mTimeSpinBox->setValue(count);
190         else
191             mSpinBox->setValue(count);
192         item = setDateOnly(perod, dateOnly, false);
193     }
194     else
195     {
196         item = defaultUnits;
197         if (item < mDateOnlyOffset)
198             item = mDateOnlyOffset;
199         else if (item > mMaxUnitShown)
200             item = mMaxUnitShown;
201         mUnitsCombo->setCurrentIndex(item - mDateOnlyOffset);
202         if ((dateOnly && !mDateOnlyOffset)  ||  (!dateOnly && mDateOnlyOffset))
203             item = setDateOnly(perod, dateOnly, false);
204     }
205     setUnitRange();
206     showHourMin(item == HoursMinutes  &&  !mNoHourMinute);
207 
208     const Duration newinterval = period();
209     if (newinterval != oldinterval)
210         Q_EMIT valueChanged(newinterval);
211 }
212 
213 /******************************************************************************
214 *  Enable/disable hours/minutes units (if hours/minutes were permitted in the
215 *  constructor).
216 */
setDateOnly(const Duration & perod,bool dateOnly,bool signal)217 TimePeriod::Units TimePeriod::setDateOnly(const Duration& perod, bool dateOnly, bool signal)
218 {
219     Duration oldinterval = 0;
220     if (signal)
221         oldinterval = period();
222     int index = mUnitsCombo->currentIndex();
223     auto units = static_cast<Units>(index + mDateOnlyOffset);
224     if (!mNoHourMinute)
225     {
226         if (!dateOnly  &&  mDateOnlyOffset)
227         {
228             // Change from date-only to allow hours/minutes
229             mUnitsCombo->insertItem(0, i18n_minutes());
230             mUnitsCombo->insertItem(1, i18n_hours_mins());
231             mDateOnlyOffset = 0;
232             adjustDayWeekShown();
233             mUnitsCombo->setCurrentIndex(index + 2);
234         }
235         else if (dateOnly  &&  !mDateOnlyOffset)
236         {
237             // Change from allowing hours/minutes to date-only
238             mUnitsCombo->removeItem(0);
239             mUnitsCombo->removeItem(0);
240             mDateOnlyOffset = 2;
241             if (index > 2)
242                 index -= 2;
243             else
244                 index = 0;
245             adjustDayWeekShown();
246             mUnitsCombo->setCurrentIndex(index);
247             if (units == HoursMinutes  ||  units == Minutes)
248             {
249                 // Set units to days and round up the warning period
250                 units = Days;
251                 mUnitsCombo->setCurrentIndex(Days - mDateOnlyOffset);
252                 mSpinBox->setValue(perod.asDays());
253             }
254             showHourMin(false);
255         }
256     }
257 
258     if (signal)
259     {
260         const Duration newinterval = period();
261         if (newinterval != oldinterval)
262             Q_EMIT valueChanged(newinterval);
263     }
264     return units;
265 }
266 
267 /******************************************************************************
268 *  Adjust the days/weeks units shown to suit the maximum days limit.
269 */
adjustDayWeekShown()270 void TimePeriod::adjustDayWeekShown()
271 {
272     const Units newMaxUnitShown = (mMaxDays >= 7) ? Weeks : (mMaxDays || mDateOnlyOffset) ? Days : HoursMinutes;
273     if (newMaxUnitShown > mMaxUnitShown)
274     {
275         if (mMaxUnitShown < Days)
276             mUnitsCombo->addItem(i18n_days());
277         if (newMaxUnitShown == Weeks)
278             mUnitsCombo->addItem(i18n_weeks());
279     }
280     else if (newMaxUnitShown < mMaxUnitShown)
281     {
282         if (mMaxUnitShown == Weeks)
283             mUnitsCombo->removeItem(Weeks - mDateOnlyOffset);
284         if (newMaxUnitShown < Days)
285             mUnitsCombo->removeItem(Days - mDateOnlyOffset);
286     }
287     mMaxUnitShown = newMaxUnitShown;
288 }
289 
290 /******************************************************************************
291 *  Set the maximum value which may be entered into the day/week count field,
292 *  depending on the current unit selection.
293 */
setUnitRange()294 void TimePeriod::setUnitRange()
295 {
296     int maxval;
297     switch (static_cast<Units>(mUnitsCombo->currentIndex() + mDateOnlyOffset))
298     {
299         case Weeks:
300             maxval = mMaxDays / 7;
301             if (maxval)
302                 break;
303             mUnitsCombo->setCurrentIndex(Days - mDateOnlyOffset);
304             // fall through to Days
305             Q_FALLTHROUGH();
306         case Days:
307             maxval = mMaxDays ? mMaxDays : 1;
308             break;
309         case Minutes:
310             maxval = mTimeSpinBox->maximum();
311             break;
312         case HoursMinutes:
313         default:
314             return;
315     }
316     mSpinBox->setRange(1, maxval);
317 }
318 
319 /******************************************************************************
320 * Set the time units selection.
321 */
setUnits(Units units)322 void TimePeriod::setUnits(Units units)
323 {
324     const auto oldUnits = static_cast<Units>(mUnitsCombo->currentIndex() + mDateOnlyOffset);
325     if (units == oldUnits)
326         return;
327     if (oldUnits == HoursMinutes  &&  units == Minutes)
328     {
329         if (mTimeSpinBox->value() > mSpinBox->maximum())
330             return;
331         mSpinBox->setValue(mTimeSpinBox->value());
332     }
333     else if (oldUnits == Minutes  &&  units == HoursMinutes)
334         mTimeSpinBox->setValue(mSpinBox->value());
335     if (units >= mDateOnlyOffset  &&  units <= mMaxUnitShown)
336     {
337         const int item = units - mDateOnlyOffset;
338         mUnitsCombo->setCurrentIndex(item);
339         slotUnitsSelected(item);
340     }
341 }
342 
343 /******************************************************************************
344 * Return the current time units selection.
345 */
units() const346 TimePeriod::Units TimePeriod::units() const
347 {
348     return static_cast<Units>(mUnitsCombo->currentIndex() + mDateOnlyOffset);
349 }
350 
351 /******************************************************************************
352 *  Called when a new item is made current in the time units combo box.
353 */
slotUnitsSelected(int index)354 void TimePeriod::slotUnitsSelected(int index)
355 {
356     setUnitRange();
357     showHourMin(index + mDateOnlyOffset == HoursMinutes);
358     Q_EMIT valueChanged(period());
359 }
360 
361 /******************************************************************************
362 *  Called when the value of the days/weeks spin box changes.
363 */
slotDaysChanged(int)364 void TimePeriod::slotDaysChanged(int)
365 {
366     if (!mHourMinuteRaised)
367         Q_EMIT valueChanged(period());
368 }
369 
370 /******************************************************************************
371 *  Called when the value of the time spin box changes.
372 */
slotTimeChanged(int)373 void TimePeriod::slotTimeChanged(int)
374 {
375     if (mHourMinuteRaised)
376         Q_EMIT valueChanged(period());
377 }
378 
379 /******************************************************************************
380  * Set the currently displayed count widget.
381  */
showHourMin(bool hourMinute)382 void TimePeriod::showHourMin(bool hourMinute)
383 {
384     if (hourMinute != mHourMinuteRaised)
385     {
386         mHourMinuteRaised = hourMinute;
387         if (hourMinute)
388         {
389             mSpinStack->setCurrentWidget(mTimeSpinBox);
390             mSpinStack->setFocusProxy(mTimeSpinBox);
391         }
392         else
393         {
394             mSpinStack->setCurrentWidget(mSpinBox);
395             mSpinStack->setFocusProxy(mSpinBox);
396         }
397     }
398 }
399 
400 /******************************************************************************
401  * Set separate WhatsThis texts for the count spinboxes and the units combobox.
402  * If the hours:minutes text is omitted, both spinboxes are set to the same
403  * WhatsThis text.
404  */
setWhatsThises(const QString & units,const QString & dayWeek,const QString & hourMin)405 void TimePeriod::setWhatsThises(const QString& units, const QString& dayWeek, const QString& hourMin)
406 {
407     mUnitsCombo->setWhatsThis(units);
408     mSpinBox->setWhatsThis(dayWeek);
409     mTimeSpinBox->setWhatsThis(hourMin.isNull() ? dayWeek : hourMin);
410 }
411 
412 // vim: et sw=4:
413