Typica is a free program for professional coffee roasters. https://typica.us
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

daterangeselector.w 18KB

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