| 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 | |