123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- @** A Widget for Selecting Date Ranges.
-
- \noindent Many of the reports in Typica operate over a range of dates. In these
- cases it should generally be possible to set that range to any arbitrary start
- or end, however there are some ranges that are commonly useful where it may be
- convenient to provide easy access to that range. While Qt provides a widget
- for selecting a single date, it does not provide a widget that allows two dates
- to be conveniently selected. One approach which Typica has previously taken is
- to simply use two |QDateEdit| widgets. This works, however validation that the
- range is valid must then be performed in every report that uses such an
- approach. Another down side to this is that changing either side of the date
- range is either going to result in a database query to obtain results in the
- new range or another button must be introduced to make setting a new range
- explicit. One typically wants to adjust both sides of the range at the same
- time and only have one trip to the database for the new data and increasing the
- number of controls required for each filter quickly creates a mess.
-
- The solution to this is the introduction of a new composite widget for
- selecting date ranges. The main widget consists of two parts. First there is a
- |QComboBox| which contains many common date ranges. A |QToolButton| is also
- provided for convenient one click access to the Custom range. Whether selected
- from the |QComboBox| or the |QToolButton|, selecting Custom creates a new pop
- up widget containing two |QCalendarWidget|s and a button to explicitly set the
- range. This button will not be available unless the selected ending date is not
- before the selected starting date.
-
- As the common use for the selected date is database operations, convenient
- access to the ISO 8601 string representation of these dates is provided.
-
- @(daterangeselector.h@>=
-
- #include <QComboBox>
-
- #ifndef TypicaDateRangeSelectorHeader
- #define TypicaDateRangeSelectorHeader
-
- class CustomDateRangePopup;
-
- class DateRangeSelector : public QWidget
- {
- @[Q_OBJECT@]@;
- public:@/
- DateRangeSelector(QWidget *parent = NULL);
- void setCustomRange(QVariant range);
- Q_INVOKABLE QVariant currentRange();@/
- @[public slots@]:@/
- void setCurrentIndex(int index);
- void setLifetimeRange(QString startDate, QString endDate);
- void removeIndex(int index);@/
- @[signals@]:@/
- void rangeUpdated(QVariant);
- @[private slots@]:@/
- void toggleCustom();
- void popupHidden();
- void updateRange(int index);@/
- private:@/
- QComboBox *quickSelector;
- CustomDateRangePopup *customRangeSelector;
- int lastIndex;
- };
-
- #endif
-
- @ Implementation details are in a different file.
-
- @(daterangeselector.cpp@>=
- #include <QCalendarWidget>
- #include <QPushButton>
- #include <QBoxLayout>
- #include <QLabel>
- #include <QToolButton>
- #include <QApplication>
- #include <QDesktopWidget>
-
- #include "daterangeselector.h"
-
- @<CustomDateRangePopup declaration@>
- @<CustomDateRangePopup implementation@>
- @<DateRangeSelector implementation@>
-
- #include "moc_daterangeselector.cpp"
-
- @ The custom range pop up is represented as a separate class which is not to be
- instantiated except by |DateRangeSelector|.
-
- @<CustomDateRangePopup declaration@>=
- class CustomDateRangePopup : public QWidget
- {
- @[Q_OBJECT@]@;
- public:@/
- CustomDateRangePopup(QWidget *parent = NULL);@/
- @[public slots@]:@/
- void applyRange();@/
- @[signals@]:@/
- void hidingPopup();@/
- protected:@/
- virtual void hideEvent(QHideEvent *event);@/
- @[private slots@]:@/
- void validateRange();@/
- private:@/
- QCalendarWidget *startDateSelector;
- QCalendarWidget *endDateSelector;
- QPushButton *applyButton;
- };
-
- @ The pop up constructor is responsible for laying out the component widgets,
- setting the dates selected in each calendar to match the currently selected
- range, and connecting the appropriate signal handlers.
-
- @<CustomDateRangePopup implementation@>=
- CustomDateRangePopup::CustomDateRangePopup(QWidget *parent) :
- QWidget(parent, Qt::Popup), startDateSelector(new QCalendarWidget),
- endDateSelector(new QCalendarWidget), applyButton(new QPushButton(tr("Apply")))
- {
- setAttribute(Qt::WA_WindowPropagation);
-
- QVBoxLayout *outerLayout = new QVBoxLayout;
- QHBoxLayout *calendarsLayout = new QHBoxLayout;
- QVBoxLayout *startDateLayout = new QVBoxLayout;
- QVBoxLayout *endDateLayout = new QVBoxLayout;
- QHBoxLayout *buttonLayout = new QHBoxLayout;
- QLabel *startDateLabel = new QLabel(tr("From"));
- QLabel *endDateLabel = new QLabel(tr("To"));
- startDateSelector->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);
- endDateSelector->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);
- DateRangeSelector *selector = qobject_cast<DateRangeSelector *>(parent);
- if(parent) {
- QStringList range = selector->currentRange().toStringList();
- startDateSelector->setSelectedDate(QDate::fromString(range.first(), Qt::ISODate));
- endDateSelector->setSelectedDate(QDate::fromString(range.last(), Qt::ISODate));
- }
- connect(startDateSelector, SIGNAL(selectionChanged()), this, SLOT(validateRange()));
- connect(endDateSelector, SIGNAL(selectionChanged()), this, SLOT(validateRange()));
-
- startDateLayout->addWidget(startDateLabel);
- startDateLayout->addWidget(startDateSelector);
- endDateLayout->addWidget(endDateLabel);
- endDateLayout->addWidget(endDateSelector);
-
- connect(applyButton, SIGNAL(clicked()), this, SLOT(applyRange()));
-
- buttonLayout->addStretch();
- buttonLayout->addWidget(applyButton);
-
- calendarsLayout->addLayout(startDateLayout);
- calendarsLayout->addLayout(endDateLayout);
- outerLayout->addLayout(calendarsLayout);
- outerLayout->addLayout(buttonLayout);
- setLayout(outerLayout);
- }
-
- @ The pop up can be hidden in two ways. Clicking anywhere outside of the widget
- will hide the pop up. Clicking the Apply button will also hide the pop up. In
- the former case, we must inform the parent widget that it is fine to destroy
- the pop up widget, which we do by emitting a signal. Note that clicking outside
- of the widget will cause the |QHideEvent| to be posted automatically.
-
- @<CustomDateRangePopup implementation@>=
- void CustomDateRangePopup::hideEvent(QHideEvent *)
- {
- emit hidingPopup();
- }
-
- @ Clicking the Apply button requires setting the Custom date range to the
- currently selected range and then hiding the pop up manually.
-
- @<CustomDateRangePopup implementation@>=
- void CustomDateRangePopup::applyRange()
- {
- DateRangeSelector *selector = qobject_cast<DateRangeSelector *>(parentWidget());
- if(selector)
- {
- selector->setCustomRange(QVariant(QStringList() <<
- startDateSelector->selectedDate().toString(Qt::ISODate) <<
- endDateSelector->selectedDate().toString(Qt::ISODate)));
- }
- hide();
- }
-
- @ The Apply button is enabled or disabled depending on if the currently
- selected dates form a valid range in which the end date does not occur before
- the start date.
-
- @<CustomDateRangePopup implementation@>=
- void CustomDateRangePopup::validateRange()
- {
- if(startDateSelector->selectedDate() > endDateSelector->selectedDate())
- {
- applyButton->setEnabled(false);
- }
- else
- {
- applyButton->setEnabled(true);
- }
- }
-
- @ The |DateRangeSelector| constructor is responsible for setting up the layout
- of the |QComboBox| and the |QToolButton|, adding appropriate items to the
- |QComboBox|, and connecting the signals required to handle the pop up
- correctly.
-
- @<DateRangeSelector implementation@>=
- DateRangeSelector::DateRangeSelector(QWidget *parent) :
- QWidget(parent), quickSelector(new QComboBox(this)),
- customRangeSelector(NULL), lastIndex(0)
- {
- connect(quickSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(updateRange(int)));
-
- QDate currentDate = QDate::currentDate();
-
- QHBoxLayout *layout = new QHBoxLayout;
- @<Set common date ranges to quick selector@>@;
- QToolButton *customButton = new QToolButton;
- customButton->setIcon(QIcon::fromTheme("office-calendar",
- QIcon(":/resources/icons/tango/scalable/apps/office-calendar.svg")));
- layout->addWidget(quickSelector);
- layout->addWidget(customButton);
- setLayout(layout);
-
- connect(customButton, SIGNAL(clicked()), this, SLOT(toggleCustom()));
- }
-
- @ The |QComboBox| provides a mechanism for associating additional data with an
- item. Several possible representations were considered, but what was ultimately
- selected was a |QVariant| containing a |QStringList| in which the first entry
- in the list is the starting date of the range and the last entry in the list is
- the ending date of the range. Note that the list may contain only one item in
- cases where the range only covers a single date, however one should not assume
- that a range covering a single date will only have a single list entry.
-
- @<Set common date ranges to quick selector@>=
- quickSelector->addItem("Yesterday", QVariant(QStringList() <<
- currentDate.addDays(-1).toString(Qt::ISODate)));
- quickSelector->addItem("Today", QVariant(QStringList() <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->insertSeparator(quickSelector->count());
- quickSelector->addItem("This Week", QVariant(QStringList() <<
- (currentDate.dayOfWeek() % 7 ?
- currentDate.addDays(-currentDate.dayOfWeek()).toString(Qt::ISODate) :
- currentDate.toString(Qt::ISODate)) <<
- currentDate.addDays(6 - (currentDate.dayOfWeek() % 7)).toString(Qt::ISODate)));
- quickSelector->addItem("This Week to Date", currentDate.dayOfWeek() % 7 ?
- QVariant(QStringList() <<
- currentDate.addDays(-currentDate.dayOfWeek()).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)) :
- QVariant(QStringList() << currentDate.toString(Qt::ISODate)));
- quickSelector->addItem("Last Week", QVariant(QStringList() <<
- currentDate.addDays(-(currentDate.dayOfWeek() % 7) - 7).toString(Qt::ISODate) <<
- currentDate.addDays(-(currentDate.dayOfWeek() % 7) - 1).toString(Qt::ISODate)));
- quickSelector->addItem("Last 7 Days", QVariant(QStringList() <<
- currentDate.addDays(-6).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->insertSeparator(quickSelector->count());
- quickSelector->addItem("This Month", QVariant(QStringList() <<
- QDate(currentDate.year(), currentDate.month(), 1).toString(Qt::ISODate) <<
- QDate(currentDate.year(), currentDate.month(),
- currentDate.daysInMonth()).toString(Qt::ISODate)));
- quickSelector->addItem("This Month to Date", (currentDate.day() == 1 ?
- (QVariant(QStringList() << currentDate.toString(Qt::ISODate))) :
- (QVariant(QStringList() <<
- QDate(currentDate.year(), currentDate.month(), 1).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)))));
- quickSelector->addItem("Last Four Weeks", QVariant(QStringList() <<
- currentDate.addDays(-27).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->addItem("Last 30 Days", QVariant(QStringList() <<
- currentDate.addDays(-29).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->insertSeparator(quickSelector->count());
- quickSelector->addItem("This Quarter", QVariant(QStringList() <<
- QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3), 1).toString(Qt::ISODate) <<
- (currentDate.month() > 9 ?
- QDate(currentDate.year(), 12, 31).toString(Qt::ISODate) :
- QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3) + 3, 1).addDays(-1).toString(Qt::ISODate))));
- quickSelector->addItem("This Quarter to Date",
- (currentDate.day() == 1 && (currentDate.month() - 1) % 3 == 0) ?
- QVariant(QStringList() << currentDate.toString(Qt::ISODate)) :
- QVariant(QStringList() <<
- QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3), 1).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->addItem("Last Quarter", currentDate.month() < 4 ?
- QVariant(QStringList() <<
- QDate(currentDate.year() - 1, 10, 1).toString(Qt::ISODate) <<
- QDate(currentDate.year() - 1, 12, 31).toString(Qt::ISODate)) :
- QVariant(QStringList() <<
- QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3) - 3, 1).toString(Qt::ISODate) <<
- QDate(currentDate.year(), currentDate.month() - ((currentDate.month() - 1) % 3), 1).addDays(-1).toString(Qt::ISODate)));
- quickSelector->addItem("Last 90 Days", QVariant(QStringList() <<
- currentDate.addDays(-89).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->insertSeparator(quickSelector->count());
- quickSelector->addItem("This Year", QVariant(QStringList() <<
- QDate(currentDate.year(), 1, 1).toString(Qt::ISODate) <<
- QDate(currentDate.year(), 12, 31).toString(Qt::ISODate)));
- quickSelector->addItem("This Year to Date", (currentDate.dayOfYear() == 1) ?
- QVariant(QStringList() << currentDate.toString(Qt::ISODate)) :
- QVariant(QStringList() << QDate(currentDate.year(), 1, 1).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->addItem("Last Year", QVariant(QStringList() <<
- QDate(currentDate.year() - 1, 1, 1).toString(Qt::ISODate) <<
- QDate(currentDate.year() - 1, 12, 31).toString(Qt::ISODate)));
- quickSelector->addItem("Last 365 Days", QVariant(QStringList() <<
- currentDate.addDays(-364).toString(Qt::ISODate) <<
- currentDate.toString(Qt::ISODate)));
- quickSelector->insertSeparator(quickSelector->count());
- quickSelector->addItem("Lifetime");
- quickSelector->addItem("Custom");
-
- @ Special handling of the Custom range is required because it is possible to
- select this from the |QComboBox| and then not set a range. This should result
- in the selection changing back to the most recent valid selection. Creating the
- pop up in this way is handled in |updateRange()|.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::updateRange(int index)
- {
- if(index != lastIndex && index == quickSelector->count() - 1)
- {
- toggleCustom();
- }
- else
- {
- lastIndex = index;
- emit rangeUpdated(quickSelector->itemData(quickSelector->currentIndex()));
- }
- }
-
- @ Resetting the range to the most recent valid selection is handled in
- |popupHidden()|.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::popupHidden()
- {
- customRangeSelector->deleteLater();
- customRangeSelector = NULL;
- quickSelector->setCurrentIndex(lastIndex);
- }
-
- @ If Custom is set to a new valid range, |lastIndex| will have been set to
- point to the appropriate item by a call to |setCustomRange()|.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::setCustomRange(QVariant range)
- {
- quickSelector->setItemData(quickSelector->count() - 1, range);
- emit rangeUpdated(range);
- lastIndex = quickSelector->count() - 1;
- quickSelector->setCurrentIndex(lastIndex);
- }
-
- @ When creating the pop up, it should ideally be placed such that the left of
- the pop up is aligned with the left of the widget that is normally shown and
- immediately under it, however if this would result in part of the pop up not
- fitting on the same screen, it should be moved to make a best effort at full
- visibility.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::toggleCustom()
- {
- if(!customRangeSelector) {
- customRangeSelector = new CustomDateRangePopup(this);
- QPoint pos = rect().bottomLeft();
- QPoint pos2 = rect().topLeft();
- pos = mapToGlobal(pos);
- pos2 = mapToGlobal(pos2);
- QSize size = customRangeSelector->sizeHint();
- QRect screen = QApplication::desktop()->availableGeometry(pos);
- if(pos.x()+size.width() > screen.right()) {
- pos.setX(screen.right()-size.width());
- }
- pos.setX(qMax(pos.x(), screen.left()));
- if(pos.y() + size.height() > screen.bottom()) {
- pos.setY(pos2.y() - size.height());
- } else if (pos.y() < screen.top()){
- pos.setY(screen.top());
- }
- if(pos.y() < screen.top()) {
- pos.setY(screen.top());
- }
- if(pos.y()+size.height() > screen.bottom()) {
- pos.setY(screen.bottom()-size.height());
- }
- customRangeSelector->move(pos);
- customRangeSelector->show();
- connect(customRangeSelector, SIGNAL(hidingPopup()),
- this, SLOT(popupHidden()));
- }
- else
- {
- customRangeSelector->close();
- customRangeSelector->deleteLater();
- customRangeSelector = NULL;
- }
- }
-
- @ While a signal is emitted when the selected range changes, it is frequently
- convenient to have a way to request the currently selected range at any time.
-
- @<DateRangeSelector implementation@>=
- QVariant DateRangeSelector::currentRange()
- {
- return quickSelector->itemData(lastIndex);
- }
-
- @ Similarly, a method is provided to set the current index of the combo box.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::setCurrentIndex(int index)
- {
- quickSelector->setCurrentIndex(index);
- }
-
- @ The Lifetime range is handled somewhat differently from other ranges as there
- is no general way to know what that range should be without making unsafe
- assumptions. As such, reports are expected to remove the option, provide a
- sensible range for it, or handle this selection in a special case. The expected
- source of the lifetime date range is the result of a database query so a method
- is provided that accepts string representations of the dates. Note that this
- method must not be called if the Lifetime option is no longer the second to
- last option in the combo box.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::setLifetimeRange(QString startDate, QString endDate)
- {
- quickSelector->setItemData(quickSelector->count() - 2,
- QVariant(QStringList() << startDate << endDate));
- }
-
- @ The |removeIndex()| method is intended for removing the Lifetime option in
- cases where this is not supported. Use of this method is strongly discouraged.
-
- @<DateRangeSelector implementation@>=
- void DateRangeSelector::removeIndex(int index)
- {
- quickSelector->removeItem(index);
- }
-
- @ To use this new control in Typica, we should provide a way to create it from
- the XML description of a window.
-
- @<Additional box layout elements@>=
- else if(currentElement.tagName() == "daterange")
- {
- addDateRangeToLayout(currentElement, widgetStack, layoutStack);
- }
-
- @ The method for adding a date range selector to a layout is currently trivial.
- The |"id"| attribute is supported as usual, as is an |"initial"| attribute for
- setting the combo box index.
-
- @<Functions for scripting@>=
- void addDateRangeToLayout(QDomElement element, QStack<QWidget *> *,@|
- QStack<QLayout *> *layoutStack)
- {
- DateRangeSelector *widget = new DateRangeSelector;
- if(element.hasAttribute("id"))
- {
- widget->setObjectName(element.attribute("id"));
- }
- if(element.hasAttribute("initial"))
- {
- widget->setCurrentIndex(element.attribute("initial").toInt());
- }
- QBoxLayout *layout = qobject_cast<QBoxLayout *>(layoutStack->top());
- layout->addWidget(widget);
- }
-
- @ The prototype needs to be specified.
-
- @<Function prototypes for scripting@>=
- void addDateRangeToLayout(QDomElement element,
- QStack<QWidget *> *widgetStack,
- QStack<QLayout *> *layoutStack);
-
- @ Our header is also required.
-
- @<Header files to include@>=
- #include "daterangeselector.h"
|