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