~ubuntu-branches/ubuntu/utopic/python-traitsui/utopic

« back to all changes in this revision

Viewing changes to traitsui/wx/date_editor.py

  • Committer: Bazaar Package Importer
  • Author(s): Varun Hiremath
  • Date: 2011-07-09 13:57:39 UTC
  • Revision ID: james.westby@ubuntu.com-20110709135739-x5u20q86huissmn1
Tags: upstream-4.0.0
ImportĀ upstreamĀ versionĀ 4.0.0

Show diffs side-by-side

added added

removed removed

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