Package Gnumed :: Package timelinelib :: Package calendar :: Package bosparanian :: Module timepicker
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.calendar.bosparanian.timepicker

  1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg 
  2  # 
  3  # This file is part of Timeline. 
  4  # 
  5  # Timeline is free software: you can redistribute it and/or modify 
  6  # it under the terms of the GNU General Public License as published by 
  7  # the Free Software Foundation, either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Timeline is distributed in the hope that it will be useful, 
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  # GNU General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU General Public License 
 16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>. 
 17   
 18   
 19  import wx 
 20   
 21  from timelinelib.calendar.bosparanian.bosparanian import BosparanianDateTime, is_valid_time 
 22  from timelinelib.calendar.bosparanian.dateformatter import BosparanianDateFormatter 
 23  from timelinelib.calendar.bosparanian.time import BosparanianDelta 
 24  from timelinelib.calendar.bosparanian.timetype import BosparanianTimeType 
 25   
 26   
27 -class BosparanianDateTimePicker(wx.Panel):
28
29 - def __init__(self, parent, show_time=True, config=None, on_change=None):
30 wx.Panel.__init__(self, parent) 31 self.config = config 32 self._create_gui(on_change) 33 self.controller = BosparanianDateTimePickerController( 34 self.date_picker, self.time_picker, BosparanianTimeType().now) 35 self.show_time(show_time) 36 self.parent = parent
37
38 - def on_return(self):
39 try: 40 self.parent.on_return() 41 except AttributeError: 42 pass
43
44 - def on_escape(self):
45 try: 46 self.parent.on_escape() 47 except AttributeError: 48 pass
49
50 - def show_time(self, show=True):
51 self.time_picker.Show(show) 52 self.GetSizer().Layout()
53
54 - def get_value(self):
55 try: 56 return self.controller.get_value() 57 except ValueError: 58 pass
59
60 - def set_value(self, value):
62
63 - def _create_gui(self, on_change):
64 self.date_picker = BosparanianDatePicker(self, on_change) 65 self.time_picker = BosparanianTimePicker(self, on_change) 66 # Layout 67 sizer = wx.BoxSizer(wx.HORIZONTAL) 68 sizer.Add(self.date_picker, proportion=1, 69 flag=wx.ALIGN_CENTER_VERTICAL) 70 sizer.Add(self.time_picker, proportion=0, 71 flag=wx.ALIGN_CENTER_VERTICAL) 72 self.SetSizerAndFit(sizer)
73 74
75 -class BosparanianDateTimePickerController(object):
76
77 - def __init__(self, date_picker, time_picker, now_fn):
78 self.date_picker = date_picker 79 self.time_picker = time_picker 80 self.now_fn = now_fn
81
82 - def get_value(self):
83 if self.time_picker.IsShown(): 84 hour, minute, second = self.time_picker.get_value() 85 else: 86 hour, minute, second = (0, 0, 0) 87 year, month, day = self.date_picker.get_value() 88 return BosparanianDateTime(year, month, day, hour, minute, second).to_time()
89
90 - def set_value(self, time):
91 if time is None: 92 time = self.now_fn() 93 self.date_picker.set_value(BosparanianDateTime.from_time(time).to_date_tuple()) 94 self.time_picker.set_value(BosparanianDateTime.from_time(time).to_time_tuple())
95 96
97 -class BosparanianDatePicker(wx.TextCtrl):
98
99 - def __init__(self, parent, on_change):
100 wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER) 101 self.controller = BosparanianDatePickerController(self, on_change=on_change) 102 self._bind_events() 103 self._resize_to_fit_text() 104 self.parent = parent
105
106 - def get_value(self):
107 return self.controller.get_value()
108
109 - def set_value(self, date):
111
112 - def get_date_string(self):
113 return self.GetValue()
114
115 - def set_date_string(self, date_string):
116 date_str, bc_year = date_string 117 return self.SetValue(date_str)
118
119 - def _bind_events(self):
120 def on_set_focus(evt): 121 # CallAfter is a trick to prevent default behavior of selecting all 122 # text when a TextCtrl is given focus 123 wx.CallAfter(self.controller.on_set_focus)
124 self.Bind(wx.EVT_SET_FOCUS, on_set_focus) 125 def on_kill_focus(evt): 126 # Trick to not make selection text disappear when focus is lost (we 127 # remove the selection instead) 128 self.controller.on_kill_focus() 129 self.SetSelection(0, 0)
130 self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) 131 def on_char(evt): 132 if evt.GetKeyCode() == wx.WXK_TAB: 133 if evt.ShiftDown(): 134 skip = self.controller.on_shift_tab() 135 else: 136 skip = self.controller.on_tab() 137 else: 138 skip = True 139 evt.Skip(skip) 140 self.Bind(wx.EVT_CHAR, on_char) 141 def on_text(evt): 142 self.controller.on_text_changed() 143 self.Bind(wx.EVT_TEXT, on_text) 144 def on_key_down(evt): 145 if evt.GetKeyCode() == wx.WXK_UP: 146 self.controller.on_up() 147 elif evt.GetKeyCode() == wx.WXK_DOWN: 148 self.controller.on_down() 149 elif (evt.GetKeyCode() == wx.WXK_NUMPAD_ENTER or 150 evt.GetKeyCode() == wx.WXK_RETURN): 151 self.parent.on_return() 152 elif (evt.GetKeyCode() == wx.WXK_ESCAPE): 153 self.parent.on_escape() 154 else: 155 evt.Skip() 156 self.Bind(wx.EVT_KEY_DOWN, on_key_down) 157
158 - def _resize_to_fit_text(self):
159 w, _ = self.GetTextExtent("0000BF-MMM-00") 160 width = w + 20 161 self.SetMinSize((width, -1))
162 163
164 -class BosparanianDatePickerController(object):
165
166 - def __init__(self, date_picker, error_bg="pink", on_change=None):
167 self.date_picker = date_picker 168 self.error_bg = error_bg 169 self.original_bg = self.date_picker.GetBackgroundColour() 170 self.date_formatter = BosparanianDateFormatter() 171 self.separator = self.date_formatter.separator() 172 self.region_year, self.region_month, self.region_day = self.date_formatter.get_regions() 173 self.region_siblings = ((self.region_year, self.region_month), 174 (self.region_month, self.region_day)) 175 self.preferred_day = None 176 self.save_preferred_day = True 177 self.last_selection = None 178 self.on_change = on_change
179
180 - def get_value(self):
181 try: 182 (year, month, day) = self._parse_year_month_day() 183 self._ensure_within_allowed_period((year, month, day)) 184 return (year, month, day) 185 except ValueError: 186 raise ValueError("Invalid date.")
187
188 - def set_value(self, value):
189 year, month, day = value 190 date_string = self.date_formatter.format(year, month, day) 191 self.date_picker.set_date_string(date_string) 192 self._on_change()
193
194 - def _on_change(self):
195 if self._current_date_is_valid() and not self.on_change is None: 196 self.on_change()
197
198 - def on_set_focus(self):
199 if self.last_selection: 200 start, end = self.last_selection 201 self.date_picker.SetSelection(start, end) 202 else: 203 self._select_region_if_possible(self.region_year) 204 self.last_selection = self.date_picker.GetSelection()
205
206 - def on_kill_focus(self):
207 if self.last_selection: 208 self.last_selection = self.date_picker.GetSelection()
209
210 - def on_tab(self):
211 for (left_region, right_region) in self.region_siblings: 212 if self._insertion_point_in_region(left_region): 213 self._select_region_if_possible(right_region) 214 return False 215 return True
216
217 - def on_shift_tab(self):
218 for (left_region, right_region) in self.region_siblings: 219 if self._insertion_point_in_region(right_region): 220 self._select_region_if_possible(left_region) 221 return False 222 return True
223
224 - def on_text_changed(self):
225 self._change_background_depending_on_date_validity() 226 if self._current_date_is_valid(): 227 current_date = self.get_value() 228 # To prevent saving of preferred day when year or month is changed 229 # in on_up() and on_down()... 230 # Save preferred day only when text is entered in the date text 231 # control and not when up or down keys has been used. 232 # When up and down keys are used, the preferred day is saved in 233 # on_up() and on_down() only when day is changed. 234 if self.save_preferred_day: 235 self._save_preferred_day(current_date) 236 self._on_change()
237
238 - def on_up(self):
239 max_year = BosparanianDateTime.from_time(BosparanianTimeType().get_max_time()).year 240 def increment_year(date): 241 year, month, day = date 242 if year < max_year - 1: 243 return self._set_valid_day(year + 1, month, day) 244 return date
245 def increment_month(date): 246 year, month, day = date 247 if month < 13: 248 return self._set_valid_day(year, month + 1, day) 249 elif year < max_year - 1: 250 return self._set_valid_day(year + 1, 1, day) 251 return date
252 def increment_day(date): 253 year, month, day = date 254 time = BosparanianDateTime.from_ymd(year, month, day).to_time() 255 if time < BosparanianTimeType().get_max_time() - BosparanianDelta.from_days(1): 256 return BosparanianDateTime.from_time(time + BosparanianDelta.from_days(1)).to_date_tuple() 257 return date 258 if not self._current_date_is_valid(): 259 return 260 selection = self.date_picker.GetSelection() 261 current_date = self.get_value() 262 if self._insertion_point_in_region(self.region_year): 263 new_date = increment_year(current_date) 264 elif self._insertion_point_in_region(self.region_month): 265 new_date = increment_month(current_date) 266 else: 267 new_date = increment_day(current_date) 268 self._save_preferred_day(new_date) 269 if current_date != new_date: 270 self._set_new_date_and_restore_selection(new_date, selection) 271 self._on_change() 272
273 - def on_down(self):
274 def decrement_year(date): 275 year, month, day = date 276 if year > BosparanianDateTime.from_time(BosparanianTimeType().get_min_time()).year: 277 return self._set_valid_day(year - 1, month, day) 278 return date
279 def decrement_month(date): 280 year, month, day = date 281 if month > 1: 282 return self._set_valid_day(year, month - 1, day) 283 elif year > BosparanianDateTime.from_time(BosparanianTimeType().get_min_time()).year: 284 return self._set_valid_day(year - 1, 13, day) 285 return date 286 def decrement_day(date): 287 year, month, day = date 288 if day > 1: 289 return self._set_valid_day(year, month, day - 1) 290 elif month > 1: 291 return self._set_valid_day(year, month - 1, 30) 292 elif year > BosparanianDateTime.from_time(BosparanianTimeType().get_min_time()).year: 293 return self._set_valid_day(year - 1, 13, 5) 294 return date 295 if not self._current_date_is_valid(): 296 return 297 selection = self.date_picker.GetSelection() 298 current_date = self.get_value() 299 if self._insertion_point_in_region(self.region_year): 300 new_date = decrement_year(current_date) 301 elif self._insertion_point_in_region(self.region_month): 302 new_date = decrement_month(current_date) 303 else: 304 year, month, day = current_date 305 BosparanianDateTime.from_ymd(year, month, day) 306 if BosparanianDateTime.from_ymd(year, month, day).to_time() == BosparanianTimeType().get_min_time(): 307 return 308 new_date = decrement_day(current_date) 309 self._save_preferred_day(new_date) 310 if current_date != new_date: 311 self._set_new_date_and_restore_selection(new_date, selection) 312 self._on_change() 313
314 - def _change_background_depending_on_date_validity(self):
315 if self._current_date_is_valid(): 316 self.date_picker.SetBackgroundColour(self.original_bg) 317 else: 318 self.date_picker.SetBackgroundColour(self.error_bg) 319 self.date_picker.SetFocus() 320 self.date_picker.Refresh()
321
322 - def _parse_year_month_day(self):
323 return self.date_formatter.parse(self.date_picker.get_date_string())
324
325 - def _ensure_within_allowed_period(self, date):
326 year, month, day = date 327 time = BosparanianDateTime(year, month, day, 0, 0, 0).to_time() 328 if (time >= BosparanianTimeType().get_max_time() or 329 time < BosparanianTimeType().get_min_time()): 330 raise ValueError()
331
332 - def _set_new_date_and_restore_selection(self, new_date, selection):
333 def restore_selection(selection): 334 self.date_picker.SetSelection(selection[0], selection[1])
335 self.save_preferred_day = False 336 if self.preferred_day is not None: 337 year, month, _ = new_date 338 new_date = self._set_valid_day(year, month, self.preferred_day) 339 self.set_value(new_date) 340 restore_selection(selection) 341 self.save_preferred_day = True 342
343 - def _set_valid_day(self, new_year, new_month, new_day):
344 done = False 345 while not done: 346 try: 347 date = BosparanianDateTime.from_ymd(new_year, new_month, new_day) 348 done = True 349 except Exception: 350 new_day -= 1 351 return date.to_date_tuple()
352
353 - def _save_preferred_day(self, date):
354 _, _, day = date 355 if day > 28: 356 self.preferred_day = day 357 else: 358 self.preferred_day = None
359
360 - def _current_date_is_valid(self):
361 try: 362 self.get_value() 363 except ValueError: 364 return False 365 return True
366
367 - def _select_region_if_possible(self, region):
368 region_range = self._get_region_range(region) 369 if region_range: 370 self.date_picker.SetSelection(region_range[0], region_range[-1])
371
372 - def _insertion_point_in_region(self, n):
373 region_range = self._get_region_range(n) 374 if region_range: 375 return self.date_picker.GetInsertionPoint() in region_range
376
377 - def _get_region_range(self, n):
378 # Returns a range of valid cursor positions for a valid region year, 379 # month or day. 380 def region_is_not_valid(region): 381 return region not in (self.region_year, self.region_month, 382 self.region_day)
383 def date_has_exactly_two_seperators(datestring): 384 return len(datestring.split(self.separator)) == 3 385 def calculate_pos_range(region, datestring): 386 pos_of_separator1 = datestring.find(self.separator) 387 pos_of_separator2 = datestring.find(self.separator, 388 pos_of_separator1 + 1) 389 if region == self.region_year: 390 return range(0, pos_of_separator1 + 1) 391 elif region == self.region_month: 392 return range(pos_of_separator1 + 1, pos_of_separator2 + 1) 393 else: 394 return range(pos_of_separator2 + 1, len(datestring) + 1) 395 if region_is_not_valid(n): 396 return None 397 date = self.date_picker.get_date_string() 398 if not date_has_exactly_two_seperators(date): 399 return None 400 pos_range = calculate_pos_range(n, date) 401 return pos_range 402 403
404 -class BosparanianTimePicker(wx.TextCtrl):
405
406 - def __init__(self, parent, on_change):
407 wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER) 408 self.controller = BosparanianTimePickerController(self, on_change) 409 self._bind_events() 410 self._resize_to_fit_text() 411 self.parent = parent
412
413 - def get_value(self):
414 return self.controller.get_value()
415
416 - def set_value(self, value):
418
419 - def _get_time_string(self):
420 return self.GetValue()
421
422 - def set_time_string(self, time_string):
424
425 - def _bind_events(self):
426 def on_set_focus(evt): 427 # CallAfter is a trick to prevent default behavior of selecting all 428 # text when a TextCtrl is given focus 429 wx.CallAfter(self.controller.on_set_focus)
430 self.Bind(wx.EVT_SET_FOCUS, on_set_focus) 431 def on_kill_focus(evt): 432 # Trick to not make selection text disappear when focus is lost (we 433 # remove the selection instead) 434 self.controller.on_kill_focus() 435 self.SetSelection(0, 0)
436 self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) 437 def on_char(evt): 438 if evt.GetKeyCode() == wx.WXK_TAB: 439 if evt.ShiftDown(): 440 skip = self.controller.on_shift_tab() 441 else: 442 skip = self.controller.on_tab() 443 else: 444 skip = True 445 evt.Skip(skip) 446 self.Bind(wx.EVT_CHAR, on_char) 447 def on_text(evt): 448 self.controller.on_text_changed() 449 self.Bind(wx.EVT_TEXT, on_text) 450 def on_key_down(evt): 451 if evt.GetKeyCode() == wx.WXK_UP: 452 self.controller.on_up() 453 elif evt.GetKeyCode() == wx.WXK_DOWN: 454 self.controller.on_down() 455 elif (evt.GetKeyCode() == wx.WXK_NUMPAD_ENTER or 456 evt.GetKeyCode() == wx.WXK_RETURN): 457 self.parent.on_return() 458 elif (evt.GetKeyCode() == wx.WXK_ESCAPE): 459 self.parent.on_escape() 460 else: 461 evt.Skip() 462 self.Bind(wx.EVT_KEY_DOWN, on_key_down) 463
464 - def _resize_to_fit_text(self):
465 w, _ = self.GetTextExtent("00:00") 466 width = w + 20 467 self.SetMinSize((width, -1))
468 469
470 -class BosparanianTimePickerController(object):
471
472 - def __init__(self, time_picker, on_change):
473 self.time_picker = time_picker 474 self.original_bg = self.time_picker.GetBackgroundColour() 475 self.separator = ":" 476 self.hour_part = 0 477 self.minute_part = 1 478 self.last_selection = None 479 self.on_change = on_change
480
481 - def get_value(self):
482 try: 483 split = self.time_picker._get_time_string().split(self.separator) 484 if len(split) != 2: 485 raise ValueError() 486 hour_string, minute_string = split 487 hour = int(hour_string) 488 minute = int(minute_string) 489 if not is_valid_time(hour, minute, 0): 490 raise ValueError() 491 return (hour, minute, 0) 492 except ValueError: 493 raise ValueError("Invalid time.")
494
495 - def set_value(self, value):
496 hour, minute, _ = value 497 time_string = "%02d:%02d" % (hour, minute) 498 self.time_picker.set_time_string(time_string) 499 self._on_change()
500
501 - def _on_change(self):
502 if self._time_is_valid() and not self.on_change is None: 503 self.on_change()
504
505 - def on_set_focus(self):
506 if self.last_selection: 507 start, end = self.last_selection 508 self.time_picker.SetSelection(start, end) 509 else: 510 self._select_part(self.hour_part)
511
512 - def on_kill_focus(self):
513 self.last_selection = self.time_picker.GetSelection()
514
515 - def on_tab(self):
516 if self._in_minute_part(): 517 return True 518 self._select_part(self.minute_part) 519 return False
520
521 - def on_shift_tab(self):
522 if self._in_hour_part(): 523 return True 524 self._select_part(self.hour_part) 525 return False
526
527 - def on_text_changed(self):
528 try: 529 self.get_value() 530 self.time_picker.SetBackgroundColour(self.original_bg) 531 except ValueError: 532 self.time_picker.SetBackgroundColour("pink") 533 self.time_picker.Refresh()
534
535 - def on_up(self):
536 def increment_hour(time): 537 hour, minute, second = time 538 new_hour = hour + 1 539 if new_hour > 23: 540 new_hour = 0 541 return (new_hour, minute, second)
542 def increment_minutes(time): 543 hour, minute, second = time 544 new_hour = hour 545 new_minute = minute + 1 546 if new_minute > 59: 547 new_minute = 0 548 new_hour = hour + 1 549 if new_hour > 23: 550 new_hour = 0 551 return (new_hour, new_minute, second)
552 if not self._time_is_valid(): 553 return 554 selection = self.time_picker.GetSelection() 555 current_time = self.get_value() 556 if self._in_hour_part(): 557 new_time = increment_hour(current_time) 558 else: 559 new_time = increment_minutes(current_time) 560 if current_time != new_time: 561 self._set_new_time_and_restore_selection(new_time, selection) 562 self._on_change() 563
564 - def on_down(self):
565 def decrement_hour(time): 566 hour, minute, second = time 567 new_hour = hour - 1 568 if new_hour < 0: 569 new_hour = 23 570 return (new_hour, minute, second)
571 def decrement_minutes(time): 572 hour, minute, second = time 573 new_hour = hour 574 new_minute = minute - 1 575 if new_minute < 0: 576 new_minute = 59 577 new_hour = hour - 1 578 if new_hour < 0: 579 new_hour = 23 580 return (new_hour, new_minute, second) 581 if not self._time_is_valid(): 582 return 583 selection = self.time_picker.GetSelection() 584 current_time = self.get_value() 585 if self._in_hour_part(): 586 new_time = decrement_hour(current_time) 587 else: 588 new_time = decrement_minutes(current_time) 589 if current_time != new_time: 590 self._set_new_time_and_restore_selection(new_time, selection) 591 self._on_change() 592
593 - def _set_new_time_and_restore_selection(self, new_time, selection):
594 def restore_selection(selection): 595 self.time_picker.SetSelection(selection[0], selection[1])
596 self.set_value(new_time) 597 restore_selection(selection) 598
599 - def _time_is_valid(self):
600 try: 601 self.get_value() 602 except ValueError: 603 return False 604 return True
605
606 - def _select_part(self, part):
607 if self._separator_pos() == -1: 608 return 609 if part == self.hour_part: 610 self.time_picker.SetSelection(0, self._separator_pos()) 611 else: 612 time_string_len = len(self.time_picker._get_time_string()) 613 self.time_picker.SetSelection(self._separator_pos() + 1, time_string_len) 614 self.preferred_part = part
615
616 - def _in_hour_part(self):
617 if self._separator_pos() == -1: 618 return 619 return self.time_picker.GetInsertionPoint() <= self._separator_pos()
620
621 - def _in_minute_part(self):
622 if self._separator_pos() == -1: 623 return 624 return self.time_picker.GetInsertionPoint() > self._separator_pos()
625
626 - def _separator_pos(self):
627 return self.time_picker._get_time_string().find(self.separator)
628