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 }