File: abstract_window.py

package info (click to toggle)
python-enable 3.3.1-3
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 10,392 kB
  • ctags: 17,135
  • sloc: cpp: 79,151; python: 29,601; makefile: 2,926; sh: 43
file content (427 lines) | stat: -rw-r--r-- 16,477 bytes parent folder | download
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

# Major library imports
from numpy import dot

# Enthought library imports
from enthought.traits.api import Any, Bool, Event, HasTraits, Instance, \
        Property, ReadOnly, Trait, Tuple, List


# Local relative imports
from base import bounds_to_coordinates, does_disjoint_intersect_coordinates, \
    union_bounds
from component import Component
from interactor import Interactor
from container import Container
from colors import ColorTrait

def Alias(name):
    return Property(lambda obj: getattr(obj, name),
                    lambda obj, val: setattr(obj, name, val))

class AbstractWindow(HasTraits):

    # The top-level component that this window houses
    component     = Instance(Component)

    # A reference to the nested component that has focus.  This is part of the
    # manual mechanism for determining keyboard focus.
    focus_owner   = Instance(Interactor)

    # If set, this is the component to which all mouse events are passed,
    # bypassing the normal event propagation mechanism.
    mouse_owner   = Instance(Interactor)

    # The transform to apply to mouse event positions to put them into the
    # relative coordinates of the mouse_owner component.
    mouse_owner_transform = Any()

    # When a component captures the mouse, it can optionally store a
    # dispatch order for events (until it releases the mouse).
    mouse_owner_dispatch_history = Trait(None, None, List)

    # The background window of the window.  The entire window first gets
    # painted with this color before the component gets to draw.
    bgcolor = ColorTrait("sys_window")

    # Unfortunately, for a while, there was a naming inconsistency and the
    # background color trait named "bg_color".  This is still provided for
    # backwards compatibility but should not be used in new code.
    bg_color = Alias("bgcolor")

    window        = ReadOnly
    alt_pressed   = Bool(False)
    ctrl_pressed  = Bool(False)
    shift_pressed = Bool(False)

    # A container that gets drawn after & on top of the main component, and
    # which receives events first.
    overlay = Instance(Container)
    
    # When the underlying toolkit control gets resized, this event gets set
    # to the new size of the window, expressed as a tuple (dx, dy).
    resized = Event

    # Whether to enable damaged region handling
    use_damaged_region = Bool(False)

    # The previous component that handled an event.  Used to generate
    # mouse_enter and mouse_leave events.  Right now this can only be
    # None, self.component, or self.overlay.
    _prev_event_handler = Instance(Component)

    # (dx, dy) integer size of the Window.
    _size = Trait(None, Tuple)
    
    # The regions to update upon redraw
    _update_region = Any
    


    #---------------------------------------------------------------------------
    #  Abstract methods that must be implemented by concrete subclasses
    #---------------------------------------------------------------------------

    def set_drag_result(self, result):
        """ Sets the result that should be returned to the system from the
        handling of the current drag operation.  Valid result values are:
        "error", "none", "copy", "move", "link", "cancel".  These have the
        meanings associated with their WX equivalents.
        """
        raise NotImplementedError

    def _capture_mouse(self):
        "Capture all future mouse events"
        raise NotImplementedError

    def _release_mouse(self):
        "Release the mouse capture"
        raise NotImplementedError

    def _create_mouse_event(self, event):
        "Convert a GUI toolkit mouse event into a MouseEvent"
        raise NotImplementedError

    def _redraw(self, coordinates=None):
        """ Request a redraw of the window, within just the (x,y,w,h) coordinates
        (if provided), or over the entire window if coordinates is None.
        """
        raise NotImplementedError

    def _get_control_size(self):
        "Get the size of the underlying toolkit control"
        raise NotImplementedError

    def _create_gc(self, size, pix_format = "bgr24"):
        """ Create a Kiva graphics context of a specified size.  This method
        only gets called when the size of the window itself has changed.  To
        perform pre-draw initialization every time in the paint loop, use
        _init_gc().
        """
        raise NotImplementedError

    def _init_gc(self):
        """ Gives a GC a chance to initialize itself before components perform
        layout and draw.  This is called every time through the paint loop.
        """
        gc = self._gc
        if self._update_region == [] or not self.use_damaged_region:
            self._update_region = None
        if self._update_region is None:
            gc.clear(self.bgcolor_)
        else:
            # Fixme: should use clip_to_rects
            update_union = reduce(union_bounds, self._update_region)
            gc.clip_to_rect(*update_union)
        return

    def _window_paint(self, event):
        "Do a GUI toolkit specific screen update"
        raise NotImplementedError

    def set_pointer(self, pointer):
        "Sets the current cursor shape"
        raise NotImplementedError

    def set_timer_interval(self, component, interval):
        "Set up or cancel a timer for a specified component"
        raise NotImplementedError

    def _set_focus(self):
        "Sets this window to have keyboard focus"
        raise NotImplementedError

    def screen_to_window(self, x, y):
        "Returns local window coordinates for given global screen coordinates"
        raise NotImplementedError


    #------------------------------------------------------------------------
    # Public methods
    #------------------------------------------------------------------------

    def __init__(self, **traits):
        self.window = self
        self._scroll_origin = (0.0, 0.0)
        self._update_region = None
        self._gc = None
        self._pointer_owner = None
        HasTraits.__init__(self, **traits)

        # Create a default component (if necessary):
        if self.component is None:
            self.component = Container()
        return

    def _component_changed(self, old, new):
        if old is not None:
            old.on_trait_change(self.component_bounds_changed, 'bounds', remove=True)
            old.window = None

        if new is None:
            self.component = Container()
            return

        new.window = self

        # If possible, size the new component according to the size of the
        # toolkit control
        size = self._get_control_size()
        if (size is not None) and hasattr(self.component, "bounds"):
            new.on_trait_change(self.component_bounds_changed, 'bounds')
            if getattr(self.component, "fit_window", False):
                self.component.outer_position = [0,0]
                self.component.outer_bounds = list(size)
            elif hasattr(self.component, "resizable"):
                if "h" in self.component.resizable:
                    self.component.outer_x = 0
                    self.component.outer_width = size[0]
                if "v" in self.component.resizable:
                    self.component.outer_y = 0
                    self.component.outer_height = size[1]
        self._update_region = None
        self.redraw()
        return

    def component_bounds_changed(self, bounds):
        """
        Dynamic trait listener that handles our component changing its size;
        bounds is a length-2 list of [width, height].
        """
        self.invalidate_draw()
        pass

    def set_mouse_owner(self, mouse_owner, transform=None, history=None):
        "Handle the 'mouse_owner' being changed"
        if mouse_owner is None:
            self._release_mouse()
            self.mouse_owner = None
            self.mouse_owner_transform = None
            self.mouse_owner_dispatch_history = None
        else:
            self._capture_mouse()
            self.mouse_owner = mouse_owner
            self.mouse_owner_transform = transform
            self.mouse_owner_dispatch_history = history
        return
    
    def invalidate_draw(self, damaged_regions=None, self_relative=False):
        if damaged_regions is not None and self._update_region is not None:
            self._update_region += damaged_regions
        else:
            self._update_region = None
#        print damaged_regions

    #---------------------------------------------------------------------------
    #  Generic mouse event handler:
    #---------------------------------------------------------------------------

    def _handle_mouse_event(self, event_name, event, set_focus=False):
        """ **event** should be a toolkit-specific opaque object that will
        be passed in to the backend's _create_mouse_event() method.  It can
        be None if the the toolkit lacks a native "mouse event" object.

        Returns True if the event has been handled within the Enable object
        hierarchy, or False otherwise.
        """
        if self._size is None:
            # PZW: Hack!
            # We need to handle the cases when the window hasn't been painted yet, but
            # it's gotten a mouse event.  In such a case, we just ignore the mouse event.
            # If the window has been painted, then _size will have some sensible value.
            return False

        mouse_event = self._create_mouse_event(event)
        mouse_owner = self.mouse_owner

        if mouse_owner is not None:
            # A mouse_owner has grabbed the mouse.  Check to see if we need to
            # compose a net transform by querying each of the objects in the
            # dispatch history in turn, or if we can just apply a saved top-level
            # transform.
            history = self.mouse_owner_dispatch_history
            if history is not None and len(history) > 0:
                # Assemble all the transforms
                transforms = [c.get_event_transform() for c in history]
                total_transform = reduce(dot, transforms[::-1])
                mouse_event.push_transform(total_transform)
            elif self.mouse_owner_transform is not None:
                mouse_event.push_transform(self.mouse_owner_transform)

            mouse_owner.dispatch(mouse_event, event_name)
            self._pointer_owner = mouse_owner
        else:
            # Normal event handling loop
            if self.overlay is not None:
                # TODO: implement this...
                pass
            if (not mouse_event.handled) and (self.component is not None):
                # Test to see if we need to generate a mouse_leave event
                if self._prev_event_handler:
                    if not self._prev_event_handler.is_in(mouse_event.x, mouse_event.y):
                        self._prev_event_handler.dispatch(mouse_event, "pre_mouse_leave")
                        mouse_event.handled = False
                        self._prev_event_handler.dispatch(mouse_event, "mouse_leave")
                        self._prev_event_handler = None

                if self.component.is_in(mouse_event.x, mouse_event.y):
                    # Test to see if we need to generate a mouse_enter event
                    if self._prev_event_handler != self.component:
                        self._prev_event_handler = self.component
                        self.component.dispatch(mouse_event, "pre_mouse_enter")
                        mouse_event.handled = False
                        self.component.dispatch(mouse_event, "mouse_enter")

                    # Fire the actual event
                    self.component.dispatch(mouse_event, "pre_" + event_name)
                    mouse_event.handled = False
                    self.component.dispatch(mouse_event, event_name)


        # If this event requires setting the keyboard focus, set the first
        # component under the mouse pointer that accepts focus as the new focus
        # owner (otherwise, nobody owns the focus):
        if set_focus:
            # If the mouse event was a click, then we set the toolkit's 
            # focus to ourselves
            if mouse_event.left_down or mouse_event.middle_down or \
                    mouse_event.right_down or mouse_event.mouse_wheel != 0:
                self._set_focus()

            if (self.component is not None) and (self.component.accepts_focus):
                if self.focus_owner is None:
                    self.focus_owner = self.component
                else:
                    pass

        return mouse_event.handled

    def set_tooltip(self, components):
        "Set the window's tooltip (if necessary)"
        raise NotImplementedError
    
    def redraw(self):
        """ Requests that the window be redrawn. """
        self._redraw()
        return

    def _needs_redraw(self, bounds):
        "Determine if a specified region intersects the update region"
        return does_disjoint_intersect_coordinates( self._update_region,
                                                    bounds_to_coordinates( bounds ) )

    def _paint(self, event=None):
        """ This method is called directly by the UI toolkit's callback
        mechanism on the paint event.
        """
        # Create a new GC if necessary
        size = self._get_control_size()
        if (self._size != tuple(size)) or (self._gc is None):
            self._size = tuple(size)
            self._gc = self._create_gc(size)
        
        # Always give the GC a chance to initialize
        self._init_gc()

        # Layout components and draw
        if hasattr(self.component, "do_layout"):
            self.component.do_layout()
        gc = self._gc
        self.component.draw(gc, view_bounds=(0, 0, size[0], size[1]))

#        damaged_regions = draw_result['damaged_regions']
        # FIXME: consolidate damaged regions if necessary
        if not self.use_damaged_region:
            self._update_region = None

        # Perform a paint of the GC to the window (only necessary on backends
        # that render to an off-screen buffer)
        self._window_paint(event)

        self._update_region = []
        return

    def __getstate__(self):
        attribs = ("component", "bgcolor", "overlay", "_scroll_origin")
        state = {}
        for attrib in attribs:
            state[attrib] = getattr(self, attrib)
        return state

    #---------------------------------------------------------------------------
    # Wire up the mouse event handlers
    #---------------------------------------------------------------------------

    def _on_left_down ( self, event ):
        self._handle_mouse_event( 'left_down', event, set_focus = True )

    def _on_left_up ( self, event ):
        self._handle_mouse_event( 'left_up', event )

    def _on_left_dclick ( self, event ):
        self._handle_mouse_event( 'left_dclick', event )

    def _on_right_down ( self, event ):
        self._handle_mouse_event( 'right_down', event, set_focus = True )

    def _on_right_up ( self, event ):
        self._handle_mouse_event( 'right_up', event )

    def _on_right_dclick ( self, event ):
        self._handle_mouse_event( 'right_dclick', event )

    def _on_middle_down ( self, event ):
        self._handle_mouse_event( 'middle_down', event )

    def _on_middle_up ( self, event ):
        self._handle_mouse_event( 'middle_up', event )

    def _on_middle_dclick ( self, event ):
        self._handle_mouse_event( 'middle_dclick', event )

    def _on_mouse_move ( self, event ):
        self._handle_mouse_event( 'mouse_move', event, 1 )

    def _on_mouse_wheel ( self, event ):
        self._handle_mouse_event( 'mouse_wheel', event )

    def _on_mouse_enter ( self, event ):
        self._handle_mouse_event( 'mouse_enter', event )

    def _on_mouse_leave ( self, event ):
        self._handle_mouse_event( 'mouse_leave', event, -1 )

    # Additional event handlers that are not part of normal Interactors
    def _on_window_enter(self, event):
        # TODO: implement this to generate a mouse_leave on self.component
        pass

    def _on_window_leave(self, event):
        if self._prev_event_handler:
            mouse_event = self._create_mouse_event(event)
            self._prev_event_handler.dispatch(mouse_event, "mouse_leave")
            self._prev_event_handler = None
        return


# EOF