| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2011 John Layt <john@layt.net> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "kdatecombobox.h" |
| 8 | |
| 9 | #include "common_helpers_p.h" |
| 10 | #include "kdatepickerpopup.h" |
| 11 | #include "kdaterangecontrol_p.h" |
| 12 | |
| 13 | #include <QAbstractItemView> |
| 14 | #include <QApplication> |
| 15 | #include <QDate> |
| 16 | #include <QKeyEvent> |
| 17 | #include <QLineEdit> |
| 18 | #include <QList> |
| 19 | #include <QMenu> |
| 20 | #include <QScreen> |
| 21 | #include <QWidgetAction> |
| 22 | |
| 23 | #include "kdatepicker.h" |
| 24 | #include "kmessagebox.h" |
| 25 | |
| 26 | class KDateComboBoxPrivate : public KDateRangeControlPrivate |
| 27 | { |
| 28 | public: |
| 29 | KDateComboBoxPrivate(KDateComboBox *qq); |
| 30 | |
| 31 | // TODO: Find a way to get that from QLocale |
| 32 | #if 0 |
| 33 | QDate defaultMinDate(); |
| 34 | QDate defaultMaxDate(); |
| 35 | #endif |
| 36 | |
| 37 | QString dateFormat(QLocale::FormatType format); |
| 38 | QString formatDate(const QDate &date); |
| 39 | |
| 40 | void initDateWidget(); |
| 41 | void updateDateWidget(); |
| 42 | void setDateRange(const QDate &minDate, const QDate &maxDate, const QString &minWarnMsg, const QString &maxWarnMsg); |
| 43 | using KDateRangeControlPrivate::setDateRange; |
| 44 | |
| 45 | void editDate(const QString &text); |
| 46 | void enterDate(const QDate &date); |
| 47 | void parseDate(); |
| 48 | bool warnDate(); |
| 49 | |
| 50 | KDateComboBox *const q; |
| 51 | KDatePickerPopup *; |
| 52 | |
| 53 | QDate m_date; |
| 54 | KDateComboBox::Options m_options; |
| 55 | QString m_minWarnMsg; |
| 56 | QString m_maxWarnMsg; |
| 57 | bool m_warningShown; |
| 58 | bool m_edited; // and dateChanged not yet emitted |
| 59 | QLocale::FormatType m_displayFormat; |
| 60 | }; |
| 61 | |
| 62 | KDateComboBoxPrivate::KDateComboBoxPrivate(KDateComboBox *qq) |
| 63 | : q(qq) |
| 64 | , m_dateMenu(new KDatePickerPopup(KDatePickerPopup::DatePicker | KDatePickerPopup::Words, QDate::currentDate(), qq)) |
| 65 | , m_warningShown(false) |
| 66 | , m_edited(false) |
| 67 | , m_displayFormat(QLocale::ShortFormat) |
| 68 | { |
| 69 | m_options = KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker | KDateComboBox::DateKeywords; |
| 70 | m_date = QDate::currentDate(); |
| 71 | // m_minDate = defaultMinDate(); |
| 72 | // m_maxDate = defaultMaxDate(); |
| 73 | } |
| 74 | |
| 75 | #if 0 |
| 76 | QDate KDateComboBoxPrivate::defaultMinDate() |
| 77 | { |
| 78 | return m_date.calendar()->earliestValidDate(); |
| 79 | } |
| 80 | |
| 81 | QDate KDateComboBoxPrivate::defaultMaxDate() |
| 82 | { |
| 83 | return m_date.calendar()->latestValidDate(); |
| 84 | } |
| 85 | #endif |
| 86 | |
| 87 | QString KDateComboBoxPrivate::dateFormat(QLocale::FormatType format) |
| 88 | { |
| 89 | return dateFormatWith4DigitYear(locale: q->locale(), format); |
| 90 | } |
| 91 | |
| 92 | QString KDateComboBoxPrivate::formatDate(const QDate &date) |
| 93 | { |
| 94 | return q->locale().toString(date, format: dateFormat(format: m_displayFormat)); |
| 95 | } |
| 96 | |
| 97 | void KDateComboBoxPrivate::initDateWidget() |
| 98 | { |
| 99 | q->blockSignals(b: true); |
| 100 | q->clear(); |
| 101 | |
| 102 | // If EditTime then set the line edit |
| 103 | q->lineEdit()->setReadOnly((m_options & KDateComboBox::EditDate) != KDateComboBox::EditDate); |
| 104 | |
| 105 | // If SelectDate then make list items visible |
| 106 | if ((m_options & KDateComboBox::SelectDate) == KDateComboBox::SelectDate // |
| 107 | || (m_options & KDateComboBox::DatePicker) == KDateComboBox::DatePicker // |
| 108 | || (m_options & KDateComboBox::DateKeywords) == KDateComboBox::DateKeywords) { |
| 109 | q->setMaxVisibleItems(1); |
| 110 | } else { |
| 111 | q->setMaxVisibleItems(0); |
| 112 | } |
| 113 | |
| 114 | q->setSizeAdjustPolicy(QComboBox::AdjustToContents); |
| 115 | q->addItem(atext: formatDate(date: m_date)); |
| 116 | q->setCurrentIndex(0); |
| 117 | q->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); |
| 118 | q->blockSignals(b: false); |
| 119 | |
| 120 | KDatePickerPopup::Modes modes; |
| 121 | if (m_options & KDateComboBox::DatePicker) { |
| 122 | modes |= KDatePickerPopup::DatePicker; |
| 123 | } |
| 124 | if (m_options & KDateComboBox::DateKeywords) { |
| 125 | modes |= KDatePickerPopup::Words; |
| 126 | } |
| 127 | m_dateMenu->setModes(modes); |
| 128 | } |
| 129 | |
| 130 | void KDateComboBoxPrivate::updateDateWidget() |
| 131 | { |
| 132 | q->blockSignals(b: true); |
| 133 | m_dateMenu->setDate(m_date); |
| 134 | int pos = q->lineEdit()->cursorPosition(); |
| 135 | q->setItemText(index: 0, text: formatDate(date: m_date)); |
| 136 | q->lineEdit()->setText(formatDate(date: m_date)); |
| 137 | q->lineEdit()->setCursorPosition(pos); |
| 138 | q->blockSignals(b: false); |
| 139 | } |
| 140 | |
| 141 | void KDateComboBoxPrivate::setDateRange(const QDate &minDate, const QDate &maxDate, const QString &minWarnMsg, const QString &maxWarnMsg) |
| 142 | { |
| 143 | if (!setDateRange(minDate, maxDate)) { |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | m_dateMenu->setDateRange(minDate, maxDate); |
| 148 | m_minWarnMsg = minWarnMsg; |
| 149 | m_maxWarnMsg = maxWarnMsg; |
| 150 | } |
| 151 | |
| 152 | void KDateComboBoxPrivate::editDate(const QString &text) |
| 153 | { |
| 154 | m_warningShown = false; |
| 155 | m_date = q->locale().toDate(string: text, format: dateFormat(format: m_displayFormat)); |
| 156 | m_edited = true; |
| 157 | Q_EMIT q->dateEdited(date: m_date); |
| 158 | } |
| 159 | |
| 160 | void KDateComboBoxPrivate::parseDate() |
| 161 | { |
| 162 | m_date = q->locale().toDate(string: q->lineEdit()->text(), format: dateFormat(format: m_displayFormat)); |
| 163 | } |
| 164 | |
| 165 | void KDateComboBoxPrivate::enterDate(const QDate &date) |
| 166 | { |
| 167 | q->setDate(date); |
| 168 | // Re-add the combo box item in order to retain the correct widget width |
| 169 | q->blockSignals(b: true); |
| 170 | q->clear(); |
| 171 | q->setSizeAdjustPolicy(QComboBox::AdjustToContents); |
| 172 | q->addItem(atext: formatDate(date: m_date)); |
| 173 | q->setCurrentIndex(0); |
| 174 | q->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); |
| 175 | q->blockSignals(b: false); |
| 176 | |
| 177 | m_dateMenu->hide(); |
| 178 | warnDate(); |
| 179 | Q_EMIT q->dateEntered(date: m_date); |
| 180 | } |
| 181 | |
| 182 | bool KDateComboBoxPrivate::warnDate() |
| 183 | { |
| 184 | if (!m_warningShown && !q->isValid() && (m_options & KDateComboBox::WarnOnInvalid) == KDateComboBox::WarnOnInvalid) { |
| 185 | QString warnMsg; |
| 186 | if (!m_date.isValid()) { |
| 187 | warnMsg = KDateComboBox::tr(s: "The date you entered is invalid" , c: "@info" ); |
| 188 | } else if (m_minDate.isValid() && m_date < m_minDate) { |
| 189 | if (m_minWarnMsg.isEmpty()) { |
| 190 | warnMsg = KDateComboBox::tr(s: "Date cannot be earlier than %1" , c: "@info" ).arg(a: formatDate(date: m_minDate)); |
| 191 | } else { |
| 192 | warnMsg = m_minWarnMsg; |
| 193 | warnMsg.replace(before: QLatin1String("%1" ), after: formatDate(date: m_minDate)); |
| 194 | } |
| 195 | } else if (m_maxDate.isValid() && m_date > m_maxDate) { |
| 196 | if (m_maxWarnMsg.isEmpty()) { |
| 197 | warnMsg = KDateComboBox::tr(s: "Date cannot be later than %1" , c: "@info" ).arg(a: formatDate(date: m_maxDate)); |
| 198 | } else { |
| 199 | warnMsg = m_maxWarnMsg; |
| 200 | warnMsg.replace(before: QLatin1String("%1" ), after: formatDate(date: m_maxDate)); |
| 201 | } |
| 202 | } |
| 203 | m_warningShown = true; |
| 204 | KMessageBox::error(parent: q, text: warnMsg); |
| 205 | return true; |
| 206 | } |
| 207 | return false; |
| 208 | } |
| 209 | |
| 210 | KDateComboBox::KDateComboBox(QWidget *parent) |
| 211 | : QComboBox(parent) |
| 212 | , d(new KDateComboBoxPrivate(this)) |
| 213 | { |
| 214 | setEditable(true); |
| 215 | setMaxVisibleItems(1); |
| 216 | setInsertPolicy(QComboBox::NoInsert); |
| 217 | d->initDateWidget(); |
| 218 | d->updateDateWidget(); |
| 219 | |
| 220 | connect(sender: d->m_dateMenu, signal: &KDatePickerPopup::dateChanged, context: this, slot: [this](QDate date) { |
| 221 | if (d->isInDateRange(date)) { |
| 222 | d->enterDate(date); |
| 223 | } |
| 224 | }); |
| 225 | |
| 226 | connect(sender: this, signal: &QComboBox::editTextChanged, context: this, slot: [this](const QString &text) { |
| 227 | d->editDate(text); |
| 228 | }); |
| 229 | |
| 230 | connect(sender: lineEdit(), signal: &QLineEdit::returnPressed, context: this, slot: [this]() { |
| 231 | if (d->m_edited) { |
| 232 | d->enterDate(date: date()); |
| 233 | Q_EMIT dateChanged(date: date()); |
| 234 | } |
| 235 | }); |
| 236 | } |
| 237 | |
| 238 | KDateComboBox::~KDateComboBox() = default; |
| 239 | |
| 240 | QDate KDateComboBox::date() const |
| 241 | { |
| 242 | d->parseDate(); |
| 243 | return d->m_date; |
| 244 | } |
| 245 | |
| 246 | void KDateComboBox::setDate(const QDate &date) |
| 247 | { |
| 248 | if (date == d->m_date) { |
| 249 | return; |
| 250 | } |
| 251 | |
| 252 | d->m_edited = false; |
| 253 | assignDate(date); |
| 254 | d->updateDateWidget(); |
| 255 | Q_EMIT dateChanged(date: d->m_date); |
| 256 | } |
| 257 | |
| 258 | void KDateComboBox::assignDate(const QDate &date) |
| 259 | { |
| 260 | d->m_date = date; |
| 261 | } |
| 262 | |
| 263 | bool KDateComboBox::isValid() const |
| 264 | { |
| 265 | d->parseDate(); |
| 266 | return d->isInDateRange(date: d->m_date); |
| 267 | } |
| 268 | |
| 269 | bool KDateComboBox::isNull() const |
| 270 | { |
| 271 | return lineEdit()->text().isEmpty(); |
| 272 | } |
| 273 | |
| 274 | KDateComboBox::Options KDateComboBox::options() const |
| 275 | { |
| 276 | return d->m_options; |
| 277 | } |
| 278 | |
| 279 | void KDateComboBox::setOptions(Options options) |
| 280 | { |
| 281 | if (options != d->m_options) { |
| 282 | d->m_options = options; |
| 283 | d->initDateWidget(); |
| 284 | d->updateDateWidget(); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | QDate KDateComboBox::minimumDate() const |
| 289 | { |
| 290 | return d->m_minDate; |
| 291 | } |
| 292 | |
| 293 | void KDateComboBox::setMinimumDate(const QDate &minDate, const QString &minWarnMsg) |
| 294 | { |
| 295 | if (minDate.isValid()) { |
| 296 | d->setDateRange(minDate, maxDate: d->m_maxDate, minWarnMsg, maxWarnMsg: d->m_maxWarnMsg); |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | void KDateComboBox::resetMinimumDate() |
| 301 | { |
| 302 | d->setDateRange(minDate: QDate(), maxDate: d->m_maxDate, minWarnMsg: QString(), maxWarnMsg: d->m_maxWarnMsg); |
| 303 | } |
| 304 | |
| 305 | QDate KDateComboBox::maximumDate() const |
| 306 | { |
| 307 | return d->m_maxDate; |
| 308 | } |
| 309 | |
| 310 | void KDateComboBox::setMaximumDate(const QDate &maxDate, const QString &maxWarnMsg) |
| 311 | { |
| 312 | if (maxDate.isValid()) { |
| 313 | d->setDateRange(minDate: d->m_minDate, maxDate, minWarnMsg: d->m_minWarnMsg, maxWarnMsg); |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | void KDateComboBox::resetMaximumDate() |
| 318 | { |
| 319 | d->setDateRange(minDate: d->m_minDate, maxDate: QDate(), minWarnMsg: d->m_minWarnMsg, maxWarnMsg: QString()); |
| 320 | } |
| 321 | |
| 322 | void KDateComboBox::setDateRange(const QDate &minDate, const QDate &maxDate, const QString &minWarnMsg, const QString &maxWarnMsg) |
| 323 | { |
| 324 | if (minDate.isValid() && maxDate.isValid()) { |
| 325 | d->setDateRange(minDate, maxDate, minWarnMsg, maxWarnMsg); |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | void KDateComboBox::resetDateRange() |
| 330 | { |
| 331 | d->setDateRange(minDate: QDate(), maxDate: QDate(), minWarnMsg: QString(), maxWarnMsg: QString()); |
| 332 | } |
| 333 | |
| 334 | QLocale::FormatType KDateComboBox::displayFormat() const |
| 335 | { |
| 336 | return d->m_displayFormat; |
| 337 | } |
| 338 | |
| 339 | void KDateComboBox::setDisplayFormat(QLocale::FormatType format) |
| 340 | { |
| 341 | if (format != d->m_displayFormat) { |
| 342 | d->m_displayFormat = format; |
| 343 | d->initDateWidget(); |
| 344 | d->updateDateWidget(); |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | QMap<QDate, QString> KDateComboBox::dateMap() const |
| 349 | { |
| 350 | return d->m_dateMenu->dateMap(); |
| 351 | } |
| 352 | |
| 353 | void KDateComboBox::setDateMap(QMap<QDate, QString> dateMap) |
| 354 | { |
| 355 | d->m_dateMenu->setDateMap(dateMap); |
| 356 | } |
| 357 | |
| 358 | bool KDateComboBox::eventFilter(QObject *object, QEvent *event) |
| 359 | { |
| 360 | return QComboBox::eventFilter(watched: object, event); |
| 361 | } |
| 362 | |
| 363 | void KDateComboBox::keyPressEvent(QKeyEvent *keyEvent) |
| 364 | { |
| 365 | QDate temp; |
| 366 | switch (keyEvent->key()) { |
| 367 | case Qt::Key_Down: |
| 368 | temp = d->m_date.addDays(days: -1); |
| 369 | break; |
| 370 | case Qt::Key_Up: |
| 371 | temp = d->m_date.addDays(days: 1); |
| 372 | break; |
| 373 | case Qt::Key_PageDown: |
| 374 | temp = d->m_date.addMonths(months: -1); |
| 375 | break; |
| 376 | case Qt::Key_PageUp: |
| 377 | temp = d->m_date.addMonths(months: 1); |
| 378 | break; |
| 379 | default: |
| 380 | QComboBox::keyPressEvent(e: keyEvent); |
| 381 | return; |
| 382 | } |
| 383 | if (d->isInDateRange(date: temp)) { |
| 384 | d->enterDate(date: temp); |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | void KDateComboBox::focusOutEvent(QFocusEvent *event) |
| 389 | { |
| 390 | d->parseDate(); |
| 391 | d->warnDate(); |
| 392 | if (d->m_edited) { |
| 393 | d->m_edited = false; |
| 394 | Q_EMIT dateEntered(date: d->m_date); |
| 395 | Q_EMIT dateChanged(date: d->m_date); |
| 396 | } |
| 397 | QComboBox::focusOutEvent(e: event); |
| 398 | } |
| 399 | |
| 400 | void KDateComboBox::() |
| 401 | { |
| 402 | if (d->warnDate()) { |
| 403 | return; |
| 404 | } |
| 405 | if (!isEditable() || !d->m_dateMenu // |
| 406 | || (d->m_options & KDateComboBox::SelectDate) != KDateComboBox::SelectDate) { |
| 407 | return; |
| 408 | } |
| 409 | |
| 410 | d->m_dateMenu->setDate(d->m_date); |
| 411 | |
| 412 | const QRect desk = screen()->geometry(); |
| 413 | |
| 414 | QPoint = mapToGlobal(QPoint(0, 0)); |
| 415 | |
| 416 | const int dateFrameHeight = d->m_dateMenu->sizeHint().height(); |
| 417 | if (popupPoint.y() + height() + dateFrameHeight > desk.bottom()) { |
| 418 | popupPoint.setY(popupPoint.y() - dateFrameHeight); |
| 419 | } else { |
| 420 | popupPoint.setY(popupPoint.y() + height()); |
| 421 | } |
| 422 | |
| 423 | const int dateFrameWidth = d->m_dateMenu->sizeHint().width(); |
| 424 | if (popupPoint.x() + dateFrameWidth > desk.right()) { |
| 425 | popupPoint.setX(desk.right() - dateFrameWidth); |
| 426 | } |
| 427 | |
| 428 | if (popupPoint.x() < desk.left()) { |
| 429 | popupPoint.setX(desk.left()); |
| 430 | } |
| 431 | |
| 432 | if (popupPoint.y() < desk.top()) { |
| 433 | popupPoint.setY(desk.top()); |
| 434 | } |
| 435 | |
| 436 | d->m_dateMenu->popup(pos: popupPoint); |
| 437 | } |
| 438 | |
| 439 | void KDateComboBox::() |
| 440 | { |
| 441 | QComboBox::hidePopup(); |
| 442 | } |
| 443 | |
| 444 | void KDateComboBox::mousePressEvent(QMouseEvent *event) |
| 445 | { |
| 446 | QComboBox::mousePressEvent(e: event); |
| 447 | } |
| 448 | |
| 449 | void KDateComboBox::wheelEvent(QWheelEvent *event) |
| 450 | { |
| 451 | QDate temp; |
| 452 | if (event->angleDelta().y() < 0) { |
| 453 | temp = d->m_date.addDays(days: -1); |
| 454 | } else { |
| 455 | temp = d->m_date.addDays(days: 1); |
| 456 | } |
| 457 | if (d->isInDateRange(date: temp)) { |
| 458 | d->enterDate(date: temp); |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | void KDateComboBox::focusInEvent(QFocusEvent *event) |
| 463 | { |
| 464 | QComboBox::focusInEvent(e: event); |
| 465 | } |
| 466 | |
| 467 | void KDateComboBox::resizeEvent(QResizeEvent *event) |
| 468 | { |
| 469 | QComboBox::resizeEvent(e: event); |
| 470 | } |
| 471 | |
| 472 | #include "moc_kdatecombobox.cpp" |
| 473 | |