1# ------------------------------------------------------------------------------ 2# 3# Copyright (c) 2005-19, Enthought, Inc. 4# All rights reserved. 5# 6# This software is provided without warranty under the terms of the BSD 7# license included in LICENSE.txt and may be redistributed only 8# under the conditions described in the aforementioned license. The license 9# is also available online at http://www.enthought.com/licenses/BSD.txt 10# 11# Thanks for using Enthought open source! 12# 13# Author: David C. Morrill 14# Date: 10/13/2004 15# 16# ------------------------------------------------------------------------------ 17 18""" Defines the concrete implementations of the traits Toolkit interface for 19 the wxPython user interface toolkit. 20""" 21 22# Make sure that importimg from this backend is OK: 23import logging 24 25from traitsui.toolkit import assert_toolkit_import 26 27assert_toolkit_import(["wx"]) 28 29import wx 30 31# Ensure that we can import Pyface backend. This starts App as a side-effect. 32from pyface.toolkit import toolkit_object as pyface_toolkit 33 34_app = pyface_toolkit("init:_app") 35 36from traits.api import HasPrivateTraits, Instance 37from traits.trait_notifiers import set_ui_handler 38from pyface.api import SystemMetrics 39from pyface.wx.drag_and_drop import PythonDropTarget 40 41from traitsui.theme import Theme 42from traitsui.ui import UI 43from traitsui.toolkit import Toolkit 44from .constants import WindowColor 45from .helper import position_window 46 47logger = logging.getLogger(__name__) 48 49#: Mapping from wx events to method suffixes. 50EventSuffix = { 51 wx.wxEVT_LEFT_DOWN: "left_down", 52 wx.wxEVT_LEFT_DCLICK: "left_dclick", 53 wx.wxEVT_LEFT_UP: "left_up", 54 wx.wxEVT_MIDDLE_DOWN: "middle_down", 55 wx.wxEVT_MIDDLE_DCLICK: "middle_dclick", 56 wx.wxEVT_MIDDLE_UP: "middle_up", 57 wx.wxEVT_RIGHT_DOWN: "right_down", 58 wx.wxEVT_RIGHT_DCLICK: "right_dclick", 59 wx.wxEVT_RIGHT_UP: "right_up", 60 wx.wxEVT_MOTION: "mouse_move", 61 wx.wxEVT_ENTER_WINDOW: "enter", 62 wx.wxEVT_LEAVE_WINDOW: "leave", 63 wx.wxEVT_MOUSEWHEEL: "mouse_wheel", 64 wx.wxEVT_PAINT: "paint", 65} 66 67#: Types of popup views: 68Popups = {"popup", "popover", "info"} 69 70 71# ------------------------------------------------------------------------- 72# Traits UI dispatch infrastructure 73# ------------------------------------------------------------------------- 74 75def ui_handler(handler, *args): 76 """ Handles UI notification handler requests that occur on a thread other 77 than the UI thread. 78 """ 79 wx.CallAfter(handler, *args) 80 81 82# Tell the traits notification handlers to use this UI handler 83set_ui_handler(ui_handler) 84 85 86# ------------------------------------------------------------------------- 87# Wx Toolkit Implementation 88# ------------------------------------------------------------------------- 89 90class GUIToolkit(Toolkit): 91 """ Implementation class for wxPython toolkit. 92 """ 93 94 def ui_panel(self, ui, parent): 95 """ Creates a wxPython panel-based user interface using information 96 from the specified UI object. 97 """ 98 from . import ui_panel 99 100 ui_panel.ui_panel(ui, parent) 101 102 def ui_subpanel(self, ui, parent): 103 """ Creates a wxPython subpanel-based user interface using information 104 from the specified UI object. 105 """ 106 from . import ui_panel 107 108 ui_panel.ui_subpanel(ui, parent) 109 110 def ui_livemodal(self, ui, parent): 111 """ Creates a wxPython modal "live update" dialog user interface using 112 information from the specified UI object. 113 """ 114 from . import ui_live 115 116 ui_live.ui_livemodal(ui, parent) 117 118 def ui_live(self, ui, parent): 119 """ Creates a wxPython non-modal "live update" window user interface 120 using information from the specified UI object. 121 """ 122 from . import ui_live 123 124 ui_live.ui_live(ui, parent) 125 126 def ui_modal(self, ui, parent): 127 """ Creates a wxPython modal dialog user interface using information 128 from the specified UI object. 129 """ 130 from . import ui_modal 131 132 ui_modal.ui_modal(ui, parent) 133 134 def ui_nonmodal(self, ui, parent): 135 """ Creates a wxPython non-modal dialog user interface using 136 information from the specified UI object. 137 """ 138 from . import ui_modal 139 140 ui_modal.ui_nonmodal(ui, parent) 141 142 def ui_popup(self, ui, parent): 143 """ Creates a wxPython temporary "live update" popup dialog user 144 interface using information from the specified UI object. 145 """ 146 from . import ui_live 147 148 ui_live.ui_popup(ui, parent) 149 150 def ui_popover(self, ui, parent): 151 """ Creates a wxPython temporary "live update" popup dialog user 152 interface using information from the specified UI object. 153 """ 154 from . import ui_live 155 156 ui_live.ui_popover(ui, parent) 157 158 def ui_info(self, ui, parent): 159 """ Creates a wxPython temporary "live update" popup dialog user 160 interface using information from the specified UI object. 161 """ 162 from . import ui_live 163 164 ui_live.ui_info(ui, parent) 165 166 def ui_wizard(self, ui, parent): 167 """ Creates a wxPython wizard dialog user interface using information 168 from the specified UI object. 169 """ 170 from . import ui_wizard 171 172 ui_wizard.ui_wizard(ui, parent) 173 174 def view_application( 175 self, 176 context, 177 view, 178 kind=None, 179 handler=None, 180 id="", 181 scrollable=None, 182 args=None, 183 ): 184 """ Creates a wxPython modal dialog user interface that 185 runs as a complete application, using information from the 186 specified View object. 187 188 Parameters 189 ---------- 190 context : object or dictionary 191 A single object or a dictionary of string/object pairs, whose trait 192 attributes are to be edited. If not specified, the current object is 193 used. 194 view : view or string 195 A View object that defines a user interface for editing trait 196 attribute values. 197 kind : string 198 The type of user interface window to create. See the 199 **traitsui.view.kind_trait** trait for values and 200 their meanings. If *kind* is unspecified or None, the **kind** 201 attribute of the View object is used. 202 handler : Handler object 203 A handler object used for event handling in the dialog box. If 204 None, the default handler for Traits UI is used. 205 id : string 206 A unique ID for persisting preferences about this user interface, 207 such as size and position. If not specified, no user preferences 208 are saved. 209 scrollable : Boolean 210 Indicates whether the dialog box should be scrollable. When set to 211 True, scroll bars appear on the dialog box if it is not large enough 212 to display all of the items in the view at one time. 213 214 """ 215 from . import view_application 216 217 return view_application.view_application( 218 context, view, kind, handler, id, scrollable, args 219 ) 220 221 def position(self, ui): 222 """ Positions the associated dialog window on the display. 223 """ 224 view = ui.view 225 window = ui.control 226 227 # Set up the default position of the window: 228 parent = window.GetParent() 229 if parent is None: 230 px, py = 0, 0 231 pdx = SystemMetrics().screen_width 232 pdy = SystemMetrics().screen_height 233 else: 234 px, py = parent.GetPosition() 235 pdx, pdy = parent.GetSize() 236 237 # Calculate the correct width and height for the window: 238 cur_width, cur_height = window.GetSize() 239 width = view.width 240 height = view.height 241 242 if width < 0.0: 243 width = cur_width 244 elif width <= 1.0: 245 width = int(width * SystemMetrics().screen_width) 246 else: 247 width = int(width) 248 249 if height < 0.0: 250 height = cur_height 251 elif height <= 1.0: 252 height = int(height * SystemMetrics().screen_height) 253 else: 254 height = int(height) 255 256 if view.kind in Popups: 257 position_window(window, width, height) 258 return 259 260 # Calculate the correct position for the window: 261 x = view.x 262 y = view.y 263 264 if x < -99999.0: 265 # BH- I think this is the case when there is a parent 266 # so this logic tries to place it in the middle of the parent 267 # if possible, otherwise tries an offset from the parent 268 x = px + (pdx - width) // 2 269 if x < 0: 270 x = px + 20 271 elif x <= -1.0: 272 x = px + pdx - width + int(x) + 1 273 elif x < 0.0: 274 x = px + pdx - width + int(x * pdx) 275 elif x <= 1.0: 276 x = px + int(x * pdx) 277 else: 278 x = int(x) 279 280 if y < -99999.0: 281 # BH- I think this is the case when there is a parent 282 # so this logic tries to place it in the middle of the parent 283 # if possible, otherwise tries an offset from the parent 284 y = py + (pdy - height) // 2 285 if y < 0: 286 y = py + 20 287 elif y <= -1.0: 288 y = py + pdy - height + int(y) + 1 289 elif x < 0.0: 290 y = py + pdy - height + int(y * pdy) 291 elif y <= 1.0: 292 y = py + int(y * pdy) 293 else: 294 y = int(y) 295 296 # make sure the position is on the visible screen, maybe 297 # the desktop had been resized? 298 x = min(x, wx.DisplaySize()[0]) 299 y = min(y, wx.DisplaySize()[1]) 300 301 # Position and size the window as requested: 302 window.SetSize(max(0, x), max(0, y), width, height) 303 304 def show_help(self, ui, control): 305 """ Shows a help window for a specified UI and control. 306 """ 307 from . import ui_panel 308 309 ui_panel.show_help(ui, control) 310 311 def save_window(self, ui): 312 """ Saves user preference information associated with a UI window. 313 """ 314 from . import helper 315 316 helper.save_window(ui) 317 318 def rebuild_ui(self, ui): 319 """ Rebuilds a UI after a change to the content of the UI. 320 """ 321 parent = size = None 322 323 if ui.control is not None: 324 size = ui.control.GetSize() 325 parent = ui.control._parent 326 info = ui.info 327 ui.recycle() 328 ui.info = info 329 info.ui = ui 330 331 ui.rebuild(ui, parent) 332 333 if parent is not None: 334 ui.control.SetSize(size) 335 sizer = parent.GetSizer() 336 if sizer is not None: 337 sizer.Add(ui.control, 1, wx.EXPAND) 338 339 def set_title(self, ui): 340 """ Sets the title for the UI window. 341 """ 342 ui.control.SetTitle(ui.title) 343 344 def set_icon(self, ui): 345 """ Sets the icon for the UI window. 346 """ 347 from pyface.image_resource import ImageResource 348 349 if isinstance(ui.icon, ImageResource): 350 ui.control.SetIcon(ui.icon.create_icon()) 351 352 def key_event_to_name(self, event): 353 """ Converts a keystroke event into a corresponding key name. 354 """ 355 from . import key_event_to_name 356 357 return key_event_to_name.key_event_to_name(event) 358 359 def hook_events(self, ui, control, events=None, handler=None): 360 """ Hooks all specified events for all controls in a UI so that they 361 can be routed to the correct event handler. 362 """ 363 if events is None: 364 events = ( 365 wx.wxEVT_LEFT_DOWN, 366 wx.wxEVT_LEFT_DCLICK, 367 wx.wxEVT_LEFT_UP, 368 wx.wxEVT_MIDDLE_DOWN, 369 wx.wxEVT_MIDDLE_DCLICK, 370 wx.wxEVT_MIDDLE_UP, 371 wx.wxEVT_RIGHT_DOWN, 372 wx.wxEVT_RIGHT_DCLICK, 373 wx.wxEVT_RIGHT_UP, 374 wx.wxEVT_MOTION, 375 wx.wxEVT_ENTER_WINDOW, 376 wx.wxEVT_LEAVE_WINDOW, 377 wx.wxEVT_MOUSEWHEEL, 378 wx.wxEVT_PAINT, 379 ) 380 control.SetDropTarget( 381 PythonDropTarget(DragHandler(ui=ui, control=control)) 382 ) 383 elif events == "keys": 384 events = (wx.wxEVT_CHAR,) 385 386 if handler is None: 387 handler = ui.route_event 388 389 id = control.GetId() 390 event_handler = EventHandlerWrapper() 391 connect = event_handler.Connect 392 393 for event in events: 394 connect(id, id, event, handler) 395 396 control.PushEventHandler(event_handler) 397 398 for child in control.GetChildren(): 399 self.hook_events(ui, child, events, handler) 400 401 def route_event(self, ui, event): 402 """ Routes a hooked event to the correct handler method. 403 """ 404 suffix = EventSuffix[event.GetEventType()] 405 control = event.GetEventObject() 406 handler = ui.handler 407 method = None 408 409 owner = getattr(control, "_owner", None) 410 if owner is not None: 411 method = getattr( 412 handler, "on_%s_%s" % (owner.get_id(), suffix), None 413 ) 414 415 if method is None: 416 method = getattr(handler, "on_%s" % suffix, None) or getattr( 417 handler, "on_any_event", None 418 ) 419 420 if (method is None) or (method(ui.info, owner, event) is False): 421 event.Skip() 422 423 def skip_event(self, event): 424 """ Indicates that an event should continue to be processed by the 425 toolkit. 426 """ 427 event.Skip() 428 429 def destroy_control(self, control): 430 """ Destroys a specified GUI toolkit control. 431 """ 432 _popEventHandlers(control) 433 434 def _destroy_control(control): 435 try: 436 control.Destroy() 437 except Exception: 438 logger.exception( 439 "Wx control %r not destroyed cleanly", control) 440 441 wx.CallAfter(_destroy_control, control) 442 443 def destroy_children(self, control): 444 """ Destroys all of the child controls of a specified GUI toolkit 445 control. 446 """ 447 for child in control.GetChildren(): 448 _popEventHandlers(child) 449 wx.CallAfter(control.DestroyChildren) 450 451 def image_size(self, image): 452 """ Returns a ( width, height ) tuple containing the size of a 453 specified toolkit image. 454 """ 455 return (image.GetWidth(), image.GetHeight()) 456 457 def constants(self): 458 """ Returns a dictionary of useful constants. 459 460 Currently, the dictionary should have the following key/value pairs: 461 462 - WindowColor': the standard window background color in the toolkit 463 specific color format. 464 """ 465 return {"WindowColor": WindowColor} 466 467 # ------------------------------------------------------------------------- 468 # GUI toolkit dependent trait definitions: 469 # ------------------------------------------------------------------------- 470 471 def color_trait(self, *args, **traits): 472 from . import color_trait as ct 473 474 return ct.WxColor(*args, **traits) 475 476 def rgb_color_trait(self, *args, **traits): 477 from . import rgb_color_trait as rgbct 478 479 return rgbct.RGBColor(*args, **traits) 480 481 def font_trait(self, *args, **traits): 482 from . import font_trait as ft 483 484 return ft.WxFont(*args, **traits) 485 486 # ------------------------------------------------------------------------- 487 # 'Editor' class methods: 488 # ------------------------------------------------------------------------- 489 490 def ui_editor(self): 491 """ Generic base UI editor. """ 492 from . import ui_editor 493 494 return ui_editor.UIEditor 495 496 def shell_editor(self, *args, **traits): 497 from . import shell_editor as se 498 499 return se.ToolkitEditorFactory(*args, **traits) 500 501 502class DragHandler(HasPrivateTraits): 503 """ Handler for drag events. 504 """ 505 506 # ------------------------------------------------------------------------- 507 # Traits definitions: 508 # ------------------------------------------------------------------------- 509 510 #: The UI associated with the drag handler 511 ui = Instance(UI) 512 513 #: The wx control associated with the drag handler 514 control = Instance(wx.Window) 515 516 # -- Drag and drop event handlers: ---------------------------------------- 517 518 def wx_dropped_on(self, x, y, data, drag_result): 519 """ Handles a Python object being dropped on the window. 520 """ 521 return self._drag_event("dropped_on", x, y, data, drag_result) 522 523 def wx_drag_over(self, x, y, data, drag_result): 524 """ Handles a Python object being dragged over the tree. 525 """ 526 return self._drag_event("drag_over", x, y, data, drag_result) 527 528 def wx_drag_leave(self, data): 529 """ Handles a dragged Python object leaving the window. 530 """ 531 return self._drag_event("drag_leave") 532 533 def _drag_event(self, suffix, x=None, y=None, data=None, drag_result=None): 534 """ Handles routing a drag event to the appropriate handler. 535 """ 536 control = self.control 537 handler = self.ui.handler 538 method = None 539 540 owner = getattr(control, "_owner", None) 541 if owner is not None: 542 method = getattr( 543 handler, "on_%s_%s" % (owner.get_id(), suffix), None 544 ) 545 546 if method is None: 547 method = getattr(handler, "on_%s" % suffix, None) 548 549 if method is None: 550 return wx.DragNone 551 552 if x is None: 553 result = method(self.ui.info, owner) 554 else: 555 result = method(self.ui.info, owner, x, y, data, drag_result) 556 if result is None: 557 result = drag_result 558 return result 559 560 561class EventHandlerWrapper(wx.EvtHandler): 562 """ Simple wrapper around wx.EvtHandler used to determine which event 563 handlers were added by traitui. 564 """ 565 pass 566 567 568def _popEventHandlers(ctrl, handler_type=EventHandlerWrapper): 569 """ Pop any event handlers that have been pushed on to a window and its 570 children. 571 """ 572 # FIXME: have to special case URLResolvingHtmlWindow because it doesn't 573 # want its EvtHandler cleaned up. See issue #752. 574 from .html_editor import URLResolvingHtmlWindow 575 576 handler = ctrl.GetEventHandler() 577 while ctrl is not handler: 578 next_handler = handler.GetNextHandler() 579 if isinstance(handler, handler_type): 580 ctrl.PopEventHandler(True) 581 handler = next_handler 582 for child in ctrl.GetChildren(): 583 _popEventHandlers(child, handler_type) 584