1 /++
2 	Add-on to [arsd.minigui] to provide date and time widgets.
3 
4 	History:
5 		Added March 22, 2022 (dub v10.7)
6 
7 	Bugs:
8 		The Linux implementation is currently extremely minimal. The Windows implementation has more actual graphical functionality.
9 +/
10 module arsd.minigui_addons.datetime_picker;
11 
12 import arsd.minigui;
13 
14 import std.datetime;
15 
16 static if(UsingWin32Widgets) {
17 	import core.sys.windows.windows;
18 	import core.sys.windows.commctrl;
19 }
20 
21 /++
22 	A DatePicker is a single row input for picking a date. It can drop down a calendar to help the user pick the date they want.
23 
24 	See also: [TimePicker], [CalendarPicker]
25 +/
26 // on Windows these support a min/max range too
27 class DatePicker : Widget {
28 	///
29 	this(Widget parent) {
30 		super(parent);
31 		static if(UsingWin32Widgets) {
32 			createWin32Window(this, "SysDateTimePick32"w, null, 0);
33 		} else {
34 			date = new LabeledLineEdit("Date (YYYY-Mon-DD)", TextAlignment.Right, this);
35 
36 			date.addEventListener((ChangeEvent!string ce) { changed(); });
37 
38 			this.tabStop = false;
39 		}
40 	}
41 
42 	private Date value_;
43 
44 	/++
45 		Current value the user selected. Please note this is NOT valid until AFTER a change event is emitted.
46 	+/
47 	Date value() {
48 		return value_;
49 	}
50 
51 	/++
52 		Changes the current value displayed. Will not send a change event.
53 	+/
54 	void value(Date v) {
55 		static if(UsingWin32Widgets) {
56 			SYSTEMTIME st;
57 			st.wYear = v.year;
58 			st.wMonth = v.month;
59 			st.wDay = v.day;
60 			SendMessage(hwnd, DTM_SETSYSTEMTIME, GDT_VALID, cast(LPARAM) &st);
61 		} else {
62 			date.content = value_.toSimpleString();
63 		}
64 	}
65 
66 	static if(UsingCustomWidgets) private {
67 		LabeledLineEdit date;
68 		string lastMsg;
69 
70 		void changed() {
71 			try {
72 				value_ = Date.fromSimpleString(date.content);
73 
74 				this.emit!(ChangeEvent!Date)(&value);
75 			} catch(Exception e) {
76 				if(e.msg != lastMsg) {
77 					messageBox(e.msg);
78 					lastMsg = e.msg;
79 				}
80 			}
81 		}
82 	}
83 
84 
85 
86 	static if(UsingWin32Widgets) {
87 		override int minHeight() { return defaultLineHeight + 6; }
88 		override int maxHeight() { return defaultLineHeight + 6; }
89 
90 		override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
91 			switch(code) {
92 				case DTN_DATETIMECHANGE:
93 					auto lpChange = cast(LPNMDATETIMECHANGE) hdr;
94 					if(true || (lpChange.dwFlags & GDT_VALID)) { // this flag only set if you use SHOWNONE
95 						auto st = lpChange.st;
96 						value_ = Date(st.wYear, st.wMonth, st.wDay);
97 
98 						this.emit!(ChangeEvent!Date)(&value);
99 
100 						mustReturn = true;
101 					}
102 				break;
103 				default:
104 			}
105 			return false;
106 		}
107 	} else {
108 		override int minHeight() { return defaultLineHeight + 4; }
109 		override int maxHeight() { return defaultLineHeight + 4; }
110 	}
111 
112 	override bool encapsulatedChildren() {
113 		return true;
114 	}
115 
116 	mixin Emits!(ChangeEvent!Date);
117 }
118 
119 /++
120 	A TimePicker is a single row input for picking a time. It does not work with timezones.
121 
122 	See also: [DatePicker]
123 +/
124 class TimePicker : Widget {
125 	///
126 	this(Widget parent) {
127 		super(parent);
128 		static if(UsingWin32Widgets) {
129 			createWin32Window(this, "SysDateTimePick32"w, null, DTS_TIMEFORMAT);
130 		} else {
131 			time = new LabeledLineEdit("Time", TextAlignment.Right, this);
132 
133 			time.addEventListener((ChangeEvent!string ce) { changed(); });
134 
135 			this.tabStop = false;
136 		}
137 
138 	}
139 
140 	private TimeOfDay value_;
141 
142 	static if(UsingCustomWidgets) private {
143 		LabeledLineEdit time;
144 		string lastMsg;
145 
146 		void changed() {
147 			try {
148 				value_ = TimeOfDay.fromISOExtString(time.content);
149 
150 				this.emit!(ChangeEvent!TimeOfDay)(&value);
151 			} catch(Exception e) {
152 				if(e.msg != lastMsg) {
153 					messageBox(e.msg);
154 					lastMsg = e.msg;
155 				}
156 			}
157 		}
158 	}
159 
160 
161 	/++
162 		Current value the user selected. Please note this is NOT valid until AFTER a change event is emitted.
163 	+/
164 	TimeOfDay value() {
165 		return value_;
166 	}
167 
168 	/++
169 		Changes the current value displayed. Will not send a change event.
170 	+/
171 	void value(TimeOfDay v) {
172 		static if(UsingWin32Widgets) {
173 			SYSTEMTIME st;
174 			st.wHour = v.hour;
175 			st.wMinute = v.minute;
176 			st.wSecond = v.second;
177 			SendMessage(hwnd, DTM_SETSYSTEMTIME, GDT_VALID, cast(LPARAM) &st);
178 		} else {
179 			time.content = value_.toISOExtString();
180 		}
181 	}
182 
183 	static if(UsingWin32Widgets) {
184 		override int minHeight() { return defaultLineHeight + 6; }
185 		override int maxHeight() { return defaultLineHeight + 6; }
186 
187 		override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
188 			switch(code) {
189 				case DTN_DATETIMECHANGE:
190 					auto lpChange = cast(LPNMDATETIMECHANGE) hdr;
191 					if(true || (lpChange.dwFlags & GDT_VALID)) { // this flag only set if you use SHOWNONE
192 						auto st = lpChange.st;
193 						value_ = TimeOfDay(st.wHour, st.wMinute, st.wSecond);
194 
195 						this.emit!(ChangeEvent!TimeOfDay)(&value);
196 
197 						mustReturn = true;
198 					}
199 				break;
200 				default:
201 			}
202 			return false;
203 		}
204 
205 	} else {
206 		override int minHeight() { return defaultLineHeight + 4; }
207 		override int maxHeight() { return defaultLineHeight + 4; }
208 	}
209 
210 	override bool encapsulatedChildren() {
211 		return true;
212 	}
213 
214 	mixin Emits!(ChangeEvent!TimeOfDay);
215 }
216 
217 /++
218 	A CalendarPicker is a rectangular input for picking a date or a range of dates on a
219 	calendar viewer.
220 
221 	The current value is an [Interval] of dates. Please note that the interval is non-inclusive,
222 	that is, the end day is one day $(I after) the final date the user selected.
223 
224 	If the user only selected one date, start will be the selection and end is the day after.
225 +/
226 /+
227 	Note the Windows control also supports bolding dates, changing the max selection count,
228 	week numbers, and more.
229 +/
230 class CalendarPicker : Widget {
231 	///
232 	this(Widget parent) {
233 		super(parent);
234 		static if(UsingWin32Widgets) {
235 			createWin32Window(this, "SysMonthCal32"w, null, MCS_MULTISELECT);
236 			SendMessage(hwnd, MCM_SETMAXSELCOUNT, int.max, 0);
237 		} else {
238 			start = new LabeledLineEdit("Start", this);
239 			end = new LabeledLineEdit("End", this);
240 
241 			start.addEventListener((ChangeEvent!string ce) { changed(); });
242 			end.addEventListener((ChangeEvent!string ce) { changed(); });
243 
244 			this.tabStop = false;
245 		}
246 	}
247 
248 	static if(UsingCustomWidgets) private {
249 		LabeledLineEdit start;
250 		LabeledLineEdit end;
251 		string lastMsg;
252 
253 		void changed() {
254 			try {
255 				value_ = Interval!Date(
256 					Date.fromSimpleString(start.content),
257 					Date.fromSimpleString(end.content) + 1.days
258 				);
259 
260 				this.emit!(ChangeEvent!(Interval!Date))(&value);
261 			} catch(Exception e) {
262 				if(e.msg != lastMsg) {
263 					messageBox(e.msg);
264 					lastMsg = e.msg;
265 				}
266 			}
267 		}
268 	}
269 
270 	private Interval!Date value_;
271 
272 	/++
273 		Current value the user selected. Please note this is NOT valid until AFTER a change event is emitted.
274 	+/
275 	Interval!Date value() { return value_; }
276 
277 	/++
278 		Sets a new interval. Remember, the end date of the interval is NOT included. You might want to `end + 1.days` when creating it.
279 	+/
280 	void value(Interval!Date v) {
281 		value_ = v;
282 
283 		auto end = v.end - 1.days;
284 
285 		static if(UsingWin32Widgets) {
286 			SYSTEMTIME[2] arr;
287 
288 			arr[0].wYear = v.begin.year;
289 			arr[0].wMonth = v.begin.month;
290 			arr[0].wDay = v.begin.day;
291 
292 			arr[1].wYear = end.year;
293 			arr[1].wMonth = end.month;
294 			arr[1].wDay = end.day;
295 
296 			SendMessage(hwnd, MCM_SETSELRANGE, 0, cast(LPARAM) arr.ptr);
297 		} else {
298 			this.start.content = v.begin.toString();
299 			this.end.content = end.toString();
300 		}
301 	}
302 
303 	static if(UsingWin32Widgets) {
304 		override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
305 			switch(code) {
306 				case MCN_SELECT:
307 					auto lpChange = cast(LPNMSELCHANGE) hdr;
308 					auto start = lpChange.stSelStart;
309 					auto end = lpChange.stSelEnd;
310 
311 					auto et = Date(end.wYear, end.wMonth, end.wDay);
312 					et += dur!"days"(1);
313 
314 					value_ = Interval!Date(
315 						Date(start.wYear, start.wMonth, start.wDay),
316 						Date(end.wYear, end.wMonth, end.wDay) + 1.days // the interval is non-inclusive
317 					);
318 
319 					this.emit!(ChangeEvent!(Interval!Date))(&value);
320 
321 					mustReturn = true;
322 				break;
323 				default:
324 			}
325 			return false;
326 		}
327 	}
328 
329 	override bool encapsulatedChildren() {
330 		return true;
331 	}
332 
333 	mixin Emits!(ChangeEvent!(Interval!Date));
334 }