File: date_editor.py

package info (click to toggle)
python-traitsui 4.4.0-1.3
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,680 kB
  • ctags: 6,394
  • sloc: python: 32,786; makefile: 16; sh: 5
file content (875 lines) | stat: -rw-r--r-- 30,833 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
#------------------------------------------------------------------------------
#
#  Copyright (c) 2005--2009, Enthought, Inc.
#  All rights reserved.
#
#  This software is provided without warranty under the terms of the BSD
#  license included in enthought/LICENSE.txt and may be redistributed only
#  under the conditions described in the aforementioned license.  The license
#  is also available online at http://www.enthought.com/licenses/BSD.txt
#
#  Thanks for using Enthought open source!
#
#  Author: Judah De Paula <judah@enthought.com>
#  Date:   2/26/2009
#
#------------------------------------------------------------------------------
"""
A Traits UI editor that wraps a WX calendar panel.

Future Work
-----------
The class needs to be extend to provide the four basic editor types,
Simple, Custom, Text, and ReadOnly.
"""
import datetime

import wx
import wx.calendar

from traits.api import Bool
from traitsui.wx.editor import Editor
from traitsui.wx.constants import WindowColor
from traitsui.wx.text_editor \
    import ReadonlyEditor as TextReadonlyEditor


#------------------------------------------------------------------------------
#--  Simple Editor
#------------------------------------------------------------------------------

class SimpleEditor (Editor):
    """
    Simple Traits UI date editor.  Shows a text box, and a date-picker widget.
    """

    def init ( self, parent ):
        """
        Finishes initializing the editor by creating the underlying widget.
        """
        # MS-Win's DatePickerCtrl comes with a check-box we don't want.
        # GenericDatePickerCtrl was exposed in wxPython version 2.8.8 only.
        if 'wxMSW' in wx.PlatformInfo and wx.VERSION > (2,8,8):
            date_widget = wx.GenericDatePickerCtrl
        else:
            # Linux / OS-X / windows
            date_widget = wx.DatePickerCtrl

        self.control = date_widget(parent,
                                   size=(120,-1),
                                   style = wx.DP_DROPDOWN
                                         | wx.DP_SHOWCENTURY
                                         | wx.DP_ALLOWNONE)
        self.control.Bind(wx.EVT_DATE_CHANGED, self.day_selected)
        return


    def day_selected(self, event):
        """
        Event for when calendar is selected, update/create date string.
        """
        date = event.GetDate()
        # WX sometimes has year == 0 temporarily when doing state changes.
        if date.IsValid() and date.GetYear() != 0:
            year = date.GetYear()
            # wx 2.8.8 has 0-indexed months.
            month = date.GetMonth() + 1
            day = date.GetDay()
            try:
                self.value = datetime.date(year, month, day)
            except ValueError:
                print 'Invalid date:', year, month, day
                raise
        return


    def update_editor ( self ):
        """
        Updates the editor when the object trait changes externally to the
        editor.
        """
        if self.value:
            date = self.control.GetValue()
            # FIXME: A Trait assignment should support fixing an invalid
            # date in the widget.
            if date.IsValid():
                # Important: set the day before setting the month, otherwise wx may fail
                # to set the month.
                date.SetYear(self.value.year)
                date.SetDay(self.value.day)
                # wx 2.8.8 has 0-indexed months.
                date.SetMonth(self.value.month - 1)
                self.control.SetValue(date)
                self.control.Refresh()
        return
#-- end SimpleEditor definition -----------------------------------------------


#------------------------------------------------------------------------------
#--  Custom Editor
#------------------------------------------------------------------------------

SELECTED_FG = wx.Colour(255, 0, 0)
UNAVAILABLE_FG = wx.Colour(192, 192, 192)
DRAG_HIGHLIGHT_FG = wx.Colour(255, 255, 255)
DRAG_HIGHLIGHT_BG = wx.Colour(128, 128, 255)
try:
    MOUSE_BOX_FILL = wx.Colour(0, 0, 255, 32)
    NORMAL_HIGHLIGHT_FG = wx.Colour(0, 0, 0, 0)
    NORMAL_HIGHLIGHT_BG = wx.Colour(255, 255, 255, 0)
# Alpha channel in wx.Colour does not exist prior to version 2.7.1.1
except TypeError:
    MOUSE_BOX_FILL = wx.Colour(0, 0, 255)
    NORMAL_HIGHLIGHT_FG = wx.Colour(0, 0, 0)
    NORMAL_HIGHLIGHT_BG = wx.Colour(255, 255, 255)

class wxMouseBoxCalendarCtrl(wx.calendar.CalendarCtrl):
    """
    Subclass to add a mouse-over box-selection tool.

    Description
    -----------
    Add a Mouse drag-box highlight feature that can be used by the
    CustomEditor to detect user selections.  CalendarCtrl must be subclassed
    to get a device context to draw on top of the Calendar, otherwise the
    calendar widgets are always painted on top of the box during repaints.
    """

    def __init__(self, *args, **kwargs):
        super(wxMouseBoxCalendarCtrl, self).__init__(*args, **kwargs)

        self.selecting = False
        self.box_selected = []
        self.sel_start = (0,0)
        self.sel_end = (0,0)
        self.Bind(wx.EVT_RIGHT_DOWN, self.start_select)
        self.Bind(wx.EVT_RIGHT_UP, self.end_select)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.end_select)
        self.Bind(wx.EVT_MOTION, self.on_select)
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.calendar.EVT_CALENDAR_SEL_CHANGED, self.highlight_changed)


    def boxed_days(self):
        """
        Compute the days that are under the box selection.

        Returns
        -------
        A list of wx.DateTime objects under the mouse box.
        """
        x1, y1 = self.sel_start
        x2, y2 = self.sel_end
        if x1 > x2:
            x1, x2 = x2, x1
        if y1 > y2:
            y1, y2 = y2, y1

        grid = []
        for i in range(x1, x2, 15):
            for j in range(y1, y2, 15):
                grid.append(wx.Point(i,j))
            grid.append(wx.Point(i, y2))
        # Avoid jitter along the edge since the final points change.
        for j in range(y1, y2, 20):
            grid.append(wx.Point(x2, j))
        grid.append(wx.Point(x2, y2))

        selected_days = []
        for point in grid:
            (result, date, weekday) = self.HitTest(point)
            if result == wx.calendar.CAL_HITTEST_DAY:
                if date not in selected_days:
                    selected_days.append(date)

        return selected_days


    def highlight_changed(self, event=None):
        """
        Hide the default highlight to take on the selected date attr.

        Description
        -----------
        A feature of the wx CalendarCtrl is that there are selected days,
        that always are shown and the user can move around with left-click.
        But it's confusing and misleading when there are multiple
        CalendarCtrl objects linked in one editor.  So we hide the
        highlights in this CalendarCtrl by making it mimic the attribute
        of the selected day.

        Highlights apparently can't take on a border style, so to be truly
        invisible, normal days cannot have borders.
        """
        if event:
            event.Skip()
        date = self.GetDate()

        attr = self.GetAttr(date.GetDay())
        if attr is None:
            bg_color = NORMAL_HIGHLIGHT_BG
            fg_color = NORMAL_HIGHLIGHT_FG
        else:
            bg_color = attr.GetBackgroundColour()
            fg_color = attr.GetTextColour()
        self.SetHighlightColours(fg_color, bg_color)
        self.Refresh()
        return


    #-- event handlers --------------------------------------------------------
    def start_select(self, event):
        event.Skip()
        self.selecting = True
        self.box_selected = []
        self.sel_start = (event.m_x, event.m_y)
        self.sel_end = self.sel_start


    def end_select(self, event):
        event.Skip()
        self.selecting = False
        self.Refresh()


    def on_select(self, event):
        event.Skip()
        if not self.selecting:
            return

        self.sel_end = (event.m_x, event.m_y)
        self.box_selected = self.boxed_days()
        self.Refresh()


    def on_paint(self, event):
        event.Skip()
        dc = wx.PaintDC(self)

        if not self.selecting:
            return

        x = self.sel_start[0]
        y = self.sel_start[1]
        w = self.sel_end[0] - x
        h = self.sel_end[1] - y

        gc = wx.GraphicsContext.Create(dc)
        pen = gc.CreatePen(wx.BLACK_PEN)
        gc.SetPen(pen)

        points = [(x,y), (x+w, y), (x+w,y+h), (x,y+h), (x,y)]

        gc.DrawLines(points)

        brush = gc.CreateBrush(wx.Brush(MOUSE_BOX_FILL))
        gc.SetBrush(brush)
        gc.DrawRectangle(x, y, w, h)
#-- end wxMouseBoxCalendarCtrl ------------------------------------------------


class MultiCalendarCtrl(wx.Panel):
    """
    WX panel containing calendar widgets for use by the CustomEditor.

    Description
    -----------
    Handles multi-selection of dates by special handling of the
    wxMouseBoxCalendarCtrl widget.  Doing single-select across multiple
    calendar widgets is also supported though most of the interesting
    functionality is then unused.
    """

    def __init__(self, parent, ID, editor, multi_select, shift_to_select,
                 on_mixed_select, allow_future, months, padding,
                 *args, **kwargs):
        super(MultiCalendarCtrl, self).__init__(parent, ID, *args, **kwargs)

        self.sizer = wx.BoxSizer()
        self.SetSizer(self.sizer)
        self.SetBackgroundColour(WindowColor)
        self.date = wx.DateTime_Now()
        self.today = self.date_from_datetime(self.date)

        # Object attributes
        self.multi_select = multi_select
        self.shift_to_select = shift_to_select
        self.on_mixed_select = on_mixed_select
        self.allow_future = allow_future
        self.editor = editor
        self.selected_days = editor.value
        self.months = months
        self.padding = padding
        self.cal_ctrls = []

        # State to remember when a user is doing a shift-click selection.
        self._first_date = None
        self._drag_select = []
        self._box_select = []

        # Set up the individual month frames.
        for i in range(-(self.months-1), 1):
            cal = self._make_calendar_widget(i)
            self.cal_ctrls.insert(0, cal)
            if i != 0:
                self.sizer.AddSpacer(wx.Size(padding, padding))

        # Initial painting
        self.selected_list_changed()
        return


    def date_from_datetime(self, dt):
        """
        Convert a wx DateTime object to a Python Date object.

        Parameters
        ----------
        dt : wx.DateTime
            A valid date to convert to a Python Date object
        """
        new_date = datetime.date(dt.GetYear(), dt.GetMonth()+1, dt.GetDay())
        return new_date


    def datetime_from_date(self, date):
        """
        Convert a Python Date object to a wx DateTime object. Ignores time.

        Parameters
        ----------
        date : datetime.Date object
            A valid date to convert to a wx.DateTime object.  Since there
            is no time information in a Date object the defaults of DateTime
            are used.
        """
        dt = wx.DateTime()
        dt.SetYear(date.year)
        dt.SetMonth(date.month-1)
        dt.SetDay(date.day)
        return dt


    def shift_datetime(self, old_date, months):
        """
        Create a new DateTime from *old_date* with an offset number of *months*.

        Parameters
        ----------
        old_date : DateTime
            The old DateTime to make a date copy of.  Does not copy time.
        months : int
            A signed int to add or subtract from the old date months.  Does
            not support jumping more than 12 months.
        """
        new_date = wx.DateTime()
        new_month = old_date.GetMonth() + months
        new_year = old_date.GetYear()
        if new_month < 0:
            new_month += 12
            new_year -= 1
        elif new_month > 11:
            new_month -= 12
            new_year += 1

        new_day = min(old_date.GetDay(), 28)
        new_date.Set(new_day, new_month, new_year)
        return new_date


    def selected_list_changed(self, evt=None):
        """ Update the date colors of the days in the widgets. """
        for cal in self.cal_ctrls:
            cur_month = cal.GetDate().GetMonth() + 1
            cur_year = cal.GetDate().GetYear()
            selected_days = self.selected_days

            # When multi_select is False wrap in a list to pass the for-loop.
            if self.multi_select == False:
                if selected_days == None:
                    selected_days = []
                else:
                    selected_days = [selected_days]

            # Reset all the days to the correct colors.
            for day in range(1,32):
                try:
                    paint_day = datetime.date(cur_year, cur_month, day)
                    if not self.allow_future and paint_day > self.today:
                        attr = wx.calendar.CalendarDateAttr(colText=UNAVAILABLE_FG)
                        cal.SetAttr(day, attr)
                    elif paint_day in selected_days:
                        attr = wx.calendar.CalendarDateAttr(colText=SELECTED_FG)
                        cal.SetAttr(day, attr)
                    else:
                        cal.ResetAttr(day)
                except ValueError:
                    # Blindly creating Date objects sometimes produces invalid.
                    pass

            cal.highlight_changed()
        return


    def _make_calendar_widget(self, month_offset):
        """
        Add a calendar widget to the screen and hook up callbacks.

        Parameters
        ----------
        month_offset : int
            The number of months from today, that the calendar should
            start at.
        """
        date = self.shift_datetime(self.date, month_offset)
        panel = wx.Panel(self, -1)
        cal = wxMouseBoxCalendarCtrl(panel,
            -1,
            date,
            style = wx.calendar.CAL_SUNDAY_FIRST
                  | wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION
                  #| wx.calendar.CAL_SHOW_HOLIDAYS
        )
        self.sizer.Add(panel)
        cal.highlight_changed()

        # Set up control to sync the other calendar widgets and coloring:
        self.Bind(wx.calendar.EVT_CALENDAR_MONTH, self.month_changed, cal)
        self.Bind(wx.calendar.EVT_CALENDAR_YEAR, self.month_changed, cal)

        wx.EVT_LEFT_DOWN(cal, self._left_down)

        if self.multi_select:
            wx.EVT_LEFT_UP(cal, self._left_up)
            wx.EVT_RIGHT_UP(cal, self._process_box_select)
            wx.EVT_LEAVE_WINDOW(cal, self._process_box_select)
            wx.EVT_MOTION(cal, self._mouse_drag)
            self.Bind(wx.calendar.EVT_CALENDAR_WEEKDAY_CLICKED,
                      self._weekday_clicked, cal)
        return cal


    def unhighlight_days(self, days):
        """
        Turn off all highlights in all cals, but leave any selected color.

        Parameters
        ----------
        days : List(Date)
            The list of dates to add.  Possibly includes dates in the future.
        """
        for cal in self.cal_ctrls:
            c = cal.GetDate()
            for date in days:
                if date.year == c.GetYear() and date.month == c.GetMonth()+1:

                    # Unselected days either need to revert to the
                    # unavailable color, or the default attribute color.
                    if (not self.allow_future and
                       ((date.year, date.month, date.day) >
                       (self.today.year, self.today.month, self.today.day))):
                        attr = wx.calendar.CalendarDateAttr(colText=UNAVAILABLE_FG)
                    else:
                        attr = wx.calendar.CalendarDateAttr(
                            colText=NORMAL_HIGHLIGHT_FG,
                            colBack=NORMAL_HIGHLIGHT_BG)
                    if date in self.selected_days:
                        attr.SetTextColour(SELECTED_FG)
                    cal.SetAttr(date.day, attr)
            cal.highlight_changed()
        return


    def highlight_days(self, days):
        """
        Color the highlighted list of days across all calendars.

        Parameters
        ----------
        days : List(Date)
            The list of dates to add.  Possibly includes dates in the future.
        """
        for cal in self.cal_ctrls:
            c = cal.GetDate()
            for date in days:
                if date.year == c.GetYear() and date.month == c.GetMonth()+1:
                    attr = wx.calendar.CalendarDateAttr(
                            colText=DRAG_HIGHLIGHT_FG,
                            colBack=DRAG_HIGHLIGHT_BG
                            )
                    cal.SetAttr(date.day, attr)
            cal.highlight_changed()
            cal.Refresh()


    def add_days_to_selection(self, days):
        """
        Add a list of days to the selection, using a specified style.

        Parameters
        ----------
        days : List(Date)
            The list of dates to add.  Possibly includes dates in the future.

        Description
        -----------
        When a user multi-selects entries and some of those entries are
        already selected and some are not, what should be the behavior for
        the seletion? Options::

            'toggle'     -- Toggle each day to it's opposite state.
            'on'         -- Always turn them on.
            'off'        -- Always turn them off.
            'max_change' -- Change all to same state, with most days changing.
                            For example 1 selected and 9 not, then they would
                            all get selected.
            'min_change' -- Change all to same state, with min days changing.
                            For example 1 selected and 9 not, then they would
                            all get unselected.
        """
        if not days:
            return
        style = self.on_mixed_select
        new_list = list(self.selected_days)

        if style == 'toggle':
            for day in days:
                if self.allow_future or day <= self.today:
                    if day in new_list:
                        new_list.remove(day)
                    else:
                        new_list.append(day)

        else:
            already_selected = len([day for day in days
                                    if day in new_list])

            if style == 'on' or already_selected == 0:
                add_items = True

            elif style == 'off' or already_selected == len(days):
                add_items = False

            elif (self.on_mixed_select == 'max_change' and
                  already_selected <= (len(days) / 2.0)):
                add_items = True

            elif (self.on_mixed_select == 'min_change' and
                  already_selected > (len(days) / 2.0)):
                add_items = True

            else:
                # Cases where max_change is off or min_change off.
                add_items = False

            for day in days:
                # Skip if we don't allow future, and it's a future day.
                if self.allow_future or day <= self.today:
                    if add_items and day not in new_list:
                        new_list.append(day)
                    elif not add_items and day in new_list:
                        new_list.remove(day)

        self.selected_days = new_list
        # Link the list back to the model to make a Traits List change event.
        self.editor.value = new_list
        return


    def single_select_day(self, dt):
        """
        In non-multiselect switch the selection to a new date.

        Parameters
        ----------
        dt : wx.DateTime
            The newly selected date that should become the new calendar
            selection.

        Description
        -----------
        Only called when we're using  the single-select mode of the
        calendar widget, so we can assume that the selected_dates is
        a None or a Date singleton.
        """
        selection = self.date_from_datetime(dt)

        if dt.IsValid() and (self.allow_future or selection <= self.today):
            self.selected_days = selection
            self.selected_list_changed()
            # Modify the trait on the editor so that the events propagate.
            self.editor.value = self.selected_days
            return


    def _shift_drag_update(self, event):
        """ Shift-drag in progress. """
        cal = event.GetEventObject()
        result, dt, weekday = cal.HitTest(event.GetPosition())

        self.unhighlight_days(self._drag_select)
        self._drag_select = []

        # Prepare for an abort, don't highlight new selections.
        if ((self.shift_to_select and not event.ShiftDown())
            or result != wx.calendar.CAL_HITTEST_DAY):

            cal.highlight_changed()
            for cal in self.cal_ctrls:
                cal.Refresh()
            return

        # Construct the list of selections.
        last_date = self.date_from_datetime(dt)
        if last_date <= self._first_date:
            first, last = last_date, self._first_date
        else:
            first, last = self._first_date, last_date
        while first <= last:
            if self.allow_future or first <= self.today:
                self._drag_select.append(first)
            first = first + datetime.timedelta(1)

        self.highlight_days(self._drag_select)
        return


    #------------------------------------------------------------------------
    # Event handlers
    #------------------------------------------------------------------------

    def _process_box_select(self, event):
        """
        Possibly move the calendar box-selected days into our selected days.
        """
        event.Skip()
        self.unhighlight_days(self._box_select)

        if not event.Leaving():
            self.add_days_to_selection(self._box_select)
            self.selected_list_changed()

        self._box_select = []


    def _weekday_clicked(self, evt):
        """ A day on the weekday bar has been clicked.  Select all days. """
        evt.Skip()
        weekday = evt.GetWeekDay()
        cal = evt.GetEventObject()
        month = cal.GetDate().GetMonth()+1
        year = cal.GetDate().GetYear()

        days = []
        # Messy math to compute the dates of each weekday in the month.
        # Python uses Monday=0, while wx uses Sunday=0.
        month_start_weekday = (datetime.date(year, month, 1).weekday()+1) %7
        weekday_offset = (weekday - month_start_weekday) % 7
        for day in range(weekday_offset, 31, 7):
            try:
                day = datetime.date(year, month, day+1)
                if self.allow_future or day <= self.today:
                    days.append(day)
            except ValueError:
                pass
        self.add_days_to_selection(days)

        self.selected_list_changed()
        return


    def _left_down(self, event):
        """ Handle user selection of days. """
        event.Skip()
        cal = event.GetEventObject()
        result, dt, weekday = cal.HitTest(event.GetPosition())

        if result == wx.calendar.CAL_HITTEST_DAY and not self.multi_select:
            self.single_select_day(dt)
            return

        # Inter-month-drag selection.  A quick no-movement mouse-click is
        # equivalent to a multi-select of a single day.
        if (result == wx.calendar.CAL_HITTEST_DAY
            and (not self.shift_to_select or event.ShiftDown())
            and not cal.selecting):

            self._first_date = self.date_from_datetime(dt)
            self._drag_select = [self._first_date]
            # Start showing the highlight colors with a mouse_drag event.
            self._mouse_drag(event)

        return


    def _left_up(self, event):
        """ Handle the end of a possible run-selection. """
        event.Skip()
        cal = event.GetEventObject()
        result, dt, weekday = cal.HitTest(event.GetPosition())

        # Complete a drag-select operation.
        if (result == wx.calendar.CAL_HITTEST_DAY
            and (not self.shift_to_select or event.ShiftDown())
            and self._first_date):

            last_date = self.date_from_datetime(dt)
            if last_date <= self._first_date:
                first, last = last_date, self._first_date
            else:
                first, last = self._first_date, last_date

            newly_selected = []
            while first <= last:
                newly_selected.append(first)
                first = first + datetime.timedelta(1)
            self.add_days_to_selection(newly_selected)
            self.unhighlight_days(newly_selected)

        # Reset a drag-select operation, even if it wasn't completed because
        # of a loss of focus or the Shift key prematurely released.
        self._first_date = None
        self._drag_select = []

        self.selected_list_changed()
        return


    def _mouse_drag(self, event):
        """ Called when the mouse in being dragged within the main panel. """
        event.Skip()
        cal = event.GetEventObject()
        if not cal.selecting and self._first_date:
            self._shift_drag_update(event)
        if cal.selecting:
            self.unhighlight_days(self._box_select)
            self._box_select = [self.date_from_datetime(dt)
                                for dt in cal.boxed_days()]
            self.highlight_days(self._box_select)
        return


    def month_changed(self, evt=None):
        """
        Link the calendars together so if one changes, they all change.

        TODO: Maybe wx.calendar.CAL_HITTEST_INCMONTH could be checked and
        the event skipped, rather than now where we undo the update after
        the event has gone through.
        """
        evt.Skip()
        cal_index = self.cal_ctrls.index(evt.GetEventObject())
        current_date = self.cal_ctrls[cal_index].GetDate()
        for i, cal in enumerate(self.cal_ctrls):
            # Current month is already updated, just need to shift the others
            if i != cal_index:
                new_date = self.shift_datetime(current_date, cal_index - i)
                cal.SetDate(new_date)
                cal.highlight_changed()

        # Back-up if we're not allowed to move into future months.
        if not self.allow_future:
            month = self.cal_ctrls[0].GetDate().GetMonth()+1
            year = self.cal_ctrls[0].GetDate().GetYear()
            if (year, month) > (self.today.year, self.today.month):
                for i, cal in enumerate(self.cal_ctrls):
                    new_date = self.shift_datetime(wx.DateTime_Now(), -i)
                    cal.SetDate(new_date)
                    cal.highlight_changed()

        # Redraw the selected days.
        self.selected_list_changed()


#-- end CalendarCtrl ----------------------------------------------------------


class CustomEditor(Editor):
    """
    Show multiple months with MultiCalendarCtrl. Allow multi-select.

    Trait Listeners
    ---------------
    The wx editor directly modifies the *value* trait of the Editor, which
    is the named trait of the corresponding Item in your View.  Therefore
    you can listen for changes to the user's selection by directly listening
    to the item changed event.

    TODO
    ----
    Some more listeners need to be hooked up.  For example, in single-select
    mode, changing the value does not cause the calendar to update.  Also,
    the selection-add and remove is noisy, triggering an event for each
    addition rather than waiting until everything has been added and removed.

    Sample
    ------
    Example usage::

        class DateListPicker(HasTraits):
            calendar = List
            traits_view = View(Item('calendar', editor=DateEditor(),
                                    style='custom', show_label=False))
    """

    #-- Editor interface ------------------------------------------------------

    def init (self, parent):
        """
        Finishes initializing the editor by creating the underlying widget.
        """
        if self.factory.multi_select and not isinstance(self.value, list):
            raise ValueError('Multi-select is True, but editing a non-list.')
        elif not self.factory.multi_select and isinstance(self.value, list):
            raise ValueError('Multi-select is False, but editing a list.')

        calendar_ctrl = MultiCalendarCtrl(parent,
                                          -1,
                                          self,
                                          self.factory.multi_select,
                                          self.factory.shift_to_select,
                                          self.factory.on_mixed_select,
                                          self.factory.allow_future,
                                          self.factory.months,
                                          self.factory.padding)
        self.control = calendar_ctrl
        return


    def update_editor ( self ):
        """
        Updates the editor when the object trait changes externally to the
        editor.
        """
        self.control.selected_list_changed()
        return
#-- end CustomEditor definition -----------------------------------------------


#------------------------------------------------------------------------------
#--  Text Editor
#------------------------------------------------------------------------------
# TODO: Write me.  Possibly use TextEditor as a model to show a string
# representation of the date, and have enter-set do a date evaluation.
class TextEditor (SimpleEditor):
    pass
#-- end TextEditor definition -------------------------------------------------


#------------------------------------------------------------------------------
#--  Readonly Editor
#------------------------------------------------------------------------------

class ReadonlyEditor (TextReadonlyEditor):
    """ Use a TextEditor for the view. """

    def _get_str_value(self):
        """ Replace the default string value with our own date verision. """
        if not self.value:
            return self.factory.message
        else:
            return self.value.strftime(self.factory.strftime)

#-- end ReadonlyEditor definition ---------------------------------------------

#-- eof -----------------------------------------------------------------------