time_input.cpp 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. // This file is part of Desktop App Toolkit,
  2. // a set of libraries for developing nice desktop applications.
  3. //
  4. // For license and copyright information please follow this link:
  5. // https://github.com/desktop-app/legal/blob/master/LEGAL
  6. //
  7. #include "ui/widgets/time_input.h"
  8. #include "ui/widgets/fields/time_part_input.h"
  9. #include "ui/qt_weak_factory.h"
  10. #include "base/qt/qt_string_view.h"
  11. #include "base/invoke_queued.h"
  12. #include <QtCore/QRegularExpression>
  13. #include <QTime>
  14. namespace Ui {
  15. namespace {
  16. QTime ValidateTime(const QString &value) {
  17. static const auto RegExp = QRegularExpression(
  18. "^(\\d{1,2})\\:(\\d\\d)$");
  19. const auto match = RegExp.match(value);
  20. if (!match.hasMatch()) {
  21. return QTime();
  22. }
  23. const auto readInt = [](const QString &value) {
  24. auto view = QStringView(value);
  25. while (!view.isEmpty() && view.at(0) == '0') {
  26. view = base::StringViewMid(view, 1);
  27. }
  28. return view.toInt();
  29. };
  30. return QTime(readInt(match.captured(1)), readInt(match.captured(2)));
  31. }
  32. QString GetHour(const QString &value) {
  33. if (const auto time = ValidateTime(value); time.isValid()) {
  34. return QString::number(time.hour());
  35. }
  36. return QString();
  37. }
  38. QString GetMinute(const QString &value) {
  39. if (const auto time = ValidateTime(value); time.isValid()) {
  40. return QString("%1").arg(time.minute(), 2, 10, QChar('0'));
  41. }
  42. return QString();
  43. }
  44. } // namespace
  45. TimeInput::TimeInput(
  46. QWidget *parent,
  47. const QString &value,
  48. const style::InputField &stField,
  49. const style::InputField &stDateField,
  50. const style::FlatLabel &stSeparator,
  51. const style::margins &stSeparatorPadding)
  52. : RpWidget(parent)
  53. , _stField(stField)
  54. , _stDateField(stDateField)
  55. , _stSeparator(stSeparator)
  56. , _stSeparatorPadding(stSeparatorPadding)
  57. , _hour(
  58. this,
  59. _stField,
  60. rpl::never<QString>(),
  61. GetHour(value))
  62. , _separator1(
  63. this,
  64. object_ptr<FlatLabel>(
  65. this,
  66. QString(":"),
  67. _stSeparator),
  68. _stSeparatorPadding)
  69. , _minute(
  70. this,
  71. _stField,
  72. rpl::never<QString>(),
  73. GetMinute(value))
  74. , _value(valueCurrent()) {
  75. const auto focused = [=](const object_ptr<TimePart> &field) {
  76. return [this, pointer = MakeWeak(field.data())]{
  77. _borderAnimationStart = pointer->borderAnimationStart()
  78. + pointer->x()
  79. - _hour->x();
  80. setFocused(true);
  81. _focuses.fire({});
  82. };
  83. };
  84. const auto blurred = [=] {
  85. setFocused(false);
  86. };
  87. const auto changed = [=] {
  88. _value = valueCurrent();
  89. };
  90. connect(_hour, &MaskedInputField::focused, focused(_hour));
  91. connect(_minute, &MaskedInputField::focused, focused(_minute));
  92. connect(_hour, &MaskedInputField::blurred, blurred);
  93. connect(_minute, &MaskedInputField::blurred, blurred);
  94. connect(_hour, &MaskedInputField::changed, changed);
  95. connect(_minute, &MaskedInputField::changed, changed);
  96. _hour->setMaxValue(23);
  97. _hour->setWheelStep(1);
  98. _hour->putNext() | rpl::start_with_next([=](QChar ch) {
  99. putNext(_minute, ch);
  100. }, lifetime());
  101. _minute->setMaxValue(59);
  102. _minute->setWheelStep(10);
  103. _minute->erasePrevious() | rpl::start_with_next([=] {
  104. erasePrevious(_hour);
  105. }, lifetime());
  106. _minute->jumpToPrevious() | rpl::start_with_next([=] {
  107. _hour->setCursorPosition(_hour->getLastText().size());
  108. _hour->setFocus();
  109. }, lifetime());
  110. _separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
  111. setMouseTracking(true);
  112. _value.changes(
  113. ) | rpl::start_with_next([=] {
  114. setErrorShown(false);
  115. }, lifetime());
  116. const auto submitHour = [=] {
  117. if (hour().has_value()) {
  118. _minute->setFocus();
  119. }
  120. };
  121. const auto submitMinute = [=] {
  122. if (minute().has_value()) {
  123. if (hour().has_value()) {
  124. _submitRequests.fire({});
  125. } else {
  126. _hour->setFocus();
  127. }
  128. }
  129. };
  130. connect(
  131. _hour,
  132. &MaskedInputField::submitted,
  133. submitHour);
  134. connect(
  135. _minute,
  136. &MaskedInputField::submitted,
  137. submitMinute);
  138. }
  139. void TimeInput::putNext(const object_ptr<TimePart> &field, QChar ch) {
  140. field->setCursorPosition(0);
  141. if (ch.unicode()) {
  142. field->setText(ch + field->getLastText());
  143. field->setCursorPosition(1);
  144. }
  145. field->onTextEdited();
  146. setFocusQueued(field);
  147. }
  148. void TimeInput::erasePrevious(const object_ptr<TimePart> &field) {
  149. const auto text = field->getLastText();
  150. if (!text.isEmpty()) {
  151. field->setCursorPosition(text.size() - 1);
  152. field->setText(text.mid(0, text.size() - 1));
  153. }
  154. setFocusQueued(field);
  155. }
  156. void TimeInput::setFocusQueued(const object_ptr<TimePart> &field) {
  157. // There was a "Stack Overflow" crash in some input method handling.
  158. //
  159. // See https://github.com/telegramdesktop/tdesktop/issues/25129
  160. //
  161. // The stack is something like:
  162. //
  163. // ...
  164. // QApplicationPrivate::sendMouseEvent
  165. // ----
  166. // QWidget::setFocus
  167. // QWindow::focusObjectChanged
  168. // QWindowsInputContext::setFocusObject
  169. // QWindowsInputContext::reset
  170. // QLineEdit::inputMethodEvent
  171. // QWidgetLineControl::finishChange
  172. // QLineEdit::textEdited
  173. // MaskedInputField::onTextEdited
  174. // TimePart::correctValue
  175. // TimeInput::putNext
  176. // ----
  177. // QWidget::setFocus
  178. // QWindow::focusObjectChanged
  179. // ...
  180. //
  181. // So we try to break this loop by focusing the widget async.
  182. const auto raw = field.data();
  183. InvokeQueued(raw, [raw] { raw->setFocus(); });
  184. }
  185. bool TimeInput::setFocusFast() {
  186. if (hour().has_value()) {
  187. _minute->setFocusFast();
  188. } else {
  189. _hour->setFocusFast();
  190. }
  191. return true;
  192. }
  193. std::optional<int> TimeInput::hour() const {
  194. return _hour->number();
  195. }
  196. std::optional<int> TimeInput::minute() const {
  197. return _minute->number();
  198. }
  199. QString TimeInput::valueCurrent() const {
  200. const auto result = QString("%1:%2"
  201. ).arg(hour().value_or(0)
  202. ).arg(minute().value_or(0), 2, 10, QChar('0'));
  203. return ValidateTime(result).isValid() ? result : QString();
  204. }
  205. rpl::producer<QString> TimeInput::value() const {
  206. return _value.value();
  207. }
  208. rpl::producer<> TimeInput::submitRequests() const {
  209. return _submitRequests.events();
  210. }
  211. rpl::producer<> TimeInput::focuses() const {
  212. return _focuses.events();
  213. }
  214. void TimeInput::paintEvent(QPaintEvent *e) {
  215. auto p = QPainter(this);
  216. const auto &_st = _stDateField;
  217. const auto height = _st.heightMin;
  218. if (_st.border) {
  219. p.fillRect(0, height - _st.border, width(), _st.border, _st.borderFg);
  220. }
  221. auto errorDegree = _a_error.value(_error ? 1. : 0.);
  222. auto borderShownDegree = _a_borderShown.value(1.);
  223. auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);
  224. if (_st.borderActive && (borderOpacity > 0.)) {
  225. auto borderStart = std::clamp(_borderAnimationStart, 0, width());
  226. auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
  227. auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
  228. if (borderTo > borderFrom) {
  229. auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
  230. p.setOpacity(borderOpacity);
  231. p.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
  232. p.setOpacity(1);
  233. }
  234. }
  235. }
  236. template <typename Widget>
  237. bool TimeInput::insideSeparator(QPoint position, const Widget &widget) const {
  238. const auto x = position.x();
  239. const auto y = position.y();
  240. return (x >= widget->x() && x < widget->x() + widget->width())
  241. && (y >= _hour->y() && y < _hour->y() + _hour->height());
  242. }
  243. void TimeInput::mouseMoveEvent(QMouseEvent *e) {
  244. const auto cursor = insideSeparator(e->pos(), _separator1)
  245. ? style::cur_text
  246. : style::cur_default;
  247. if (_cursor != cursor) {
  248. _cursor = cursor;
  249. setCursor(_cursor);
  250. }
  251. }
  252. void TimeInput::mousePressEvent(QMouseEvent *e) {
  253. const auto x = e->pos().x();
  254. const auto focus1 = [&] {
  255. if (_hour->getLastText().size() > 1) {
  256. _minute->setFocus();
  257. } else {
  258. _hour->setFocus();
  259. }
  260. };
  261. if (insideSeparator(e->pos(), _separator1)) {
  262. focus1();
  263. _borderAnimationStart = x - _hour->x();
  264. }
  265. }
  266. int TimeInput::resizeGetHeight(int width) {
  267. const auto &_st = _stField;
  268. const auto &font = _st.placeholderFont;
  269. const auto addToWidth = _stSeparatorPadding.left();
  270. const auto hourWidth = _st.textMargins.left()
  271. + _st.placeholderMargins.left()
  272. + font->width(QString("23"))
  273. + _st.placeholderMargins.right()
  274. + _st.textMargins.right()
  275. + addToWidth;
  276. const auto minuteWidth = _st.textMargins.left()
  277. + _st.placeholderMargins.left()
  278. + font->width(QString("59"))
  279. + _st.placeholderMargins.right()
  280. + _st.textMargins.right()
  281. + addToWidth;
  282. const auto full = hourWidth
  283. - addToWidth
  284. + _separator1->width()
  285. + minuteWidth
  286. - addToWidth;
  287. auto left = (width - full) / 2;
  288. auto top = 0;
  289. _hour->setGeometry(left, top, hourWidth, _hour->height());
  290. left += hourWidth - addToWidth;
  291. _separator1->resizeToNaturalWidth(width);
  292. _separator1->move(left, top);
  293. left += _separator1->width();
  294. _minute->setGeometry(left, top, minuteWidth, _minute->height());
  295. return _stDateField.heightMin;
  296. }
  297. void TimeInput::showError() {
  298. setErrorShown(true);
  299. if (!_focused) {
  300. setInnerFocus();
  301. }
  302. }
  303. void TimeInput::setInnerFocus() {
  304. if (hour().has_value()) {
  305. _minute->setFocus();
  306. } else {
  307. _hour->setFocus();
  308. }
  309. }
  310. void TimeInput::setErrorShown(bool error) {
  311. if (_error != error) {
  312. _error = error;
  313. _a_error.start(
  314. [=] { update(); },
  315. _error ? 0. : 1.,
  316. _error ? 1. : 0.,
  317. _stDateField.duration);
  318. startBorderAnimation();
  319. }
  320. }
  321. void TimeInput::setFocused(bool focused) {
  322. if (_focused != focused) {
  323. _focused = focused;
  324. _a_focused.start(
  325. [=] { update(); },
  326. _focused ? 0. : 1.,
  327. _focused ? 1. : 0.,
  328. _stDateField.duration);
  329. startBorderAnimation();
  330. }
  331. }
  332. void TimeInput::startBorderAnimation() {
  333. auto borderVisible = (_error || _focused);
  334. if (_borderVisible != borderVisible) {
  335. _borderVisible = borderVisible;
  336. const auto duration = _stDateField.duration;
  337. if (_borderVisible) {
  338. if (_a_borderOpacity.animating()) {
  339. _a_borderOpacity.start([=] { update(); }, 0., 1., duration);
  340. } else {
  341. _a_borderShown.start([=] { update(); }, 0., 1., duration);
  342. }
  343. } else {
  344. _a_borderOpacity.start([=] { update(); }, 1., 0., duration);
  345. }
  346. }
  347. }
  348. } // namespace Ui