Typica is a free program for professional coffee roasters. https://typica.us
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

daterangeselector.w 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. @** A Widget for Selecting Date Ranges.
  2. \noindent Many of the reports in Typica operate over a range of dates. In these
  3. cases it should generally be possible to set that range to any arbitrary start
  4. or end, however there are some ranges that are commonly useful where it may be
  5. convenient to provide easy access to that range. While Qt provides a widget
  6. for selecting a single date, it does not provide a widget that allows two dates
  7. to be conveniently selected. One approach which Typica has previously taken is
  8. to simply use two |QDateEdit| widgets. This works, however validation that the
  9. range is valid must then be performed in every report that uses such an
  10. approach. Another down side to this is that changing either side of the date
  11. range is either going to result in a database query to obtain results in the
  12. new range or another button must be introduced to make setting a new range
  13. explicit. One typically wants to adjust both sides of the range at the same
  14. time and only have one trip to the database for the new data and increasing the
  15. number of controls required for each filter quickly creates a mess.
  16. The solution to this is the introduction of a new composite widget for
  17. selecting date ranges. The main widget consists of two parts. First there is a
  18. |QComboBox| which contains many common date ranges. A |QToolButton| is also
  19. provided for convenient one click access to the Custom range. Whether selected
  20. from the |QComboBox| or the |QToolButton|, selecting Custom creates a new pop
  21. up widget containing two |QCalendarWidget|s and a button to explicitly set the
  22. range. This button will not be available unless the selected ending date is not
  23. before the selected starting date.
  24. As the common use for the selected date is database operations, convenient
  25. access to the ISO 8601 string representation of these dates is provided.
  26. @(daterangeselector.h@>=
  27. #include <QComboBox>
  28. #include <QPushButton>
  29. #include <QCalendarWidget>
  30. #ifndef TypicaDateRangeSelectorHeader
  31. #define TypicaDateRangeSelectorHeader
  32. @<CustomDateRangePopup declaration@>@;
  33. class DateRangeSelector : public QWidget
  34. {
  35. @[Q_OBJECT@]@;
  36. Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex)@/
  37. public:@/
  38. DateRangeSelector(QWidget *parent = NULL);
  39. void setCustomRange(QVariant range);
  40. Q_INVOKABLE QVariant currentRange();
  41. int currentIndex();@/
  42. @[public slots@]:@/
  43. void setCurrentIndex(int index);
  44. void setLifetimeRange(QString startDate, QString endDate);
  45. void removeIndex(int index);@/
  46. @[signals@]:@/
  47. void rangeUpdated(QVariant);
  48. @[private slots@]:@/
  49. void toggleCustom();
  50. void popupHidden();
  51. void updateRange(int index);@/
  52. private:@/
  53. QComboBox *quickSelector;
  54. CustomDateRangePopup *customRangeSelector;
  55. int lastIndex;
  56. };
  57. #endif
  58. @ Implementation details are in a different file.
  59. @(daterangeselector.cpp@>=
  60. #include <QCalendarWidget>
  61. #include <QPushButton>
  62. #include <QBoxLayout>
  63. #include <QLabel>
  64. #include <QToolButton>
  65. #include <QApplication>
  66. #include <QDesktopWidget>
  67. #include "daterangeselector.h"
  68. @<CustomDateRangePopup implementation@>
  69. @<DateRangeSelector implementation@>
  70. #if __APPLE__
  71. #include "moc_daterangeselector.cpp"
  72. #endif
  73. @ The custom range pop up is represented as a separate class which is not to be
  74. instantiated except by |DateRangeSelector|.
  75. @<CustomDateRangePopup declaration@>=
  76. class CustomDateRangePopup : public QWidget
  77. {
  78. @[Q_OBJECT@]@;
  79. public:@/
  80. CustomDateRangePopup(QWidget *parent = NULL);@/
  81. @[public slots@]:@/
  82. void applyRange();@/
  83. @[signals@]:@/
  84. void hidingPopup();@/
  85. protected:@/
  86. virtual void hideEvent(QHideEvent *event);@/
  87. @[private slots@]:@/
  88. void validateRange();@/
  89. private:@/
  90. QCalendarWidget *startDateSelector;
  91. QCalendarWidget *endDateSelector;
  92. QPushButton *applyButton;
  93. };
  94. @ The pop up constructor is responsible for laying out the component widgets,
  95. setting the dates selected in each calendar to match the currently selected
  96. range, and connecting the appropriate signal handlers.
  97. @<CustomDateRangePopup implementation@>=
  98. CustomDateRangePopup::CustomDateRangePopup(QWidget *parent) :
  99. QWidget(parent, Qt::Popup), startDateSelector(new QCalendarWidget),
  100. endDateSelector(new QCalendarWidget), applyButton(new QPushButton(tr("Apply")))
  101. {
  102. setAttribute(Qt::WA_WindowPropagation);
  103. QVBoxLayout *outerLayout = new QVBoxLayout;
  104. QHBoxLayout *calendarsLayout = new QHBoxLayout;
  105. QVBoxLayout *startDateLayout = new QVBoxLayout;
  106. QVBoxLayout *endDateLayout = new QVBoxLayout;
  107. QHBoxLayout *buttonLayout = new QHBoxLayout;
  108. QLabel *startDateLabel = new QLabel(tr("From"));
  109. QLabel *endDateLabel = new QLabel(tr("To"));
  110. startDateSelector->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);
  111. endDateSelector->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);
  112. DateRangeSelector *selector = qobject_cast<DateRangeSelector *>(parent);
  113. if(parent) {
  114. QStringList range = selector->currentRange().toStringList();
  115. startDateSelector->setSelectedDate(QDate::fromString(range.first(), Qt::ISODate));
  116. endDateSelector->setSelectedDate(QDate::fromString(range.last(), Qt::ISODate));
  117. }
  118. connect(startDateSelector, SIGNAL(selectionChanged()), this, SLOT(validateRange()));
  119. connect(endDateSelector, SIGNAL(selectionChanged()), this, SLOT(validateRange()));
  120. startDateLayout->addWidget(startDateLabel);
  121. startDateLayout->addWidget(startDateSelector);
  122. endDateLayout->addWidget(endDateLabel);
  123. endDateLayout->addWidget(endDateSelector);
  124. connect(applyButton, SIGNAL(clicked()), this, SLOT(applyRange()));
  125. buttonLayout->addStretch();
  126. buttonLayout->addWidget(applyButton);
  127. calendarsLayout->addLayout(startDateLayout);
  128. calendarsLayout->addLayout(endDateLayout);
  129. outerLayout->addLayout(calendarsLayout);
  130. outerLayout->addLayout(buttonLayout);
  131. setLayout(outerLayout);
  132. }
  133. @ The pop up can be hidden in two ways. Clicking anywhere outside of the widget
  134. will hide the pop up. Clicking the Apply button will also hide the pop up. In
  135. the former case, we must inform the parent widget that it is fine to destroy
  136. the pop up widget, which we do by emitting a signal. Note that clicking outside
  137. of the widget will cause the |QHideEvent| to be posted automatically.
  138. @<CustomDateRangePopup implementation@>=
  139. void CustomDateRangePopup::hideEvent(QHideEvent *)
  140. {
  141. emit hidingPopup();
  142. }
  143. @ Clicking the Apply button requires setting the Custom date range to the
  144. currently selected range and then hiding the pop up manually.
  145. @<CustomDateRangePopup implementation@>=
  146. void CustomDateRangePopup::applyRange()
  147. {
  148. DateRangeSelector *selector = qobject_cast<DateRangeSelector *>(parentWidget());
  149. if(selector)
  150. {
  151. selector->setCustomRange(QVariant(QStringList() <<
  152. startDateSelector->selectedDate().toString(Qt::ISODate) <<
  153. endDateSelector->selectedDate().toString(Qt::ISODate)));
  154. }
  155. hide();
  156. }
  157. @ The Apply button is enabled or disabled depending on if the currently
  158. selected dates form a valid range in which the end date does not occur before
  159. the start date.
  160. @<CustomDateRangePopup implementation@>=
  161. void CustomDateRangePopup::validateRange()
  162. {
  163. if(startDateSelector->selectedDate() > endDateSelector->selectedDate())
  164. {
  165. applyButton->setEnabled(false);
  166. }
  167. else
  168. {
  169. applyButton->setEnabled(true);
  170. }
  171. }
  172. @ The |DateRangeSelector| constructor is responsible for setting up the layout
  173. of the |QComboBox| and the |QToolButton|, adding appropriate items to the
  174. |QComboBox|, and connecting the signals required to handle the pop up
  175. correctly.
  176. @<DateRangeSelector implementation@>=
  177. DateRangeSelector::DateRangeSelector(QWidget *parent) :
  178. QWidget(parent), quickSelector(new QComboBox(this)),
  179. customRangeSelector(NULL), lastIndex(0)
  180. {
  181. connect(quickSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(updateRange(int)));
  182. QDate currentDate = QDate::currentDate();
  183. QHBoxLayout *layout = new QHBoxLayout;
  184. @<Set common date ranges to quick selector@>@;
  185. QToolButton *customButton = new QToolButton;
  186. customButton->setIcon(QIcon::fromTheme("office-calendar",
  187. QIcon(":/resources/icons/tango/scalable/apps/office-calendar.svg")));
  188. layout->addWidget(quickSelector);
  189. layout->addWidget(customButton);
  190. setLayout(layout);
  191. connect(customButton, SIGNAL(clicked()), this, SLOT(toggleCustom()));
  192. }
  193. @ The |QComboBox| provides a mechanism for associating additional data with an
  194. item. Several possible representations were considered, but what was ultimately
  195. selected was a |QVariant| containing a |QStringList| in which the first entry
  196. in the list is the starting date of the range and the last entry in the list is
  197. the ending date of the range. Note that the list may contain only one item in
  198. cases where the range only covers a single date, however one should not assume
  199. that a range covering a single date will only have a single list entry.
  200. @<Set common date ranges to quick selector@>=
  201. quickSelector->addItem("Yesterday", QVariant(QStringList() <<
  202. currentDate.addDays(-1).toString(Qt::ISODate)));
  203. quickSelector->addItem("Today", QVariant(QStringList() <<
  204. currentDate.toString(Qt::ISODate)));
  205. quickSelector->insertSeparator(quickSelector->count());
  206. quickSelector->addItem("This Week", QVariant(QStringList() <<
  207. (currentDate.dayOfWeek() % 7 ?
  208. currentDate.addDays(-currentDate.dayOfWeek()).toString(Qt::ISODate) :
  209. currentDate.toString(Qt::ISODate)) <<
  210. currentDate.addDays(6 - (currentDate.dayOfWeek() % 7)).toString(Qt::ISODate)));
  211. quickSelector->addItem("This Week to Date", currentDate.dayOfWeek() % 7 ?
  212. QVariant(QStringList() <<
  213. currentDate.addDays(-currentDate.dayOfWeek()).toString(Qt::ISODate) <<
  214. currentDate.toString(Qt::ISODate)) :
  215. QVariant(QStringList() << currentDate.toString(Qt::ISODate)));
  216. quickSelector->addItem("Last Week", QVariant(QStringList() <<
  217. currentDate.addDays(-(currentDate.dayOfWeek() % 7) - 7).toString(Qt::ISODate) <<
  218. currentDate.addDays(-(currentDate.dayOfWeek() % 7) - 1).toString(Qt::ISODate)));
  219. quickSelector->addItem("Last 7 Days", QVariant(QStringList() <<
  220. currentDate.addDays(-6).toString(Qt::ISODate) <<
  221. currentDate.toString(Qt::ISODate)));
  222. quickSelector->insertSeparator(quickSelector->count());
  223. quickSelector->addItem("This Month", QVariant(QStringList() <<
  224. QDate(currentDate.year(), currentDate.month(), 1).toString(Qt::ISODate) <<
  225. QDate(currentDate.year(), currentDate.month(),
  226. currentDate.daysInMonth()).toString(Qt::ISODate)));
  227. quickSelector->addItem("This Month to Date", (currentDate.day() == 1 ?
  228. (QVariant(QStringList() << currentDate.toString(Qt::ISODate))) :
  229. (QVariant(QStringList() <<
  230. QDate(currentDate.year(), currentDate.month(), 1).toString(Qt::ISODate) <<
  231. currentDate.toString(Qt::ISODate)))));
  232. quickSelector->addItem("Last Four Weeks", QVariant(QStringList() <<
  233. currentDate.addDays(-27).toString(Qt::ISODate) <<
  234. currentDate.toString(Qt::ISODate)));
  235. quickSelector->addItem("Last 30 Days", QVariant(QStringList() <<
  236. currentDate.addDays(-29).toString(Qt::ISODate) <<
  237. currentDate.toString(Qt::ISODate)));
  238. quickSelector->insertSeparator(quickSelector->count());
  239. quickSelector->addItem("This Quarter", QVariant(QStringList() <<
  240. QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3), 1).toString(Qt::ISODate) <<
  241. (currentDate.month() > 9 ?
  242. QDate(currentDate.year(), 12, 31).toString(Qt::ISODate) :
  243. QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3) + 3, 1).addDays(-1).toString(Qt::ISODate))));
  244. quickSelector->addItem("This Quarter to Date",
  245. (currentDate.day() == 1 && (currentDate.month() - 1) % 3 == 0) ?
  246. QVariant(QStringList() << currentDate.toString(Qt::ISODate)) :
  247. QVariant(QStringList() <<
  248. QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3), 1).toString(Qt::ISODate) <<
  249. currentDate.toString(Qt::ISODate)));
  250. quickSelector->addItem("Last Quarter", currentDate.month() < 4 ?
  251. QVariant(QStringList() <<
  252. QDate(currentDate.year() - 1, 10, 1).toString(Qt::ISODate) <<
  253. QDate(currentDate.year() - 1, 12, 31).toString(Qt::ISODate)) :
  254. QVariant(QStringList() <<
  255. QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3) - 3, 1).toString(Qt::ISODate) <<
  256. QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3), 1).addDays(-1).toString(Qt::ISODate)));
  257. quickSelector->addItem("Last 90 Days", QVariant(QStringList() <<
  258. currentDate.addDays(-89).toString(Qt::ISODate) <<
  259. currentDate.toString(Qt::ISODate)));
  260. quickSelector->insertSeparator(quickSelector->count());
  261. quickSelector->addItem("This Year", QVariant(QStringList() <<
  262. QDate(currentDate.year(), 1, 1).toString(Qt::ISODate) <<
  263. QDate(currentDate.year(), 12, 31).toString(Qt::ISODate)));
  264. quickSelector->addItem("This Year to Date", (currentDate.dayOfYear() == 1) ?
  265. QVariant(QStringList() << currentDate.toString(Qt::ISODate)) :
  266. QVariant(QStringList() << QDate(currentDate.year(), 1, 1).toString(Qt::ISODate) <<
  267. currentDate.toString(Qt::ISODate)));
  268. quickSelector->addItem("Last Year", QVariant(QStringList() <<
  269. QDate(currentDate.year() - 1, 1, 1).toString(Qt::ISODate) <<
  270. QDate(currentDate.year() - 1, 12, 31).toString(Qt::ISODate)));
  271. quickSelector->addItem("Last 365 Days", QVariant(QStringList() <<
  272. currentDate.addDays(-364).toString(Qt::ISODate) <<
  273. currentDate.toString(Qt::ISODate)));
  274. quickSelector->insertSeparator(quickSelector->count());
  275. quickSelector->addItem("Lifetime");
  276. quickSelector->addItem("Custom");
  277. @ Special handling of the Custom range is required because it is possible to
  278. select this from the |QComboBox| and then not set a range. This should result
  279. in the selection changing back to the most recent valid selection. Creating the
  280. pop up in this way is handled in |updateRange()|.
  281. @<DateRangeSelector implementation@>=
  282. void DateRangeSelector::updateRange(int index)
  283. {
  284. if(index != lastIndex && index == quickSelector->count() - 1)
  285. {
  286. toggleCustom();
  287. }
  288. else
  289. {
  290. lastIndex = index;
  291. emit rangeUpdated(quickSelector->itemData(quickSelector->currentIndex()));
  292. }
  293. }
  294. @ Resetting the range to the most recent valid selection is handled in
  295. |popupHidden()|.
  296. @<DateRangeSelector implementation@>=
  297. void DateRangeSelector::popupHidden()
  298. {
  299. customRangeSelector->deleteLater();
  300. customRangeSelector = NULL;
  301. quickSelector->setCurrentIndex(lastIndex);
  302. }
  303. @ If Custom is set to a new valid range, |lastIndex| will have been set to
  304. point to the appropriate item by a call to |setCustomRange()|.
  305. @<DateRangeSelector implementation@>=
  306. void DateRangeSelector::setCustomRange(QVariant range)
  307. {
  308. quickSelector->setItemData(quickSelector->count() - 1, range);
  309. emit rangeUpdated(range);
  310. lastIndex = quickSelector->count() - 1;
  311. quickSelector->setCurrentIndex(lastIndex);
  312. }
  313. @ When creating the pop up, it should ideally be placed such that the left of
  314. the pop up is aligned with the left of the widget that is normally shown and
  315. immediately under it, however if this would result in part of the pop up not
  316. fitting on the same screen, it should be moved to make a best effort at full
  317. visibility.
  318. @<DateRangeSelector implementation@>=
  319. void DateRangeSelector::toggleCustom()
  320. {
  321. if(!customRangeSelector) {
  322. customRangeSelector = new CustomDateRangePopup(this);
  323. QPoint pos = rect().bottomLeft();
  324. QPoint pos2 = rect().topLeft();
  325. pos = mapToGlobal(pos);
  326. pos2 = mapToGlobal(pos2);
  327. QSize size = customRangeSelector->sizeHint();
  328. QRect screen = QApplication::desktop()->availableGeometry(pos);
  329. if(pos.x()+size.width() > screen.right()) {
  330. pos.setX(screen.right()-size.width());
  331. }
  332. pos.setX(qMax(pos.x(), screen.left()));
  333. if(pos.y() + size.height() > screen.bottom()) {
  334. pos.setY(pos2.y() - size.height());
  335. } else if (pos.y() < screen.top()){
  336. pos.setY(screen.top());
  337. }
  338. if(pos.y() < screen.top()) {
  339. pos.setY(screen.top());
  340. }
  341. if(pos.y()+size.height() > screen.bottom()) {
  342. pos.setY(screen.bottom()-size.height());
  343. }
  344. customRangeSelector->move(pos);
  345. customRangeSelector->show();
  346. connect(customRangeSelector, SIGNAL(hidingPopup()),
  347. this, SLOT(popupHidden()));
  348. }
  349. else
  350. {
  351. customRangeSelector->close();
  352. customRangeSelector->deleteLater();
  353. customRangeSelector = NULL;
  354. }
  355. }
  356. @ While a signal is emitted when the selected range changes, it is frequently
  357. convenient to have a way to request the currently selected range at any time.
  358. @<DateRangeSelector implementation@>=
  359. QVariant DateRangeSelector::currentRange()
  360. {
  361. return quickSelector->itemData(lastIndex);
  362. }
  363. @ Methods are provided to get and set the current index of the combo box.
  364. @<DateRangeSelector implementation@>=
  365. void DateRangeSelector::setCurrentIndex(int index)
  366. {
  367. quickSelector->setCurrentIndex(index);
  368. }
  369. int DateRangeSelector::currentIndex()
  370. {
  371. return quickSelector->currentIndex();
  372. }
  373. @ The Lifetime range is handled somewhat differently from other ranges as there
  374. is no general way to know what that range should be without making unsafe
  375. assumptions. As such, reports are expected to remove the option, provide a
  376. sensible range for it, or handle this selection in a special case. The expected
  377. source of the lifetime date range is the result of a database query so a method
  378. is provided that accepts string representations of the dates. Note that this
  379. method must not be called if the Lifetime option is no longer the second to
  380. last option in the combo box.
  381. @<DateRangeSelector implementation@>=
  382. void DateRangeSelector::setLifetimeRange(QString startDate, QString endDate)
  383. {
  384. quickSelector->setItemData(quickSelector->count() - 2,
  385. QVariant(QStringList() << startDate << endDate));
  386. }
  387. @ The |removeIndex()| method is intended for removing the Lifetime option in
  388. cases where this is not supported. Use of this method is strongly discouraged.
  389. @<DateRangeSelector implementation@>=
  390. void DateRangeSelector::removeIndex(int index)
  391. {
  392. quickSelector->removeItem(index);
  393. }
  394. @ To use this new control in Typica, we should provide a way to create it from
  395. the XML description of a window.
  396. @<Additional box layout elements@>=
  397. else if(currentElement.tagName() == "daterange")
  398. {
  399. addDateRangeToLayout(currentElement, widgetStack, layoutStack);
  400. }
  401. @ The method for adding a date range selector to a layout is currently trivial.
  402. The |"id"| attribute is supported as usual, as is an |"initial"| attribute for
  403. setting the combo box index.
  404. @<Functions for scripting@>=
  405. void addDateRangeToLayout(QDomElement element, QStack<QWidget *> *,@|
  406. QStack<QLayout *> *layoutStack)
  407. {
  408. DateRangeSelector *widget = new DateRangeSelector;
  409. if(element.hasAttribute("id"))
  410. {
  411. widget->setObjectName(element.attribute("id"));
  412. }
  413. if(element.hasAttribute("initial"))
  414. {
  415. widget->setCurrentIndex(element.attribute("initial").toInt());
  416. }
  417. QBoxLayout *layout = qobject_cast<QBoxLayout *>(layoutStack->top());
  418. layout->addWidget(widget);
  419. }
  420. @ The prototype needs to be specified.
  421. @<Function prototypes for scripting@>=
  422. void addDateRangeToLayout(QDomElement element,
  423. QStack<QWidget *> *widgetStack,
  424. QStack<QLayout *> *layoutStack);
  425. @ Our header is also required.
  426. @<Header files to include@>=
  427. #include "daterangeselector.h"