1 /*
2     SPDX-FileCopyrightText: 2011 John Layt <john@layt.net>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "ktimecombobox.h"
8 
9 #include <QKeyEvent>
10 #include <QLineEdit>
11 #include <QTime>
12 
13 #include "kmessagebox.h"
14 
15 class KTimeComboBoxPrivate
16 {
17 public:
18     KTimeComboBoxPrivate(KTimeComboBox *qq);
19     virtual ~KTimeComboBoxPrivate();
20 
21     QTime defaultMinTime();
22     QTime defaultMaxTime();
23 
24     std::pair<QString, QString> timeFormatToInputMask(const QString &format);
25     QTime nearestIntervalTime(const QTime &time);
26     QString formatTime(const QTime &time);
27 
28     void initTimeWidget();
29     void updateTimeWidget();
30 
31     // Private slots
32     void selectTime(int index);
33     void editTime(const QString &text);
34     void enterTime(const QTime &time);
35     void parseTime();
36     void warnTime();
37 
38     KTimeComboBox *const q;
39 
40     QTime m_time;
41     KTimeComboBox::Options m_options;
42     QTime m_minTime;
43     QTime m_maxTime;
44     QString m_minWarnMsg;
45     QString m_maxWarnMsg;
46     QString m_nullString;
47     bool m_warningShown;
48     QLocale::FormatType m_displayFormat;
49     int m_timeListInterval;
50     QList<QTime> m_timeList;
51 };
52 
KTimeComboBoxPrivate(KTimeComboBox * qq)53 KTimeComboBoxPrivate::KTimeComboBoxPrivate(KTimeComboBox *qq)
54     : q(qq)
55     , m_time(QTime(0, 0, 0))
56     , m_warningShown(false)
57     , m_displayFormat(QLocale::ShortFormat)
58     , m_timeListInterval(15)
59 {
60     m_options = KTimeComboBox::EditTime | KTimeComboBox::SelectTime;
61     m_minTime = defaultMinTime();
62     m_maxTime = defaultMaxTime();
63 }
64 
~KTimeComboBoxPrivate()65 KTimeComboBoxPrivate::~KTimeComboBoxPrivate()
66 {
67 }
68 
defaultMinTime()69 QTime KTimeComboBoxPrivate::defaultMinTime()
70 {
71     return QTime(0, 0, 0, 0);
72 }
73 
defaultMaxTime()74 QTime KTimeComboBoxPrivate::defaultMaxTime()
75 {
76     return QTime(23, 59, 59, 999);
77 }
78 
timeFormatToInputMask(const QString & format)79 std::pair<QString, QString> KTimeComboBoxPrivate::timeFormatToInputMask(const QString &format)
80 {
81     const QLocale locale = q->locale();
82 
83     QString example = formatTime(QTime(12, 34, 56, 789));
84     // Replace time components with edit mask characters.
85     example.replace(locale.toString(12), QLatin1String("09"));
86     example.replace(locale.toString(34), QLatin1String("99"));
87     example.replace(locale.toString(56), QLatin1String("99"));
88     example.replace(locale.toString(789), QLatin1String("900"));
89 
90     // See if this time format contains a specifier for
91     // AM/PM, regardless of case.
92     int ampmPos = format.indexOf(QLatin1String("AP"), 0, Qt::CaseInsensitive);
93 
94     if (ampmPos != -1) {
95         // Get the locale aware am/pm strings
96         QString am = locale.amText();
97         QString pm = locale.pmText();
98 
99         // Convert the am/pm strings to the same case
100         // as the input format. This is necessary to
101         // provide a correct mask to the line edit.
102         if (format[ampmPos].isUpper()) {
103             am = am.toUpper();
104             pm = pm.toUpper();
105         } else {
106             am = am.toLower();
107             pm = pm.toLower();
108         }
109 
110         int ampmLen = qMax(am.length(), pm.length());
111         const QString ampmMask(ampmLen, QLatin1Char('x'));
112         example.replace(pm, ampmMask);
113     }
114 
115     // Build a mask by copying mask characters and escaping the rest.
116     QString mask;
117     QString null;
118     for (const QChar c : example) {
119         if (c == QLatin1Char('0') || c == QLatin1Char('9') || c == QLatin1Char('x')) {
120             mask.append(c);
121         } else {
122             mask.append(QLatin1Char('\\'));
123             mask.append(c);
124             null.append(c);
125         }
126     }
127 
128     return std::make_pair(mask, null);
129 }
130 
nearestIntervalTime(const QTime & time)131 QTime KTimeComboBoxPrivate::nearestIntervalTime(const QTime &time)
132 {
133     int i = 0;
134     while (q->itemData(i).toTime() < time) {
135         ++i;
136     }
137     QTime before = q->itemData(i).toTime();
138     QTime after = q->itemData(i + 1).toTime();
139     if (before.secsTo(time) <= time.secsTo(after)) {
140         return before;
141     } else {
142         return after;
143     }
144 }
145 
formatTime(const QTime & time)146 QString KTimeComboBoxPrivate::formatTime(const QTime &time)
147 {
148     return q->locale().toString(time, m_displayFormat);
149 }
150 
initTimeWidget()151 void KTimeComboBoxPrivate::initTimeWidget()
152 {
153     q->blockSignals(true);
154     q->clear();
155 
156     // Set the input mask from the current format
157     QString mask;
158     std::tie(mask, m_nullString) = timeFormatToInputMask(q->locale().timeFormat(m_displayFormat));
159     q->lineEdit()->setInputMask(mask);
160 
161     // If EditTime then set the line edit
162     q->lineEdit()->setReadOnly((m_options & KTimeComboBox::EditTime) != KTimeComboBox::EditTime);
163 
164     // If SelectTime then make list items visible
165     if ((m_options & KTimeComboBox::SelectTime) == KTimeComboBox::SelectTime) {
166         q->setMaxVisibleItems(10);
167     } else {
168         q->setMaxVisibleItems(0);
169     }
170 
171     // Populate the drop-down time list
172     // If no time list set the use the time interval
173     if (m_timeList.isEmpty()) {
174         QTime startTime = m_minTime;
175         QTime thisTime(startTime.hour(), 0, 0, 0);
176         while (thisTime.isValid() && thisTime <= startTime) {
177             thisTime = thisTime.addSecs(m_timeListInterval * 60);
178         }
179         QTime endTime = m_maxTime;
180         q->addItem(formatTime(startTime), startTime);
181         while (thisTime.isValid() && thisTime < endTime) {
182             q->addItem(formatTime(thisTime), thisTime);
183             QTime newTime = thisTime.addSecs(m_timeListInterval * 60);
184             if (newTime.isValid() && newTime > thisTime) {
185                 thisTime = newTime;
186             } else {
187                 thisTime = QTime();
188             }
189         }
190         q->addItem(formatTime(endTime), endTime);
191     } else {
192         for (const QTime &thisTime : std::as_const(m_timeList)) {
193             if (thisTime.isValid() && thisTime >= m_minTime && thisTime <= m_maxTime) {
194                 q->addItem(formatTime(thisTime), thisTime);
195             }
196         }
197     }
198     q->blockSignals(false);
199 }
200 
updateTimeWidget()201 void KTimeComboBoxPrivate::updateTimeWidget()
202 {
203     q->blockSignals(true);
204     int pos = q->lineEdit()->cursorPosition();
205     // Set index before setting text otherwise it overwrites
206     int i = 0;
207     if (!m_time.isValid() || m_time < m_minTime) {
208         i = 0;
209     } else if (m_time > m_maxTime) {
210         i = q->count() - 1;
211     } else {
212         while (q->itemData(i).toTime() < m_time && i < q->count() - 1) {
213             ++i;
214         }
215     }
216     q->setCurrentIndex(i);
217     if (m_time.isValid()) {
218         q->lineEdit()->setText(formatTime(m_time));
219     } else {
220         q->lineEdit()->setText(QString());
221     }
222     q->lineEdit()->setCursorPosition(pos);
223     q->blockSignals(false);
224 }
225 
selectTime(int index)226 void KTimeComboBoxPrivate::selectTime(int index)
227 {
228     enterTime(q->itemData(index).toTime());
229 }
230 
editTime(const QString & text)231 void KTimeComboBoxPrivate::editTime(const QString &text)
232 {
233     m_warningShown = false;
234     Q_EMIT q->timeEdited(q->locale().toTime(text, m_displayFormat));
235 }
236 
parseTime()237 void KTimeComboBoxPrivate::parseTime()
238 {
239     m_time = q->locale().toTime(q->lineEdit()->text(), m_displayFormat);
240 }
241 
enterTime(const QTime & time)242 void KTimeComboBoxPrivate::enterTime(const QTime &time)
243 {
244     q->setTime(time);
245     warnTime();
246     Q_EMIT q->timeEntered(m_time);
247 }
248 
warnTime()249 void KTimeComboBoxPrivate::warnTime()
250 {
251     if (!m_warningShown && !q->isValid() && (m_options & KTimeComboBox::WarnOnInvalid) == KTimeComboBox::WarnOnInvalid) {
252         QString warnMsg;
253         if (!m_time.isValid()) {
254             warnMsg = KTimeComboBox::tr("The time you entered is invalid", "@info");
255         } else if (m_time < m_minTime) {
256             if (m_minWarnMsg.isEmpty()) {
257                 warnMsg = KTimeComboBox::tr("Time cannot be earlier than %1", "@info").arg(formatTime(m_minTime));
258             } else {
259                 warnMsg = m_minWarnMsg;
260                 warnMsg.replace(QLatin1String("%1"), formatTime(m_minTime));
261             }
262         } else if (m_time > m_maxTime) {
263             if (m_maxWarnMsg.isEmpty()) {
264                 warnMsg = KTimeComboBox::tr("Time cannot be later than %1", "@info").arg(formatTime(m_maxTime));
265             } else {
266                 warnMsg = m_maxWarnMsg;
267                 warnMsg.replace(QLatin1String("%1"), formatTime(m_maxTime));
268             }
269         }
270         m_warningShown = true;
271         KMessageBox::sorry(q, warnMsg);
272     }
273 }
274 
KTimeComboBox(QWidget * parent)275 KTimeComboBox::KTimeComboBox(QWidget *parent)
276     : QComboBox(parent)
277     , d(new KTimeComboBoxPrivate(this))
278 {
279     setEditable(true);
280     setInsertPolicy(QComboBox::NoInsert);
281     setSizeAdjustPolicy(QComboBox::AdjustToContents);
282     d->initTimeWidget();
283     d->updateTimeWidget();
284 
285     connect(this, qOverload<int>(&QComboBox::activated), this, [this](int value) {
286         d->selectTime(value);
287     });
288     connect(this, &QComboBox::editTextChanged, this, [this](const QString &str) {
289         d->editTime(str);
290     });
291 }
292 
293 KTimeComboBox::~KTimeComboBox() = default;
294 
time() const295 QTime KTimeComboBox::time() const
296 {
297     d->parseTime();
298     return d->m_time;
299 }
300 
setTime(const QTime & time)301 void KTimeComboBox::setTime(const QTime &time)
302 {
303     if (time == d->m_time) {
304         return;
305     }
306 
307     if ((d->m_options & KTimeComboBox::ForceTime) == KTimeComboBox::ForceTime) {
308         assignTime(d->nearestIntervalTime(time));
309     } else {
310         assignTime(time);
311     }
312 
313     d->updateTimeWidget();
314     Q_EMIT timeChanged(d->m_time);
315 }
316 
assignTime(const QTime & time)317 void KTimeComboBox::assignTime(const QTime &time)
318 {
319     d->m_time = time;
320 }
321 
isValid() const322 bool KTimeComboBox::isValid() const
323 {
324     d->parseTime();
325     return d->m_time.isValid() && d->m_time >= d->m_minTime && d->m_time <= d->m_maxTime;
326 }
327 
isNull() const328 bool KTimeComboBox::isNull() const
329 {
330     return lineEdit()->text() == d->m_nullString;
331 }
332 
options() const333 KTimeComboBox::Options KTimeComboBox::options() const
334 {
335     return d->m_options;
336 }
337 
setOptions(Options options)338 void KTimeComboBox::setOptions(Options options)
339 {
340     if (options != d->m_options) {
341         d->m_options = options;
342         d->initTimeWidget();
343         d->updateTimeWidget();
344     }
345 }
346 
minimumTime() const347 QTime KTimeComboBox::minimumTime() const
348 {
349     return d->m_minTime;
350 }
351 
setMinimumTime(const QTime & minTime,const QString & minWarnMsg)352 void KTimeComboBox::setMinimumTime(const QTime &minTime, const QString &minWarnMsg)
353 {
354     setTimeRange(minTime, d->m_maxTime, minWarnMsg, d->m_maxWarnMsg);
355 }
356 
resetMinimumTime()357 void KTimeComboBox::resetMinimumTime()
358 {
359     setTimeRange(d->defaultMinTime(), d->m_maxTime, QString(), d->m_maxWarnMsg);
360 }
361 
maximumTime() const362 QTime KTimeComboBox::maximumTime() const
363 {
364     return d->m_maxTime;
365 }
366 
setMaximumTime(const QTime & maxTime,const QString & maxWarnMsg)367 void KTimeComboBox::setMaximumTime(const QTime &maxTime, const QString &maxWarnMsg)
368 {
369     setTimeRange(d->m_minTime, maxTime, d->m_minWarnMsg, maxWarnMsg);
370 }
371 
resetMaximumTime()372 void KTimeComboBox::resetMaximumTime()
373 {
374     setTimeRange(d->m_minTime, d->defaultMaxTime(), d->m_minWarnMsg, QString());
375 }
376 
setTimeRange(const QTime & minTime,const QTime & maxTime,const QString & minWarnMsg,const QString & maxWarnMsg)377 void KTimeComboBox::setTimeRange(const QTime &minTime, const QTime &maxTime, const QString &minWarnMsg, const QString &maxWarnMsg)
378 {
379     if (!minTime.isValid() || !maxTime.isValid() || minTime > maxTime) {
380         return;
381     }
382 
383     if (minTime != d->m_minTime || maxTime != d->m_maxTime //
384         || minWarnMsg != d->m_minWarnMsg || maxWarnMsg != d->m_maxWarnMsg) {
385         d->m_minTime = minTime;
386         d->m_maxTime = maxTime;
387         d->m_minWarnMsg = minWarnMsg;
388         d->m_maxWarnMsg = maxWarnMsg;
389         d->initTimeWidget();
390         d->updateTimeWidget();
391     }
392 }
393 
resetTimeRange()394 void KTimeComboBox::resetTimeRange()
395 {
396     setTimeRange(d->defaultMinTime(), d->defaultMaxTime(), QString(), QString());
397 }
398 
displayFormat() const399 QLocale::FormatType KTimeComboBox::displayFormat() const
400 {
401     return d->m_displayFormat;
402 }
403 
setDisplayFormat(QLocale::FormatType format)404 void KTimeComboBox::setDisplayFormat(QLocale::FormatType format)
405 {
406     if (format != d->m_displayFormat) {
407         d->m_displayFormat = format;
408         d->initTimeWidget();
409         d->updateTimeWidget();
410     }
411 }
412 
timeListInterval() const413 int KTimeComboBox::timeListInterval() const
414 {
415     return d->m_timeListInterval;
416 }
417 
setTimeListInterval(int minutes)418 void KTimeComboBox::setTimeListInterval(int minutes)
419 {
420     if (minutes != d->m_timeListInterval) {
421         // Must be able to exactly divide the valid time period
422         int lowMins = (d->m_minTime.hour() * 60) + d->m_minTime.minute();
423         int hiMins = (d->m_maxTime.hour() * 60) + d->m_maxTime.minute();
424         if (d->m_minTime.minute() == 0 && d->m_maxTime.minute() == 59) {
425             ++hiMins;
426         }
427         if ((hiMins - lowMins) % minutes == 0) {
428             d->m_timeListInterval = minutes;
429             d->m_timeList.clear();
430         } else {
431             return;
432         }
433         d->initTimeWidget();
434     }
435 }
436 
timeList() const437 QList<QTime> KTimeComboBox::timeList() const
438 {
439     // Return the drop down list as it is what can be selected currently
440     QList<QTime> list;
441     int c = count();
442     list.reserve(c);
443     for (int i = 0; i < c; ++i) {
444         list.append(itemData(i).toTime());
445     }
446     return list;
447 }
448 
setTimeList(QList<QTime> timeList,const QString & minWarnMsg,const QString & maxWarnMsg)449 void KTimeComboBox::setTimeList(QList<QTime> timeList, const QString &minWarnMsg, const QString &maxWarnMsg)
450 {
451     if (timeList != d->m_timeList) {
452         d->m_timeList.clear();
453         for (const QTime &time : std::as_const(timeList)) {
454             if (time.isValid() && !d->m_timeList.contains(time)) {
455                 d->m_timeList.append(time);
456             }
457         }
458         std::sort(d->m_timeList.begin(), d->m_timeList.end());
459         // Does the updateTimeWidget call for us
460         setTimeRange(d->m_timeList.first(), d->m_timeList.last(), minWarnMsg, maxWarnMsg);
461     }
462 }
463 
eventFilter(QObject * object,QEvent * event)464 bool KTimeComboBox::eventFilter(QObject *object, QEvent *event)
465 {
466     return QComboBox::eventFilter(object, event);
467 }
468 
keyPressEvent(QKeyEvent * keyEvent)469 void KTimeComboBox::keyPressEvent(QKeyEvent *keyEvent)
470 {
471     QTime temp;
472     switch (keyEvent->key()) {
473     case Qt::Key_Down:
474         temp = d->m_time.addSecs(-60);
475         break;
476     case Qt::Key_Up:
477         temp = d->m_time.addSecs(60);
478         break;
479     case Qt::Key_PageDown:
480         temp = d->m_time.addSecs(-3600);
481         break;
482     case Qt::Key_PageUp:
483         temp = d->m_time.addSecs(3600);
484         break;
485     default:
486         QComboBox::keyPressEvent(keyEvent);
487         return;
488     }
489     if (temp.isValid() && temp >= d->m_minTime && temp <= d->m_maxTime) {
490         d->enterTime(temp);
491     }
492 }
493 
focusOutEvent(QFocusEvent * event)494 void KTimeComboBox::focusOutEvent(QFocusEvent *event)
495 {
496     d->parseTime();
497     d->warnTime();
498     QComboBox::focusOutEvent(event);
499 }
500 
showPopup()501 void KTimeComboBox::showPopup()
502 {
503     QComboBox::showPopup();
504 }
505 
hidePopup()506 void KTimeComboBox::hidePopup()
507 {
508     QComboBox::hidePopup();
509 }
510 
mousePressEvent(QMouseEvent * event)511 void KTimeComboBox::mousePressEvent(QMouseEvent *event)
512 {
513     QComboBox::mousePressEvent(event);
514 }
515 
wheelEvent(QWheelEvent * event)516 void KTimeComboBox::wheelEvent(QWheelEvent *event)
517 {
518     QComboBox::wheelEvent(event);
519 }
520 
focusInEvent(QFocusEvent * event)521 void KTimeComboBox::focusInEvent(QFocusEvent *event)
522 {
523     QComboBox::focusInEvent(event);
524 }
525 
resizeEvent(QResizeEvent * event)526 void KTimeComboBox::resizeEvent(QResizeEvent *event)
527 {
528     QComboBox::resizeEvent(event);
529 }
530 
531 #include "moc_ktimecombobox.cpp"
532