Browse Source

Merge branch 'daterange' into development

Neal Wilson 10 years ago
parent
commit
291083dc86
8 changed files with 551 additions and 86 deletions
  1. 12
    17
      config/Reports/chart.xml
  2. 13
    16
      config/Reports/fypurchase.xml
  3. 18
    13
      config/Reports/invchange.xml
  4. 4
    2
      src/Typica.pro
  5. 478
    0
      src/daterangeselector.w
  6. 19
    38
      src/rate.w
  7. 4
    0
      src/resources.qrc
  8. 3
    0
      src/typica.w

+ 12
- 17
config/Reports/chart.xml View File

@@ -2,10 +2,7 @@
2 2
 	<reporttitle>Production:->Previous Year Production Comparison</reporttitle>
3 3
     <layout type="vertical">
4 4
         <layout type="horizontal">
5
-            <label>Start Date:</label>
6
-            <calendar id="startdate" />
7
-            <label>End Date:</label>
8
-            <calendar id="enddate" />
5
+			<daterange id="dates" initial="19" /><!-- Current Year to Date -->
9 6
             <label>Days to Average</label>
10 7
             <line validator="integer" id="days">7</line>
11 8
 			<label>Weight Unit:</label>
@@ -20,9 +17,8 @@
20 17
     <program>
21 18
         <![CDATA[
22 19
             this.windowTitle = "Typica - Previous Year Production Comparison";
23
-            var startDateField = findChildObject(this, 'startdate');
24
-            startDateField.setDate(startDateField.year(), 1, 1);
25
-            var endDateField = findChildObject(this, 'enddate');
20
+			var dateSelect = findChildObject(this, 'dates');
21
+			dateSelect.removeIndex(23); // Remove Lifetime range
26 22
             var view = findChildObject(this, 'report');
27 23
             var printMenu = findChildObject(this, 'print');
28 24
             printMenu.triggered.connect(function() {
@@ -79,10 +75,12 @@
79 75
                 output.writeStartElement("tbody");
80 76
                 var query = new QSqlQuery();
81 77
                 query.exec("START TRANSACTION");
82
-                var curStartDate = "'"+startDateField.year()+"-"+startDateField.month()+"-"+startDateField.day()+"'";
83
-                query.exec("SELECT "+curStartDate+"::date - interval '1 year', '"+endDateField.year()+"-"+endDateField.month()+"-"+endDateField.day()+"'::date - interval '1 year' + interval '1 day', '"+endDateField.year()+"-"+endDateField.month()+"-"+endDateField.day()+"'::date + interval '1 day'");
84
-                query.next();
85
-                var curEndDate = "'"+query.value(2)+"'";
78
+				var dateRange = dateSelect.currentRange();
79
+				var curStartDate = "'"+dateRange[0]+"'";
80
+				var curEndDate = "'"+dateRange[dateRange.length - 1]+"'";
81
+                query.exec("SELECT "+curStartDate+"::date - interval '1 year', "+curEndDate+"::date - interval '1 year' + interval '1 day', "+curEndDate+"::date + interval '1 day'");
82
+				query.next();
83
+                curEndDate = "'"+query.value(2)+"'";
86 84
                 var prevStartDate = "'"+query.value(0)+"'";
87 85
                 var prevEndDate = "'"+query.value(1)+"'";
88 86
                 var q = "CREATE TEMPORARY TABLE previous ON COMMIT DROP AS SELECT roasted_id, sum(roasted_quantity) AS p FROM roasting_log WHERE time > "+prevStartDate+" AND time < "+prevEndDate+" AND roasted_id IS NOT NULL GROUP BY roasted_id";
@@ -501,12 +499,9 @@
501 499
                 buffer.close();
502 500
             }
503 501
             refresh();
504
-            startDateField.dateChanged.connect(function() {
505
-                refresh();
506
-            });
507
-            endDateField.dateChanged.connect(function() {
508
-                refresh();
509
-            });
502
+			dateSelect.rangeUpdated.connect(function() {
503
+				refresh();
504
+			});
510 505
 			avgField.editingFinished.connect(function() {
511 506
 				refresh();
512 507
 			});

+ 13
- 16
config/Reports/fypurchase.xml View File

@@ -2,10 +2,7 @@
2 2
 	<reporttitle>Purchase:->Coffee Purchase Previous Years Comparison</reporttitle>
3 3
 	<layout type="vertical">
4 4
 		<layout type="horizontal">
5
-			<label>Start Date:</label>
6
-			<calendar id="startdate" />
7
-			<label>End Date:</label>
8
-			<calendar id="enddate" />
5
+			<daterange id = "dates" initial="23" /><!--Lifetime-->
9 6
 			<label>Weight Unit:</label>
10 7
 			<sqldrop id="unit" />
11 8
 			<stretch />
@@ -18,16 +15,16 @@
18 15
 	<program>
19 16
 		<![CDATA[
20 17
 			this.windowTitle = "Typica - Coffee Purchase Previous Years Comparison";
21
-			/* Set starting year to the first year on record. */
22
-			var startDateField = findChildObject(this, 'startdate');
18
+			/* Set Lifetime range. */
19
+			var dateSelect = findChildObject(this, 'dates');
23 20
 			var query = new QSqlQuery();
24
-			query.exec("SELECT EXTRACT(YEAR FROM time) FROM purchase WHERE time = (SELECT min(time) FROM purchase)");
21
+			query.exec("SELECT concat(EXTRACT(YEAR FROM time::date), '-01-01') FROM purchase WHERE time = (SELECT min(time) FROM purchase) UNION SELECT concat(EXTRACT(YEAR FROM 'now'::date), '-12-31')");
25 22
 			query.next();
26
-			startDateField.setDate(query.value(0), 1, 1);
23
+			var lifetimeStartDate = query.value(0);
24
+			query.next();
25
+			var lifetimeEndDate = query.value(0);
26
+			dateSelect.setLifetimeRange(lifetimeStartDate, lifetimeEndDate);
27 27
 			query = query.invalidate();
28
-			/* Set ending year to the current year. */
29
-			var endDateField = findChildObject(this, 'enddate');
30
-			endDateField.setDate(endDateField.year(), 12, 31);
31 28
 			/* Enable printing */
32 29
 			var view = findChildObject(this, 'report');
33 30
 			var printMenu = findChildObject(this, 'print');
@@ -81,7 +78,10 @@
81 78
 				var unittotal = 0;
82 79
 				var costtotal = 0;
83 80
 				var query = new QSqlQuery();
84
-				for(var i = startDateField.year(); i <= endDateField.year(); i++)
81
+				var dateRange = dateSelect.currentRange();
82
+				var startYear = Number(dateRange[0].substr(0, 4));
83
+				var endYear = Number(dateRange[dateRange.length - 1].substr(0, 4));
84
+				for(var i = startYear; i <= endYear; i++)
85 85
 				{
86 86
 					output.writeStartElement("tr");
87 87
 					output.writeAttribute("id", "y"+i);
@@ -121,10 +121,7 @@
121 121
 			}
122 122
 			refresh();
123 123
 			/* Update report as needed. */
124
-			startDateField.dateChanged.connect(function() {
125
-				refresh();
126
-			});
127
-			endDateField.dateChanged.connect(function() {
124
+			dateSelect.rangeUpdated.connect(function() {
128 125
 				refresh();
129 126
 			});
130 127
 			/* Expand year data */

+ 18
- 13
config/Reports/invchange.xml View File

@@ -2,10 +2,7 @@
2 2
 	<reporttitle>Inventory:->Inventory Change Summary</reporttitle>
3 3
 	<layout type="vertical">
4 4
 		<layout type="horizontal">
5
-			<label>Start Date:</label>
6
-			<calendar id="startdate" />
7
-			<label>End Date:</label>
8
-			<calendar id="enddate" />
5
+			<daterange id="dates" initial="19" /><!-- Current Year to Date-->
9 6
 			<label>Weight Unit:</label>
10 7
 			<sqldrop id="unit" />
11 8
 			<stretch />
@@ -18,9 +15,19 @@
18 15
 	<program>
19 16
 		<![CDATA[
20 17
 			this.windowTitle = "Typica - Inventory Change Summary";
21
-			var startDateField = findChildObject(this, 'startdate');
22
-			startDateField.setDate(startDateField.year(), 1, 1);
23
-			var endDateField = findChildObject(this, 'enddate');
18
+			var dateSelect = findChildObject(this, 'dates');
19
+			var dateQuery = new QSqlQuery();
20
+			dateQuery.exec("SELECT time::date FROM transactions WHERE time = (SELECT min(time) FROM transactions) OR time = (SELECT max(time) FROM transactions) ORDER BY time ASC");
21
+			dateQuery.next();
22
+			var lifetimeStartDate = dateQuery.value(0);
23
+			var lifetimeEndDate;
24
+			if(dateQuery.next()) {
25
+				lifetimeEndDate = dateQuery.value(0);
26
+			} else {
27
+				lifetimeEndDate = lifetimeStartDate;
28
+			}
29
+			dateSelect.setLifetimeRange(lifetimeStartDate, lifetimeEndDate);
30
+			dateQuery = dateQuery.invalidate();
24 31
 			var unitBox = findChildObject(this, 'unit');
25 32
 			unitBox.addItem("Kg");
26 33
 			unitBox.addItem("Lb");
@@ -46,8 +53,9 @@
46 53
 				output.writeTextElement("title", "Inventory Change Summary");
47 54
 				output.writeEndElement();
48 55
 				output.writeStartElement("body");
49
-				var startDate = "" + startDateField.year() + "-" + startDateField.month() + "-" + startDateField.day();
50
-				var endDate = "" + endDateField.year() + "-" + endDateField.month() + "-" + endDateField.day();
56
+				var dateRange = dateSelect.currentRange();
57
+				var startDate = dateRange[0];
58
+				var endDate = dateRange[dateRange.length - 1];
51 59
 				output.writeTextElement("h1", "Inventory Change Summary: " + startDate + " – " + endDate);
52 60
 				var conversion = 1;
53 61
 				if(unitBox.currentIndex == 0) {
@@ -206,10 +214,7 @@
206 214
 				query = query.invalidate();
207 215
 			}
208 216
 			refresh();
209
-			startDateField.dateChanged.connect(function() {
210
-				refresh();
211
-			});
212
-			endDateField.dateChanged.connect(function() {
217
+			dateSelect.rangeUpdated.connect(function() {
213 218
 				refresh();
214 219
 			});
215 220
 			view.scriptLinkClicked.connect(function(url) {

+ 4
- 2
src/Typica.pro View File

@@ -23,7 +23,8 @@ HEADERS += moc_typica.cpp \
23 23
     webview.h \
24 24
     webelement.h \
25 25
     scale.h \
26
-    draglabel.h
26
+    draglabel.h \
27
+    daterangeselector.h
27 28
 SOURCES += typica.cpp \
28 29
     helpmenu.cpp \
29 30
     abouttypica.cpp \
@@ -31,7 +32,8 @@ SOURCES += typica.cpp \
31 32
     webview.cpp \
32 33
     webelement.cpp \
33 34
     scale.cpp \
34
-    draglabel.cpp
35
+    draglabel.cpp \
36
+    daterangeselector.cpp
35 37
 
36 38
 RESOURCES += \
37 39
     resources.qrc

+ 478
- 0
src/daterangeselector.w View File

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

+ 19
- 38
src/rate.w View File

@@ -29,7 +29,6 @@ class RateOfChange : public QObject
29 29
 		int ct;
30 30
 		int st;
31 31
 		QList<Measurement> cache;
32
-		QMap<double,double> smoothCache;
33 32
 };
34 33
 
35 34
 @ The interesting part of this class is in the |newMeasurement()| method. This
@@ -88,51 +87,33 @@ if(cache.size() > 2)
88 87
 	}
89 88
 }
90 89
 
91
-@ The calculation method here is subject to change as this is still noisier
92
-than I would like. What we are doing here is calculating the rate of change
93
-between each pair of adjacent measurements in the cache and averaging them to
94
-produce something that is a little less noisy than just using the first and
95
-last measurements in the cache. Other techniques may be useful for reducing the
96
-noise further.
90
+@ Rather than work directly with changes from one measurement to the next and
91
+attempting to filter out the noise, we instead calculate the slope of a simple
92
+linear regression on the current window of data.
97 93
 
98 94
 The measurement will carry the fact that it is a relative measurement.
99 95
 
100 96
 @<Calculate rate of change@>=
101
-QList<double> rates;
102
-for(int i = 1; i < cache.size(); i++)
97
+int N = cache.size();
98
+double SXY = 0;
99
+double SX = 0;
100
+double SXX = 0;
101
+double SY = 0;
102
+double y;
103
+double x;
104
+for(int i = 0; i < N; i++)
103 105
 {
104
-	double mdiff = cache.at(i).temperature() - cache.at(i-1).temperature();
105
-	double tdiff = (double)(cache.at(i-1).time().msecsTo(cache.at(i).time())) / 1000.0;
106
-	rates.append(mdiff/tdiff);
106
+	y = cache.at(i).temperature();
107
+	SY += y;
108
+	x = cache.at(0).time().msecsTo(cache.at(i).time()) / 1000.0;
109
+	SX += x;
110
+	SXX += (x*x);
111
+	SXY += (x*y);
107 112
 }
108
-double acc = 0.0;
109
-for(int i = 0; i < rates.size(); i++)
110
-{
111
-	acc += rates.at(i);
112
-}
113
-double pavg = acc /= rates.size();
114
-double v2 = pavg * st;
115
-double refm = cache.back().temperature() - cache.front().temperature();
116
-double reft = (double)(cache.front().time().msecsTo(cache.back().time())) / 1000.0;
117
-double ref = refm/reft;
118
-Measurement value(v2, cache.back().time(), cache.back().scale());
113
+double M = ((N * SXY) - (SX * SY)) / ((N * SXX) - (SX * SX));
114
+Measurement value(M * st, cache.back().time(), cache.back().scale());
119 115
 value.insert("relative", true);
120 116
 emit newData(value);
121
-double calcdiff = ref - pavg;
122
-if(calcdiff < 0)
123
-{
124
-	calcdiff = -calcdiff;
125
-}
126
-if(pavg < 0)
127
-{
128
-	pavg = -pavg;
129
-}
130
-if(calcdiff > (pavg * 0.2))
131
-{
132
-	Measurement save = cache.back();
133
-	cache.clear();
134
-	cache.append(save);
135
-}
136 117
 
137 118
 @ The rest of the class implementation is trivial.
138 119
 

+ 4
- 0
src/resources.qrc View File

@@ -23,5 +23,9 @@
23 23
         <file>resources/icons/tango/22x22/categories/applications-graphics.png</file>
24 24
         <file>resources/icons/tango/32x32/categories/applications-graphics.png</file>
25 25
         <file>resources/icons/tango/scalable/categories/applications-graphics.svg</file>
26
+        <file>resources/icons/tango/16x16/apps/office-calendar.png</file>
27
+        <file>resources/icons/tango/22x22/apps/office-calendar.png</file>
28
+        <file>resources/icons/tango/32x32/apps/office-calendar.png</file>
29
+        <file>resources/icons/tango/scalable/apps/office-calendar.svg</file>
26 30
     </qresource>
27 31
 </RCC>

+ 3
- 0
src/typica.w View File

@@ -4263,6 +4263,7 @@ void populateBoxLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4263 4263
 				QBoxLayout *layout = qobject_cast<QBoxLayout *>(layoutStack->top());
4264 4264
 				layout->addStretch();
4265 4265
 			}
4266
+			@<Additional box layout elements@>@;
4266 4267
 		}
4267 4268
 	}
4268 4269
 }
@@ -13268,6 +13269,8 @@ void setQTextEditProperties(QScriptValue value, QScriptEngine *engine)
13268 13269
 	value.setProperty("print", engine->newFunction(QTextEdit_print));
13269 13270
 }
13270 13271
 
13272
+@i daterangeselector.w
13273
+
13271 13274
 @** An area for repeated user interface elements.
13272 13275
 
13273 13276
 \noindent There are multiple use cases in which it is useful to specify a

Loading…
Cancel
Save