Package Gnumed :: Package timelinelib :: Package wxgui :: Package components :: Module pydatetimepicker
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.wxgui.components.pydatetimepicker

  1  # Copyright (C) 2009, 2010, 2011  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 os.path 
 20  import datetime 
 21   
 22  import wx.calendar 
 23   
 24  from timelinelib.config.paths import ICONS_DIR 
 25  from timelinelib.time import PyTimeType 
 26  from timelinelib.wxgui.utils import _display_error_message 
 27   
 28   
29 -class PyDateTimePicker(wx.Panel):
30
31 - def __init__(self, parent, show_time=True, config=None):
32 wx.Panel.__init__(self, parent) 33 self.config = config 34 self._create_gui() 35 self.controller = PyDateTimePickerController( 36 self.date_picker, self.time_picker, datetime.datetime.now) 37 self.show_time(show_time)
38
39 - def show_time(self, show=True):
40 self.time_picker.Show(show) 41 self.GetSizer().Layout()
42
43 - def get_value(self):
44 return self.controller.get_value()
45
46 - def set_value(self, value):
47 self.controller.set_value(value)
48
49 - def _create_gui(self):
50 self.date_picker = PyDatePicker(self) 51 image = wx.Bitmap(os.path.join(ICONS_DIR, "calendar.png")) 52 self.date_button = wx.BitmapButton(self, bitmap=image) 53 self.Bind(wx.EVT_BUTTON, self._date_button_on_click, self.date_button) 54 self.time_picker = PyTimePicker(self) 55 # Layout 56 sizer = wx.BoxSizer(wx.HORIZONTAL) 57 sizer.Add(self.date_picker, proportion=1, 58 flag=wx.ALIGN_CENTER_VERTICAL) 59 sizer.Add(self.date_button, proportion=0, 60 flag=wx.ALIGN_CENTER_VERTICAL) 61 sizer.Add(self.time_picker, proportion=0, 62 flag=wx.ALIGN_CENTER_VERTICAL) 63 self.SetSizerAndFit(sizer)
64
65 - def _date_button_on_click(self, evt):
66 try: 67 wx_date = self._py_date_to_wx_date(self.date_picker.get_py_date()) 68 calendar_popup = CalendarPopup(self, wx_date, self.config) 69 calendar_popup.Bind(wx.calendar.EVT_CALENDAR_SEL_CHANGED, 70 self._calendar_on_date_changed) 71 calendar_popup.Bind(wx.calendar.EVT_CALENDAR, 72 self._calendar_on_date_changed_dclick) 73 btn = evt.GetEventObject() 74 pos = btn.ClientToScreen((0,0)) 75 sz = btn.GetSize() 76 calendar_popup.Position(pos, (0, sz[1])) 77 calendar_popup.Popup() 78 self.calendar_popup = calendar_popup 79 except ValueError: 80 _display_error_message(_("Invalid date"))
81 82
83 - def _calendar_on_date_changed(self, evt):
84 wx_date = evt.GetEventObject().GetDate() 85 py_date = datetime.datetime(wx_date.Year, wx_date.Month+1, wx_date.Day) 86 self.date_picker.set_py_date(py_date)
87
88 - def _calendar_on_date_changed_dclick(self, evt):
89 self.time_picker.SetFocus() 90 self.calendar_popup.Dismiss()
91
92 - def _py_date_to_wx_date(self, py_date):
93 return wx.DateTimeFromDMY(py_date.day, py_date.month-1, py_date.year, 94 0, 0, 0)
95 96
97 -class PyDateTimePickerController(object):
98
99 - def __init__(self, date_picker, time_picker, now_fn):
100 self.date_picker = date_picker 101 self.time_picker = time_picker 102 self.now_fn = now_fn
103
104 - def get_value(self):
105 time = datetime.time(0, 0) 106 if self.time_picker.IsShown(): 107 time = self.time_picker.get_py_time() 108 return datetime.datetime.combine(self.date_picker.get_py_date(), time)
109
110 - def set_value(self, py_date_time):
111 if py_date_time == None: 112 py_date_time = self.now_fn() 113 self.date_picker.set_py_date(py_date_time.date()) 114 self.time_picker.set_py_time(py_date_time.time())
115 116
117 -class CalendarPopup(wx.PopupTransientWindow):
118
119 - def __init__(self, parent, wx_date, config):
120 self.config = config 121 wx.PopupTransientWindow.__init__(self, parent, style=wx.BORDER_NONE) 122 self._create_gui(wx_date) 123 self.controller = CalendarPopupController(self) 124 self._bind_events()
125
126 - def _create_gui(self, wx_date):
127 BORDER = 2 128 self.cal = self._create_calendar_control(wx_date, BORDER) 129 size = self.cal.GetBestSize() 130 self.SetSize((size.width + BORDER * 2, size.height + BORDER * 2))
131
132 - def _create_calendar_control(self, wx_date, border):
133 style = self._get_cal_style() 134 cal = wx.calendar.CalendarCtrl(self, -1, wx_date, 135 pos=(border,border), style=style) 136 self._set_cal_range(cal) 137 return cal
138
139 - def _get_cal_style(self):
140 style = (wx.calendar.CAL_SHOW_HOLIDAYS | 141 wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION) 142 if self.config.week_start == "monday": 143 style |= wx.calendar.CAL_MONDAY_FIRST 144 else: 145 style |= wx.calendar.CAL_SUNDAY_FIRST 146 return style
147
148 - def _set_cal_range(self, cal):
149 min_date, msg = PyTimeType().get_min_time() 150 max_date, msg = PyTimeType().get_max_time() 151 min_date = self._py_date_to_wx_date(min_date) 152 max_date = self._py_date_to_wx_date(max_date) - wx.DateSpan.Day() 153 cal.SetDateRange(min_date, max_date)
154
155 - def _py_date_to_wx_date(self, py_date):
156 return wx.DateTimeFromDMY(py_date.day, py_date.month - 1, py_date.year, 157 0, 0, 0)
158
159 - def _bind_events(self):
160 def on_month(evt): 161 self.controller.on_month()
162 def on_day(evt): 163 self.controller.on_day()
164 self.cal.Bind(wx.calendar.EVT_CALENDAR_MONTH, on_month) 165 self.cal.Bind(wx.calendar.EVT_CALENDAR_DAY, on_day) 166
167 - def OnDismiss(self):
168 self.controller.on_dismiss()
169 170
171 -class CalendarPopupController(object):
172
173 - def __init__(self, calendar_popup):
174 self.calendar_popup = calendar_popup 175 self.repop = False 176 self.repoped = False
177
178 - def on_month(self):
179 self.repop = True
180
181 - def on_day(self):
182 self.repop = True
183
184 - def on_dismiss(self):
185 # This funny code makes the calender control stay open when you change 186 # month or day. The control is closed on a double-click on a day or 187 # a single click outside of the control 188 if self.repop and not self.repoped: 189 self.calendar_popup.Popup() 190 self.repoped = True
191 192
193 -class PyDatePicker(wx.TextCtrl):
194
195 - def __init__(self, parent):
196 wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER) 197 self.controller = PyDatePickerController(self) 198 self._bind_events() 199 self._resize_to_fit_text()
200
201 - def get_py_date(self):
202 return self.controller.get_py_date()
203
204 - def set_py_date(self, py_date):
205 self.controller.set_py_date(py_date)
206
207 - def get_date_string(self):
208 return self.GetValue()
209
210 - def set_date_string(self, date_string):
211 return self.SetValue(date_string)
212
213 - def _bind_events(self):
214 def on_set_focus(evt): 215 # CallAfter is a trick to prevent default behavior of selecting all 216 # text when a TextCtrl is given focus 217 wx.CallAfter(self.controller.on_set_focus)
218 self.Bind(wx.EVT_SET_FOCUS, on_set_focus) 219 def on_kill_focus(evt): 220 # Trick to not make selection text disappear when focus is lost (we 221 # remove the selection instead) 222 self.controller.on_kill_focus() 223 self.SetSelection(0, 0)
224 self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) 225 def on_char(evt): 226 if evt.GetKeyCode() == wx.WXK_TAB: 227 if evt.ShiftDown(): 228 skip = self.controller.on_shift_tab() 229 else: 230 skip = self.controller.on_tab() 231 else: 232 skip = True 233 evt.Skip(skip) 234 self.Bind(wx.EVT_CHAR, on_char) 235 def on_text(evt): 236 self.controller.on_text_changed() 237 self.Bind(wx.EVT_TEXT, on_text) 238 def on_key_down(evt): 239 if evt.GetKeyCode() == wx.WXK_UP: 240 self.controller.on_up() 241 elif evt.GetKeyCode() == wx.WXK_DOWN: 242 self.controller.on_down() 243 else: 244 evt.Skip() 245 self.Bind(wx.EVT_KEY_DOWN, on_key_down) 246
247 - def _resize_to_fit_text(self):
248 w, h = self.GetTextExtent("0000-00-00") 249 width = w + 20 250 self.SetMinSize((width, -1))
251 252
253 -class PyDatePickerController(object):
254
255 - def __init__(self, py_date_picker, error_bg="pink"):
256 self.py_date_picker = py_date_picker 257 self.error_bg = error_bg 258 self.original_bg = self.py_date_picker.GetBackgroundColour() 259 self.separator = PyTimeType().event_date_string(PyTimeType().now())[4] 260 self.region_year = 0 261 self.region_month = 1 262 self.region_day = 2 263 self.region_siblings = ((self.region_year, self.region_month), 264 (self.region_month, self.region_day)) 265 self.preferred_day = None 266 self.save_preferred_day = True 267 self.last_selection = None
268
269 - def get_py_date(self):
270 try: 271 (year, month, day) = self._parse_year_month_day() 272 py_date = datetime.date(year, month, day) 273 self._ensure_within_allowed_period(py_date) 274 return py_date 275 except ValueError: 276 raise ValueError("Invalid date.")
277
278 - def set_py_date(self, py_date):
279 date_string = PyTimeType().event_date_string(py_date) 280 self.py_date_picker.set_date_string(date_string)
281
282 - def on_set_focus(self):
283 if self.last_selection: 284 start, end = self.last_selection 285 self.py_date_picker.SetSelection(start, end) 286 else: 287 self._select_region_if_possible(self.region_year) 288 self.last_selection = self.py_date_picker.GetSelection()
289
290 - def on_kill_focus(self):
291 if self.last_selection: 292 self.last_selection = self.py_date_picker.GetSelection()
293
294 - def on_tab(self):
295 for (left_region, right_region) in self.region_siblings: 296 if self._insertion_point_in_region(left_region): 297 self._select_region_if_possible(right_region) 298 return False 299 return True
300
301 - def on_shift_tab(self):
302 for (left_region, right_region) in self.region_siblings: 303 if self._insertion_point_in_region(right_region): 304 self._select_region_if_possible(left_region) 305 return False 306 return True
307
308 - def on_text_changed(self):
309 self._change_background_depending_on_date_validity() 310 if self._current_date_is_valid(): 311 current_date = self.get_py_date() 312 # To prevent saving of preferred day when year or month is changed 313 # in on_up() and on_down()... 314 # Save preferred day only when text is entered in the date text 315 # control and not when up or down keys has been used. 316 # When up and down keys are used, the preferred day is saved in 317 # on_up() and on_down() only when day is changed. 318 if self.save_preferred_day: 319 self._save_preferred_day(current_date)
320
321 - def on_up(self):
322 def increment_year(date): 323 if date.year < PyTimeType().get_max_time()[0].year - 1: 324 return self._set_valid_day(date.year + 1, date.month, date.day) 325 return date
326 def increment_month(date): 327 if date.month < 12: 328 return self._set_valid_day(date.year, date.month + 1, 329 date.day) 330 elif date.year < PyTimeType().get_max_time()[0].year - 1: 331 return self._set_valid_day(date.year + 1, 1, date.day) 332 return date
333 def increment_day(date): 334 if date < PyTimeType().get_max_time()[0].date() - datetime.timedelta(days=1): 335 return date + datetime.timedelta(days=1) 336 return date 337 if not self._current_date_is_valid(): 338 return 339 selection = self.py_date_picker.GetSelection() 340 current_date = self.get_py_date() 341 if self._insertion_point_in_region(self.region_year): 342 new_date = increment_year(current_date) 343 elif self._insertion_point_in_region(self.region_month): 344 new_date = increment_month(current_date) 345 else: 346 new_date = increment_day(current_date) 347 self._save_preferred_day(new_date) 348 if current_date != new_date: 349 self._set_new_date_and_restore_selection(new_date, selection) 350
351 - def on_down(self):
352 def decrement_year(date): 353 if date.year > PyTimeType().get_min_time()[0].year: 354 return self._set_valid_day(date.year - 1, date.month, date.day) 355 return date
356 def decrement_month(date): 357 if date.month > 1: 358 return self._set_valid_day(date.year, date.month - 1, date.day) 359 elif date.year > PyTimeType().get_min_time()[0].year: 360 return self._set_valid_day(date.year - 1, 12, date.day) 361 return date 362 def decrement_day(date): 363 if date.day > 1: 364 return date.replace(day=date.day - 1) 365 elif date.month > 1: 366 return self._set_valid_day(date.year, date.month - 1, 31) 367 elif date.year > PyTimeType().get_min_time()[0].year: 368 return self._set_valid_day(date.year - 1, 12, 31) 369 return date 370 if not self._current_date_is_valid(): 371 return 372 selection = self.py_date_picker.GetSelection() 373 current_date = self.get_py_date() 374 if self._insertion_point_in_region(self.region_year): 375 new_date = decrement_year(current_date) 376 elif self._insertion_point_in_region(self.region_month): 377 new_date = decrement_month(current_date) 378 else: 379 new_date = decrement_day(current_date) 380 self._save_preferred_day(new_date) 381 if current_date != new_date: 382 self._set_new_date_and_restore_selection(new_date, selection) 383
384 - def _change_background_depending_on_date_validity(self):
385 if self._current_date_is_valid(): 386 self.py_date_picker.SetBackgroundColour(self.original_bg) 387 else: 388 self.py_date_picker.SetBackgroundColour(self.error_bg) 389 self.py_date_picker.SetFocus() 390 self.py_date_picker.Refresh()
391
392 - def _parse_year_month_day(self):
393 components = self.py_date_picker.get_date_string().split(self.separator) 394 if len(components) != 3: 395 raise ValueError() 396 year = int(components[self.region_year]) 397 month = int(components[self.region_month]) 398 day = int(components[self.region_day]) 399 return (year, month, day)
400
401 - def _ensure_within_allowed_period(self, py_date):
402 py_date_time = datetime.datetime(py_date.year, py_date.month, py_date.day) 403 if (py_date_time >= PyTimeType().get_max_time()[0] or 404 py_date_time < PyTimeType().get_min_time()[0]): 405 raise ValueError()
406
407 - def _set_new_date_and_restore_selection(self, new_date, selection):
408 def restore_selection(selection): 409 self.py_date_picker.SetSelection(selection[0], selection[1])
410 self.save_preferred_day = False 411 if self.preferred_day != None: 412 new_date = self._set_valid_day(new_date.year, new_date.month, 413 self.preferred_day) 414 self.set_py_date(new_date) 415 restore_selection(selection) 416 self.save_preferred_day = True 417
418 - def _set_valid_day(self, new_year, new_month, new_day):
419 done = False 420 while not done: 421 try: 422 date = datetime.date(year=new_year, month=new_month, day=new_day) 423 done = True 424 except Exception, ex: 425 new_day -= 1 426 return date
427
428 - def _save_preferred_day(self, date):
429 if date.day > 28: 430 self.preferred_day = date.day 431 else: 432 self.preferred_day = None
433
434 - def _current_date_is_valid(self):
435 try: 436 self.get_py_date() 437 except ValueError: 438 return False 439 return True
440
441 - def _select_region_if_possible(self, region):
442 region_range = self._get_region_range(region) 443 if region_range: 444 self.py_date_picker.SetSelection(region_range[0], region_range[-1])
445
446 - def _insertion_point_in_region(self, n):
447 region_range = self._get_region_range(n) 448 if region_range: 449 return self.py_date_picker.GetInsertionPoint() in region_range
450
451 - def _get_region_range(self, n):
452 # Returns a range of valid cursor positions for a valid region year, 453 # month or day. 454 def region_is_not_valid(region): 455 return region not in (self.region_year, self.region_month, 456 self.region_day)
457 def date_has_exactly_two_seperators(datestring): 458 return len(datestring.split(self.separator)) == 3 459 def calculate_pos_range(region, datestring): 460 pos_of_separator1 = datestring.find(self.separator) 461 pos_of_separator2 = datestring.find(self.separator, 462 pos_of_separator1 + 1) 463 if region == self.region_year: 464 return range(0, pos_of_separator1 + 1) 465 elif region == self.region_month: 466 return range(pos_of_separator1 + 1, pos_of_separator2 + 1) 467 else: 468 return range(pos_of_separator2 + 1, len(datestring) + 1) 469 if region_is_not_valid(n): 470 return None 471 date = self.py_date_picker.get_date_string() 472 if not date_has_exactly_two_seperators(date): 473 return None 474 pos_range = calculate_pos_range(n, date) 475 return pos_range 476 477
478 -class PyTimePicker(wx.TextCtrl):
479
480 - def __init__(self, parent):
481 wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER) 482 self.controller = PyTimePickerController(self) 483 self._bind_events() 484 self._resize_to_fit_text()
485
486 - def get_py_time(self):
487 return self.controller.get_py_time()
488
489 - def set_py_time(self, py_time):
490 self.controller.set_py_time(py_time)
491
492 - def get_time_string(self):
493 return self.GetValue()
494
495 - def set_time_string(self, time_string):
497
498 - def _bind_events(self):
499 def on_set_focus(evt): 500 # CallAfter is a trick to prevent default behavior of selecting all 501 # text when a TextCtrl is given focus 502 wx.CallAfter(self.controller.on_set_focus)
503 self.Bind(wx.EVT_SET_FOCUS, on_set_focus) 504 def on_kill_focus(evt): 505 # Trick to not make selection text disappear when focus is lost (we 506 # remove the selection instead) 507 self.controller.on_kill_focus() 508 self.SetSelection(0, 0)
509 self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) 510 def on_char(evt): 511 if evt.GetKeyCode() == wx.WXK_TAB: 512 if evt.ShiftDown(): 513 skip = self.controller.on_shift_tab() 514 else: 515 skip = self.controller.on_tab() 516 else: 517 skip = True 518 evt.Skip(skip) 519 self.Bind(wx.EVT_CHAR, on_char) 520 def on_text(evt): 521 self.controller.on_text_changed() 522 self.Bind(wx.EVT_TEXT, on_text) 523 def on_key_down(evt): 524 if evt.GetKeyCode() == wx.WXK_UP: 525 self.controller.on_up() 526 elif evt.GetKeyCode() == wx.WXK_DOWN: 527 self.controller.on_down() 528 else: 529 evt.Skip() 530 self.Bind(wx.EVT_KEY_DOWN, on_key_down) 531
532 - def _resize_to_fit_text(self):
533 w, h = self.GetTextExtent("00:00") 534 width = w + 20 535 self.SetMinSize((width, -1))
536 537
538 -class PyTimePickerController(object):
539
540 - def __init__(self, py_time_picker):
541 self.py_time_picker = py_time_picker 542 self.original_bg = self.py_time_picker.GetBackgroundColour() 543 self.separator = PyTimeType().event_time_string(PyTimeType().now())[2] 544 self.hour_part = 0 545 self.minute_part = 1 546 self.last_selection = None
547
548 - def get_py_time(self):
549 try: 550 split = self.py_time_picker.get_time_string().split(self.separator) 551 if len(split) != 2: 552 raise ValueError() 553 hour_string, minute_string = split 554 hour = int(hour_string) 555 minute = int(minute_string) 556 return datetime.time(hour, minute) 557 except ValueError: 558 raise ValueError("Invalid time.")
559
560 - def set_py_time(self, py_time):
561 time_string = PyTimeType().event_time_string(py_time) 562 self.py_time_picker.set_time_string(time_string)
563
564 - def on_set_focus(self):
565 if self.last_selection: 566 start, end = self.last_selection 567 self.py_time_picker.SetSelection(start, end) 568 else: 569 self._select_part(self.hour_part)
570
571 - def on_kill_focus(self):
572 self.last_selection = self.py_time_picker.GetSelection()
573
574 - def on_tab(self):
575 if self._in_minute_part(): 576 return True 577 self._select_part(self.minute_part) 578 return False
579
580 - def on_shift_tab(self):
581 if self._in_hour_part(): 582 return True 583 self._select_part(self.hour_part) 584 return False
585
586 - def on_text_changed(self):
587 try: 588 self.get_py_time() 589 self.py_time_picker.SetBackgroundColour(self.original_bg) 590 except ValueError: 591 self.py_time_picker.SetBackgroundColour("pink") 592 self.py_time_picker.Refresh()
593
594 - def on_up(self):
595 def increment_hour(time): 596 new_hour = time.hour + 1 597 if new_hour > 23: 598 new_hour = 0 599 return time.replace(hour=new_hour)
600 def increment_minutes(time): 601 new_hour = time.hour 602 new_minute = time.minute + 1 603 if new_minute > 59: 604 new_minute = 0 605 new_hour = time.hour + 1 606 if new_hour > 23: 607 new_hour = 0 608 return time.replace(hour=new_hour, minute=new_minute)
609 if not self._time_is_valid(): 610 return 611 selection = self.py_time_picker.GetSelection() 612 current_time = self.get_py_time() 613 if self._in_hour_part(): 614 new_time = increment_hour(current_time) 615 else: 616 new_time = increment_minutes(current_time) 617 if current_time != new_time: 618 self._set_new_time_and_restore_selection(new_time, selection) 619
620 - def on_down(self):
621 def decrement_hour(time): 622 new_hour = time.hour - 1 623 if new_hour < 0: 624 new_hour = 23 625 return time.replace(hour=new_hour)
626 def decrement_minutes(time): 627 new_hour = time.hour 628 new_minute = time.minute - 1 629 if new_minute < 0: 630 new_minute = 59 631 new_hour = time.hour - 1 632 if new_hour < 0: 633 new_hour = 23 634 return time.replace(hour=new_hour, minute=new_minute) 635 if not self._time_is_valid(): 636 return 637 selection = self.py_time_picker.GetSelection() 638 current_time = self.get_py_time() 639 if self._in_hour_part(): 640 new_time = decrement_hour(current_time) 641 else: 642 new_time = decrement_minutes(current_time) 643 if current_time != new_time: 644 self._set_new_time_and_restore_selection(new_time, selection) 645
646 - def _set_new_time_and_restore_selection(self, new_time, selection):
647 def restore_selection(selection): 648 self.py_time_picker.SetSelection(selection[0], selection[1])
649 self.set_py_time(new_time) 650 restore_selection(selection) 651
652 - def _time_is_valid(self):
653 try: 654 self.get_py_time() 655 except ValueError: 656 return False 657 return True
658
659 - def _select_part(self, part):
660 if self._separator_pos() == -1: 661 return 662 if part == self.hour_part: 663 self.py_time_picker.SetSelection(0, self._separator_pos()) 664 else: 665 time_string_len = len(self.py_time_picker.get_time_string()) 666 self.py_time_picker.SetSelection(self._separator_pos() + 1, time_string_len) 667 self.preferred_part = part
668
669 - def _in_hour_part(self):
670 if self._separator_pos() == -1: 671 return 672 return self.py_time_picker.GetInsertionPoint() <= self._separator_pos()
673
674 - def _in_minute_part(self):
675 if self._separator_pos() == -1: 676 return 677 return self.py_time_picker.GetInsertionPoint() > self._separator_pos()
678
679 - def _separator_pos(self):
680 return self.py_time_picker.get_time_string().find(self.separator)
681