Commit d7af6395 authored by David Jarvie's avatar David Jarvie
Browse files

Make time edit spinboxes show time using localised format and numbers

parent 26431462
Pipeline #96844 skipped
KAlarm Change Log
=== Version 3.3.3 (KDE Applications 21.12) --- 6 November 2021 ===
=== Version 3.3.3 (KDE Applications 21.12) --- 7 November 2021 ===
* Show numbers in localised form.
* Make time edit spinboxes show time using localised format and numbers.
* Fix infinite loop due to invalid recurrence date when using locale with non-ASCII numbers (requires kalarmcal 21.12).
=== Version 3.3.2 (KDE Applications 21.08.3) --- 30 October 2021 ===
* Make time edit field arrows work with Breeze application style [KDE Bug 443062]
......
......@@ -45,8 +45,6 @@ void SpinBox::init()
int step = QSpinBox::singleStep();
mLineStep = step;
mLineShiftStep = step;
mLineControlStep = 0;
mCurrentButton = NO_BUTTON;
lineEdit()->installEventFilter(this); // handle shift-up/down arrow presses
......
......@@ -99,6 +99,9 @@ void SpinBox2::init()
connect(mSpinbox, &QSpinBox::valueChanged, this, &SpinBox2::valueChanged);
connect(mSpinbox2, &SpinBox::stepped, this, &SpinBox2::stepPage);
connect(mSpinbox2, &ExtraSpinBox::painted, this, &SpinBox2::paintTimer);
mShowUpdown2 = false; // ensure that setShowUpdown2(true) actually does something
setShowUpdown2(true);
}
void SpinBox2::setReadOnly(bool ro)
......@@ -116,8 +119,8 @@ void SpinBox2::setReverseWithLayout(bool reverse)
if (reverse != mReverseWithLayout)
{
mReverseWithLayout = reverse;
setSteps(mSingleStep, mPageStep);
setShiftSteps(mSingleShiftStep, mPageShiftStep, mSingleControlStep, mModControlStep);
setSteps();
setShiftSteps();
}
}
......@@ -158,15 +161,20 @@ void SpinBox2::setSteps(int single, int page)
{
mSingleStep = single;
mPageStep = page;
if (reverseButtons())
setSteps();
}
void SpinBox2::setSteps() const
{
if (reverseButtons() && mShowUpdown2)
{
mSpinbox2->setSingleStep(single); // reverse layout, but still set the right buttons
mSpinbox->setSingleStep(page);
mSpinbox2->setSingleStep(mSingleStep); // reverse layout, but still set the right buttons
mSpinbox->setSingleStep(mPageStep);
}
else
{
mSpinbox->setSingleStep(single);
mSpinbox2->setSingleStep(page);
mSpinbox->setSingleStep(mSingleStep);
mSpinbox2->setSingleStep(mPageStep);
}
}
......@@ -176,18 +184,25 @@ void SpinBox2::setShiftSteps(int single, int page, int control, bool modControl)
mPageShiftStep = page;
mSingleControlStep = control;
mModControlStep = modControl;
if (reverseButtons())
setShiftSteps();
}
void SpinBox2::setShiftSteps() const
{
if (reverseButtons() && mShowUpdown2)
{
mSpinbox2->setSingleShiftStep(single); // reverse layout, but still set the right buttons
mSpinbox->setSingleShiftStep(page);
mSpinbox2->setSingleShiftStep(mSingleShiftStep); // reverse layout, but still set the right buttons
mSpinbox->setSingleShiftStep(mPageShiftStep);
}
else
{
mSpinbox->setSingleShiftStep(single);
mSpinbox2->setSingleShiftStep(page);
mSpinbox->setSingleShiftStep(mSingleShiftStep);
mSpinbox2->setSingleShiftStep(mPageShiftStep);
}
if (!mShowUpdown2)
mSpinbox->setSingleControlStep(control, modControl);
if (mShowUpdown2)
mSpinbox->setSingleControlStep(0);
else
mSpinbox->setSingleControlStep(mSingleControlStep, mModControlStep);
}
void SpinBox2::setButtonSymbols(QSpinBox::ButtonSymbols newSymbols)
......@@ -455,11 +470,9 @@ void SpinBox2::getMetrics() const
// and if not, show only the normal spinbox without extra spin buttons.
const QRect upRect = mSpinbox->style()->subControlRect(QStyle::CC_SpinBox, &option, QStyle::SC_SpinBoxUp);
const QRect downRect = mSpinbox->style()->subControlRect(QStyle::CC_SpinBox, &option, QStyle::SC_SpinBoxDown);
mShowUpdown2 = ((upRect.left() > editRect.left()) && (downRect.left() > editRect.left()))
|| ((upRect.right() < editRect.right()) && (downRect.right() < editRect.right()));
mSpinbox2->setVisible(mShowUpdown2);
mSpinMirror->setVisible(mShowUpdown2);
mSpinbox->setSingleControlStep(mShowUpdown2 ? 0 : mSingleControlStep, !mShowUpdown2 && mModControlStep);
bool showUpdown2 = ((upRect.left() > editRect.left()) && (downRect.left() > editRect.left()))
|| ((upRect.right() < editRect.right()) && (downRect.right() < editRect.right()));
setShowUpdown2(showUpdown2);
if (!mShowUpdown2)
return;
}
......@@ -518,6 +531,21 @@ void SpinBox2::stepPage(int step, bool modified)
mSpinMirror->setButtonsImage();
}
/******************************************************************************
* Set whether the second pair of spin buttons should be shown.
*/
void SpinBox2::setShowUpdown2(bool show) const
{
if (show != mShowUpdown2)
{
mShowUpdown2 = show;
mSpinbox2->setVisible(mShowUpdown2);
mSpinMirror->setVisible(mShowUpdown2);
setSteps();
setShiftSteps();
}
}
/*=============================================================================
= Class SpinBox2::MainSpinBox
......
......@@ -208,13 +208,14 @@ public:
/** Sets the shifted step increments for the two pairs of spin buttons,
* i.e. the amount by which the spin box value changes when a spin button
* is clicked while the shift key is pressed.
* is clicked while the shift key is pressed. It also sets the increment
* when the left spin button is clicked when the control key is pressed.
* @param line The shift step increment for the right-hand spin buttons.
* @param page The shift step increment for the left-hand spin buttons.
* @param control The control step increment for the left-hand spin buttons.
* N.B. Qt multiplies the step increment by 10 when the Control
* key is pressed, so this parameter value should be
* @param modControl Control steps should always set value to multiple of @p step.
* @param control The increment for the left-hand spin buttons when Control,
* is pressed, or 0 to use default Qt handling which
* multiplies the single step by 10.
* @param modControl Control steps should always set value to multiple of @p control.
*/
void setShiftSteps(int line, int page, int control, bool modControl = true);
......@@ -290,12 +291,15 @@ private Q_SLOTS:
private:
void init();
void setSteps() const;
void setShiftSteps() const;
void rearrange();
void arrange();
void updateMirror();
bool eventFilter(QObject*, QEvent*) override;
void spinboxResized(QResizeEvent*);
void setUpdown2Size();
void setShowUpdown2(bool show) const;
// Visible spin box class.
// Declared here to allow use of mSpinBox in inline methods.
......
......@@ -31,10 +31,9 @@ TimeSpinBox::TimeSpinBox(bool use24hour, QWidget* parent)
, mPm(false)
{
setWrapping(true);
setReverseWithLayout(false); // keep buttons the same way round even if right-to-left language
setShiftSteps(5, 360, 60, false); // shift-left button increments 5 min / 6 hours
setSelectOnStep(false);
setAlignment(Qt::AlignHCenter);
init();
connect(this, &TimeSpinBox::valueChanged, this, &TimeSpinBox::slotValueChanged);
}
......@@ -46,10 +45,77 @@ TimeSpinBox::TimeSpinBox(int minMinute, int maxMinute, QWidget* parent)
, mMinimumValue(minMinute)
, m12Hour(false)
{
setReverseWithLayout(false); // keep buttons the same way round even if right-to-left language
setShiftSteps(5, 300, 60, false); // shift-left button increments 5 min / 5 hours
setSelectOnStep(false);
setAlignment(Qt::AlignRight);
init();
}
void TimeSpinBox::init()
{
setReverseWithLayout(false); // keep buttons the same way round even if right-to-left language
setSelectOnStep(false);
// Determine the time format, including only hours and minutes.
// Find the separator between hours and minutes, for the current locale.
const QString timeFormat = QLocale().timeFormat(QLocale::ShortFormat);
bool done = false;
bool quote = false;
int searching = 0; // -1 for hours, 1 for minutes
for (int i = 0; i < timeFormat.size() && !done; ++i)
{
const QChar qch = timeFormat.at(i);
const char ch = qch.toLatin1();
if (quote && ch != '\'')
{
if (searching)
mSeparator += qch;
continue;
}
switch (ch)
{
case 'h':
case 'H':
if (searching == 0)
searching = 1;
else if (searching == 1) // searching for minutes
mSeparator.clear();
else if (searching == -1) // searching for hours
{
mReversed = true; // minutes are before hours
done = true;
}
if (i < timeFormat.size() - 1 && timeFormat.at(i + 1) == qch)
++i;
break;
case 'm':
if (searching == 0)
searching = -1;
else if (searching == -1) // searching for minutes
mSeparator.clear();
else if (searching == 1) // searching for hours
done = true;
if (i < timeFormat.size() - 1 && timeFormat.at(i + 1) == qch)
++i;
break;
case '\'':
if (!quote && searching
&& i < timeFormat.size() - 1 && timeFormat.at(i + 1) == qch)
{
mSeparator += qch; // two consecutive single quotes means a literal quote
++i;
}
else
quote = !quote;
break;
default:
if (searching)
mSeparator += qch;
break;
}
}
}
QString TimeSpinBox::shiftWhatsThis()
......@@ -71,8 +137,14 @@ QString TimeSpinBox::textFromValue(int v) const
else if (v >= 780)
v -= 720; // convert 13 - 23 hours to 1 - 11
}
const QString s = QString::asprintf((wrapping() ? "%02d:%02d" : "%d:%02d"), v/60, v%60);
return s;
QLocale locale;
QString hours = locale.toString(v / 60);
if (wrapping() && hours.size() == 1)
hours.prepend(locale.zeroDigit());
QString mins = locale.toString(v % 60);
if (mins.size() == 1)
mins.prepend(locale.zeroDigit());
return mReversed ? mins + mSeparator + hours : hours + mSeparator + mins;
}
/******************************************************************************
......@@ -84,21 +156,24 @@ QString TimeSpinBox::textFromValue(int v) const
*/
int TimeSpinBox::valueFromText(const QString&) const
{
QLocale locale;
const QString text = cleanText();
const int colon = text.indexOf(QLatin1Char(':'));
const int colon = text.indexOf(mSeparator);
if (colon >= 0)
{
// [h]:m format for any time value
const QString hour = text.left(colon).trimmed();
const QString minute = text.mid(colon + 1).trimmed();
const QString first = text.left(colon).trimmed();
const QString second = text.mid(colon + mSeparator.size()).trimmed();
const QString hour = mReversed ? second : first;
const QString minute = mReversed ? first : second;
if (!minute.isEmpty())
{
bool okmin;
bool okhour = true;
const int m = minute.toUInt(&okmin);
const int m = locale.toUInt(minute, &okmin);
int h = 0;
if (!hour.isEmpty())
h = hour.toUInt(&okhour);
h = locale.toUInt(hour, &okhour);
if (okhour && okmin && m < 60)
{
if (m12Hour)
......@@ -116,11 +191,11 @@ int TimeSpinBox::valueFromText(const QString&) const
}
}
}
else if (text.length() == 4)
else if (text.length() == 4 && !mReversed)
{
// hhmm format for time of day
bool okn;
const int mins = text.toUInt(&okn);
const int mins = locale.toUInt(text, &okn);
if (okn)
{
const int m = mins % 100;
......@@ -162,7 +237,7 @@ void TimeSpinBox::setValid(bool valid)
{
mInvalid = true;
SpinBox2::setMinimum(mMinimumValue - 1);
setSpecialValueText(QStringLiteral("**:**"));
setSpecialValueText(QStringLiteral("**%1**").arg(mSeparator));
SpinBox2::setValue(mMinimumValue - 1);
}
}
......@@ -227,14 +302,14 @@ QSize TimeSpinBox::sizeHint() const
{
const QSize sz = SpinBox2::sizeHint();
const QFontMetrics fm(font());
return {sz.width() + fm.horizontalAdvance(QLatin1Char(':')), sz.height()};
return {sz.width() + fm.horizontalAdvance(mSeparator), sz.height()};
}
QSize TimeSpinBox::minimumSizeHint() const
{
const QSize sz = SpinBox2::minimumSizeHint();
const QFontMetrics fm(font());
return {sz.width() + fm.horizontalAdvance(QLatin1Char(':')), sz.height()};
return {sz.width() + fm.horizontalAdvance(mSeparator), sz.height()};
}
/******************************************************************************
......@@ -249,29 +324,35 @@ QValidator::State TimeSpinBox::validate(QString& text, int&) const
return QValidator::Intermediate;
QValidator::State state = QValidator::Acceptable;
const int maxMinute = maximum();
QLocale locale;
QString hour;
bool ok;
int hr = 0;
int mn = 0;
const int colon = cleanText.indexOf(QLatin1Char(':'));
const int colon = cleanText.indexOf(mSeparator);
if (colon >= 0)
{
const QString minute = cleanText.mid(colon + 1);
const QString first = cleanText.left(colon);
const QString second = cleanText.mid(colon + mSeparator.size());
const QString minute = mReversed ? first : second;
if (minute.isEmpty())
state = QValidator::Intermediate;
else if ((mn = minute.toUInt(&ok)) >= 60 || !ok)
else if ((mn = locale.toUInt(minute, &ok)) >= 60 || !ok)
return QValidator::Invalid;
hour = cleanText.left(colon);
hour = mReversed ? second : first;
}
else if (maxMinute >= 1440)
else if (!wrapping())
{
// The hhmm form of entry is only allowed for time-of-day, i.e. <= 2359
// It's a time duration, so the hhmm form of entry is not allowed.
hour = cleanText;
state = QValidator::Intermediate;
}
else
else if (!mReversed)
{
// It's a time of day, where the hhmm form of entry is allowed as long
// as the order is hours followed by minutes.
if (cleanText.length() > 4)
return QValidator::Invalid;
if (cleanText.length() < 4)
......@@ -279,13 +360,13 @@ QValidator::State TimeSpinBox::validate(QString& text, int&) const
hour = cleanText.left(2);
const QString minute = cleanText.mid(2);
if (!minute.isEmpty()
&& ((mn = minute.toUInt(&ok)) >= 60 || !ok))
&& ((mn = locale.toUInt(minute, &ok)) >= 60 || !ok))
return QValidator::Invalid;
}
if (!hour.isEmpty())
{
hr = hour.toUInt(&ok);
hr = locale.toUInt(hour, &ok);
if (ok && m12Hour)
{
if (hr == 0)
......
/*
* timespinbox.h - time spinbox widget
* Program: kalarm
* SPDX-FileCopyrightText: 2001-2019 David Jarvie <djarvie@kde.org>
* SPDX-FileCopyrightText: 2001-2021 David Jarvie <djarvie@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
......@@ -30,81 +30,96 @@
*/
class TimeSpinBox : public SpinBox2
{
Q_OBJECT
public:
/** Constructor for a wrapping time spin box which can be used to enter a time of day.
* @param use24hour True for entry of 24-hour clock times (range 00:00 to 23:59).
* False for entry of 12-hour clock times (range 12:00 to 11:59).
* @param parent The parent object of this widget.
*/
explicit TimeSpinBox(bool use24hour, QWidget* parent = nullptr);
/** Constructor for a non-wrapping time spin box which can be used to enter a length of time.
* @param minMinute The minimum value which the spin box can hold, in minutes.
* @param maxMinute The maximum value which the spin box can hold, in minutes.
* @param parent The parent object of this widget.
*/
TimeSpinBox(int minMinute, int maxMinute, QWidget* parent = nullptr);
/** Returns true if the spin box holds a valid value.
* An invalid value is displayed as asterisks.
*/
bool isValid() const;
/** Sets the spin box as holding a valid or invalid value.
* If newly invalid, the value is displayed as asterisks.
* If newly valid, the value is set to the minimum value.
*/
void setValid(bool);
/** Determine whether the current input is valid. */
QValidator::State validate(QString&, int& pos) const override;
/** Returns the current value held in the spin box.
* If an invalid value is displayed, returns a value lower than the minimum value.
*/
QTime time() const;
/** Sets the maximum value which can be held in the spin box.
* @param minutes The maximum value expressed in minutes.
*/
void setMinimum(int minutes) override;
/** Sets the maximum value which can be held in the spin box.
* @param minutes The maximum value expressed in minutes.
*/
void setMaximum(int minutes) override { SpinBox2::setMaximum(minutes); }
/** Sets the maximum value which can be held in the spin box. */
void setMaximum(const QTime& t) { SpinBox2::setMaximum(t.hour()*60 + t.minute()); }
/** Returns the maximum value which can be held in the spin box. */
QTime maxTime() const { int mv = maximum(); return {mv/60, mv%60}; }
/** Called whenever the user triggers a step, to adjust the value of the spin box.
* If the value was previously invalid, the spin box is set to the minimum value.
*/
void stepBy(int increment) override;
/** Returns a text describing use of the shift key as an accelerator for
* the spin buttons, designed for incorporation into WhatsThis texts.
*/
static QString shiftWhatsThis();
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
public Q_SLOTS:
/** Sets the value of the spin box.
* @param minutes The new value of the spin box, expressed in minutes.
*/
virtual void setValue(int minutes);
/** Sets the value of the spin box. */
void setValue(const QTime& t) { setValue(t.hour()*60 + t.minute()); }
protected:
QString textFromValue(int v) const override;
int valueFromText(const QString&) const override;
private Q_SLOTS:
void slotValueChanged(int value);
private:
int mMinimumValue; // real minimum value, excluding special value for "**:**"
bool m12Hour; // use 12-hour clock
bool mPm; // use PM for manually entered values (with 12-hour clock)
bool mInvalid {false}; // value is currently invalid (asterisks)
bool mEnteredSetValue {false}; // to prevent infinite recursion in setValue()
};
Q_OBJECT
public:
/** Constructor for a wrapping time spin box which can be used to enter a time of day.
* @param use24hour True for entry of 24-hour clock times (range 00:00 to 23:59).
* False for entry of 12-hour clock times (range 12:00 to 11:59).
* @param parent The parent object of this widget.
*/
explicit TimeSpinBox(bool use24hour, QWidget* parent = nullptr);
/** Constructor for a non-wrapping time spin box which can be used to enter a length of time.
* @param minMinute The minimum value which the spin box can hold, in minutes.
* @param maxMinute The maximum value which the spin box can hold, in minutes.
* @param parent The parent object of this widget.
*/
TimeSpinBox(int minMinute, int maxMinute, QWidget* parent = nullptr);
/** Returns true if the spin box holds a valid value.
* An invalid value is displayed as asterisks.
*/
bool isValid() const;
/** Sets the spin box as holding a valid or invalid value.
* If newly invalid, the value is displayed as asterisks.
* If newly valid, the value is set to the minimum value.
*/
void setValid(bool);
/** Determine whether the current input is valid. */
QValidator::State validate(QString&, int& pos) const override;
/** Returns the current value held in the spin box.
* If an invalid value is displayed, returns a value lower than the minimum value.
*/
QTime time() const;
/** Sets the maximum value which can be held in the spin box.
* @param minutes The maximum value expressed in minutes.
*/
void setMinimum(int minutes) override;
/** Sets the maximum value which can be held in the spin box.
* @param minutes The maximum value expressed in minutes.
*/
void setMaximum(int minutes) override { SpinBox2::setMaximum(minutes); }
/** Sets the maximum value which can be held in the spin box. */
void setMaximum(const QTime& t) { SpinBox2::setMaximum(t.hour()*60 + t.minute()); }
/** Returns the maximum value which can be held in the spin box. */
QTime maxTime() const { int mv = maximum(); return {mv/60, mv%60}; }
/** Called whenever the user triggers a step, to adjust the value of the spin box.
* If the value was previously invalid, the spin box is set to the minimum value.
*/
void stepBy(int increment) override;
/** Returns a text describing use of the shift key as an accelerator for
* the spin buttons, designed for incorporation into WhatsThis texts.
*/
static QString shiftWhatsThis();
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
public Q_SLOTS:
/** Sets the value of the spin box.
* @param minutes The new value of the spin box, expressed in minutes.
*/
virtual void setValue(int minutes);
/** Sets the value of the spin box. */
void setValue(const QTime& t) { setValue(t.hour()*60 + t.minute()); }
protected:
QString textFromValue(int v) const override;
int valueFromText(const QString&) const override;
private Q_SLOTS:
void slotValueChanged(int value);
private:
void init();
int mMinimumValue; // real minimum value, excluding special value for "**:**"
QString mSeparator; // separator between hours and minutes values, in the locale
bool mReversed {false}; // minutes value is before hours, in the locale
bool m12Hour; // use 12-hour clock
bool mPm; // use PM for manually entered values (with 12-hour clock)
bool mInvalid {false}; // value is currently invalid (asterisks)
bool mEnteredSetValue {false}; // to prevent infinite recursion in setValue()
};
// vim: et sw=4:
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment