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"