1# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX 2# All rights reserved. 3# 4# This software is provided without warranty under the terms of the BSD 5# license included in LICENSE.txt and may be redistributed only under 6# the conditions described in the aforementioned license. The license 7# is also available online at http://www.enthought.com/licenses/BSD.txt 8# 9# Thanks for using Enthought open source! 10 11""" The wx specific implementations the action manager internal classes. 12""" 13 14 15from inspect import getfullargspec 16 17 18import wx 19 20 21from traits.api import Any, Bool, HasTraits 22 23 24from pyface.action.action_event import ActionEvent 25 26 27_STYLE_TO_KIND_MAP = { 28 "push": wx.ITEM_NORMAL, 29 "radio": wx.ITEM_RADIO, 30 "toggle": wx.ITEM_CHECK, 31 "widget": None, 32} 33 34 35class _MenuItem(HasTraits): 36 """ A menu item representation of an action item. """ 37 38 # '_MenuItem' interface ------------------------------------------------ 39 40 # Is the item checked? 41 checked = Bool(False) 42 43 # A controller object we delegate taking actions through (if any). 44 controller = Any() 45 46 # Is the item enabled? 47 enabled = Bool(True) 48 49 # Is the item visible? 50 visible = Bool(True) 51 52 # The radio group we are part of (None if the menu item is not part of such 53 # a group). 54 group = Any() 55 56 # ------------------------------------------------------------------------ 57 # 'object' interface. 58 # ------------------------------------------------------------------------ 59 60 def __init__(self, parent, menu, item, controller): 61 """ Creates a new menu item for an action item. """ 62 63 self.item = item 64 65 # Create an appropriate menu item depending on the style of the action. 66 # 67 # N.B. Don't try to use -1 as the Id for the menu item... wx does not 68 # like it! 69 action = item.action 70 label = action.name 71 kind = _STYLE_TO_KIND_MAP[action.style] 72 longtip = action.description or action.tooltip 73 74 if action.style == "widget": 75 raise NotImplementedError( 76 "WxPython does not support widgets in menus" 77 ) 78 79 if len(action.accelerator) > 0: 80 label = label + "\t" + action.accelerator 81 82 # This just helps with debugging when people forget to specify a name 83 # for their action (without this wx just barfs which is not very 84 # helpful!). 85 if len(label) == 0: 86 label = item.action.__class__.__name__ 87 88 if getattr(action, "menu_role", False): 89 if action.menu_role == "About": 90 self.control_id = wx.ID_ABOUT 91 elif action.menu_role == "Preferences": 92 self.control_id = wx.ID_PREFERENCES 93 elif action.menu_role == "Quit": 94 self.control_id = wx.ID_EXIT 95 else: 96 self.control_id = wx.ID_ANY 97 self.control = wx.MenuItem(menu, self.control_id, label, longtip, kind) 98 99 # If the action has an image then display it. 100 if action.image is not None: 101 try: 102 self.control.SetBitmap(action.image.create_bitmap()) 103 except Exception: 104 # Some platforms don't allow radio buttons to have 105 # bitmaps, so just ignore the exception if it happens 106 pass 107 108 menu.Append(self.control) 109 menu.menu_items.append(self) 110 111 # Set the initial enabled/disabled state of the action. 112 self.control.Enable(action.enabled and action.visible) 113 114 # Set the initial checked state. 115 if action.style in ["radio", "toggle"]: 116 self.control.Check(action.checked) 117 118 # Wire it up...create an ugly flag since some platforms dont skip the 119 # event when we thought they would 120 self._skip_menu_event = False 121 parent.Bind(wx.EVT_MENU, self._on_menu, self.control) 122 123 # Listen for trait changes on the action (so that we can update its 124 # enabled/disabled/checked state etc). 125 action.on_trait_change(self._on_action_enabled_changed, "enabled") 126 action.on_trait_change(self._on_action_visible_changed, "visible") 127 action.on_trait_change(self._on_action_checked_changed, "checked") 128 action.on_trait_change(self._on_action_name_changed, "name") 129 action.on_trait_change(self._on_action_image_changed, "image") 130 131 if controller is not None: 132 self.controller = controller 133 controller.add_to_menu(self) 134 135 def dispose(self): 136 action = self.item.action 137 action.on_trait_change( 138 self._on_action_enabled_changed, "enabled", remove=True 139 ) 140 action.on_trait_change( 141 self._on_action_visible_changed, "visible", remove=True 142 ) 143 action.on_trait_change( 144 self._on_action_checked_changed, "checked", remove=True 145 ) 146 action.on_trait_change( 147 self._on_action_name_changed, "name", remove=True 148 ) 149 150 # ------------------------------------------------------------------------ 151 # Private interface. 152 # ------------------------------------------------------------------------ 153 154 # Trait event handlers ------------------------------------------------- 155 156 def _enabled_changed(self): 157 """ Called when our 'enabled' trait is changed. """ 158 159 self.control.Enable(self.enabled and self.visible) 160 161 def _visible_changed(self): 162 """ Called when our 'visible' trait is changed. """ 163 164 self.control.Enable(self.visible and self.enabled) 165 166 def _checked_changed(self): 167 """ Called when our 'checked' trait is changed. """ 168 169 if self.item.action.style == "radio": 170 # fixme: Not sure why this is even here, we had to guard it to 171 # make it work? Must take a look at svn blame! 172 # FIXME v3: Note that menu_checked() doesn't seem to exist, so we 173 # comment it out and do the following instead. 174 # if self.group is not None: 175 # self.group.menu_checked(self) 176 177 # If we're turning this one on, then we need to turn all the others 178 # off. But if we're turning this one off, don't worry about the 179 # others. 180 if self.checked: 181 for item in self.item.parent.items: 182 if item is not self.item: 183 item.action.checked = False 184 185 self.control.Check(self.checked) 186 187 def _on_action_enabled_changed(self, action, trait_name, old, new): 188 """ Called when the enabled trait is changed on an action. """ 189 190 self.control.Enable(action.enabled and action.visible) 191 192 def _on_action_visible_changed(self, action, trait_name, old, new): 193 """ Called when the visible trait is changed on an action. """ 194 195 self.control.Enable(action.visible and action.enabled) 196 197 def _on_action_checked_changed(self, action, trait_name, old, new): 198 """ Called when the checked trait is changed on an action. """ 199 200 if self.item.action.style == "radio": 201 # fixme: Not sure why this is even here, we had to guard it to 202 # make it work? Must take a look at svn blame! 203 # FIXME v3: Note that menu_checked() doesn't seem to exist, so we 204 # comment it out and do the following instead. 205 # if self.group is not None: 206 # self.group.menu_checked(self) 207 208 # If we're turning this one on, then we need to turn all the others 209 # off. But if we're turning this one off, don't worry about the 210 # others. 211 if action.checked: 212 for item in self.item.parent.items: 213 if item is not self.item: 214 item.action.checked = False 215 216 # This will *not* emit a menu event because of this ugly flag 217 self._skip_menu_event = True 218 self.control.Check(action.checked) 219 self._skip_menu_event = False 220 221 def _on_action_name_changed(self, action, trait_name, old, new): 222 """ Called when the name trait is changed on an action. """ 223 224 label = action.name 225 if len(action.accelerator) > 0: 226 label = label + "\t" + action.accelerator 227 self.control.SetText(label) 228 229 def _on_action_image_changed(self, action, trait_name, old, new): 230 """ Called when the name trait is changed on an action. """ 231 232 if self.control is not None: 233 self.control.SetIcon(action.image.create_icon()) 234 235 return 236 237 # wx event handlers ---------------------------------------------------- 238 239 def _on_menu(self, event): 240 """ Called when the menu item is clicked. """ 241 242 # if the ugly flag is set, do not perform the menu event 243 if self._skip_menu_event: 244 return 245 246 action = self.item.action 247 action_event = ActionEvent() 248 249 is_checkable = action.style in ["radio", "toggle"] 250 251 # Perform the action! 252 if self.controller is not None: 253 if is_checkable: 254 # fixme: There is a difference here between having a controller 255 # and not in that in this case we do not set the checked state 256 # of the action! This is confusing if you start off without a 257 # controller and then set one as the action now behaves 258 # differently! 259 self.checked = self.control.IsChecked() == 1 260 261 # Most of the time, action's do no care about the event (it 262 # contains information about the time the event occurred etc), so 263 # we only pass it if the perform method requires it. This is also 264 # useful as Traits UI controllers *never* require the event. 265 argspec = getfullargspec(self.controller.perform) 266 267 # If the only arguments are 'self' and 'action' then don't pass 268 # the event! 269 if len(argspec.args) == 2: 270 self.controller.perform(action) 271 272 else: 273 self.controller.perform(action, action_event) 274 275 else: 276 if is_checkable: 277 action.checked = self.control.IsChecked() == 1 278 279 # Most of the time, action's do no care about the event (it 280 # contains information about the time the event occurred etc), so 281 # we only pass it if the perform method requires it. 282 argspec = getfullargspec(action.perform) 283 284 # If the only argument is 'self' then don't pass the event! 285 if len(argspec.args) == 1: 286 action.perform() 287 288 else: 289 action.perform(action_event) 290 291 292class _Tool(HasTraits): 293 """ A tool bar tool representation of an action item. """ 294 295 # '_Tool' interface ---------------------------------------------------- 296 297 # Is the item checked? 298 checked = Bool(False) 299 300 # A controller object we delegate taking actions through (if any). 301 controller = Any() 302 303 # Is the item enabled? 304 enabled = Bool(True) 305 306 # Is the item visible? 307 visible = Bool(True) 308 309 # The radio group we are part of (None if the tool is not part of such a 310 # group). 311 group = Any() 312 313 # ------------------------------------------------------------------------ 314 # 'object' interface. 315 # ------------------------------------------------------------------------ 316 317 def __init__( 318 self, parent, tool_bar, image_cache, item, controller, show_labels 319 ): 320 """ Creates a new tool bar tool for an action item. """ 321 322 self.item = item 323 self.tool_bar = tool_bar 324 325 # Create an appropriate tool depending on the style of the action. 326 action = self.item.action 327 label = action.name 328 329 # Tool bar tools never have '...' at the end! 330 if label.endswith("..."): 331 label = label[:-3] 332 333 # And they never contain shortcuts. 334 label = label.replace("&", "") 335 336 # If the action has an image then convert it to a bitmap (as required 337 # by the toolbar). 338 if action.image is not None: 339 image = action.image.create_image( 340 self.tool_bar.GetToolBitmapSize() 341 ) 342 path = action.image.absolute_path 343 bmp = image_cache.get_bitmap(path) 344 345 else: 346 from pyface.api import ImageResource 347 348 image = ImageResource("image_not_found") 349 bmp = image.create_bitmap() 350 351 kind = _STYLE_TO_KIND_MAP[action.style] 352 tooltip = action.tooltip 353 longtip = action.description 354 355 if not show_labels: 356 label = "" 357 358 else: 359 self.tool_bar.SetSize((-1, 50)) 360 361 if action.style == "widget": 362 widget = action.create_control(self.tool_bar) 363 self.control = tool_bar.AddControl(widget, label) 364 self.control_id = self.control.GetId() 365 else: 366 self.control = tool_bar.AddTool( 367 wx.ID_ANY, 368 label, 369 bmp, 370 wx.NullBitmap, 371 kind, 372 tooltip, 373 longtip, 374 None, 375 ) 376 self.control_id = self.control.GetId() 377 378 # Set the initial checked state. 379 tool_bar.ToggleTool(self.control_id, action.checked) 380 381 if hasattr(tool_bar, "ShowTool"): 382 # Set the initial enabled/disabled state of the action. 383 tool_bar.EnableTool(self.control_id, action.enabled) 384 385 # Set the initial visibility 386 tool_bar.ShowTool(self.control_id, action.visible) 387 else: 388 # Set the initial enabled/disabled state of the action. 389 tool_bar.EnableTool( 390 self.control_id, action.enabled and action.visible 391 ) 392 393 # Wire it up. 394 parent.Bind(wx.EVT_TOOL, self._on_tool, self.control) 395 396 # Listen for trait changes on the action (so that we can update its 397 # enabled/disabled/checked state etc). 398 action.on_trait_change(self._on_action_enabled_changed, "enabled") 399 action.on_trait_change(self._on_action_visible_changed, "visible") 400 action.on_trait_change(self._on_action_checked_changed, "checked") 401 402 if controller is not None: 403 self.controller = controller 404 controller.add_to_toolbar(self) 405 406 # ------------------------------------------------------------------------ 407 # Private interface. 408 # ------------------------------------------------------------------------ 409 410 # Trait event handlers ------------------------------------------------- 411 412 def _enabled_changed(self): 413 """ Called when our 'enabled' trait is changed. """ 414 415 if hasattr(self.tool_bar, "ShowTool"): 416 self.tool_bar.EnableTool(self.control_id, self.enabled) 417 else: 418 self.tool_bar.EnableTool( 419 self.control_id, self.enabled and self.visible 420 ) 421 422 def _visible_changed(self): 423 """ Called when our 'visible' trait is changed. """ 424 425 if hasattr(self.tool_bar, "ShowTool"): 426 self.tool_bar.ShowTool(self.control_id, self.visible) 427 else: 428 self.tool_bar.EnableTool( 429 self.control_id, self.enabled and self.visible 430 ) 431 432 def _checked_changed(self): 433 """ Called when our 'checked' trait is changed. """ 434 435 if self.item.action.style == "radio": 436 # FIXME v3: Note that toolbar_checked() doesn't seem to exist, so 437 # we comment it out and do the following instead. 438 # self.group.toolbar_checked(self) 439 440 # If we're turning this one on, then we need to turn all the others 441 # off. But if we're turning this one off, don't worry about the 442 # others. 443 if self.checked: 444 for item in self.item.parent.items: 445 if item is not self.item: 446 item.action.checked = False 447 448 self.tool_bar.ToggleTool(self.control_id, self.checked) 449 450 def _on_action_enabled_changed(self, action, trait_name, old, new): 451 """ Called when the enabled trait is changed on an action. """ 452 453 if hasattr(self.tool_bar, "ShowTool"): 454 self.tool_bar.EnableTool(self.control_id, action.enabled) 455 else: 456 self.tool_bar.EnableTool( 457 self.control_id, action.enabled and action.visible 458 ) 459 460 def _on_action_visible_changed(self, action, trait_name, old, new): 461 """ Called when the visible trait is changed on an action. """ 462 463 if hasattr(self.tool_bar, "ShowTool"): 464 self.tool_bar.ShowTool(self.control_id, action.visible) 465 else: 466 self.tool_bar.EnableTool( 467 self.control_id, self.enabled and action.visible 468 ) 469 470 def _on_action_checked_changed(self, action, trait_name, old, new): 471 """ Called when the checked trait is changed on an action. """ 472 473 if action.style == "radio": 474 # If we're turning this one on, then we need to turn all the others 475 # off. But if we're turning this one off, don't worry about the 476 # others. 477 if new: 478 for item in self.item.parent.items: 479 if item is not self.item: 480 item.action.checked = False 481 482 # This will *not* emit a tool event. 483 self.tool_bar.ToggleTool(self.control_id, new) 484 485 return 486 487 # wx event handlers ---------------------------------------------------- 488 489 def _on_tool(self, event): 490 """ Called when the tool bar tool is clicked. """ 491 492 action = self.item.action 493 action_event = ActionEvent() 494 495 is_checkable = action.style == "radio" or action.style == "check" 496 497 # Perform the action! 498 if self.controller is not None: 499 # fixme: There is a difference here between having a controller 500 # and not in that in this case we do not set the checked state 501 # of the action! This is confusing if you start off without a 502 # controller and then set one as the action now behaves 503 # differently! 504 self.checked = self.tool_bar.GetToolState(self.control_id) == 1 505 506 # Most of the time, action's do no care about the event (it 507 # contains information about the time the event occurred etc), so 508 # we only pass it if the perform method requires it. This is also 509 # useful as Traits UI controllers *never* require the event. 510 argspec = getfullargspec(self.controller.perform) 511 512 # If the only arguments are 'self' and 'action' then don't pass 513 # the event! 514 if len(argspec.args) == 2: 515 self.controller.perform(action) 516 517 else: 518 self.controller.perform(action, action_event) 519 520 else: 521 action.checked = self.tool_bar.GetToolState(self.control_id) == 1 522 523 # Most of the time, action's do no care about the event (it 524 # contains information about the time the event occurred etc), so 525 # we only pass it if the perform method requires it. 526 argspec = getfullargspec(action.perform) 527 528 # If the only argument is 'self' then don't pass the event! 529 if len(argspec.args) == 1: 530 action.perform() 531 532 else: 533 action.perform(action_event) 534 535 536class _PaletteTool(HasTraits): 537 """ A tool palette representation of an action item. """ 538 539 # '_PaletteTool' interface --------------------------------------------- 540 541 # The radio group we are part of (None if the tool is not part of such a 542 # group). 543 group = Any() 544 545 # ------------------------------------------------------------------------ 546 # 'object' interface. 547 # ------------------------------------------------------------------------ 548 549 def __init__(self, tool_palette, image_cache, item, show_labels): 550 """ Creates a new tool palette tool for an action item. """ 551 552 self.item = item 553 self.tool_palette = tool_palette 554 555 action = self.item.action 556 label = action.name 557 558 if action.style == "widget": 559 raise NotImplementedError( 560 "WxPython does not support widgets in palettes" 561 ) 562 563 # Tool palette tools never have '...' at the end. 564 if label.endswith("..."): 565 label = label[:-3] 566 567 # And they never contain shortcuts. 568 label = label.replace("&", "") 569 570 image = action.image.create_image() 571 path = action.image.absolute_path 572 bmp = image_cache.get_bitmap(path) 573 574 kind = action.style 575 tooltip = action.tooltip 576 longtip = action.description 577 578 if not show_labels: 579 label = "" 580 581 # Add the tool to the tool palette. 582 self.tool_id = tool_palette.add_tool( 583 label, bmp, kind, tooltip, longtip 584 ) 585 tool_palette.toggle_tool(self.tool_id, action.checked) 586 tool_palette.enable_tool(self.tool_id, action.enabled) 587 tool_palette.on_tool_event(self.tool_id, self._on_tool) 588 589 # Listen to the trait changes on the action (so that we can update its 590 # enabled/disabled/checked state etc). 591 action.on_trait_change(self._on_action_enabled_changed, "enabled") 592 action.on_trait_change(self._on_action_checked_changed, "checked") 593 594 return 595 596 # ------------------------------------------------------------------------ 597 # Private interface. 598 # ------------------------------------------------------------------------ 599 600 # Trait event handlers ------------------------------------------------- 601 602 def _on_action_enabled_changed(self, action, trait_name, old, new): 603 """ Called when the enabled trait is changed on an action. """ 604 605 self.tool_palette.enable_tool(self.tool_id, action.enabled) 606 607 def _on_action_checked_changed(self, action, trait_name, old, new): 608 """ Called when the checked trait is changed on an action. """ 609 610 if action.style == "radio": 611 # If we're turning this one on, then we need to turn all the others 612 # off. But if we're turning this one off, don't worry about the 613 # others. 614 if new: 615 for item in self.item.parent.items: 616 if item is not self.item: 617 item.action.checked = False 618 619 # This will *not* emit a tool event. 620 self.tool_palette.toggle_tool(self.tool_id, new) 621 622 return 623 624 # Tool palette event handlers -----------------------------------------# 625 626 def _on_tool(self, event): 627 """ Called when the tool palette button is clicked. """ 628 629 action = self.item.action 630 action_event = ActionEvent() 631 632 is_checkable = action.style == "radio" or action.style == "check" 633 634 # Perform the action! 635 action.checked = self.tool_palette.get_tool_state(self.tool_id) == 1 636 action.perform(action_event) 637 638 return 639