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: