File indexing completed on 2025-02-16 04:48:47

0001 /*
0002  *  timespinbox.cpp  -  time spinbox widget
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2001-2021 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "timespinbox.h"
0010 
0011 #include <KLocalizedString>
0012 
0013 /*=============================================================================
0014 = Class TimeSpinBox
0015 = This is a spin box displaying a time in the format hh:mm, with a pair of
0016 = spin buttons for each of the hour and minute values.
0017 = It can operate in 3 modes:
0018 =  1) a time of day using the 24-hour clock.
0019 =  2) a time of day using the 12-hour clock. The value is held as 0:00 - 23:59,
0020 =     but is displayed as 12:00 - 11:59. This is for use in a TimeEdit widget.
0021 =  3) a length of time, not restricted to the length of a day.
0022 =============================================================================*/
0023 
0024 /******************************************************************************
0025  * Construct a wrapping 00:00 - 23:59, or 12:00 - 11:59 time spin box.
0026  */
0027 TimeSpinBox::TimeSpinBox(bool use24hour, QWidget* parent)
0028     : SpinBox2(0, 1439, 60, parent)
0029     , mMinimumValue(0)
0030     , m12Hour(!use24hour)
0031     , mPm(false)
0032 {
0033     setWrapping(true);
0034     setShiftSteps(5, 360, 60, false);    // shift-left button increments 5 min / 6 hours
0035     setAlignment(Qt::AlignHCenter);
0036     init();
0037     connect(this, &TimeSpinBox::valueChanged, this, &TimeSpinBox::slotValueChanged);
0038 }
0039 
0040 /******************************************************************************
0041  * Construct a non-wrapping time spin box.
0042  */
0043 TimeSpinBox::TimeSpinBox(int minMinute, int maxMinute, QWidget* parent)
0044     : SpinBox2(minMinute, maxMinute, 60, parent)
0045     , mMinimumValue(minMinute)
0046     , m12Hour(false)
0047 {
0048     setShiftSteps(5, 300, 60, false);    // shift-left button increments 5 min / 5 hours
0049     setAlignment(Qt::AlignRight);
0050     init();
0051 }
0052 
0053 void TimeSpinBox::init()
0054 {
0055     setReverseWithLayout(false);   // keep buttons the same way round even if right-to-left language
0056     setSelectOnStep(false);
0057 
0058     // Determine the time format, including only hours and minutes.
0059     // Find the separator between hours and minutes, for the current locale.
0060     const QString timeFormat = QLocale().timeFormat(QLocale::ShortFormat);
0061     bool done = false;
0062     bool quote = false;
0063     int searching = 0;   // -1 for hours, 1 for minutes
0064     for (int i = 0;  i < timeFormat.size() && !done;  ++i)
0065     {
0066         const QChar qch = timeFormat.at(i);
0067         const char ch = qch.toLatin1();
0068         if (quote  &&  ch != '\'')
0069         {
0070             if (searching)
0071                 mSeparator += qch;
0072             continue;
0073         }
0074         switch (ch)
0075         {
0076             case 'h':
0077             case 'H':
0078                 if (searching == 0)
0079                     searching = 1;
0080                 else if (searching == 1)   // searching for minutes
0081                     mSeparator.clear();
0082                 else if (searching == -1)   // searching for hours
0083                 {
0084                     mReversed = true;   // minutes are before hours
0085                     done = true;
0086                 }
0087                 if (i < timeFormat.size() - 1  &&  timeFormat.at(i + 1) == qch)
0088                     ++i;
0089                 break;
0090 
0091             case 'm':
0092                 if (searching == 0)
0093                     searching = -1;
0094                 else if (searching == -1)   // searching for minutes
0095                     mSeparator.clear();
0096                 else if (searching == 1)    // searching for hours
0097                     done = true;
0098                 if (i < timeFormat.size() - 1  &&  timeFormat.at(i + 1) == qch)
0099                     ++i;
0100                 break;
0101 
0102             case '\'':
0103                 if (!quote  &&  searching
0104                 &&  i < timeFormat.size() - 1  &&  timeFormat.at(i + 1) == qch)
0105                 {
0106                     mSeparator += qch;    // two consecutive single quotes means a literal quote
0107                     ++i;
0108                 }
0109                 else
0110                     quote = !quote;
0111                 break;
0112 
0113             default:
0114                 if (searching)
0115                     mSeparator += qch;
0116                 break;
0117         }
0118     }
0119 }
0120 
0121 QString TimeSpinBox::shiftWhatsThis()
0122 {
0123     return i18nc("@info:whatsthis", "Press the Shift key while clicking the spin buttons to adjust the time by a larger step (6 hours / 5 minutes).");
0124 }
0125 
0126 QTime TimeSpinBox::time() const
0127 {
0128     return {value() / 60, value() % 60};
0129 }
0130 
0131 QString TimeSpinBox::textFromValue(int v) const
0132 {
0133     if (m12Hour)
0134     {
0135         if (v < 60)
0136             v += 720;      // convert 0:nn to 12:nn
0137         else if (v >= 780)
0138             v -= 720;      // convert 13 - 23 hours to 1 - 11
0139     }
0140     QLocale locale;
0141     QString hours = locale.toString(v / 60);
0142     if (wrapping()  &&  hours.size() == 1)
0143         hours.prepend(locale.zeroDigit());
0144     QString mins = locale.toString(v % 60);
0145     if (mins.size() == 1)
0146         mins.prepend(locale.zeroDigit());
0147     return mReversed ? mins + mSeparator + hours : hours + mSeparator + mins;
0148 }
0149 
0150 /******************************************************************************
0151  * Convert the user-entered text to a value in minutes.
0152  * The allowed formats are:
0153  *    [hour]:[minute], where minute must be non-blank, or
0154  *    hhmm, 4 digits, where hour < 24.
0155  * Reply = 0 if error.
0156  */
0157 int TimeSpinBox::valueFromText(const QString&) const
0158 {
0159     QLocale locale;
0160     const QString text = cleanText();
0161     const int colon = text.indexOf(mSeparator);
0162     if (colon >= 0)
0163     {
0164         // [h]:m format for any time value
0165         const QString first  = text.left(colon).trimmed();
0166         const QString second = text.mid(colon + mSeparator.size()).trimmed();
0167         const QString hour   = mReversed ? second : first;
0168         const QString minute = mReversed ? first : second;
0169         if (!minute.isEmpty())
0170         {
0171             bool okmin;
0172             bool okhour = true;
0173             const int m = locale.toUInt(minute, &okmin);
0174             int h = 0;
0175             if (!hour.isEmpty())
0176                 h = locale.toUInt(hour, &okhour);
0177             if (okhour  &&  okmin  &&  m < 60)
0178             {
0179                 if (m12Hour)
0180                 {
0181                     if (h == 0  ||  h > 12)
0182                         h = 100;     // error
0183                     else if (h == 12)
0184                         h = 0;       // convert 12:nn to 0:nn
0185                     if (mPm)
0186                         h += 12;     // convert to PM
0187                 }
0188                 const int t = h * 60 + m;
0189                 if (t >= mMinimumValue  &&  t <= maximum())
0190                     return t;
0191             }
0192         }
0193     }
0194     else if (text.length() == 4  &&  !mReversed)
0195     {
0196         // hhmm format for time of day
0197         bool okn;
0198         const int mins = locale.toUInt(text, &okn);
0199         if (okn)
0200         {
0201             const int m = mins % 100;
0202             int h = mins / 100;
0203             if (m12Hour)
0204             {
0205                 if (h == 0  ||  h > 12)
0206                     h = 100;    // error
0207                 else if (h == 12)
0208                     h = 0;      // convert 12:nn to 0:nn
0209                 if (mPm)
0210                     h += 12;    // convert to PM
0211             }
0212             const int t = h * 60 + m;
0213             if (h < 24  &&  m < 60  &&  t >= mMinimumValue  &&  t <= maximum())
0214                 return t;
0215         }
0216 
0217     }
0218     return 0;
0219 }
0220 
0221 /******************************************************************************
0222  * Set the spin box as valid or invalid.
0223  * If newly invalid, the value is displayed as asterisks.
0224  * If newly valid, the value is set to the minimum value.
0225  */
0226 void TimeSpinBox::setValid(bool valid)
0227 {
0228     if (valid  &&  mInvalid)
0229     {
0230         mInvalid = false;
0231         if (value() < mMinimumValue)
0232             SpinBox2::setValue(mMinimumValue);
0233         setSpecialValueText(QString());
0234         SpinBox2::setMinimum(mMinimumValue);
0235     }
0236     else if (!valid  &&  !mInvalid)
0237     {
0238         mInvalid = true;
0239         SpinBox2::setMinimum(mMinimumValue - 1);
0240         setSpecialValueText(QStringLiteral("**%1**").arg(mSeparator));
0241         SpinBox2::setValue(mMinimumValue - 1);
0242     }
0243 }
0244 
0245 /******************************************************************************
0246 * Set the spin box's minimum value.
0247 */
0248 void TimeSpinBox::setMinimum(int minutes)
0249 {
0250     mMinimumValue = minutes;
0251     SpinBox2::setMinimum(mMinimumValue - (mInvalid ? 1 : 0));
0252 }
0253 
0254 /******************************************************************************
0255  * Set the spin box's value.
0256  */
0257 void TimeSpinBox::setValue(int minutes)
0258 {
0259     if (!mEnteredSetValue)
0260     {
0261         mEnteredSetValue = true;
0262         mPm = (minutes >= 720);
0263         if (minutes > maximum())
0264             setValid(false);
0265         else
0266         {
0267             if (mInvalid)
0268             {
0269                 mInvalid = false;
0270                 setSpecialValueText(QString());
0271                 SpinBox2::setMinimum(mMinimumValue);
0272             }
0273             SpinBox2::setValue(minutes);
0274             mEnteredSetValue = false;
0275         }
0276     }
0277 }
0278 
0279 /******************************************************************************
0280  * Step the spin box value.
0281  * If it was invalid, set it valid and set the value to the minimum.
0282  */
0283 void TimeSpinBox::stepBy(int increment)
0284 {
0285     if (mInvalid)
0286         setValid(true);
0287     else
0288         SpinBox2::stepBy(increment);
0289 }
0290 
0291 bool TimeSpinBox::isValid() const
0292 {
0293     return value() >= mMinimumValue;
0294 }
0295 
0296 void TimeSpinBox::slotValueChanged(int value)
0297 {
0298     mPm = (value >= 720);
0299 }
0300 
0301 QSize TimeSpinBox::sizeHint() const
0302 {
0303     const QSize sz = SpinBox2::sizeHint();
0304     const QFontMetrics fm(font());
0305     return {sz.width() + fm.horizontalAdvance(mSeparator), sz.height()};
0306 }
0307 
0308 QSize TimeSpinBox::minimumSizeHint() const
0309 {
0310     const QSize sz = SpinBox2::minimumSizeHint();
0311     const QFontMetrics fm(font());
0312     return {sz.width() + fm.horizontalAdvance(mSeparator), sz.height()};
0313 }
0314 
0315 /******************************************************************************
0316  * Validate the time spin box input.
0317  * The entered time must either be 4 digits, or it must contain a colon, but
0318  * hours may be blank.
0319  */
0320 QValidator::State TimeSpinBox::validate(QString& text, int&) const
0321 {
0322     const QString cleanText = text.trimmed();
0323     if (cleanText.isEmpty())
0324         return QValidator::Intermediate;
0325     QValidator::State state = QValidator::Acceptable;
0326     const int maxMinute = maximum();
0327     QLocale locale;
0328     QString hour;
0329     bool ok;
0330     int hr = 0;
0331     int mn = 0;
0332     const int colon = cleanText.indexOf(mSeparator);
0333     if (colon >= 0)
0334     {
0335         const QString first  = cleanText.left(colon);
0336         const QString second = cleanText.mid(colon + mSeparator.size());
0337 
0338         const QString minute = mReversed ? first : second;
0339         if (minute.isEmpty())
0340             state = QValidator::Intermediate;
0341         else if ((mn = locale.toUInt(minute, &ok)) >= 60  ||  !ok)
0342             return QValidator::Invalid;
0343 
0344         hour = mReversed ? second : first;
0345     }
0346     else if (!wrapping())
0347     {
0348         // It's a time duration, so the hhmm form of entry is not allowed.
0349         hour = cleanText;
0350         state = QValidator::Intermediate;
0351     }
0352     else if (!mReversed)
0353     {
0354         // It's a time of day, where the hhmm form of entry is allowed as long
0355         // as the order is hours followed by minutes.
0356         if (cleanText.length() > 4)
0357             return QValidator::Invalid;
0358         if (cleanText.length() < 4)
0359             state = QValidator::Intermediate;
0360         hour = cleanText.left(2);
0361         const QString minute = cleanText.mid(2);
0362         if (!minute.isEmpty()
0363         &&  ((mn = locale.toUInt(minute, &ok)) >= 60  ||  !ok))
0364             return QValidator::Invalid;
0365     }
0366 
0367     if (!hour.isEmpty())
0368     {
0369         hr = locale.toUInt(hour, &ok);
0370         if (ok  &&  m12Hour)
0371         {
0372             if (hr == 0)
0373                 return QValidator::Intermediate;
0374             if (hr > 12)
0375                 hr = 100;    // error;
0376             else if (hr == 12)
0377                 hr = 0;      // convert 12:nn to 0:nn
0378             if (mPm)
0379                 hr += 12;    // convert to PM
0380         }
0381         if (!ok  ||  hr > maxMinute/60)
0382             return QValidator::Invalid;
0383     }
0384     else if (m12Hour)
0385         return QValidator::Intermediate;
0386 
0387     if (state == QValidator::Acceptable)
0388     {
0389         const int t = hr * 60 + mn;
0390         if (t < minimum()  ||  t > maxMinute)
0391             return QValidator::Invalid;
0392     }
0393     return state;
0394 }
0395 
0396 #include "moc_timespinbox.cpp"
0397 
0398 // vim: et sw=4: