1# ------------------------------------------------------------------------------ 2# 3# Copyright (c) 2005--2009, Enthought, Inc. 4# All rights reserved. 5# 6# This software is provided without warranty under the terms of the BSD 7# license included in LICENSE.txt and may be redistributed only 8# under the conditions described in the aforementioned license. The license 9# is also available online at http://www.enthought.com/licenses/BSD.txt 10# 11# Thanks for using Enthought open source! 12# 13# Author: Judah De Paula <judah@enthought.com> 14# Date: 2/26/2009 15# 16# ------------------------------------------------------------------------------ 17""" 18A Traits UI editor that wraps a WX calendar panel. 19 20Future Work 21----------- 22The class needs to be extend to provide the four basic editor types, 23Simple, Custom, Text, and ReadOnly. 24""" 25 26import datetime 27import logging 28 29import wx 30import wx.adv 31 32from traitsui.wx.editor import Editor 33from traitsui.wx.constants import WindowColor 34from traitsui.wx.text_editor import ReadonlyEditor as TextReadonlyEditor 35 36 37logger = logging.getLogger(__name__) 38 39 40# ------------------------------------------------------------------------------ 41# -- Simple Editor 42# ------------------------------------------------------------------------------ 43 44 45class SimpleEditor(Editor): 46 """ 47 Simple Traits UI date editor. Shows a text box, and a date-picker widget. 48 """ 49 50 def init(self, parent): 51 """ 52 Finishes initializing the editor by creating the underlying widget. 53 """ 54 date_widget = wx.adv.DatePickerCtrl 55 56 self.control = date_widget( 57 parent, 58 size=(120, -1), 59 style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY | wx.adv.DP_ALLOWNONE, 60 ) 61 self.control.Bind(wx.adv.EVT_DATE_CHANGED, self.day_selected) 62 return 63 64 def day_selected(self, event): 65 """ 66 Event for when calendar is selected, update/create date string. 67 """ 68 date = event.GetDate() 69 # WX sometimes has year == 0 temporarily when doing state changes. 70 if date.IsValid() and date.GetYear() != 0: 71 year = date.GetYear() 72 # wx 2.8.8 has 0-indexed months. 73 month = date.GetMonth() + 1 74 day = date.GetDay() 75 try: 76 self.value = datetime.date(year, month, day) 77 except ValueError: 78 logger.exception("Invalid date: %d-%d-%d (y-m-d)", (year, month, day)) 79 raise 80 return 81 82 def update_editor(self): 83 """ 84 Updates the editor when the object trait changes externally to the 85 editor. 86 """ 87 if self.value: 88 date = self.control.GetValue() 89 # FIXME: A Trait assignment should support fixing an invalid 90 # date in the widget. 91 if date.IsValid(): 92 # Important: set the day before setting the month, otherwise wx may fail 93 # to set the month. 94 date.SetYear(self.value.year) 95 date.SetDay(self.value.day) 96 # wx 2.8.8 has 0-indexed months. 97 date.SetMonth(self.value.month - 1) 98 self.control.SetValue(date) 99 self.control.Refresh() 100 return 101 102 103# -- end SimpleEditor definition ----------------------------------------------- 104 105 106# ------------------------------------------------------------------------------ 107# -- Custom Editor 108# ------------------------------------------------------------------------------ 109 110SELECTED_FG = wx.Colour(255, 0, 0) 111UNAVAILABLE_FG = wx.Colour(192, 192, 192) 112DRAG_HIGHLIGHT_FG = wx.Colour(255, 255, 255) 113DRAG_HIGHLIGHT_BG = wx.Colour(128, 128, 255) 114try: 115 MOUSE_BOX_FILL = wx.Colour(0, 0, 255, 32) 116 NORMAL_HIGHLIGHT_FG = wx.Colour(0, 0, 0, 0) 117 NORMAL_HIGHLIGHT_BG = wx.Colour(255, 255, 255, 0) 118# Alpha channel in wx.Colour does not exist prior to version 2.7.1.1 119except TypeError: 120 MOUSE_BOX_FILL = wx.Colour(0, 0, 255) 121 NORMAL_HIGHLIGHT_FG = wx.Colour(0, 0, 0) 122 NORMAL_HIGHLIGHT_BG = wx.Colour(255, 255, 255) 123 124 125class wxMouseBoxCalendarCtrl(wx.adv.CalendarCtrl): 126 """ 127 Subclass to add a mouse-over box-selection tool. 128 129 Description 130 ----------- 131 Add a Mouse drag-box highlight feature that can be used by the 132 CustomEditor to detect user selections. CalendarCtrl must be subclassed 133 to get a device context to draw on top of the Calendar, otherwise the 134 calendar widgets are always painted on top of the box during repaints. 135 """ 136 137 def __init__(self, *args, **kwargs): 138 super(wxMouseBoxCalendarCtrl, self).__init__(*args, **kwargs) 139 140 self.selecting = False 141 self.box_selected = [] 142 self.sel_start = (0, 0) 143 self.sel_end = (0, 0) 144 self.Bind(wx.EVT_RIGHT_DOWN, self.start_select) 145 self.Bind(wx.EVT_RIGHT_UP, self.end_select) 146 self.Bind(wx.EVT_LEAVE_WINDOW, self.end_select) 147 self.Bind(wx.EVT_MOTION, self.on_select) 148 self.Bind(wx.EVT_PAINT, self.on_paint) 149 self.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.highlight_changed) 150 151 def boxed_days(self): 152 """ 153 Compute the days that are under the box selection. 154 155 Returns 156 ------- 157 A list of wx.DateTime objects under the mouse box. 158 """ 159 x1, y1 = self.sel_start 160 x2, y2 = self.sel_end 161 if x1 > x2: 162 x1, x2 = x2, x1 163 if y1 > y2: 164 y1, y2 = y2, y1 165 166 grid = [] 167 for i in range(x1, x2, 15): 168 for j in range(y1, y2, 15): 169 grid.append(wx.Point(i, j)) 170 grid.append(wx.Point(i, y2)) 171 # Avoid jitter along the edge since the final points change. 172 for j in range(y1, y2, 20): 173 grid.append(wx.Point(x2, j)) 174 grid.append(wx.Point(x2, y2)) 175 176 selected_days = [] 177 for point in grid: 178 (result, date, weekday) = self.HitTest(point) 179 if result == wx.adv.CAL_HITTEST_DAY: 180 if date not in selected_days: 181 selected_days.append(date) 182 183 return selected_days 184 185 def highlight_changed(self, event=None): 186 """ 187 Hide the default highlight to take on the selected date attr. 188 189 Description 190 ----------- 191 A feature of the wx CalendarCtrl is that there are selected days, 192 that always are shown and the user can move around with left-click. 193 But it's confusing and misleading when there are multiple 194 CalendarCtrl objects linked in one editor. So we hide the 195 highlights in this CalendarCtrl by making it mimic the attribute 196 of the selected day. 197 198 Highlights apparently can't take on a border style, so to be truly 199 invisible, normal days cannot have borders. 200 """ 201 if event: 202 event.Skip() 203 date = self.GetDate() 204 205 attr = self.GetAttr(date.GetDay()) 206 if attr is None: 207 bg_color = NORMAL_HIGHLIGHT_BG 208 fg_color = NORMAL_HIGHLIGHT_FG 209 else: 210 bg_color = attr.GetBackgroundColour() 211 fg_color = attr.GetTextColour() 212 self.SetHighlightColours(fg_color, bg_color) 213 self.Refresh() 214 return 215 216 # -- event handlers -------------------------------------------------------- 217 def start_select(self, event): 218 event.Skip() 219 self.selecting = True 220 self.box_selected = [] 221 self.sel_start = (event.m_x, event.m_y) 222 self.sel_end = self.sel_start 223 224 def end_select(self, event): 225 event.Skip() 226 self.selecting = False 227 self.Refresh() 228 229 def on_select(self, event): 230 event.Skip() 231 if not self.selecting: 232 return 233 234 self.sel_end = (event.m_x, event.m_y) 235 self.box_selected = self.boxed_days() 236 self.Refresh() 237 238 def on_paint(self, event): 239 event.Skip() 240 dc = wx.PaintDC(self) 241 242 if not self.selecting: 243 return 244 245 x = self.sel_start[0] 246 y = self.sel_start[1] 247 w = self.sel_end[0] - x 248 h = self.sel_end[1] - y 249 250 gc = wx.GraphicsContext.Create(dc) 251 pen = gc.CreatePen(wx.BLACK_PEN) 252 gc.SetPen(pen) 253 254 points = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] 255 256 gc.DrawLines(points) 257 258 brush = gc.CreateBrush(wx.Brush(MOUSE_BOX_FILL)) 259 gc.SetBrush(brush) 260 gc.DrawRectangle(x, y, w, h) 261 262# -- end wxMouseBoxCalendarCtrl ------------------------------------------------ 263 264 265class MultiCalendarCtrl(wx.Panel): 266 """ 267 WX panel containing calendar widgets for use by the CustomEditor. 268 269 Description 270 ----------- 271 Handles multi-selection of dates by special handling of the 272 wxMouseBoxCalendarCtrl widget. Doing single-select across multiple 273 calendar widgets is also supported though most of the interesting 274 functionality is then unused. 275 """ 276 277 def __init__( 278 self, 279 parent, 280 ID, 281 editor, 282 multi_select, 283 shift_to_select, 284 on_mixed_select, 285 allow_future, 286 months, 287 padding, 288 *args, 289 **kwargs 290 ): 291 super(MultiCalendarCtrl, self).__init__(parent, ID, *args, **kwargs) 292 293 self.sizer = wx.BoxSizer() 294 self.SetSizer(self.sizer) 295 self.SetBackgroundColour(WindowColor) 296 self.date = wx.DateTime.Now() 297 self.today = self.date_from_datetime(self.date) 298 299 # Object attributes 300 self.multi_select = multi_select 301 self.shift_to_select = shift_to_select 302 self.on_mixed_select = on_mixed_select 303 self.allow_future = allow_future 304 self.editor = editor 305 self.selected_days = editor.value 306 self.months = months 307 self.padding = padding 308 self.cal_ctrls = [] 309 310 # State to remember when a user is doing a shift-click selection. 311 self._first_date = None 312 self._drag_select = [] 313 self._box_select = [] 314 315 # Set up the individual month frames. 316 for i in range(-(self.months - 1), 1): 317 cal = self._make_calendar_widget(i) 318 self.cal_ctrls.insert(0, cal) 319 if i != 0: 320 self.sizer.AddSpacer(padding) 321 322 # Initial painting 323 self.selected_list_changed() 324 return 325 326 def date_from_datetime(self, dt): 327 """ 328 Convert a wx DateTime object to a Python Date object. 329 330 Parameters 331 ---------- 332 dt : wx.DateTime 333 A valid date to convert to a Python Date object 334 """ 335 new_date = datetime.date(dt.GetYear(), dt.GetMonth() + 1, dt.GetDay()) 336 return new_date 337 338 def datetime_from_date(self, date): 339 """ 340 Convert a Python Date object to a wx DateTime object. Ignores time. 341 342 Parameters 343 ---------- 344 date : datetime.Date object 345 A valid date to convert to a wx.DateTime object. Since there 346 is no time information in a Date object the defaults of DateTime 347 are used. 348 """ 349 dt = wx.DateTime() 350 dt.SetYear(date.year) 351 dt.SetMonth(date.month - 1) 352 dt.SetDay(date.day) 353 return dt 354 355 def shift_datetime(self, old_date, months): 356 """ 357 Create a new DateTime from *old_date* with an offset number of *months*. 358 359 Parameters 360 ---------- 361 old_date : DateTime 362 The old DateTime to make a date copy of. Does not copy time. 363 months : int 364 A signed int to add or subtract from the old date months. Does 365 not support jumping more than 12 months. 366 """ 367 new_date = wx.DateTime() 368 new_month = old_date.GetMonth() + months 369 new_year = old_date.GetYear() 370 if new_month < 0: 371 new_month += 12 372 new_year -= 1 373 elif new_month > 11: 374 new_month -= 12 375 new_year += 1 376 377 new_day = min(old_date.GetDay(), 28) 378 new_date.Set(new_day, new_month, new_year) 379 return new_date 380 381 def selected_list_changed(self, evt=None): 382 """ Update the date colors of the days in the widgets. """ 383 for cal in self.cal_ctrls: 384 cur_month = cal.GetDate().GetMonth() + 1 385 cur_year = cal.GetDate().GetYear() 386 selected_days = self.selected_days 387 388 # When multi_select is False wrap in a list to pass the for-loop. 389 if not self.multi_select: 390 if selected_days is None: 391 selected_days = [] 392 else: 393 selected_days = [selected_days] 394 395 # Reset all the days to the correct colors. 396 for day in range(1, 32): 397 try: 398 paint_day = datetime.date(cur_year, cur_month, day) 399 if not self.allow_future and paint_day > self.today: 400 attr = wx.adv.CalendarDateAttr( 401 colText=UNAVAILABLE_FG 402 ) 403 cal.SetAttr(day, attr) 404 elif paint_day in selected_days: 405 attr = wx.adv.CalendarDateAttr( 406 colText=SELECTED_FG 407 ) 408 cal.SetAttr(day, attr) 409 else: 410 cal.ResetAttr(day) 411 except ValueError: 412 # Blindly creating Date objects sometimes produces invalid. 413 pass 414 415 cal.highlight_changed() 416 return 417 418 def _make_calendar_widget(self, month_offset): 419 """ 420 Add a calendar widget to the screen and hook up callbacks. 421 422 Parameters 423 ---------- 424 month_offset : int 425 The number of months from today, that the calendar should 426 start at. 427 """ 428 date = self.shift_datetime(self.date, month_offset) 429 panel = wx.Panel(self, -1) 430 cal = wxMouseBoxCalendarCtrl( 431 panel, 432 -1, 433 date, 434 style=wx.adv.CAL_SUNDAY_FIRST 435 | wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION 436 # | wx.adv.CAL_SHOW_HOLIDAYS 437 ) 438 self.sizer.Add(panel) 439 cal.highlight_changed() 440 441 # Set up control to sync the other calendar widgets and coloring: 442 cal.Bind(wx.adv.EVT_CALENDAR_MONTH, self.month_changed) 443 cal.Bind(wx.adv.EVT_CALENDAR_YEAR, self.month_changed) 444 445 cal.Bind(wx.EVT_LEFT_DOWN, self._left_down) 446 447 if self.multi_select: 448 cal.Bind(wx.EVT_LEFT_UP, self._left_up) 449 cal.Bind(wx.EVT_RIGHT_UP, self._process_box_select) 450 cal.Bind(wx.EVT_LEAVE_WINDOW, self._process_box_select) 451 cal.Bind(wx.EVT_MOTION, self._mouse_drag) 452 self.Bind( 453 wx.adv.EVT_CALENDAR_WEEKDAY_CLICKED, 454 self._weekday_clicked, 455 cal, 456 ) 457 return cal 458 459 def unhighlight_days(self, days): 460 """ 461 Turn off all highlights in all cals, but leave any selected color. 462 463 Parameters 464 ---------- 465 days : List(Date) 466 The list of dates to add. Possibly includes dates in the future. 467 """ 468 for cal in self.cal_ctrls: 469 c = cal.GetDate() 470 for date in days: 471 if date.year == c.GetYear() and date.month == c.GetMonth() + 1: 472 473 # Unselected days either need to revert to the 474 # unavailable color, or the default attribute color. 475 if not self.allow_future and ( 476 (date.year, date.month, date.day) 477 > (self.today.year, self.today.month, self.today.day) 478 ): 479 attr = wx.adv.CalendarDateAttr( 480 colText=UNAVAILABLE_FG 481 ) 482 else: 483 attr = wx.adv.CalendarDateAttr( 484 colText=NORMAL_HIGHLIGHT_FG, 485 colBack=NORMAL_HIGHLIGHT_BG, 486 ) 487 if date in self.selected_days: 488 attr.SetTextColour(SELECTED_FG) 489 cal.SetAttr(date.day, attr) 490 cal.highlight_changed() 491 return 492 493 def highlight_days(self, days): 494 """ 495 Color the highlighted list of days across all calendars. 496 497 Parameters 498 ---------- 499 days : List(Date) 500 The list of dates to add. Possibly includes dates in the future. 501 """ 502 for cal in self.cal_ctrls: 503 c = cal.GetDate() 504 for date in days: 505 if date.year == c.GetYear() and date.month == c.GetMonth() + 1: 506 attr = wx.adv.CalendarDateAttr( 507 colText=DRAG_HIGHLIGHT_FG, colBack=DRAG_HIGHLIGHT_BG 508 ) 509 cal.SetAttr(date.day, attr) 510 cal.highlight_changed() 511 cal.Refresh() 512 513 def add_days_to_selection(self, days): 514 """ 515 Add a list of days to the selection, using a specified style. 516 517 Parameters 518 ---------- 519 days : List(Date) 520 The list of dates to add. Possibly includes dates in the future. 521 522 Description 523 ----------- 524 When a user multi-selects entries and some of those entries are 525 already selected and some are not, what should be the behavior for 526 the seletion? Options:: 527 528 'toggle' -- Toggle each day to it's opposite state. 529 'on' -- Always turn them on. 530 'off' -- Always turn them off. 531 'max_change' -- Change all to same state, with most days changing. 532 For example 1 selected and 9 not, then they would 533 all get selected. 534 'min_change' -- Change all to same state, with min days changing. 535 For example 1 selected and 9 not, then they would 536 all get unselected. 537 """ 538 if not days: 539 return 540 style = self.on_mixed_select 541 new_list = list(self.selected_days) 542 543 if style == "toggle": 544 for day in days: 545 if self.allow_future or day <= self.today: 546 if day in new_list: 547 new_list.remove(day) 548 else: 549 new_list.append(day) 550 551 else: 552 already_selected = len([day for day in days if day in new_list]) 553 554 if style == "on" or already_selected == 0: 555 add_items = True 556 557 elif style == "off" or already_selected == len(days): 558 add_items = False 559 560 elif self.on_mixed_select == "max_change" and already_selected <= ( 561 len(days) / 2.0 562 ): 563 add_items = True 564 565 elif self.on_mixed_select == "min_change" and already_selected > ( 566 len(days) / 2.0 567 ): 568 add_items = True 569 570 else: 571 # Cases where max_change is off or min_change off. 572 add_items = False 573 574 for day in days: 575 # Skip if we don't allow future, and it's a future day. 576 if self.allow_future or day <= self.today: 577 if add_items and day not in new_list: 578 new_list.append(day) 579 elif not add_items and day in new_list: 580 new_list.remove(day) 581 582 self.selected_days = new_list 583 # Link the list back to the model to make a Traits List change event. 584 self.editor.value = new_list 585 return 586 587 def single_select_day(self, dt): 588 """ 589 In non-multiselect switch the selection to a new date. 590 591 Parameters 592 ---------- 593 dt : wx.DateTime 594 The newly selected date that should become the new calendar 595 selection. 596 597 Description 598 ----------- 599 Only called when we're using the single-select mode of the 600 calendar widget, so we can assume that the selected_dates is 601 a None or a Date singleton. 602 """ 603 selection = self.date_from_datetime(dt) 604 605 if dt.IsValid() and (self.allow_future or selection <= self.today): 606 self.selected_days = selection 607 self.selected_list_changed() 608 # Modify the trait on the editor so that the events propagate. 609 self.editor.value = self.selected_days 610 return 611 612 def _shift_drag_update(self, event): 613 """ Shift-drag in progress. """ 614 cal = event.GetEventObject() 615 result, dt, weekday = cal.HitTest(event.GetPosition()) 616 617 self.unhighlight_days(self._drag_select) 618 self._drag_select = [] 619 620 # Prepare for an abort, don't highlight new selections. 621 if ( 622 self.shift_to_select and not event.ShiftDown() 623 ) or result != wx.adv.CAL_HITTEST_DAY: 624 625 cal.highlight_changed() 626 for cal in self.cal_ctrls: 627 cal.Refresh() 628 return 629 630 # Construct the list of selections. 631 last_date = self.date_from_datetime(dt) 632 if last_date <= self._first_date: 633 first, last = last_date, self._first_date 634 else: 635 first, last = self._first_date, last_date 636 while first <= last: 637 if self.allow_future or first <= self.today: 638 self._drag_select.append(first) 639 first = first + datetime.timedelta(1) 640 641 self.highlight_days(self._drag_select) 642 return 643 644 # ------------------------------------------------------------------------ 645 # Event handlers 646 # ------------------------------------------------------------------------ 647 648 def _process_box_select(self, event): 649 """ 650 Possibly move the calendar box-selected days into our selected days. 651 """ 652 event.Skip() 653 self.unhighlight_days(self._box_select) 654 655 if not event.Leaving(): 656 self.add_days_to_selection(self._box_select) 657 self.selected_list_changed() 658 659 self._box_select = [] 660 661 def _weekday_clicked(self, evt): 662 """ A day on the weekday bar has been clicked. Select all days. """ 663 evt.Skip() 664 weekday = evt.GetWeekDay() 665 cal = evt.GetEventObject() 666 month = cal.GetDate().GetMonth() + 1 667 year = cal.GetDate().GetYear() 668 669 days = [] 670 # Messy math to compute the dates of each weekday in the month. 671 # Python uses Monday=0, while wx uses Sunday=0. 672 month_start_weekday = (datetime.date(year, month, 1).weekday() + 1) % 7 673 weekday_offset = (weekday - month_start_weekday) % 7 674 for day in range(weekday_offset, 31, 7): 675 try: 676 day = datetime.date(year, month, day + 1) 677 if self.allow_future or day <= self.today: 678 days.append(day) 679 except ValueError: 680 pass 681 self.add_days_to_selection(days) 682 683 self.selected_list_changed() 684 return 685 686 def _left_down(self, event): 687 """ Handle user selection of days. """ 688 event.Skip() 689 cal = event.GetEventObject() 690 result, dt, weekday = cal.HitTest(event.GetPosition()) 691 692 if result == wx.adv.CAL_HITTEST_DAY and not self.multi_select: 693 self.single_select_day(dt) 694 return 695 696 # Inter-month-drag selection. A quick no-movement mouse-click is 697 # equivalent to a multi-select of a single day. 698 if ( 699 result == wx.adv.CAL_HITTEST_DAY 700 and (not self.shift_to_select or event.ShiftDown()) 701 and not cal.selecting 702 ): 703 704 self._first_date = self.date_from_datetime(dt) 705 self._drag_select = [self._first_date] 706 # Start showing the highlight colors with a mouse_drag event. 707 self._mouse_drag(event) 708 709 return 710 711 def _left_up(self, event): 712 """ Handle the end of a possible run-selection. """ 713 event.Skip() 714 cal = event.GetEventObject() 715 result, dt, weekday = cal.HitTest(event.GetPosition()) 716 717 # Complete a drag-select operation. 718 if ( 719 result == wx.adv.CAL_HITTEST_DAY 720 and (not self.shift_to_select or event.ShiftDown()) 721 and self._first_date 722 ): 723 724 last_date = self.date_from_datetime(dt) 725 if last_date <= self._first_date: 726 first, last = last_date, self._first_date 727 else: 728 first, last = self._first_date, last_date 729 730 newly_selected = [] 731 while first <= last: 732 newly_selected.append(first) 733 first = first + datetime.timedelta(1) 734 self.add_days_to_selection(newly_selected) 735 self.unhighlight_days(newly_selected) 736 737 # Reset a drag-select operation, even if it wasn't completed because 738 # of a loss of focus or the Shift key prematurely released. 739 self._first_date = None 740 self._drag_select = [] 741 742 self.selected_list_changed() 743 return 744 745 def _mouse_drag(self, event): 746 """ Called when the mouse in being dragged within the main panel. """ 747 event.Skip() 748 cal = event.GetEventObject() 749 if not cal.selecting and self._first_date: 750 self._shift_drag_update(event) 751 if cal.selecting: 752 self.unhighlight_days(self._box_select) 753 self._box_select = [ 754 self.date_from_datetime(dt) for dt in cal.boxed_days() 755 ] 756 self.highlight_days(self._box_select) 757 return 758 759 def month_changed(self, evt=None): 760 """ 761 Link the calendars together so if one changes, they all change. 762 763 TODO: Maybe wx.adv.CAL_HITTEST_INCMONTH could be checked and 764 the event skipped, rather than now where we undo the update after 765 the event has gone through. 766 """ 767 evt.Skip() 768 cal_index = self.cal_ctrls.index(evt.GetEventObject()) 769 current_date = self.cal_ctrls[cal_index].GetDate() 770 for i, cal in enumerate(self.cal_ctrls): 771 # Current month is already updated, just need to shift the others 772 if i != cal_index: 773 new_date = self.shift_datetime(current_date, cal_index - i) 774 cal.SetDate(new_date) 775 cal.highlight_changed() 776 777 # Back-up if we're not allowed to move into future months. 778 if not self.allow_future: 779 month = self.cal_ctrls[0].GetDate().GetMonth() + 1 780 year = self.cal_ctrls[0].GetDate().GetYear() 781 if (year, month) > (self.today.year, self.today.month): 782 for i, cal in enumerate(self.cal_ctrls): 783 new_date = self.shift_datetime(wx.DateTime.Now(), -i) 784 cal.SetDate(new_date) 785 cal.highlight_changed() 786 787 # Redraw the selected days. 788 self.selected_list_changed() 789 790 791# -- end CalendarCtrl ---------------------------------------------------------- 792 793 794class CustomEditor(Editor): 795 """ 796 Show multiple months with MultiCalendarCtrl. Allow multi-select. 797 798 Trait Listeners 799 --------------- 800 The wx editor directly modifies the *value* trait of the Editor, which 801 is the named trait of the corresponding Item in your View. Therefore 802 you can listen for changes to the user's selection by directly listening 803 to the item changed event. 804 805 TODO 806 ---- 807 Some more listeners need to be hooked up. For example, in single-select 808 mode, changing the value does not cause the calendar to update. Also, 809 the selection-add and remove is noisy, triggering an event for each 810 addition rather than waiting until everything has been added and removed. 811 812 Sample 813 ------ 814 Example usage:: 815 816 class DateListPicker(HasTraits): 817 calendar = List() 818 traits_view = View(Item('calendar', editor=DateEditor(), 819 style='custom', show_label=False)) 820 """ 821 822 # -- Editor interface ------------------------------------------------------ 823 824 def init(self, parent): 825 """ 826 Finishes initializing the editor by creating the underlying widget. 827 """ 828 if self.factory.multi_select and not isinstance(self.value, list): 829 raise ValueError("Multi-select is True, but editing a non-list.") 830 elif not self.factory.multi_select and isinstance(self.value, list): 831 raise ValueError("Multi-select is False, but editing a list.") 832 833 calendar_ctrl = MultiCalendarCtrl( 834 parent, 835 -1, 836 self, 837 self.factory.multi_select, 838 self.factory.shift_to_select, 839 self.factory.on_mixed_select, 840 self.factory.allow_future, 841 self.factory.months, 842 self.factory.padding, 843 ) 844 self.control = calendar_ctrl 845 return 846 847 def update_editor(self): 848 """ 849 Updates the editor when the object trait changes externally to the 850 editor. 851 """ 852 self.control.selected_list_changed() 853 return 854 855 856# -- end CustomEditor definition ----------------------------------------------- 857 858 859# ------------------------------------------------------------------------------ 860# -- Text Editor 861# ------------------------------------------------------------------------------ 862# TODO: Write me. Possibly use TextEditor as a model to show a string 863# representation of the date, and have enter-set do a date evaluation. 864class TextEditor(SimpleEditor): 865 pass 866 867 868# -- end TextEditor definition ------------------------------------------------- 869 870 871# ------------------------------------------------------------------------------ 872# -- Readonly Editor 873# ------------------------------------------------------------------------------ 874 875 876class ReadonlyEditor(TextReadonlyEditor): 877 """ Use a TextEditor for the view. """ 878 879 def _get_str_value(self): 880 """ Replace the default string value with our own date verision. """ 881 if not self.value: 882 return self.factory.message 883 else: 884 return self.value.strftime(self.factory.strftime) 885 886 887# -- end ReadonlyEditor definition --------------------------------------------- 888 889# -- eof ----------------------------------------------------------------------- 890