calendar_box.cpp 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "ui/boxes/calendar_box.h"
  8. #include "ui/widgets/buttons.h"
  9. #include "ui/widgets/scroll_area.h"
  10. #include "ui/effects/ripple_animation.h"
  11. #include "ui/chat/chat_style.h"
  12. #include "ui/ui_utility.h"
  13. #include "ui/painter.h"
  14. #include "ui/cached_round_corners.h"
  15. #include "lang/lang_keys.h"
  16. #include "styles/style_boxes.h"
  17. #include "styles/style_chat.h"
  18. #include <QtCore/QLocale>
  19. namespace Ui {
  20. namespace {
  21. constexpr auto kDaysInWeek = 7;
  22. constexpr auto kTooltipDelay = crl::time(1000);
  23. constexpr auto kJumpDelay = 2 * crl::time(1000);
  24. } // namespace
  25. class CalendarBox::Context {
  26. public:
  27. Context(QDate month, QDate highlighted);
  28. void setAllowsSelection(bool allowsSelection);
  29. [[nodiscard]] bool allowsSelection() const {
  30. return _allowsSelection;
  31. }
  32. void setMinDate(QDate date);
  33. void setMaxDate(QDate date);
  34. [[nodiscard]] int minDayIndex() const {
  35. return _minDayIndex;
  36. }
  37. [[nodiscard]] int maxDayIndex() const {
  38. return _maxDayIndex;
  39. }
  40. void skipMonth(int skip);
  41. void showMonth(QDate month);
  42. [[nodiscard]] bool showsMonthOf(QDate date) const;
  43. [[nodiscard]] int highlightedIndex() const {
  44. return _highlightedIndex;
  45. }
  46. [[nodiscard]] int rowsCount() const {
  47. return _rowsCount;
  48. }
  49. [[nodiscard]] int rowsCountMax() const {
  50. return 6;
  51. }
  52. [[nodiscard]] int daysShift() const {
  53. return _daysShift;
  54. }
  55. [[nodiscard]] int daysCount() const {
  56. return _daysCount;
  57. }
  58. [[nodiscard]] bool isEnabled(int index) const {
  59. return (index >= _minDayIndex) && (index <= _maxDayIndex);
  60. }
  61. [[nodiscard]] QDate month() const {
  62. return _month.current();
  63. }
  64. [[nodiscard]] rpl::producer<QDate> monthValue() const {
  65. return _month.value();
  66. }
  67. [[nodiscard]] int firstDayShift() const {
  68. return _firstDayShift;
  69. }
  70. [[nodiscard]] QDate dateFromIndex(int index) const;
  71. [[nodiscard]] QString labelFromIndex(int index) const;
  72. void toggleSelectionMode(bool enabled);
  73. [[nodiscard]] bool selectionMode() const;
  74. [[nodiscard]] rpl::producer<> selectionUpdates() const;
  75. [[nodiscard]] std::optional<int> selectedMin() const;
  76. [[nodiscard]] std::optional<int> selectedMax() const;
  77. void startSelection(int index);
  78. void updateSelection(int index);
  79. private:
  80. struct Selection {
  81. QDate min;
  82. QDate max;
  83. int minIndex = 0;
  84. int maxIndex = 0;
  85. };
  86. void applyMonth(const QDate &month, bool forced = false);
  87. static int DaysShiftForMonth(QDate month, QDate min, int firstDayShift);
  88. static int RowsCountForMonth(
  89. QDate month,
  90. QDate min,
  91. QDate max,
  92. int firstDayShift);
  93. const int _firstDayShift = 0;
  94. bool _allowsSelection = false;
  95. rpl::variable<QDate> _month;
  96. QDate _min, _max;
  97. QDate _highlighted;
  98. Fn<QString(int)> _dayOfWeek;
  99. Fn<QString(int, int)> _monthOfYear;
  100. int _highlightedIndex = 0;
  101. int _minDayIndex = 0;
  102. int _maxDayIndex = 0;
  103. int _daysCount = 0;
  104. int _daysShift = 0;
  105. int _rowsCount = 0;
  106. Selection _selection;
  107. QDate _selectionStart;
  108. int _selectionStartIndex = 0;
  109. rpl::event_stream<> _selectionUpdates;
  110. bool _selectionMode = false;
  111. };
  112. CalendarBox::Context::Context(QDate month, QDate highlighted)
  113. : _firstDayShift(static_cast<int>(QLocale().firstDayOfWeek())
  114. - static_cast<int>(Qt::Monday))
  115. , _highlighted(highlighted) {
  116. showMonth(month);
  117. }
  118. void CalendarBox::Context::setAllowsSelection(bool allows) {
  119. _allowsSelection = allows;
  120. }
  121. void CalendarBox::Context::setMinDate(QDate date) {
  122. _min = date;
  123. applyMonth(_month.current(), true);
  124. }
  125. void CalendarBox::Context::setMaxDate(QDate date) {
  126. _max = date;
  127. applyMonth(_month.current(), true);
  128. }
  129. void CalendarBox::Context::showMonth(QDate month) {
  130. if (month.day() != 1) {
  131. month = QDate(month.year(), month.month(), 1);
  132. }
  133. applyMonth(month);
  134. }
  135. bool CalendarBox::Context::showsMonthOf(QDate date) const {
  136. const auto shown = _month.current();
  137. return (shown.year() == date.year()) && (shown.month() == date.month());
  138. }
  139. void CalendarBox::Context::applyMonth(const QDate &month, bool forced) {
  140. const auto was = _month.current();
  141. _daysCount = month.daysInMonth();
  142. _daysShift = DaysShiftForMonth(month, _min, _firstDayShift);
  143. _rowsCount = RowsCountForMonth(month, _min, _max, _firstDayShift);
  144. _highlightedIndex = month.daysTo(_highlighted);
  145. _minDayIndex = _min.isNull() ? INT_MIN : month.daysTo(_min);
  146. _maxDayIndex = _max.isNull() ? INT_MAX : month.daysTo(_max);
  147. const auto shift = was.isNull() ? 0 : month.daysTo(was);
  148. auto updated = false;
  149. const auto update = [&](const QDate &date, int &index) {
  150. if (shift && !date.isNull()) {
  151. index += shift;
  152. }
  153. };
  154. update(_selection.min, _selection.minIndex);
  155. update(_selection.max, _selection.maxIndex);
  156. update(_selectionStart, _selectionStartIndex);
  157. if (forced) {
  158. _month.force_assign(month);
  159. } else {
  160. _month = month;
  161. }
  162. if (updated) {
  163. _selectionUpdates.fire({});
  164. }
  165. }
  166. void CalendarBox::Context::skipMonth(int skip) {
  167. auto year = _month.current().year();
  168. auto month = _month.current().month();
  169. month += skip;
  170. while (month < 1) {
  171. --year;
  172. month += 12;
  173. }
  174. while (month > 12) {
  175. ++year;
  176. month -= 12;
  177. }
  178. showMonth(QDate(year, month, 1));
  179. }
  180. int CalendarBox::Context::DaysShiftForMonth(
  181. QDate month,
  182. QDate min,
  183. int firstDayShift) {
  184. Expects(!month.isNull());
  185. constexpr auto kMaxRows = 6;
  186. const auto inMonthIndex = month.day() - 1;
  187. const auto inWeekIndex = month.dayOfWeek() - 1;
  188. const auto from = ((kMaxRows * kDaysInWeek) + inWeekIndex - inMonthIndex)
  189. % kDaysInWeek;
  190. if (min.isNull()) {
  191. min = month.addYears(-1);
  192. } else if (min >= month) {
  193. return from - firstDayShift;
  194. }
  195. if (min.day() != 1) {
  196. min = QDate(min.year(), min.month(), 1);
  197. }
  198. const auto add = min.daysTo(month) - inWeekIndex + (min.dayOfWeek() - 1);
  199. return from + add - firstDayShift;
  200. }
  201. int CalendarBox::Context::RowsCountForMonth(
  202. QDate month,
  203. QDate min,
  204. QDate max,
  205. int firstDayShift) {
  206. Expects(!month.isNull());
  207. const auto daysShift = DaysShiftForMonth(month, min, firstDayShift);
  208. const auto daysCount = month.daysInMonth();
  209. const auto cellsCount = daysShift + daysCount;
  210. auto result = (cellsCount / kDaysInWeek);
  211. if (cellsCount % kDaysInWeek) {
  212. ++result;
  213. }
  214. if (max.isNull()) {
  215. max = month.addYears(1);
  216. }
  217. if (max < month.addMonths(1)) {
  218. return result;
  219. }
  220. if (max.day() != 1) {
  221. max = QDate(max.year(), max.month(), 1);
  222. }
  223. max = max.addMonths(1);
  224. max = max.addDays(1 - max.dayOfWeek());
  225. const auto cellsFull = daysShift + (month.day() - 1) + month.daysTo(max);
  226. return cellsFull / kDaysInWeek;
  227. }
  228. QDate CalendarBox::Context::dateFromIndex(int index) const {
  229. constexpr auto kMonthsCount = 12;
  230. auto month = _month.current().month();
  231. auto year = _month.current().year();
  232. while (index < 0) {
  233. if (!--month) {
  234. month += kMonthsCount;
  235. --year;
  236. }
  237. index += QDate(year, month, 1).daysInMonth();
  238. }
  239. for (auto maxIndex = QDate(year, month, 1).daysInMonth(); index >= maxIndex; maxIndex = QDate(year, month, 1).daysInMonth()) {
  240. index -= maxIndex;
  241. if (month++ == kMonthsCount) {
  242. month -= kMonthsCount;
  243. ++year;
  244. }
  245. }
  246. return QDate(year, month, index + 1);
  247. }
  248. QString CalendarBox::Context::labelFromIndex(int index) const {
  249. auto day = [this, index] {
  250. if (index >= 0 && index < daysCount()) {
  251. return index + 1;
  252. }
  253. return dateFromIndex(index).day();
  254. };
  255. return QString::number(day());
  256. }
  257. void CalendarBox::Context::toggleSelectionMode(bool enabled) {
  258. if (_selectionMode == enabled) {
  259. return;
  260. }
  261. _selectionMode = enabled;
  262. _selectionStart = {};
  263. _selection = {};
  264. _selectionUpdates.fire({});
  265. }
  266. bool CalendarBox::Context::selectionMode() const {
  267. return _selectionMode;
  268. }
  269. rpl::producer<> CalendarBox::Context::selectionUpdates() const {
  270. return _selectionUpdates.events();
  271. }
  272. std::optional<int> CalendarBox::Context::selectedMin() const {
  273. return _selection.min.isNull()
  274. ? std::optional<int>()
  275. : _selection.minIndex;
  276. }
  277. std::optional<int> CalendarBox::Context::selectedMax() const {
  278. return _selection.max.isNull()
  279. ? std::optional<int>()
  280. : _selection.maxIndex;
  281. }
  282. void CalendarBox::Context::startSelection(int index) {
  283. Expects(_selectionMode);
  284. if (!_selectionStart.isNull() && _selectionStartIndex == index) {
  285. return;
  286. }
  287. _selectionStartIndex = index;
  288. _selectionStart = dateFromIndex(index);
  289. updateSelection(index);
  290. }
  291. void CalendarBox::Context::updateSelection(int index) {
  292. Expects(_selectionMode);
  293. Expects(!_selectionStart.isNull());
  294. index = std::clamp(index, minDayIndex(), maxDayIndex());
  295. const auto start = _selectionStartIndex;
  296. const auto min = std::min(index, start);
  297. const auto max = std::max(index, start);
  298. if (!_selection.min.isNull()
  299. && _selection.minIndex == min
  300. && !_selection.max.isNull()
  301. && _selection.maxIndex == max) {
  302. return;
  303. }
  304. _selection = Selection{
  305. .min = dateFromIndex(min),
  306. .max = dateFromIndex(max),
  307. .minIndex = min,
  308. .maxIndex = max,
  309. };
  310. _selectionUpdates.fire({});
  311. }
  312. class CalendarBox::Inner final : public RpWidget {
  313. public:
  314. Inner(
  315. QWidget *parent,
  316. not_null<Context*> context,
  317. const style::CalendarSizes &st,
  318. const style::CalendarColors &styleColors);
  319. [[nodiscard]] int countMaxHeight() const;
  320. void setDateChosenCallback(Fn<void(QDate)> callback);
  321. ~Inner();
  322. protected:
  323. void paintEvent(QPaintEvent *e) override;
  324. void mouseMoveEvent(QMouseEvent *e) override;
  325. void mousePressEvent(QMouseEvent *e) override;
  326. void mouseReleaseEvent(QMouseEvent *e) override;
  327. private:
  328. void monthChanged(QDate month);
  329. void setSelected(int selected);
  330. void setPressed(int pressed);
  331. int rowsLeft() const;
  332. int rowsTop() const;
  333. void resizeToCurrent();
  334. void paintRows(QPainter &p, QRect clip);
  335. const style::CalendarSizes &_st;
  336. const style::CalendarColors &_styleColors;
  337. const not_null<Context*> _context;
  338. bool _twoPressSelectionStarted = false;
  339. std::map<int, std::unique_ptr<RippleAnimation>> _ripples;
  340. Fn<void(QDate)> _dateChosenCallback;
  341. static constexpr auto kEmptySelection = INT_MIN / 2;
  342. int _selected = kEmptySelection;
  343. int _pressed = kEmptySelection;
  344. bool _pointerCursor = false;
  345. bool _cursorSetWithoutMouseMove = false;
  346. QPoint _lastGlobalPosition;
  347. bool _mouseMoved = false;
  348. };
  349. class CalendarBox::FloatingDate final {
  350. public:
  351. FloatingDate(QWidget *parent, not_null<Context*> context);
  352. [[nodiscard]] rpl::producer<int> widthValue() const;
  353. void move(int x, int y);
  354. [[nodiscard]] rpl::lifetime &lifetime();
  355. private:
  356. void paint();
  357. const not_null<Context*> _context;
  358. RpWidget _widget;
  359. CornersPixmaps _corners;
  360. QString _text;
  361. };
  362. CalendarBox::FloatingDate::FloatingDate(
  363. QWidget *parent,
  364. not_null<Context*> context)
  365. : _context(context)
  366. , _widget(parent)
  367. , _corners(
  368. PrepareCornerPixmaps(
  369. HistoryServiceMsgRadius(),
  370. st::roundedBg)) {
  371. _context->monthValue(
  372. ) | rpl::start_with_next([=](QDate month) {
  373. _text = langMonthOfYearFull(month.month(), month.year());
  374. const auto width = st::msgServiceFont->width(_text);
  375. const auto rect = QRect(0, 0, width, st::msgServiceFont->height);
  376. _widget.resize(rect.marginsAdded(st::msgServicePadding).size());
  377. _widget.update();
  378. }, _widget.lifetime());
  379. _widget.paintRequest(
  380. ) | rpl::start_with_next([=] {
  381. paint();
  382. }, _widget.lifetime());
  383. _widget.setAttribute(Qt::WA_TransparentForMouseEvents);
  384. _widget.show();
  385. }
  386. rpl::producer<int> CalendarBox::FloatingDate::widthValue() const {
  387. return _widget.widthValue();
  388. }
  389. void CalendarBox::FloatingDate::move(int x, int y) {
  390. _widget.move(x, y);
  391. }
  392. rpl::lifetime &CalendarBox::FloatingDate::lifetime() {
  393. return _widget.lifetime();
  394. }
  395. void CalendarBox::FloatingDate::paint() {
  396. auto p = QPainter(&_widget);
  397. FillRoundRect(p, _widget.rect(), st::roundedBg, _corners);
  398. p.setFont(st::msgServiceFont);
  399. p.setPen(st::roundedFg);
  400. p.drawText(
  401. st::msgServicePadding.left(),
  402. st::msgServicePadding.top() + st::msgServiceFont->ascent,
  403. _text);
  404. }
  405. CalendarBox::Inner::Inner(
  406. QWidget *parent,
  407. not_null<Context*> context,
  408. const style::CalendarSizes &st,
  409. const style::CalendarColors &styleColors)
  410. : RpWidget(parent)
  411. , _st(st)
  412. , _styleColors(styleColors)
  413. , _context(context) {
  414. setMouseTracking(true);
  415. context->monthValue(
  416. ) | rpl::start_with_next([=](QDate month) {
  417. monthChanged(month);
  418. }, lifetime());
  419. context->selectionUpdates(
  420. ) | rpl::start_with_next([=] {
  421. update();
  422. }, lifetime());
  423. }
  424. void CalendarBox::Inner::monthChanged(QDate month) {
  425. setSelected(kEmptySelection);
  426. _ripples.clear();
  427. resizeToCurrent();
  428. update();
  429. SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);
  430. }
  431. void CalendarBox::Inner::resizeToCurrent() {
  432. const auto height = _context->rowsCount() * _st.cellSize.height();
  433. resize(_st.width, _st.padding.top() + height + _st.padding.bottom());
  434. }
  435. void CalendarBox::Inner::paintEvent(QPaintEvent *e) {
  436. auto p = QPainter(this);
  437. auto clip = e->rect();
  438. paintRows(p, clip);
  439. }
  440. int CalendarBox::Inner::rowsLeft() const {
  441. return _st.padding.left();
  442. }
  443. int CalendarBox::Inner::rowsTop() const {
  444. return _st.padding.top();
  445. }
  446. void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) {
  447. p.setFont(st::calendarDaysFont);
  448. auto y = rowsTop();
  449. auto index = -_context->daysShift();
  450. const auto selectionMode = _context->selectionMode();
  451. const auto impossible = index - 45;
  452. const auto selectedMin = _context->selectedMin().value_or(impossible);
  453. const auto selectedMax = _context->selectedMax().value_or(impossible);
  454. const auto highlightedIndex = selectionMode
  455. ? impossible
  456. : _context->highlightedIndex();
  457. const auto daysCount = _context->daysCount();
  458. const auto rowsCount = _context->rowsCount();
  459. const auto rowHeight = _st.cellSize.height();
  460. const auto fromRow = std::max(clip.y() - y, 0) / rowHeight;
  461. const auto tillRow = std::min(
  462. (clip.y() + clip.height() + rowHeight - 1) / rowHeight,
  463. rowsCount);
  464. y += fromRow * rowHeight;
  465. index += fromRow * kDaysInWeek;
  466. const auto innerSkipLeft = (_st.cellSize.width() - _st.cellInner) / 2;
  467. const auto innerSkipTop = (_st.cellSize.height() - _st.cellInner) / 2;
  468. const auto fromCol = _context->firstDayShift();
  469. const auto toCol = fromCol + kDaysInWeek;
  470. for (auto row = fromRow; row != tillRow; ++row, y += rowHeight) {
  471. auto x = rowsLeft();
  472. const auto fromIndex = index;
  473. const auto tillIndex = (index + kDaysInWeek);
  474. const auto selectedFrom = std::max(fromIndex, selectedMin);
  475. const auto selectedTill = std::min(tillIndex, selectedMax + 1);
  476. const auto selectedInRow = (selectedTill - selectedFrom);
  477. if (selectedInRow > 0) {
  478. auto hq = PainterHighQualityEnabler(p);
  479. p.setPen(Qt::NoPen);
  480. p.setBrush(st::activeButtonBg);
  481. p.drawRoundedRect(
  482. (x
  483. + (selectedFrom - index) * _st.cellSize.width()
  484. + innerSkipLeft
  485. - st::lineWidth),
  486. y + innerSkipTop - st::lineWidth,
  487. ((selectedInRow - 1) * _st.cellSize.width()
  488. + 2 * st::lineWidth
  489. + _st.cellInner),
  490. _st.cellInner + 2 * st::lineWidth,
  491. (_st.cellInner / 2.) + st::lineWidth,
  492. (_st.cellInner / 2.) + st::lineWidth);
  493. p.setBrush(Qt::NoBrush);
  494. }
  495. for (auto col = fromCol; col != toCol; ++col, ++index, x += _st.cellSize.width()) {
  496. const auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.cellSize.height());
  497. const auto selected = (index >= selectedMin) && (index <= selectedMax);
  498. const auto grayedOut = !selected && (index < 0 || index >= daysCount);
  499. const auto highlighted = (index == highlightedIndex);
  500. const auto enabled = _context->isEnabled(index);
  501. const auto innerLeft = x + innerSkipLeft;
  502. const auto innerTop = y + innerSkipTop;
  503. if (highlighted) {
  504. auto hq = PainterHighQualityEnabler(p);
  505. p.setPen(Qt::NoPen);
  506. p.setBrush(grayedOut ? st::windowBgOver : st::dialogsBgActive);
  507. p.drawEllipse(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner));
  508. p.setBrush(Qt::NoBrush);
  509. }
  510. const auto it = _ripples.find(index);
  511. if (it != _ripples.cend() && !selectionMode) {
  512. const auto colorOverride = (!highlighted
  513. ? _styleColors.rippleColor
  514. : grayedOut
  515. ? _styleColors.rippleGrayedOutColor
  516. : _styleColors.rippleColorHighlighted)->c;
  517. it->second->paint(p, innerLeft, innerTop, width(), &colorOverride);
  518. if (it->second->empty()) {
  519. _ripples.erase(it);
  520. }
  521. }
  522. p.setPen(selected
  523. ? st::activeButtonFg
  524. : highlighted
  525. ? (grayedOut
  526. ? _styleColors.dayTextGrayedOutColor
  527. : st::dialogsNameFgActive)
  528. : enabled
  529. ? (grayedOut
  530. ? _styleColors.dayTextGrayedOutColor
  531. : _styleColors.dayTextColor)
  532. : st::windowSubTextFg);
  533. p.drawText(rect, _context->labelFromIndex(index), style::al_center);
  534. }
  535. }
  536. }
  537. void CalendarBox::Inner::mouseMoveEvent(QMouseEvent *e) {
  538. const auto globalPosition = e->globalPos();
  539. _mouseMoved = (_lastGlobalPosition != globalPosition);
  540. _lastGlobalPosition = globalPosition;
  541. const auto size = _st.cellSize;
  542. const auto point = e->pos();
  543. const auto inner = QRect(
  544. rowsLeft(),
  545. rowsTop(),
  546. kDaysInWeek * size.width(),
  547. _context->rowsCount() * size.height());
  548. if (inner.contains(point)) {
  549. const auto row = (point.y() - rowsTop()) / size.height();
  550. const auto col = (point.x() - rowsLeft()) / size.width();
  551. const auto index = row * kDaysInWeek + col - _context->daysShift();
  552. setSelected(index);
  553. } else {
  554. setSelected(kEmptySelection);
  555. }
  556. if (_pressed != kEmptySelection && _context->selectionMode()) {
  557. const auto row = (point.y() >= rowsTop())
  558. ? (point.y() - rowsTop()) / size.height()
  559. : -1;
  560. const auto col = (point.y() < rowsTop())
  561. ? 0
  562. : (point.x() >= rowsLeft())
  563. ? std::min(
  564. (point.x() - rowsLeft()) / size.width(),
  565. kDaysInWeek - 1)
  566. : 0;
  567. const auto index = row * kDaysInWeek + col - _context->daysShift();
  568. _context->updateSelection(index);
  569. }
  570. }
  571. void CalendarBox::Inner::setSelected(int selected) {
  572. if (selected != kEmptySelection && !_context->isEnabled(selected)) {
  573. selected = kEmptySelection;
  574. }
  575. _selected = selected;
  576. const auto pointer = (_selected != kEmptySelection);
  577. const auto force = (_mouseMoved && _cursorSetWithoutMouseMove);
  578. if (_pointerCursor != pointer || force) {
  579. if (force) {
  580. // Workaround some strange bug. When I call setCursor while
  581. // scrolling by touchpad the new cursor is not applied and
  582. // then it is not applied until it is changed.
  583. setCursor(pointer ? style::cur_default : style::cur_pointer);
  584. }
  585. setCursor(pointer ? style::cur_pointer : style::cur_default);
  586. _cursorSetWithoutMouseMove = !_mouseMoved;
  587. _pointerCursor = pointer;
  588. }
  589. _mouseMoved = false;
  590. }
  591. void CalendarBox::Inner::mousePressEvent(QMouseEvent *e) {
  592. setPressed(_selected);
  593. if (_selected != kEmptySelection) {
  594. auto index = _selected + _context->daysShift();
  595. Assert(index >= 0);
  596. auto row = index / kDaysInWeek;
  597. auto col = index % kDaysInWeek;
  598. auto cell = QRect(rowsLeft() + col * _st.cellSize.width(), rowsTop() + row * _st.cellSize.height(), _st.cellSize.width(), _st.cellSize.height());
  599. auto it = _ripples.find(_selected);
  600. if (it == _ripples.cend()) {
  601. auto mask = RippleAnimation::EllipseMask(QSize(_st.cellInner, _st.cellInner));
  602. auto update = [this, cell] { rtlupdate(cell); };
  603. it = _ripples.emplace(_selected, std::make_unique<RippleAnimation>(st::defaultRippleAnimation, std::move(mask), std::move(update))).first;
  604. }
  605. auto ripplePosition = QPoint(cell.x() + (_st.cellSize.width() - _st.cellInner) / 2, cell.y() + (_st.cellSize.height() - _st.cellInner) / 2);
  606. it->second->add(e->pos() - ripplePosition);
  607. if (_context->selectionMode()) {
  608. if (_context->selectedMin().has_value()
  609. && ((e->modifiers() & Qt::ShiftModifier)
  610. || (_twoPressSelectionStarted
  611. && (_context->selectedMin()
  612. == _context->selectedMax())))) {
  613. _context->updateSelection(_selected);
  614. _twoPressSelectionStarted = false;
  615. } else {
  616. _context->startSelection(_selected);
  617. _twoPressSelectionStarted = true;
  618. }
  619. }
  620. }
  621. }
  622. void CalendarBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
  623. auto pressed = _pressed;
  624. setPressed(kEmptySelection);
  625. if (pressed != kEmptySelection
  626. && pressed == _selected
  627. && !_context->selectionMode()) {
  628. crl::on_main(this, [=] {
  629. const auto onstack = _dateChosenCallback;
  630. onstack(_context->dateFromIndex(pressed));
  631. });
  632. }
  633. }
  634. void CalendarBox::Inner::setPressed(int pressed) {
  635. if (_pressed != pressed) {
  636. if (_pressed != kEmptySelection) {
  637. auto it = _ripples.find(_pressed);
  638. if (it != _ripples.cend()) {
  639. it->second->lastStop();
  640. }
  641. }
  642. _pressed = pressed;
  643. }
  644. }
  645. int CalendarBox::Inner::countMaxHeight() const {
  646. const auto innerHeight = _context->rowsCountMax() * _st.cellSize.height();
  647. return _st.padding.top()
  648. + innerHeight
  649. + _st.padding.bottom();
  650. }
  651. void CalendarBox::Inner::setDateChosenCallback(Fn<void(QDate)> callback) {
  652. _dateChosenCallback = std::move(callback);
  653. }
  654. CalendarBox::Inner::~Inner() = default;
  655. class CalendarBox::Title final : public RpWidget {
  656. public:
  657. Title(
  658. QWidget *parent,
  659. not_null<Context*> context,
  660. const style::CalendarSizes &st,
  661. const style::CalendarColors &styleColors);
  662. protected:
  663. void paintEvent(QPaintEvent *e);
  664. private:
  665. void setTextFromMonth(QDate month);
  666. void setText(QString text);
  667. void paintDayNames(Painter &p, QRect clip);
  668. const style::CalendarSizes &_st;
  669. const style::CalendarColors &_styleColors;
  670. const not_null<Context*> _context;
  671. QString _text;
  672. int _textWidth = 0;
  673. int _textLeft = 0;
  674. };
  675. CalendarBox::Title::Title(
  676. QWidget *parent,
  677. not_null<Context*> context,
  678. const style::CalendarSizes &st,
  679. const style::CalendarColors &styleColors)
  680. : RpWidget(parent)
  681. , _st(st)
  682. , _styleColors(styleColors)
  683. , _context(context) {
  684. const auto dayWidth = st::calendarDaysFont->width(langDayOfWeek(1));
  685. _textLeft = _st.padding.left() + (_st.cellSize.width() - dayWidth) / 2;
  686. _context->monthValue(
  687. ) | rpl::filter([=] {
  688. return !_context->selectionMode();
  689. }) | rpl::start_with_next([=](QDate date) {
  690. setTextFromMonth(date);
  691. }, lifetime());
  692. _context->selectionUpdates(
  693. ) | rpl::start_with_next([=] {
  694. if (!_context->selectionMode()) {
  695. setTextFromMonth(_context->month());
  696. } else if (!_context->selectedMin()) {
  697. setText(tr::lng_calendar_select_days(tr::now));
  698. } else {
  699. setText(tr::lng_calendar_days(
  700. tr::now,
  701. lt_count,
  702. (1 + *_context->selectedMax() - *_context->selectedMin())));
  703. }
  704. }, lifetime());
  705. }
  706. void CalendarBox::Title::setTextFromMonth(QDate month) {
  707. setText(langMonthOfYearFull(month.month(), month.year()));
  708. }
  709. void CalendarBox::Title::setText(QString text) {
  710. _text = std::move(text);
  711. _textWidth = st::calendarTitleFont->width(_text);
  712. update();
  713. }
  714. void CalendarBox::Title::paintEvent(QPaintEvent *e) {
  715. Painter p(this);
  716. const auto clip = e->rect();
  717. p.setFont(st::calendarTitleFont);
  718. p.setPen(_styleColors.titleTextColor);
  719. p.drawTextLeft(
  720. _textLeft,
  721. (st::calendarTitleHeight - st::calendarTitleFont->height) / 2,
  722. width(),
  723. _text,
  724. _textWidth);
  725. paintDayNames(p, clip);
  726. }
  727. void CalendarBox::Title::paintDayNames(Painter &p, QRect clip) {
  728. p.setFont(st::calendarDaysFont);
  729. p.setPen(st::calendarDaysFg);
  730. auto y = st::calendarTitleHeight + _st.padding.top();
  731. auto x = _st.padding.left();
  732. if (!myrtlrect(x, y, _st.cellSize.width() * kDaysInWeek, _st.daysHeight).intersects(clip)) {
  733. return;
  734. }
  735. const auto from = _context->firstDayShift();
  736. const auto to = from + kDaysInWeek;
  737. for (auto i = from; i != to; ++i, x += _st.cellSize.width()) {
  738. auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.daysHeight);
  739. if (!rect.intersects(clip)) {
  740. continue;
  741. }
  742. p.drawText(rect, langDayOfWeek((i % 7) + 1), style::al_top);
  743. }
  744. }
  745. CalendarBox::CalendarBox(QWidget*, CalendarBoxArgs &&args)
  746. : _st(args.st)
  747. , _styleColors(args.stColors)
  748. , _context(
  749. std::make_unique<Context>(args.month.value(), args.highlighted.value()))
  750. , _scroll(std::make_unique<ScrollArea>(this, st::calendarScroll))
  751. , _inner(_scroll->setOwnedWidget(object_ptr<Inner>(
  752. this,
  753. _context.get(),
  754. _st,
  755. _styleColors)))
  756. , _title(this, _context.get(), _st, _styleColors)
  757. , _previous(this, _styleColors.iconButtonPrevious)
  758. , _next(this, _styleColors.iconButtonNext)
  759. , _callback(std::move(args.callback.value()))
  760. , _finalize(std::move(args.finalize))
  761. , _jumpTimer([=] { jump(_jumpButton); })
  762. , _selectionChanged(std::move(args.selectionChanged)) {
  763. _title->setAttribute(Qt::WA_TransparentForMouseEvents);
  764. _context->setAllowsSelection(args.allowsSelection);
  765. _context->setMinDate(args.minDate);
  766. _context->setMaxDate(args.maxDate);
  767. _scroll->scrolls(
  768. ) | rpl::filter([=] {
  769. return _watchScroll;
  770. }) | rpl::start_with_next([=] {
  771. processScroll();
  772. }, lifetime());
  773. const auto setupJumps = [&](
  774. not_null<IconButton*> button,
  775. not_null<bool*> enabled) {
  776. button->events(
  777. ) | rpl::filter([=] {
  778. return *enabled;
  779. }) | rpl::start_with_next([=](not_null<QEvent*> e) {
  780. const auto type = e->type();
  781. if (type == QEvent::MouseMove
  782. && !(static_cast<QMouseEvent*>(e.get())->buttons()
  783. & Qt::LeftButton)) {
  784. showJumpTooltip(button);
  785. } else if (type == QEvent::Leave) {
  786. Ui::Tooltip::Hide();
  787. } else if (type == QEvent::MouseButtonPress
  788. && (static_cast<QMouseEvent*>(e.get())->button()
  789. == Qt::LeftButton)) {
  790. jumpAfterDelay(button);
  791. } else if (type == QEvent::MouseButtonRelease
  792. && (static_cast<QMouseEvent*>(e.get())->button()
  793. == Qt::LeftButton)) {
  794. _jumpTimer.cancel();
  795. }
  796. }, lifetime());
  797. };
  798. setupJumps(_previous.data(), &_previousEnabled);
  799. setupJumps(_next.data(), &_nextEnabled);
  800. _context->selectionUpdates(
  801. ) | rpl::start_with_next([=] {
  802. if (!_context->selectionMode()) {
  803. _floatingDate = nullptr;
  804. } else if (!_floatingDate) {
  805. _floatingDate = std::make_unique<FloatingDate>(
  806. this,
  807. _context.get());
  808. rpl::combine(
  809. _scroll->geometryValue(),
  810. _floatingDate->widthValue()
  811. ) | rpl::start_with_next([=](QRect scroll, int width) {
  812. const auto shift = _st.daysHeight
  813. - _st.padding.top()
  814. - st::calendarDaysFont->height;
  815. _floatingDate->move(
  816. scroll.x() + (scroll.width() - width) / 2,
  817. scroll.y() - shift);
  818. }, _floatingDate->lifetime());
  819. }
  820. }, lifetime());
  821. }
  822. CalendarBox::~CalendarBox() = default;
  823. void CalendarBox::toggleSelectionMode(bool enabled) {
  824. _context->toggleSelectionMode(enabled);
  825. }
  826. QDate CalendarBox::selectedFirstDate() const {
  827. const auto min = _context->selectedMin();
  828. return min.has_value() ? _context->dateFromIndex(*min) : QDate();
  829. }
  830. QDate CalendarBox::selectedLastDate() const {
  831. const auto max = _context->selectedMax();
  832. return max.has_value() ? _context->dateFromIndex(*max) : QDate();
  833. }
  834. void CalendarBox::showJumpTooltip(not_null<IconButton*> button) {
  835. _tooltipButton = button;
  836. Ui::Tooltip::Show(kTooltipDelay, this);
  837. }
  838. void CalendarBox::jumpAfterDelay(not_null<IconButton*> button) {
  839. _jumpButton = button;
  840. _jumpTimer.callOnce(kJumpDelay);
  841. Ui::Tooltip::Hide();
  842. }
  843. void CalendarBox::jump(QPointer<IconButton> button) {
  844. const auto jumpToIndex = [&](int index) {
  845. _watchScroll = false;
  846. _context->showMonth(_context->dateFromIndex(index));
  847. setExactScroll();
  848. };
  849. if (button == _previous.data() && _previousEnabled) {
  850. jumpToIndex(_context->minDayIndex());
  851. } else if (button == _next.data() && _nextEnabled) {
  852. jumpToIndex(_context->maxDayIndex());
  853. }
  854. _jumpButton = nullptr;
  855. _jumpTimer.cancel();
  856. }
  857. void CalendarBox::prepare() {
  858. _previous->setClickedCallback([=] { goPreviousMonth(); });
  859. _next->setClickedCallback([=] { goNextMonth(); });
  860. _inner->setDateChosenCallback(std::move(_callback));
  861. _context->monthValue(
  862. ) | rpl::start_with_next([=](QDate month) {
  863. monthChanged(month);
  864. }, lifetime());
  865. setExactScroll();
  866. _context->selectionUpdates(
  867. ) | rpl::start_with_next([=] {
  868. _selectionMode = _context->selectionMode();
  869. if (_selectionChanged) {
  870. const auto count = !_selectionMode
  871. ? std::optional<int>()
  872. : !_context->selectedMin()
  873. ? 0
  874. : (1 + *_context->selectedMax() - *_context->selectedMin());
  875. _selectionChanged(this, count);
  876. }
  877. if (!_selectionMode) {
  878. clearButtons();
  879. createButtons();
  880. }
  881. }, lifetime());
  882. createButtons();
  883. if (_finalize) {
  884. _finalize(this);
  885. }
  886. }
  887. bool CalendarBox::isPreviousEnabled() const {
  888. return (_context->minDayIndex() < 0);
  889. }
  890. bool CalendarBox::isNextEnabled() const {
  891. return (_context->maxDayIndex() >= _context->daysCount());
  892. }
  893. void CalendarBox::goPreviousMonth() {
  894. if (isPreviousEnabled()) {
  895. _watchScroll = false;
  896. _context->skipMonth(-1);
  897. setExactScroll();
  898. }
  899. }
  900. void CalendarBox::goNextMonth() {
  901. if (isNextEnabled()) {
  902. _watchScroll = false;
  903. _context->skipMonth(1);
  904. setExactScroll();
  905. }
  906. }
  907. void CalendarBox::setExactScroll() {
  908. const auto top = _st.padding.top()
  909. + (_context->daysShift() / kDaysInWeek) * _st.cellSize.height();
  910. _scroll->scrollToY(top);
  911. _watchScroll = true;
  912. }
  913. void CalendarBox::processScroll() {
  914. const auto wasTop = _scroll->scrollTop();
  915. const auto wasShift = _context->daysShift();
  916. const auto point = _scroll->rect().center() + QPoint(0, wasTop);
  917. const auto row = (point.y() - _st.padding.top()) / _st.cellSize.height();
  918. const auto col = (point.x() - _st.padding.left()) / _st.cellSize.width();
  919. const auto index = row * kDaysInWeek + col;
  920. const auto date = _context->dateFromIndex(index - wasShift);
  921. if (_context->showsMonthOf(date)) {
  922. return;
  923. }
  924. const auto wasFirst = _context->dateFromIndex(-wasShift);
  925. const auto month = QDate(date.year(), date.month(), 1);
  926. _watchScroll = false;
  927. _context->showMonth(month);
  928. const auto nowShift = _context->daysShift();
  929. const auto nowFirst = _context->dateFromIndex(-nowShift);
  930. const auto delta = nowFirst.daysTo(wasFirst) / kDaysInWeek;
  931. _scroll->scrollToY(wasTop + delta * _st.cellSize.height());
  932. _watchScroll = true;
  933. }
  934. void CalendarBox::createButtons() {
  935. if (!_context->allowsSelection()) {
  936. addButton(tr::lng_close(), [=] { closeBox(); });
  937. } else if (!_context->selectionMode()) {
  938. addButton(tr::lng_close(), [=] { closeBox(); });
  939. addLeftButton(tr::lng_calendar_select_days(), [=] {
  940. _context->toggleSelectionMode(true);
  941. });
  942. } else {
  943. addButton(tr::lng_cancel(), [=] {
  944. _context->toggleSelectionMode(false);
  945. });
  946. }
  947. }
  948. QString CalendarBox::tooltipText() const {
  949. if (_tooltipButton == _previous.data()) {
  950. return tr::lng_calendar_start_tip(tr::now);
  951. } else if (_tooltipButton == _next.data()) {
  952. return tr::lng_calendar_end_tip(tr::now);
  953. }
  954. return QString();
  955. }
  956. QPoint CalendarBox::tooltipPos() const {
  957. return QCursor::pos();
  958. }
  959. bool CalendarBox::tooltipWindowActive() const {
  960. return window()->isActiveWindow();
  961. }
  962. void CalendarBox::monthChanged(QDate month) {
  963. setDimensions(
  964. _st.width,
  965. st::calendarTitleHeight + _st.daysHeight + _inner->countMaxHeight());
  966. _previousEnabled = isPreviousEnabled();
  967. _previous->setIconOverride(_previousEnabled
  968. ? nullptr
  969. : &_styleColors.iconButtonPreviousDisabled);
  970. _previous->setRippleColorOverride(_previousEnabled
  971. ? nullptr
  972. : &_styleColors.iconButtonRippleColorDisabled);
  973. _previous->setPointerCursor(_previousEnabled);
  974. if (!_previousEnabled) {
  975. _previous->clearState();
  976. }
  977. _nextEnabled = isNextEnabled();
  978. _next->setIconOverride(_nextEnabled
  979. ? nullptr
  980. : &_styleColors.iconButtonNextDisabled);
  981. _next->setRippleColorOverride(_nextEnabled
  982. ? nullptr
  983. : &_styleColors.iconButtonRippleColorDisabled);
  984. _next->setPointerCursor(_nextEnabled);
  985. if (!_nextEnabled) {
  986. _next->clearState();
  987. }
  988. }
  989. void CalendarBox::resizeEvent(QResizeEvent *e) {
  990. const auto dayWidth = st::calendarDaysFont->width(langDayOfWeek(7));
  991. const auto skip = _st.padding.left()
  992. + _st.cellSize.width() * (kDaysInWeek - 1)
  993. + (_st.cellSize.width() - dayWidth) / 2
  994. + dayWidth;
  995. const auto right = width() - skip;
  996. const auto shift = _next->width()
  997. - (_next->width() - st::calendarPrevious.icon.width()) / 2
  998. - st::calendarPrevious.icon.width();
  999. _next->moveToRight(right - shift, 0);
  1000. _previous->moveToRight(right - shift + _next->width(), 0);
  1001. const auto title = st::calendarTitleHeight + _st.daysHeight;
  1002. _title->setGeometryToLeft(0, 0, width(), title);
  1003. _scroll->setGeometryToLeft(0, title, width(), height() - title);
  1004. BoxContent::resizeEvent(e);
  1005. }
  1006. void CalendarBox::keyPressEvent(QKeyEvent *e) {
  1007. if (e->key() == Qt::Key_Escape) {
  1008. if (_context->selectionMode()) {
  1009. _context->toggleSelectionMode(false);
  1010. } else {
  1011. e->ignore();
  1012. }
  1013. } else if (e->key() == Qt::Key_Home) {
  1014. jump(_previous.data());
  1015. } else if (e->key() == Qt::Key_End) {
  1016. jump(_next.data());
  1017. } else if (e->key() == Qt::Key_Left
  1018. || e->key() == Qt::Key_Up
  1019. || e->key() == Qt::Key_PageUp) {
  1020. goPreviousMonth();
  1021. } else if (e->key() == Qt::Key_Right
  1022. || e->key() == Qt::Key_Down
  1023. || e->key() == Qt::Key_PageDown) {
  1024. goNextMonth();
  1025. }
  1026. }
  1027. } // namespace Ui