@** 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 #include #include #ifndef TypicaDateRangeSelectorHeader #define TypicaDateRangeSelectorHeader @@; class DateRangeSelector : public QWidget { @[Q_OBJECT@]@; Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex)@/ public:@/ DateRangeSelector(QWidget *parent = NULL); void setCustomRange(QVariant range); Q_INVOKABLE QVariant currentRange(); int currentIndex();@/ @[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 #include #include #include #include #include #include #include "daterangeselector.h" @ @ #if __APPLE__ #include "moc_daterangeselector.cpp" #endif @ The custom range pop up is represented as a separate class which is not to be instantiated except by |DateRangeSelector|. @= 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::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(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. @= 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. @= void CustomDateRangePopup::applyRange() { DateRangeSelector *selector = qobject_cast(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. @= 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::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; @@; 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. @= 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()|. @= 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()|. @= 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()|. @= 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. @= 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. @= QVariant DateRangeSelector::currentRange() { return quickSelector->itemData(lastIndex); } @ Methods are provided to get and set the current index of the combo box. @= void DateRangeSelector::setCurrentIndex(int index) { quickSelector->setCurrentIndex(index); } int DateRangeSelector::currentIndex() { return quickSelector->currentIndex(); } @ 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. @= 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. @= 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. @= 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. @= void addDateRangeToLayout(QDomElement element, QStack *,@| QStack *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(layoutStack->top()); layout->addWidget(widget); } @ The prototype needs to be specified. @= void addDateRangeToLayout(QDomElement element, QStack *widgetStack, QStack *layoutStack); @ Our header is also required. @
= #include "daterangeselector.h"