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 | int ampmLen = qMax(a: am.length(), b: pm.length()); |
100 | const QString ampmMask(ampmLen, QLatin1Char('x')); |
101 | example.replace(before: pm, after: ampmMask, cs: Qt::CaseInsensitive); |
102 | } |
103 | |
104 | // Build a mask by copying mask characters and escaping the rest. |
105 | QString mask; |
106 | QString null; |
107 | for (const QChar c : example) { |
108 | if (c == QLatin1Char('0') || c == QLatin1Char('9') || c == QLatin1Char('x')) { |
109 | mask.append(c); |
110 | } else { |
111 | mask.append(c: QLatin1Char('\\')); |
112 | mask.append(c); |
113 | null.append(c); |
114 | } |
115 | } |
116 | |
117 | return std::make_pair(x&: mask, y&: null); |
118 | } |
119 | |
120 | QTime KTimeComboBoxPrivate::nearestIntervalTime(const QTime &time) |
121 | { |
122 | int i = 0; |
123 | while (q->itemData(index: i).toTime() < time) { |
124 | ++i; |
125 | } |
126 | QTime before = q->itemData(index: i).toTime(); |
127 | QTime after = q->itemData(index: i + 1).toTime(); |
128 | if (before.secsTo(t: time) <= time.secsTo(t: after)) { |
129 | return before; |
130 | } else { |
131 | return after; |
132 | } |
133 | } |
134 | |
135 | QString KTimeComboBoxPrivate::formatTime(const QTime &time) |
136 | { |
137 | return q->locale().toString(time, format: m_displayFormat); |
138 | } |
139 | |
140 | void KTimeComboBoxPrivate::initTimeWidget() |
141 | { |
142 | q->blockSignals(b: true); |
143 | q->clear(); |
144 | |
145 | // Set the input mask from the current format |
146 | QString mask; |
147 | std::tie(args&: mask, args&: m_nullString) = timeFormatToInputMask(format: q->locale().timeFormat(format: m_displayFormat)); |
148 | q->lineEdit()->setInputMask(mask); |
149 | |
150 | // If EditTime then set the line edit |
151 | q->lineEdit()->setReadOnly((m_options & KTimeComboBox::EditTime) != KTimeComboBox::EditTime); |
152 | |
153 | // If SelectTime then make list items visible |
154 | if ((m_options & KTimeComboBox::SelectTime) == KTimeComboBox::SelectTime) { |
155 | q->setMaxVisibleItems(10); |
156 | } else { |
157 | q->setMaxVisibleItems(0); |
158 | } |
159 | |
160 | // Populate the drop-down time list |
161 | // If no time list set the use the time interval |
162 | if (m_timeList.isEmpty()) { |
163 | QTime startTime = m_minTime; |
164 | QTime thisTime(startTime.hour(), 0, 0, 0); |
165 | while (thisTime.isValid() && thisTime <= startTime) { |
166 | thisTime = thisTime.addSecs(secs: m_timeListInterval * 60); |
167 | } |
168 | QTime endTime = m_maxTime; |
169 | q->addItem(atext: formatTime(time: startTime), auserData: startTime); |
170 | while (thisTime.isValid() && thisTime < endTime) { |
171 | q->addItem(atext: formatTime(time: thisTime), auserData: thisTime); |
172 | QTime newTime = thisTime.addSecs(secs: m_timeListInterval * 60); |
173 | if (newTime.isValid() && newTime > thisTime) { |
174 | thisTime = newTime; |
175 | } else { |
176 | thisTime = QTime(); |
177 | } |
178 | } |
179 | q->addItem(atext: formatTime(time: endTime), auserData: endTime); |
180 | } else { |
181 | for (const QTime &thisTime : std::as_const(t&: m_timeList)) { |
182 | if (thisTime.isValid() && thisTime >= m_minTime && thisTime <= m_maxTime) { |
183 | q->addItem(atext: formatTime(time: thisTime), auserData: thisTime); |
184 | } |
185 | } |
186 | } |
187 | q->blockSignals(b: false); |
188 | } |
189 | |
190 | void KTimeComboBoxPrivate::updateTimeWidget() |
191 | { |
192 | q->blockSignals(b: true); |
193 | int pos = q->lineEdit()->cursorPosition(); |
194 | // Set index before setting text otherwise it overwrites |
195 | int i = 0; |
196 | if (!m_time.isValid() || m_time < m_minTime) { |
197 | i = 0; |
198 | } else if (m_time > m_maxTime) { |
199 | i = q->count() - 1; |
200 | } else { |
201 | while (q->itemData(index: i).toTime() < m_time && i < q->count() - 1) { |
202 | ++i; |
203 | } |
204 | } |
205 | q->setCurrentIndex(i); |
206 | if (m_time.isValid()) { |
207 | q->lineEdit()->setText(formatTime(time: m_time)); |
208 | } else { |
209 | q->lineEdit()->setText(QString()); |
210 | } |
211 | q->lineEdit()->setCursorPosition(pos); |
212 | q->blockSignals(b: false); |
213 | } |
214 | |
215 | void KTimeComboBoxPrivate::selectTime(int index) |
216 | { |
217 | enterTime(time: q->itemData(index).toTime()); |
218 | } |
219 | |
220 | void KTimeComboBoxPrivate::editTime(const QString &text) |
221 | { |
222 | m_warningShown = false; |
223 | Q_EMIT q->timeEdited(time: q->locale().toTime(string: text, m_displayFormat)); |
224 | } |
225 | |
226 | void KTimeComboBoxPrivate::parseTime() |
227 | { |
228 | m_time = q->locale().toTime(string: q->lineEdit()->text(), m_displayFormat); |
229 | } |
230 | |
231 | void KTimeComboBoxPrivate::enterTime(const QTime &time) |
232 | { |
233 | q->setTime(time); |
234 | warnTime(); |
235 | Q_EMIT q->timeEntered(time: m_time); |
236 | } |
237 | |
238 | void KTimeComboBoxPrivate::warnTime() |
239 | { |
240 | if (!m_warningShown && !q->isValid() && (m_options & KTimeComboBox::WarnOnInvalid) == KTimeComboBox::WarnOnInvalid) { |
241 | QString warnMsg; |
242 | if (!m_time.isValid()) { |
243 | warnMsg = KTimeComboBox::tr(s: "The time you entered is invalid" , c: "@info" ); |
244 | } else if (m_time < m_minTime) { |
245 | if (m_minWarnMsg.isEmpty()) { |
246 | warnMsg = KTimeComboBox::tr(s: "Time cannot be earlier than %1" , c: "@info" ).arg(a: formatTime(time: m_minTime)); |
247 | } else { |
248 | warnMsg = m_minWarnMsg; |
249 | warnMsg.replace(before: QLatin1String("%1" ), after: formatTime(time: m_minTime)); |
250 | } |
251 | } else if (m_time > m_maxTime) { |
252 | if (m_maxWarnMsg.isEmpty()) { |
253 | warnMsg = KTimeComboBox::tr(s: "Time cannot be later than %1" , c: "@info" ).arg(a: formatTime(time: m_maxTime)); |
254 | } else { |
255 | warnMsg = m_maxWarnMsg; |
256 | warnMsg.replace(before: QLatin1String("%1" ), after: formatTime(time: m_maxTime)); |
257 | } |
258 | } |
259 | m_warningShown = true; |
260 | KMessageBox::error(parent: q, text: warnMsg); |
261 | } |
262 | } |
263 | |
264 | KTimeComboBox::KTimeComboBox(QWidget *parent) |
265 | : QComboBox(parent) |
266 | , d(new KTimeComboBoxPrivate(this)) |
267 | { |
268 | setEditable(true); |
269 | setInsertPolicy(QComboBox::NoInsert); |
270 | setSizeAdjustPolicy(QComboBox::AdjustToContents); |
271 | d->initTimeWidget(); |
272 | d->updateTimeWidget(); |
273 | |
274 | connect(sender: this, signal: &QComboBox::activated, context: this, slot: [this](int value) { |
275 | d->selectTime(index: value); |
276 | }); |
277 | connect(sender: this, signal: &QComboBox::editTextChanged, context: this, slot: [this](const QString &str) { |
278 | d->editTime(text: str); |
279 | }); |
280 | } |
281 | |
282 | KTimeComboBox::~KTimeComboBox() = default; |
283 | |
284 | QTime KTimeComboBox::time() const |
285 | { |
286 | d->parseTime(); |
287 | return d->m_time; |
288 | } |
289 | |
290 | void KTimeComboBox::setTime(const QTime &time) |
291 | { |
292 | if (time == d->m_time) { |
293 | return; |
294 | } |
295 | |
296 | if ((d->m_options & KTimeComboBox::ForceTime) == KTimeComboBox::ForceTime) { |
297 | assignTime(time: d->nearestIntervalTime(time)); |
298 | } else { |
299 | assignTime(time); |
300 | } |
301 | |
302 | d->updateTimeWidget(); |
303 | Q_EMIT timeChanged(time: d->m_time); |
304 | } |
305 | |
306 | void KTimeComboBox::assignTime(const QTime &time) |
307 | { |
308 | d->m_time = time; |
309 | } |
310 | |
311 | bool KTimeComboBox::isValid() const |
312 | { |
313 | d->parseTime(); |
314 | return d->m_time.isValid() && d->m_time >= d->m_minTime && d->m_time <= d->m_maxTime; |
315 | } |
316 | |
317 | bool KTimeComboBox::isNull() const |
318 | { |
319 | return lineEdit()->text() == d->m_nullString; |
320 | } |
321 | |
322 | KTimeComboBox::Options KTimeComboBox::options() const |
323 | { |
324 | return d->m_options; |
325 | } |
326 | |
327 | void KTimeComboBox::setOptions(Options options) |
328 | { |
329 | if (options != d->m_options) { |
330 | d->m_options = options; |
331 | d->initTimeWidget(); |
332 | d->updateTimeWidget(); |
333 | } |
334 | } |
335 | |
336 | QTime KTimeComboBox::minimumTime() const |
337 | { |
338 | return d->m_minTime; |
339 | } |
340 | |
341 | void KTimeComboBox::setMinimumTime(const QTime &minTime, const QString &minWarnMsg) |
342 | { |
343 | setTimeRange(minTime, maxTime: d->m_maxTime, minWarnMsg, maxWarnMsg: d->m_maxWarnMsg); |
344 | } |
345 | |
346 | void KTimeComboBox::resetMinimumTime() |
347 | { |
348 | setTimeRange(minTime: d->defaultMinTime(), maxTime: d->m_maxTime, minWarnMsg: QString(), maxWarnMsg: d->m_maxWarnMsg); |
349 | } |
350 | |
351 | QTime KTimeComboBox::maximumTime() const |
352 | { |
353 | return d->m_maxTime; |
354 | } |
355 | |
356 | void KTimeComboBox::setMaximumTime(const QTime &maxTime, const QString &maxWarnMsg) |
357 | { |
358 | setTimeRange(minTime: d->m_minTime, maxTime, minWarnMsg: d->m_minWarnMsg, maxWarnMsg); |
359 | } |
360 | |
361 | void KTimeComboBox::resetMaximumTime() |
362 | { |
363 | setTimeRange(minTime: d->m_minTime, maxTime: d->defaultMaxTime(), minWarnMsg: d->m_minWarnMsg, maxWarnMsg: QString()); |
364 | } |
365 | |
366 | void KTimeComboBox::setTimeRange(const QTime &minTime, const QTime &maxTime, const QString &minWarnMsg, const QString &maxWarnMsg) |
367 | { |
368 | if (!minTime.isValid() || !maxTime.isValid() || minTime > maxTime) { |
369 | return; |
370 | } |
371 | |
372 | if (minTime != d->m_minTime || maxTime != d->m_maxTime // |
373 | || minWarnMsg != d->m_minWarnMsg || maxWarnMsg != d->m_maxWarnMsg) { |
374 | d->m_minTime = minTime; |
375 | d->m_maxTime = maxTime; |
376 | d->m_minWarnMsg = minWarnMsg; |
377 | d->m_maxWarnMsg = maxWarnMsg; |
378 | d->initTimeWidget(); |
379 | d->updateTimeWidget(); |
380 | } |
381 | } |
382 | |
383 | void KTimeComboBox::resetTimeRange() |
384 | { |
385 | setTimeRange(minTime: d->defaultMinTime(), maxTime: d->defaultMaxTime(), minWarnMsg: QString(), maxWarnMsg: QString()); |
386 | } |
387 | |
388 | QLocale::FormatType KTimeComboBox::displayFormat() const |
389 | { |
390 | return d->m_displayFormat; |
391 | } |
392 | |
393 | void KTimeComboBox::setDisplayFormat(QLocale::FormatType format) |
394 | { |
395 | if (format != d->m_displayFormat) { |
396 | d->m_displayFormat = format; |
397 | d->initTimeWidget(); |
398 | d->updateTimeWidget(); |
399 | } |
400 | } |
401 | |
402 | int KTimeComboBox::timeListInterval() const |
403 | { |
404 | return d->m_timeListInterval; |
405 | } |
406 | |
407 | void KTimeComboBox::setTimeListInterval(int minutes) |
408 | { |
409 | if (minutes != d->m_timeListInterval) { |
410 | // Must be able to exactly divide the valid time period |
411 | int lowMins = (d->m_minTime.hour() * 60) + d->m_minTime.minute(); |
412 | int hiMins = (d->m_maxTime.hour() * 60) + d->m_maxTime.minute(); |
413 | if (d->m_minTime.minute() == 0 && d->m_maxTime.minute() == 59) { |
414 | ++hiMins; |
415 | } |
416 | if ((hiMins - lowMins) % minutes == 0) { |
417 | d->m_timeListInterval = minutes; |
418 | d->m_timeList.clear(); |
419 | } else { |
420 | return; |
421 | } |
422 | d->initTimeWidget(); |
423 | } |
424 | } |
425 | |
426 | QList<QTime> KTimeComboBox::timeList() const |
427 | { |
428 | // Return the drop down list as it is what can be selected currently |
429 | QList<QTime> list; |
430 | int c = count(); |
431 | list.reserve(asize: c); |
432 | for (int i = 0; i < c; ++i) { |
433 | list.append(t: itemData(index: i).toTime()); |
434 | } |
435 | return list; |
436 | } |
437 | |
438 | void KTimeComboBox::setTimeList(QList<QTime> timeList, const QString &minWarnMsg, const QString &maxWarnMsg) |
439 | { |
440 | if (timeList != d->m_timeList) { |
441 | d->m_timeList.clear(); |
442 | for (const QTime &time : std::as_const(t&: timeList)) { |
443 | if (time.isValid() && !d->m_timeList.contains(t: time)) { |
444 | d->m_timeList.append(t: time); |
445 | } |
446 | } |
447 | std::sort(first: d->m_timeList.begin(), last: d->m_timeList.end()); |
448 | // Does the updateTimeWidget call for us |
449 | setTimeRange(minTime: d->m_timeList.first(), maxTime: d->m_timeList.last(), minWarnMsg, maxWarnMsg); |
450 | } |
451 | } |
452 | |
453 | bool KTimeComboBox::eventFilter(QObject *object, QEvent *event) |
454 | { |
455 | return QComboBox::eventFilter(watched: object, event); |
456 | } |
457 | |
458 | void KTimeComboBox::keyPressEvent(QKeyEvent *keyEvent) |
459 | { |
460 | QTime temp; |
461 | switch (keyEvent->key()) { |
462 | case Qt::Key_Down: |
463 | temp = d->m_time.addSecs(secs: -60); |
464 | break; |
465 | case Qt::Key_Up: |
466 | temp = d->m_time.addSecs(secs: 60); |
467 | break; |
468 | case Qt::Key_PageDown: |
469 | temp = d->m_time.addSecs(secs: -3600); |
470 | break; |
471 | case Qt::Key_PageUp: |
472 | temp = d->m_time.addSecs(secs: 3600); |
473 | break; |
474 | default: |
475 | QComboBox::keyPressEvent(e: keyEvent); |
476 | return; |
477 | } |
478 | if (temp.isValid() && temp >= d->m_minTime && temp <= d->m_maxTime) { |
479 | d->enterTime(time: temp); |
480 | } |
481 | } |
482 | |
483 | void KTimeComboBox::focusOutEvent(QFocusEvent *event) |
484 | { |
485 | d->parseTime(); |
486 | d->warnTime(); |
487 | QComboBox::focusOutEvent(e: event); |
488 | } |
489 | |
490 | void KTimeComboBox::() |
491 | { |
492 | QComboBox::showPopup(); |
493 | } |
494 | |
495 | void KTimeComboBox::() |
496 | { |
497 | QComboBox::hidePopup(); |
498 | } |
499 | |
500 | void KTimeComboBox::mousePressEvent(QMouseEvent *event) |
501 | { |
502 | QComboBox::mousePressEvent(e: event); |
503 | } |
504 | |
505 | void KTimeComboBox::wheelEvent(QWheelEvent *event) |
506 | { |
507 | QComboBox::wheelEvent(e: event); |
508 | } |
509 | |
510 | void KTimeComboBox::focusInEvent(QFocusEvent *event) |
511 | { |
512 | QComboBox::focusInEvent(e: event); |
513 | } |
514 | |
515 | void KTimeComboBox::resizeEvent(QResizeEvent *event) |
516 | { |
517 | QComboBox::resizeEvent(e: event); |
518 | } |
519 | |
520 | #include "moc_ktimecombobox.cpp" |
521 | |