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 18KB

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