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 | |
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 | |
65 | KTimeComboBoxPrivate::~KTimeComboBoxPrivate() |
66 | { |
67 | } |
68 | |
69 | QTime KTimeComboBoxPrivate::defaultMinTime() |
70 | { |
71 | return QTime(0, 0, 0, 0); |
72 | } |
73 | |
74 | QTime KTimeComboBoxPrivate::defaultMaxTime() |
75 | { |
76 | return QTime(23, 59, 59, 999); |
77 | } |
78 | |
79 | std::pair<QString, QString> KTimeComboBoxPrivate::timeFormatToInputMask(const QString &format) |
80 | { |
81 | const QLocale locale = q->locale(); |
82 | |
83 | QString example = formatTime(time: QTime(12, 34, 56, 789)); |
84 | // Replace time components with edit mask characters. |
85 | example.replace(before: locale.toString(i: 12), after: QLatin1String("09" )); |
86 | example.replace(before: locale.toString(i: 34), after: QLatin1String("99" )); |
87 | example.replace(before: locale.toString(i: 56), after: QLatin1String("99" )); |
88 | example.replace(before: locale.toString(i: 789), after: QLatin1String("900" )); |
89 | |
90 | // See if this time format contains a specifier for |
91 | // AM/PM, regardless of case. |
92 | int ampmPos = format.indexOf(s: QLatin1String("AP" ), from: 0, cs: 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(a: am.length(), b: pm.length()); |
111 | const QString ampmMask(ampmLen, QLatin1Char('x')); |
112 | example.replace(before: pm, after: 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(c: QLatin1Char('\\')); |
123 | mask.append(c); |
124 | null.append(c); |
125 | } |
126 | } |
127 | |
128 | return std::make_pair(x&: mask, y&: null); |
129 | } |
130 | |
131 | QTime KTimeComboBoxPrivate::nearestIntervalTime(const QTime &time) |
132 | { |
133 | int i = 0; |
134 | while (q->itemData(index: i).toTime() < time) { |
135 | ++i; |
136 | } |
137 | QTime before = q->itemData(index: i).toTime(); |
138 | QTime after = q->itemData(index: i + 1).toTime(); |
139 | if (before.secsTo(t: time) <= time.secsTo(t: after)) { |
140 | return before; |
141 | } else { |
142 | return after; |
143 | } |
144 | } |
145 | |
146 | QString KTimeComboBoxPrivate::formatTime(const QTime &time) |
147 | { |
148 | return q->locale().toString(time, format: m_displayFormat); |
149 | } |
150 | |
151 | void KTimeComboBoxPrivate::initTimeWidget() |
152 | { |
153 | q->blockSignals(b: true); |
154 | q->clear(); |
155 | |
156 | // Set the input mask from the current format |
157 | QString mask; |
158 | std::tie(args&: mask, args&: m_nullString) = timeFormatToInputMask(format: q->locale().timeFormat(format: 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(secs: m_timeListInterval * 60); |
178 | } |
179 | QTime endTime = m_maxTime; |
180 | q->addItem(atext: formatTime(time: startTime), auserData: startTime); |
181 | while (thisTime.isValid() && thisTime < endTime) { |
182 | q->addItem(atext: formatTime(time: thisTime), auserData: thisTime); |
183 | QTime newTime = thisTime.addSecs(secs: m_timeListInterval * 60); |
184 | if (newTime.isValid() && newTime > thisTime) { |
185 | thisTime = newTime; |
186 | } else { |
187 | thisTime = QTime(); |
188 | } |
189 | } |
190 | q->addItem(atext: formatTime(time: endTime), auserData: endTime); |
191 | } else { |
192 | for (const QTime &thisTime : std::as_const(t&: m_timeList)) { |
193 | if (thisTime.isValid() && thisTime >= m_minTime && thisTime <= m_maxTime) { |
194 | q->addItem(atext: formatTime(time: thisTime), auserData: thisTime); |
195 | } |
196 | } |
197 | } |
198 | q->blockSignals(b: false); |
199 | } |
200 | |
201 | void KTimeComboBoxPrivate::updateTimeWidget() |
202 | { |
203 | q->blockSignals(b: 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(index: 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(time: m_time)); |
219 | } else { |
220 | q->lineEdit()->setText(QString()); |
221 | } |
222 | q->lineEdit()->setCursorPosition(pos); |
223 | q->blockSignals(b: false); |
224 | } |
225 | |
226 | void KTimeComboBoxPrivate::selectTime(int index) |
227 | { |
228 | enterTime(time: q->itemData(index).toTime()); |
229 | } |
230 | |
231 | void KTimeComboBoxPrivate::editTime(const QString &text) |
232 | { |
233 | m_warningShown = false; |
234 | Q_EMIT q->timeEdited(time: q->locale().toTime(string: text, m_displayFormat)); |
235 | } |
236 | |
237 | void KTimeComboBoxPrivate::parseTime() |
238 | { |
239 | m_time = q->locale().toTime(string: q->lineEdit()->text(), m_displayFormat); |
240 | } |
241 | |
242 | void KTimeComboBoxPrivate::enterTime(const QTime &time) |
243 | { |
244 | q->setTime(time); |
245 | warnTime(); |
246 | Q_EMIT q->timeEntered(time: m_time); |
247 | } |
248 | |
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(s: "The time you entered is invalid" , c: "@info" ); |
255 | } else if (m_time < m_minTime) { |
256 | if (m_minWarnMsg.isEmpty()) { |
257 | warnMsg = KTimeComboBox::tr(s: "Time cannot be earlier than %1" , c: "@info" ).arg(a: formatTime(time: m_minTime)); |
258 | } else { |
259 | warnMsg = m_minWarnMsg; |
260 | warnMsg.replace(before: QLatin1String("%1" ), after: formatTime(time: m_minTime)); |
261 | } |
262 | } else if (m_time > m_maxTime) { |
263 | if (m_maxWarnMsg.isEmpty()) { |
264 | warnMsg = KTimeComboBox::tr(s: "Time cannot be later than %1" , c: "@info" ).arg(a: formatTime(time: m_maxTime)); |
265 | } else { |
266 | warnMsg = m_maxWarnMsg; |
267 | warnMsg.replace(before: QLatin1String("%1" ), after: formatTime(time: m_maxTime)); |
268 | } |
269 | } |
270 | m_warningShown = true; |
271 | KMessageBox::error(parent: q, text: warnMsg); |
272 | } |
273 | } |
274 | |
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(sender: this, signal: &QComboBox::activated, context: this, slot: [this](int value) { |
286 | d->selectTime(index: value); |
287 | }); |
288 | connect(sender: this, signal: &QComboBox::editTextChanged, context: this, slot: [this](const QString &str) { |
289 | d->editTime(text: str); |
290 | }); |
291 | } |
292 | |
293 | KTimeComboBox::~KTimeComboBox() = default; |
294 | |
295 | QTime KTimeComboBox::time() const |
296 | { |
297 | d->parseTime(); |
298 | return d->m_time; |
299 | } |
300 | |
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(time: d->nearestIntervalTime(time)); |
309 | } else { |
310 | assignTime(time); |
311 | } |
312 | |
313 | d->updateTimeWidget(); |
314 | Q_EMIT timeChanged(time: d->m_time); |
315 | } |
316 | |
317 | void KTimeComboBox::assignTime(const QTime &time) |
318 | { |
319 | d->m_time = time; |
320 | } |
321 | |
322 | 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 | |
328 | bool KTimeComboBox::isNull() const |
329 | { |
330 | return lineEdit()->text() == d->m_nullString; |
331 | } |
332 | |
333 | KTimeComboBox::Options KTimeComboBox::options() const |
334 | { |
335 | return d->m_options; |
336 | } |
337 | |
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 | |
347 | QTime KTimeComboBox::minimumTime() const |
348 | { |
349 | return d->m_minTime; |
350 | } |
351 | |
352 | void KTimeComboBox::setMinimumTime(const QTime &minTime, const QString &minWarnMsg) |
353 | { |
354 | setTimeRange(minTime, maxTime: d->m_maxTime, minWarnMsg, maxWarnMsg: d->m_maxWarnMsg); |
355 | } |
356 | |
357 | void KTimeComboBox::resetMinimumTime() |
358 | { |
359 | setTimeRange(minTime: d->defaultMinTime(), maxTime: d->m_maxTime, minWarnMsg: QString(), maxWarnMsg: d->m_maxWarnMsg); |
360 | } |
361 | |
362 | QTime KTimeComboBox::maximumTime() const |
363 | { |
364 | return d->m_maxTime; |
365 | } |
366 | |
367 | void KTimeComboBox::setMaximumTime(const QTime &maxTime, const QString &maxWarnMsg) |
368 | { |
369 | setTimeRange(minTime: d->m_minTime, maxTime, minWarnMsg: d->m_minWarnMsg, maxWarnMsg); |
370 | } |
371 | |
372 | void KTimeComboBox::resetMaximumTime() |
373 | { |
374 | setTimeRange(minTime: d->m_minTime, maxTime: d->defaultMaxTime(), minWarnMsg: d->m_minWarnMsg, maxWarnMsg: QString()); |
375 | } |
376 | |
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 | |
394 | void KTimeComboBox::resetTimeRange() |
395 | { |
396 | setTimeRange(minTime: d->defaultMinTime(), maxTime: d->defaultMaxTime(), minWarnMsg: QString(), maxWarnMsg: QString()); |
397 | } |
398 | |
399 | QLocale::FormatType KTimeComboBox::displayFormat() const |
400 | { |
401 | return d->m_displayFormat; |
402 | } |
403 | |
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 | |
413 | int KTimeComboBox::timeListInterval() const |
414 | { |
415 | return d->m_timeListInterval; |
416 | } |
417 | |
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 | |
437 | 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(asize: c); |
443 | for (int i = 0; i < c; ++i) { |
444 | list.append(t: itemData(index: i).toTime()); |
445 | } |
446 | return list; |
447 | } |
448 | |
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(t&: timeList)) { |
454 | if (time.isValid() && !d->m_timeList.contains(t: time)) { |
455 | d->m_timeList.append(t: time); |
456 | } |
457 | } |
458 | std::sort(first: d->m_timeList.begin(), last: d->m_timeList.end()); |
459 | // Does the updateTimeWidget call for us |
460 | setTimeRange(minTime: d->m_timeList.first(), maxTime: d->m_timeList.last(), minWarnMsg, maxWarnMsg); |
461 | } |
462 | } |
463 | |
464 | bool KTimeComboBox::eventFilter(QObject *object, QEvent *event) |
465 | { |
466 | return QComboBox::eventFilter(watched: object, event); |
467 | } |
468 | |
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(secs: -60); |
475 | break; |
476 | case Qt::Key_Up: |
477 | temp = d->m_time.addSecs(secs: 60); |
478 | break; |
479 | case Qt::Key_PageDown: |
480 | temp = d->m_time.addSecs(secs: -3600); |
481 | break; |
482 | case Qt::Key_PageUp: |
483 | temp = d->m_time.addSecs(secs: 3600); |
484 | break; |
485 | default: |
486 | QComboBox::keyPressEvent(e: keyEvent); |
487 | return; |
488 | } |
489 | if (temp.isValid() && temp >= d->m_minTime && temp <= d->m_maxTime) { |
490 | d->enterTime(time: temp); |
491 | } |
492 | } |
493 | |
494 | void KTimeComboBox::focusOutEvent(QFocusEvent *event) |
495 | { |
496 | d->parseTime(); |
497 | d->warnTime(); |
498 | QComboBox::focusOutEvent(e: event); |
499 | } |
500 | |
501 | void KTimeComboBox::() |
502 | { |
503 | QComboBox::showPopup(); |
504 | } |
505 | |
506 | void KTimeComboBox::() |
507 | { |
508 | QComboBox::hidePopup(); |
509 | } |
510 | |
511 | void KTimeComboBox::mousePressEvent(QMouseEvent *event) |
512 | { |
513 | QComboBox::mousePressEvent(e: event); |
514 | } |
515 | |
516 | void KTimeComboBox::wheelEvent(QWheelEvent *event) |
517 | { |
518 | QComboBox::wheelEvent(e: event); |
519 | } |
520 | |
521 | void KTimeComboBox::focusInEvent(QFocusEvent *event) |
522 | { |
523 | QComboBox::focusInEvent(e: event); |
524 | } |
525 | |
526 | void KTimeComboBox::resizeEvent(QResizeEvent *event) |
527 | { |
528 | QComboBox::resizeEvent(e: event); |
529 | } |
530 | |
531 | #include "moc_ktimecombobox.cpp" |
532 | |