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
|
import warnings
from tvtk.api import tvtk
from tvtk import messenger
from traits.api import HasTraits, Any, Callable, Property, Instance, \
Bool, Enum, Int, on_trait_change
from numpy import arange, zeros, ascontiguousarray, reshape, uint8, any
from enable.api import AbstractWindow, MouseEvent, KeyEvent, \
CoordinateBox
from enable.graphics_context import ImageGraphicsContextEnable
# Local imports.
from constants import KEY_MAP
class EnableVTKWindow(AbstractWindow, CoordinateBox):
# The render window we will be drawing into
# TODO: Eventually when we move to using the Picker, we will change
# from observing the RenderWindowInteractor
control = Instance(tvtk.RenderWindowInteractor)
# A callable that will be called to request a render. If this
# is None then we will call render on the control.
request_render = Callable
# If events don't get handled by anything in Enable, do they get passed
# through to the underlying VTK InteractorStyle?
#
# This defaults to False because for opaque objects and backgrounds event
# pass-through can be surprising. However, it can be useful in the cases
# when a transparent Enable container or collection of objects overlays
# a VTK window.
event_passthrough = Bool(False)
#------------------------------------------------------------------------
# Layout traits
#------------------------------------------------------------------------
# If we are resizable in a dimension, then when the screen resizes,
# we will stretch to maintain the amount of empty space between us
# and the closest border of the screen.
resizable = Enum("hv", "h", "v", "")
# The amount of space to put on the left side
padding_left = Int(0)
# The amount of space to put on the right side
padding_right = Int(0)
# The amount of space to put on top
padding_top = Int(0)
# The amount of space to put below
padding_bottom = Int(0)
# This property allows a way to set the padding in bulk. It can either be
# set to a single Int (which sets padding on all sides) or a tuple/list of
# 4 Ints representing the left, right, top, bottom padding amounts. When
# it is read, this property always returns the padding as a list of four
# elements even if they are all the same.
padding = Property
# Readonly property expressing the total amount of horizontal padding
hpadding = Property
# Readonly property expressing the total amount of vertical padding
vpadding = Property
_layout_needed = Bool(False)
#------------------------------------------------------------------------
# VTK pipeline objects for rendering and event handling
#------------------------------------------------------------------------
# The tvtk.InteractorStyle() object we use
interactor_style = Any()
# This is the renderer that we create
renderer = Any()
# The VTK ImageData object that hosts the data from our GC's
# bmp_array.
_vtk_image_data = Any()
# The TVTK ImageMapper and Actor2D that render our _gc
_mapper = Any()
_actor2d = Any()
#------------------------------------------------------------------------
# Private traits for keeping track of mouse state
#------------------------------------------------------------------------
# The amount of wheel motion during the previous wheel event
_wheel_amount = Int(0)
_left_down = Bool(False)
_middle_down = Bool(False)
_right_down = Bool(False)
#------------------------------------------------------------------------
# Private traits for managing redraw
#------------------------------------------------------------------------
_redraw_needed = Bool(True)
# Flag to keep from recursing in _vtk_render_event
_rendering = Bool(False)
def __init__(self, render_window_interactor, renderer,
istyle_class=tvtk.InteractorStyle, **traits):
AbstractWindow.__init__(self, **traits)
self.control = render_window_interactor
self.renderer = renderer
rwi = render_window_interactor
rwi.interactor_style = istyle_class()
istyle = rwi.interactor_style
self._add_observer(istyle, "LeftButtonPressEvent", self._vtk_mouse_button_event)
self._add_observer(istyle, "LeftButtonReleaseEvent", self._vtk_mouse_button_event)
self._add_observer(istyle, "MiddleButtonPressEvent", self._vtk_mouse_button_event)
self._add_observer(istyle, "MiddleButtonReleaseEvent", self._vtk_mouse_button_event)
self._add_observer(istyle, "RightButtonPressEvent", self._vtk_mouse_button_event)
self._add_observer(istyle, "RightButtonReleaseEvent", self._vtk_mouse_button_event)
self._add_observer(istyle, "MouseMoveEvent", self._vtk_mouse_move)
self._add_observer(istyle, "MouseWheelForwardEvent", self._vtk_mouse_wheel)
self._add_observer(istyle, "MouseWheelBackwardEvent", self._vtk_mouse_wheel)
self._add_observer(istyle, "KeyPressEvent", self._on_key_pressed)
self._add_observer(istyle, "KeyReleaseEvent", self._on_key_released)
self._add_observer(istyle, "CharEvent", self._on_character)
# We want _vtk_render_event to be called before rendering, so we
# observe the StartEvent on the RenderWindow.
self._add_observer(rwi.render_window, "StartEvent", self._vtk_render_event)
self._add_observer(istyle, "ExposeEvent", self._vtk_expose_event)
self.interactor_style = istyle
self._actor2d = tvtk.Actor2D()
self.renderer.add_actor(self._actor2d)
self._mapper = tvtk.ImageMapper()
self._mapper.color_window = 255
self._mapper.color_level = 255/2.0
self._actor2d.mapper = self._mapper
#self._size = tuple(self._get_control_size())
self._size = [0,0]
self._redraw_needed = True
self._layout_needed = True
#self._gc = self._create_gc(self._size)
rwi.initialize()
#if self.component is not None:
# self._paint()
def _add_observer(self, obj, event, cb):
""" Adds a vtk observer using messenger to avoid generating uncollectable objects. """
obj.add_observer(event, messenger.send)
messenger.connect(tvtk.to_vtk(obj), event, cb)
def _vtk_render_event(self, vtk_obj, eventname):
""" Redraw the Enable window, if needed. """
if not self._rendering:
self._rendering = True
try:
if self._size != self._get_control_size():
self._layout_needed = True
if self._redraw_needed or self._layout_needed:
self._paint()
finally:
self._rendering = False
def _vtk_expose_event(self, vtk_obj, eventname):
#print "Good gods! A VTK ExposeEvent!"
pass
def _pass_event_to_vtk(self, vtk_obj, eventname):
""" Method to dispatch a particular event name to the appropriate
method on the vtkInteractorStyle instance
"""
if "Button" in eventname:
meth_name = "On"
if "Left" in eventname:
meth_name += "LeftButton"
elif "Right" in eventname:
meth_name += "RightButton"
elif "Middle" in eventname:
meth_name += "MiddleButton"
if "Press" in eventname:
meth_name += "Down"
elif "Release" in eventname:
meth_name += "Up"
elif "MouseWheel" in eventname:
meth_name = "OnMouseWheel"
if "Forward" in eventname:
meth_name += "Forward"
else:
meth_name += "Backward"
elif "Key" in eventname:
meth_name = "OnKey"
if "Press" in eventname:
meth_name += "Press"
else:
meth_name += "Release"
elif "Char" in eventname:
meth_name = "OnChar"
elif eventname == "MouseMoveEvent":
meth_name = "OnMouseMove"
meth = getattr(vtk_obj, meth_name, None)
if meth is not None:
meth()
else:
warnings.warn("Unable to pass through mouse event '%s' to vtkInteractionStyle" % eventname)
return
def _vtk_mouse_button_event(self, vtk_obj, eventname):
""" Dispatces to self._handle_mouse_event """
# Check to see if the event falls within the window
x, y = self.control.event_position
if not (self.x <= x <= self.x2 and self.y <= y <= self.y2):
return self._pass_event_to_vtk(vtk_obj, eventname)
button_map = dict(Left="left", Right="right", Middle="middle")
action_map = dict(Press="down", Release="up")
if eventname.startswith("Left"):
button = "left"
elif eventname.startswith("Right"):
button = "right"
elif eventname.startswith("Middle"):
button = "middle"
else:
# Unable to figure out the appropriate method to dispatch to
warnings.warn("Unable to create event for", eventname)
return
if "Press" in eventname:
action = "down"
setattr(self, "_%s_down"%button, True)
elif "Release" in eventname:
action = "up"
setattr(self, "_%s_down"%button, False)
else:
# Unable to figure out the appropriate method to dispatch to
warnings.warn("Unable to create event for", eventname)
return
event_name = button + "_" + action
handled = self._handle_mouse_event(event_name, action)
if self.event_passthrough and not handled:
self._pass_event_to_vtk(vtk_obj, eventname)
def _vtk_mouse_move(self, vtk_obj, eventname):
x, y = self.control.event_position
if not (self.x <= x <= self.x2 and self.y <= y <= self.y2):
return self._pass_event_to_vtk(vtk_obj, eventname)
handled = self._handle_mouse_event("mouse_move", "move")
if self.event_passthrough and not handled:
self._pass_event_to_vtk(vtk_obj, eventname)
def _vtk_mouse_wheel(self, vtk_obj, eventname):
x, y = self.control.event_position
if not (self.x <= x <= self.x2 and self.y <= y <= self.y2):
return self._pass_event_to_vtk(vtk_obj, eventname)
if "Forward" in eventname:
self._wheel_amount = 1
else:
self._wheel_amount = -1
handled = self._handle_mouse_event("mouse_wheel", "wheel")
if self.event_passthrough and not handled:
self._pass_event_to_vtk(vtk_obj, eventname)
def _create_key_event(self, vtk_event, event_type):
focus_owner = self.focus_owner
if focus_owner is None:
focus_owner = self.component
if focus_owner is None:
return self._pass_event_to_vtk(vtk_obj, eventname)
if event_type == 'character':
key = unicode(self.control.key_sym)
else:
key = KEY_MAP.get(self.control.key_sym, None)
if key is None:
key = unicode(self.control.key_sym)
if not key:
return
x, y = self.control.event_position
return KeyEvent(event_type = event_type,
character=key, x=x, y=y,
alt_down=bool(self.control.alt_key),
shift_down=bool(self.control.shift_key),
control_down=bool(self.control.control_key),
event=eventname,
window=self.control)
def _create_mouse_event(self, event_string):
""" Returns an enable.MouseEvent that reflects the VTK mouse event.
**event_string** is just a string: "up", "down", "move", or "wheel".
It is set in _vtk_mouse_button_event.
"""
# VTK gives us no event object, so we query the interactor
# for additional event state.
rwi = self.control
x, y = rwi.event_position
# Offset the event appropriately given our bounds
x -= self.padding_left
y -= self.padding_bottom
wheel = 0
if event_string in ("up", "down"):
pass
elif event_string == "move":
pass
elif event_string == "wheel":
wheel = self._wheel_amount
# Reset the wheel amount for next time
self._wheel_amount = 0
tmp = MouseEvent(x = x, y = y,
alt_down = bool(rwi.alt_key),
control_down = bool(rwi.control_key),
shift_down = bool(rwi.shift_key),
left_down = self._left_down,
right_down = self._right_down,
middle_down = self._middle_down,
mouse_wheel = wheel,
window = self)
return tmp
def _get_control_size(self):
if self.control is not None:
return tuple(self.control.size)
else:
return (0,0)
def _redraw(self, coordinates=None):
" Called by the contained component to request a redraw "
self._redraw_needed = True
if self._actor2d is not None:
self._paint()
def _on_size(self):
pass
def _layout(self, size):
""" Given a size, set the proper location and bounds of the
actor and mapper inside our RenderWindow.
"""
if self.component is None:
return
self._size = size
self.position = [self.padding_left, self.padding_bottom]
if "h" in self.resizable:
new_width = size[0] - (self.position[0] + self.padding_right)
if new_width < 0:
self.bounds[0] = 0
else:
self.bounds[0] = new_width
if "v" in self.resizable:
new_height = size[1] - (self.position[1] + self.padding_top)
if new_height < 0:
self.bounds[1] = 0
else:
self.bounds[1] = new_height
comp = self.component
dx, dy = size
if getattr(comp, "fit_window", False):
comp.outer_position = [0,0]
comp.outer_bounds = [dx, dy]
elif hasattr(comp, "resizable"):
if "h" in comp.resizable:
comp.outer_x = 0
comp.outer_width = dx
if "v" in comp.resizable:
comp.outer_y = 0
comp.outer_height = dy
comp.do_layout(force=True)
# Invalidate the GC and the draw flag
self._gc = None
self._redraw_needed = True
self._layout_needed = False
def _create_gc(self, size, pix_format="rgba32"):
# Add 1 to each dimension because Kiva uses 0.5 to refer to the center of
# a pixel.
width = size[0] + 1
height = size[1] + 1
gc = ImageGraphicsContextEnable((width, height), pix_format = pix_format, window=self )
gc.translate_ctm(0.5, 0.5)
gc.clear((0,0,0,0))
imagedata_dimensions = (width, height, 1)
if self._vtk_image_data is None or any(self._vtk_image_data.dimensions != imagedata_dimensions):
sz = (width, height, 4)
img = tvtk.ImageData()
img.whole_extent = (0, width-1, 0, height-1, 0, 0)
# note the transposed height and width for VTK (row, column, depth)
img.dimensions = imagedata_dimensions
# create a 2d view of the array. This is a bit superfluous because
# the GC should be blank at this point in time, but we need to hand
# the ImageData something.
try:
ary = ascontiguousarray(gc.bmp_array[::-1, :, :4])
ary_2d = reshape(ary, (width * height, 4))
img.point_data.scalars = ary_2d
except:
return self._gc
if self._vtk_image_data is not None:
# Just for safety, drop the reference to the previous bmp_array
# so we don't leak it
self._vtk_image_data.point_data.scalars = None
self._actor2d.width = width
self._actor2d.height = height
self._mapper.input = img
self._vtk_image_data = img
return gc
def _paint(self, event=None):
control_size = self._get_control_size()
size = list(control_size)
if self._layout_needed or (size != self._size):
self._layout(size)
if not self._redraw_needed:
return
if self._gc is None:
self._gc = self._create_gc(self.bounds)
# 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()
self.component.draw(self._gc, view_bounds=(0, 0, size[0], size[1]))
# Now transform the image and render it into VTK
width, height = self.component.outer_bounds
width += 1
height += 1
try:
ary = ascontiguousarray(self._gc.bmp_array[::-1, :, :4])
ary_2d = reshape(ary, (width * height, 4))
except Exception, e:
warnings.warn("Error reshaping array of shape %s to width and height of (%d, %d)" % (str(ary.shape), width, height))
return
# Make sure we paint to the right location on the mapper
self._vtk_image_data.point_data.scalars = ary_2d
self._vtk_image_data.modified()
#self._window_paint(event)
if self.request_render is not None:
self.request_render()
else:
self.control.render()
self._redraw_needed = False
def _set_focus(self):
#print "set_focus unimplemented"
pass
def _capture_mouse(self):
#print "Capture mouse unimplemented"
pass
def _release_mouse(self):
#print "Release mouse unimplemented"
pass
def screen_to_window(self, x, y):
pass
def set_pointer(self, pointer):
pass
def _set_tooltip(self, tooltip):
pass
#------------------------------------------------------------------------
# Trait property setters/getters
#------------------------------------------------------------------------
def _get_padding(self):
return [self.padding_left, self.padding_right,
self.padding_top, self.padding_bottom]
def _set_padding(self, val):
old_padding = self.padding
if type(val) == int:
self.padding_left = self.padding_right = \
self.padding_top = self.padding_bottom = val
self.trait_property_changed("padding", old_padding, [val]*4)
else:
# assume padding is some sort of array type
if len(val) != 4:
raise RuntimeError, "Padding must be a 4-element sequence type or an int. Instead, got" + str(val)
self.padding_left = val[0]
self.padding_right = val[1]
self.padding_top = val[2]
self.padding_bottom = val[3]
self.trait_property_changed("padding", old_padding, val)
return
def _get_hpadding(self):
return 2*self._get_visible_border() + self.padding_right + \
self.padding_left
def _get_vpadding(self):
return 2*self._get_visible_border() + self.padding_bottom + \
self.padding_top
#------------------------------------------------------------------------
# Trait event handlers
#------------------------------------------------------------------------
@on_trait_change("position,position_items")
def _pos_bounds_changed(self):
if self._actor2d is not None:
self._actor2d.position = self.position
self._redraw_needed
@on_trait_change("bounds,bounds_items")
def _bounds_changed(self):
if self._actor2d is not None:
self._actor2d.width = self.width
self._actor2d.height = self.height
self._redraw_needed = True
self._layout_needed = True
|