File indexing completed on 2024-04-28 15:39:43

0001 // SPDX-FileCopyrightText: 2003-2018 Jesper K. Pedersen <blackie@kde.org>
0002 // SPDX-FileCopyrightText: 2020-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 /**
0007  * A date editing widget that consists of an editable combo box.
0008  * The combo box contains the date in text form, and clicking the combo
0009  * box arrow will display a 'popup' style date picker.
0010  *
0011  * This widget also supports advanced features like allowing the user
0012  * to type in the day name to get the date. The following keywords
0013  * are supported (in the native language): tomorrow, yesterday, today,
0014  * monday, tuesday, wednesday, thursday, friday, saturday, sunday.
0015  *
0016  * @author Cornelius Schumacher <schumacher@kde.org>
0017  * @author Mike Pilone <mpilone@slac.com>
0018  * @author David Jarvie <software@astrojar.org.uk>
0019  * @author Jesper Pedersen <blackie@kde.org>
0020  */
0021 
0022 #include "DateEdit.h"
0023 
0024 #include <KDatePicker>
0025 #include <KLocalizedString>
0026 #include <QApplication>
0027 #include <QDate>
0028 #include <QDesktopWidget>
0029 #include <QEvent>
0030 #include <QKeyEvent>
0031 #include <QLineEdit>
0032 #include <QMouseEvent>
0033 #include <QVBoxLayout>
0034 
0035 AnnotationDialog::DateEdit::DateEdit(bool isStartEdit, QWidget *parent)
0036     : QComboBox(parent)
0037     , m_defaultValue(QDate::currentDate())
0038     , m_ReadOnly(false)
0039     , m_DiscardNextMousePress(false)
0040     , m_IsStartEdit(isStartEdit)
0041 {
0042     setEditable(true);
0043     setMaxCount(1); // need at least one entry for popup to work
0044     m_value = m_defaultValue;
0045     addItem(QString::fromLatin1(""));
0046     setCurrentIndex(0);
0047     setItemText(0, QString::fromLatin1(""));
0048     setMinimumSize(sizeHint());
0049 
0050     m_DateFrame = new QFrame;
0051     m_DateFrame->setWindowFlags(Qt::Popup);
0052     QVBoxLayout *layout = new QVBoxLayout(m_DateFrame);
0053     m_DateFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
0054     m_DateFrame->setLineWidth(3);
0055     m_DateFrame->hide();
0056     m_DateFrame->installEventFilter(this);
0057 
0058     m_DatePicker = new KDatePicker(m_value, m_DateFrame);
0059     layout->addWidget(m_DatePicker);
0060 
0061     connect(lineEdit(), &QLineEdit::editingFinished, this, &DateEdit::lineEnterPressed);
0062     connect(this, &QComboBox::currentTextChanged, this, &DateEdit::slotTextChanged);
0063 
0064     connect(m_DatePicker, &KDatePicker::dateEntered, this, &DateEdit::dateEntered);
0065     connect(m_DatePicker, &KDatePicker::dateSelected, this, &DateEdit::dateSelected);
0066 
0067     // Create the keyword list. This will be used to match against when the user
0068     // enters information.
0069     m_KeywordMap[i18n("tomorrow")] = 1;
0070     m_KeywordMap[i18n("today")] = 0;
0071     m_KeywordMap[i18n("yesterday")] = -1;
0072 
0073     for (int i = 1; i <= 7; ++i) {
0074         QString dayName = QLocale().dayName(i, QLocale::LongFormat).toLower();
0075         m_KeywordMap[dayName] = i + 100;
0076     }
0077     lineEdit()->installEventFilter(this); // handle keyword entry
0078 
0079     m_TextChanged = false;
0080     m_HandleInvalid = false;
0081 }
0082 
0083 AnnotationDialog::DateEdit::~DateEdit()
0084 {
0085 }
0086 
0087 void AnnotationDialog::DateEdit::setDate(const QDate &newDate)
0088 {
0089     QString dateString = QString::fromLatin1("");
0090     if (newDate.isValid())
0091         dateString = DB::ImageDate(newDate).toString(false);
0092 
0093     m_TextChanged = false;
0094 
0095     // We do not want to generate a signal here, since we explicitly setting
0096     // the date
0097     bool b = signalsBlocked();
0098     blockSignals(true);
0099     setItemText(0, dateString);
0100     blockSignals(b);
0101 
0102     m_value = newDate;
0103 }
0104 
0105 void AnnotationDialog::DateEdit::setHandleInvalid(bool handleInvalid)
0106 {
0107     m_HandleInvalid = handleInvalid;
0108 }
0109 
0110 bool AnnotationDialog::DateEdit::handlesInvalid() const
0111 {
0112     return m_HandleInvalid;
0113 }
0114 
0115 void AnnotationDialog::DateEdit::setReadOnly(bool readOnly)
0116 {
0117     m_ReadOnly = readOnly;
0118     lineEdit()->setReadOnly(readOnly);
0119 }
0120 
0121 bool AnnotationDialog::DateEdit::isReadOnly() const
0122 {
0123     return m_ReadOnly;
0124 }
0125 
0126 bool AnnotationDialog::DateEdit::validate(const QDate &)
0127 {
0128     return true;
0129 }
0130 
0131 QDate AnnotationDialog::DateEdit::date() const
0132 {
0133     QDate dt;
0134     readDate(dt, nullptr);
0135     return dt;
0136 }
0137 
0138 QDate AnnotationDialog::DateEdit::defaultDate() const
0139 {
0140     return m_defaultValue;
0141 }
0142 
0143 void AnnotationDialog::DateEdit::setDefaultDate(const QDate &date)
0144 {
0145     m_defaultValue = date;
0146 }
0147 
0148 void AnnotationDialog::DateEdit::showPopup()
0149 {
0150     if (m_ReadOnly)
0151         return;
0152 
0153     QRect desk = QApplication::desktop()->availableGeometry(this);
0154 
0155     // ensure that the popup is fully visible even when the DateEdit is off-screen
0156     QPoint popupPoint = mapToGlobal(QPoint(0, 0));
0157     if (popupPoint.x() < desk.left()) {
0158         popupPoint.setX(desk.x());
0159     } else if (popupPoint.x() + width() > desk.right()) {
0160         popupPoint.setX(desk.right() - width());
0161     }
0162     int dateFrameHeight = m_DateFrame->sizeHint().height();
0163     if (popupPoint.y() + height() + dateFrameHeight > desk.bottom()) {
0164         popupPoint.setY(popupPoint.y() - dateFrameHeight);
0165     } else {
0166         popupPoint.setY(popupPoint.y() + height());
0167     }
0168 
0169     m_DateFrame->move(popupPoint);
0170 
0171     QDate newDate;
0172     readDate(newDate, nullptr);
0173     if (newDate.isValid()) {
0174         m_DatePicker->setDate(newDate);
0175     } else {
0176         m_DatePicker->setDate(m_defaultValue);
0177     }
0178 
0179     m_DateFrame->show();
0180 }
0181 
0182 void AnnotationDialog::DateEdit::dateSelected(QDate newDate)
0183 {
0184     if ((m_HandleInvalid || newDate.isValid()) && validate(newDate)) {
0185         setDate(newDate);
0186         Q_EMIT dateChanged(newDate);
0187         Q_EMIT dateChanged(DB::ImageDate(newDate.startOfDay(), newDate.startOfDay()));
0188         m_DateFrame->hide();
0189     }
0190 }
0191 
0192 void AnnotationDialog::DateEdit::dateEntered(QDate newDate)
0193 {
0194     if ((m_HandleInvalid || newDate.isValid()) && validate(newDate)) {
0195         setDate(newDate);
0196         Q_EMIT dateChanged(newDate);
0197         Q_EMIT dateChanged(DB::ImageDate(newDate.startOfDay(), newDate.startOfDay()));
0198     }
0199 }
0200 
0201 void AnnotationDialog::DateEdit::lineEnterPressed()
0202 {
0203     if (!m_TextChanged)
0204         return;
0205 
0206     QDate newDate;
0207     QDate end;
0208     if (readDate(newDate, &end) && (m_HandleInvalid || newDate.isValid()) && validate(newDate)) {
0209         // Update the edit. This is needed if the user has entered a
0210         // word rather than the actual date.
0211         setDate(newDate);
0212         Q_EMIT dateChanged(newDate);
0213         Q_EMIT dateChanged(DB::ImageDate(newDate.startOfDay(), end.startOfDay()));
0214     } else {
0215         // Invalid or unacceptable date - revert to previous value
0216         setDate(m_value);
0217         Q_EMIT invalidDateEntered();
0218     }
0219 }
0220 
0221 bool AnnotationDialog::DateEdit::inputIsValid() const
0222 {
0223     QDate inputDate;
0224     return readDate(inputDate, nullptr) && inputDate.isValid();
0225 }
0226 
0227 /* Reads the text from the line edit. If the text is a keyword, the
0228  * word will be translated to a date. If the text is not a keyword, the
0229  * text will be interpreted as a date.
0230  * Returns true if the date text is blank or valid, false otherwise.
0231  */
0232 bool AnnotationDialog::DateEdit::readDate(QDate &result, QDate *end) const
0233 {
0234     QString text = currentText();
0235 
0236     if (text.isEmpty()) {
0237         result = QDate();
0238     } else if (m_KeywordMap.contains(text.toLower())) {
0239         QDate today = QDate::currentDate();
0240         int i = m_KeywordMap[text.toLower()];
0241         if (i >= 100) {
0242             /* A day name has been entered. Convert to offset from today.
0243              * This uses some math tricks to figure out the offset in days
0244              * to the next date the given day of the week occurs. There
0245              * are two cases, that the new day is >= the current day, which means
0246              * the new day has not occurred yet or that the new day < the current day,
0247              * which means the new day is already passed (so we need to find the
0248              * day in the next week).
0249              */
0250             i -= 100;
0251             int currentDay = today.dayOfWeek();
0252             if (i >= currentDay)
0253                 i -= currentDay;
0254             else
0255                 i += 7 - currentDay;
0256         }
0257         result = today.addDays(i);
0258     } else {
0259         result = DB::parseDateString(text, m_IsStartEdit);
0260         if (end)
0261             *end = DB::parseDateString(text, false);
0262         return result.isValid();
0263     }
0264 
0265     return true;
0266 }
0267 
0268 void AnnotationDialog::DateEdit::keyPressEvent(QKeyEvent *event)
0269 {
0270     int step = 0;
0271 
0272     if (event->key() == Qt::Key_Up)
0273         step = 1;
0274     else if (event->key() == Qt::Key_Down)
0275         step = -1;
0276 
0277     setDate(m_value.addDays(step));
0278     QComboBox::keyPressEvent(event);
0279 }
0280 
0281 /* Checks for a focus out event. The display of the date is updated
0282  * to display the proper date when the focus leaves.
0283  */
0284 bool AnnotationDialog::DateEdit::eventFilter(QObject *obj, QEvent *e)
0285 {
0286     if (obj == lineEdit()) {
0287         if (e->type() == QEvent::Wheel) {
0288             // Up and down arrow keys step the date
0289             QWheelEvent *we = dynamic_cast<QWheelEvent *>(e);
0290             Q_ASSERT(we != nullptr);
0291 
0292             const auto rawDelta = we->angleDelta();
0293             const bool isHorizontal = (qAbs(rawDelta.x()) > qAbs(rawDelta.y()));
0294             const auto angleDelta = isHorizontal ? rawDelta.x() : rawDelta.y();
0295             int step = 0;
0296             // angleDelta = eigths of a degree
0297             // scrolling down/left means back in time, just like in the date picker
0298             step = qBound(-1, (int)(-angleDelta), 1);
0299             setDate(m_value.addDays(step));
0300         }
0301     } else {
0302         // It's a date picker event
0303         switch (e->type()) {
0304         case QEvent::MouseButtonDblClick:
0305         case QEvent::MouseButtonPress: {
0306             QMouseEvent *me = dynamic_cast<QMouseEvent *>(e);
0307             if (!m_DateFrame->rect().contains(me->pos())) {
0308                 QPoint globalPos = m_DateFrame->mapToGlobal(me->pos());
0309                 if (QApplication::widgetAt(globalPos) == this) {
0310                     // The date picker is being closed by a click on the
0311                     // DateEdit widget. Avoid popping it up again immediately.
0312                     m_DiscardNextMousePress = true;
0313                 }
0314             }
0315             break;
0316         }
0317         default:
0318             break;
0319         }
0320     }
0321 
0322     return false;
0323 }
0324 
0325 void AnnotationDialog::DateEdit::mousePressEvent(QMouseEvent *e)
0326 {
0327     if (e->button() == Qt::LeftButton && m_DiscardNextMousePress) {
0328         m_DiscardNextMousePress = false;
0329         return;
0330     }
0331     QComboBox::mousePressEvent(e);
0332 }
0333 
0334 void AnnotationDialog::DateEdit::slotTextChanged(const QString &)
0335 {
0336     m_TextChanged = true;
0337 }
0338 // vi:expandtab:tabstop=4 shiftwidth=4:
0339 
0340 #include "moc_DateEdit.cpp"