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