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# (C) Copyright 2008 Riverbank Computing Limited 11# All rights reserved. 12# 13# This software is provided without warranty under the terms of the BSD license. 14# However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply 15 16 17import logging 18 19 20from pyface.qt import QtCore, QtGui 21 22 23from traits.api import Instance, on_trait_change 24 25 26from pyface.message_dialog import error 27from pyface.workbench.i_workbench_window_layout import MWorkbenchWindowLayout 28from .split_tab_widget import SplitTabWidget 29 30 31# Logging. 32logger = logging.getLogger(__name__) 33 34 35# For mapping positions relative to the editor area. 36_EDIT_AREA_MAP = { 37 "left": QtCore.Qt.LeftDockWidgetArea, 38 "right": QtCore.Qt.RightDockWidgetArea, 39 "top": QtCore.Qt.TopDockWidgetArea, 40 "bottom": QtCore.Qt.BottomDockWidgetArea, 41} 42 43# For mapping positions relative to another view. 44_VIEW_AREA_MAP = { 45 "left": (QtCore.Qt.Horizontal, True), 46 "right": (QtCore.Qt.Horizontal, False), 47 "top": (QtCore.Qt.Vertical, True), 48 "bottom": (QtCore.Qt.Vertical, False), 49} 50 51 52class WorkbenchWindowLayout(MWorkbenchWindowLayout): 53 """ The Qt4 implementation of the workbench window layout interface. 54 55 See the 'IWorkbenchWindowLayout' interface for the API documentation. 56 57 """ 58 59 # Private interface ---------------------------------------------------- 60 61 # The widget that provides the editor area. We keep (and use) this 62 # separate reference because we can't always assume that it has been set to 63 # be the main window's central widget. 64 _qt4_editor_area = Instance(SplitTabWidget) 65 66 # ------------------------------------------------------------------------ 67 # 'IWorkbenchWindowLayout' interface. 68 # ------------------------------------------------------------------------ 69 70 def activate_editor(self, editor): 71 if editor.control is not None: 72 editor.control.show() 73 self._qt4_editor_area.setCurrentWidget(editor.control) 74 editor.set_focus() 75 76 return editor 77 78 def activate_view(self, view): 79 # FIXME v3: This probably doesn't work as expected. 80 view.control.raise_() 81 view.set_focus() 82 83 return view 84 85 def add_editor(self, editor, title): 86 if editor is None: 87 return None 88 89 try: 90 self._qt4_editor_area.addTab( 91 self._qt4_get_editor_control(editor), title 92 ) 93 94 if editor._loading_on_open: 95 self._qt4_editor_tab_spinner(editor, "", True) 96 except Exception: 97 logger.exception("error creating editor control [%s]", editor.id) 98 99 return editor 100 101 def add_view(self, view, position=None, relative_to=None, size=(-1, -1)): 102 if view is None: 103 return None 104 105 try: 106 self._qt4_add_view(view, position, relative_to, size) 107 view.visible = True 108 except Exception: 109 logger.exception("error creating view control [%s]", view.id) 110 111 # Even though we caught the exception, it sometimes happens that 112 # the view's control has been created as a child of the application 113 # window (or maybe even the dock control). We should destroy the 114 # control to avoid bad UI effects. 115 view.destroy_control() 116 117 # Additionally, display an error message to the user. 118 error( 119 self.window.control, 120 "Unable to add view [%s]" % view.id, 121 "Workbench Plugin Error", 122 ) 123 124 return view 125 126 def close_editor(self, editor): 127 if editor.control is not None: 128 editor.control.close() 129 130 return editor 131 132 def close_view(self, view): 133 self.hide_view(view) 134 135 return view 136 137 def close(self): 138 # Don't fire signals for editors that have destroyed their controls. 139 self._qt4_editor_area.editor_has_focus.disconnect( 140 self._qt4_editor_focus 141 ) 142 143 self._qt4_editor_area.clear() 144 145 # Delete all dock widgets. 146 for v in self.window.views: 147 if self.contains_view(v): 148 self._qt4_delete_view_dock_widget(v) 149 150 def create_initial_layout(self, parent): 151 self._qt4_editor_area = editor_area = SplitTabWidget(parent) 152 153 editor_area.editor_has_focus.connect(self._qt4_editor_focus) 154 155 # We are interested in focus changes but we get them from the editor 156 # area rather than qApp to allow the editor area to restrict them when 157 # needed. 158 editor_area.focus_changed.connect(self._qt4_view_focus_changed) 159 160 editor_area.tabTextChanged.connect(self._qt4_editor_title_changed) 161 editor_area.new_window_request.connect(self._qt4_new_window_request) 162 editor_area.tab_close_request.connect(self._qt4_tab_close_request) 163 editor_area.tab_window_changed.connect(self._qt4_tab_window_changed) 164 165 return editor_area 166 167 def contains_view(self, view): 168 return hasattr(view, "_qt4_dock") 169 170 def hide_editor_area(self): 171 self._qt4_editor_area.hide() 172 173 def hide_view(self, view): 174 view._qt4_dock.hide() 175 view.visible = False 176 177 return view 178 179 def refresh(self): 180 # Nothing to do. 181 pass 182 183 def reset_editors(self): 184 self._qt4_editor_area.setCurrentIndex(0) 185 186 def reset_views(self): 187 # Qt doesn't provide information about the order of dock widgets in a 188 # dock area. 189 pass 190 191 def show_editor_area(self): 192 self._qt4_editor_area.show() 193 194 def show_view(self, view): 195 view._qt4_dock.show() 196 view.visible = True 197 198 # Methods for saving and restoring the layout -------------------------# 199 200 def get_view_memento(self): 201 # Get the IDs of the views in the main window. This information is 202 # also in the QMainWindow state, but that is opaque. 203 view_ids = [v.id for v in self.window.views if self.contains_view(v)] 204 205 # Everything else is provided by QMainWindow. 206 state = self.window.control.saveState() 207 208 return (0, (view_ids, state)) 209 210 def set_view_memento(self, memento): 211 version, mdata = memento 212 213 # There has only ever been version 0 so far so check with an assert. 214 assert version == 0 215 216 # Now we know the structure of the memento we can "parse" it. 217 view_ids, state = mdata 218 219 # Get a list of all views that have dock widgets and mark them. 220 dock_views = [v for v in self.window.views if self.contains_view(v)] 221 222 for v in dock_views: 223 v._qt4_gone = True 224 225 # Create a dock window for all views that had one last time. 226 for v in self.window.views: 227 # Make sure this is in a known state. 228 v.visible = False 229 230 for vid in view_ids: 231 if vid == v.id: 232 # Create the dock widget if needed and make sure that it is 233 # invisible so that it matches the state of the visible 234 # trait. Things will all come right when the main window 235 # state is restored below. 236 self._qt4_create_view_dock_widget(v).setVisible(False) 237 238 if v in dock_views: 239 delattr(v, "_qt4_gone") 240 241 break 242 243 # Remove any remain unused dock widgets. 244 for v in dock_views: 245 try: 246 delattr(v, "_qt4_gone") 247 except AttributeError: 248 pass 249 else: 250 self._qt4_delete_view_dock_widget(v) 251 252 # Restore the state. This will update the view's visible trait through 253 # the dock window's toggle action. 254 self.window.control.restoreState(state) 255 256 def get_editor_memento(self): 257 # Get the layout of the editors. 258 editor_layout = self._qt4_editor_area.saveState() 259 260 # Get a memento for each editor that describes its contents. 261 editor_references = self._get_editor_references() 262 263 return (0, (editor_layout, editor_references)) 264 265 def set_editor_memento(self, memento): 266 version, mdata = memento 267 268 # There has only ever been version 0 so far so check with an assert. 269 assert version == 0 270 271 # Now we know the structure of the memento we can "parse" it. 272 editor_layout, editor_references = mdata 273 274 def resolve_id(id): 275 # Get the memento for the editor contents (if any). 276 editor_memento = editor_references.get(id) 277 278 if editor_memento is None: 279 return None 280 281 # Create the restored editor. 282 editor = self.window.editor_manager.set_editor_memento( 283 editor_memento 284 ) 285 if editor is None: 286 return None 287 288 # Save the editor. 289 self.window.editors.append(editor) 290 291 # Create the control if needed and return it. 292 return self._qt4_get_editor_control(editor) 293 294 self._qt4_editor_area.restoreState(editor_layout, resolve_id) 295 296 def get_toolkit_memento(self): 297 return (0, {"geometry": self.window.control.saveGeometry()}) 298 299 def set_toolkit_memento(self, memento): 300 if hasattr(memento, "toolkit_data"): 301 data = memento.toolkit_data 302 if isinstance(data, tuple) and len(data) == 2: 303 version, datadict = data 304 if version == 0: 305 geometry = datadict.pop("geometry", None) 306 if geometry is not None: 307 self.window.control.restoreGeometry(geometry) 308 309 def is_editor_area_visible(self): 310 return self._qt4_editor_area.isVisible() 311 312 # ------------------------------------------------------------------------ 313 # Private interface. 314 # ------------------------------------------------------------------------ 315 316 def _qt4_editor_focus(self, new): 317 """ Handle an editor getting the focus. """ 318 319 for editor in self.window.editors: 320 control = editor.control 321 editor.has_focus = control is new or ( 322 control is not None and new in control.children() 323 ) 324 325 def _qt4_editor_title_changed(self, control, title): 326 """ Handle the title being changed """ 327 for editor in self.window.editors: 328 if editor.control == control: 329 editor.name = str(title) 330 331 def _qt4_editor_tab_spinner(self, editor, name, new): 332 # Do we need to do this verification? 333 tw, tidx = self._qt4_editor_area._tab_widget(editor.control) 334 335 if new: 336 tw.show_button(tidx) 337 else: 338 tw.hide_button(tidx) 339 340 if not new and not editor == self.window.active_editor: 341 self._qt4_editor_area.setTabTextColor( 342 editor.control, QtCore.Qt.red 343 ) 344 345 @on_trait_change("window:active_editor") 346 def _qt4_active_editor_changed(self, old, new): 347 """ Handle change of active editor """ 348 # Reset tab title to foreground color 349 if new is not None: 350 self._qt4_editor_area.setTabTextColor(new.control) 351 352 def _qt4_view_focus_changed(self, old, new): 353 """ Handle the change of focus for a view. """ 354 355 focus_part = None 356 357 if new is not None: 358 # Handle focus changes to views. 359 for view in self.window.views: 360 if view.control is not None and view.control.isAncestorOf(new): 361 view.has_focus = True 362 focus_part = view 363 break 364 365 if old is not None: 366 # Handle focus changes from views. 367 for view in self.window.views: 368 if ( 369 view is not focus_part 370 and view.control is not None 371 and view.control.isAncestorOf(old) 372 ): 373 view.has_focus = False 374 break 375 376 def _qt4_new_window_request(self, pos, control): 377 """ Handle a tab tear-out request from the splitter widget. """ 378 379 editor = self._qt4_remove_editor_with_control(control) 380 kind = self.window.editor_manager.get_editor_kind(editor) 381 382 window = self.window.workbench.create_window() 383 window.open() 384 window.add_editor(editor) 385 window.editor_manager.add_editor(editor, kind) 386 window.position = (pos.x(), pos.y()) 387 window.size = self.window.size 388 window.activate_editor(editor) 389 editor.window = window 390 391 def _qt4_tab_close_request(self, control): 392 """ Handle a tabCloseRequest from the splitter widget. """ 393 394 for editor in self.window.editors: 395 if editor.control == control: 396 editor.close() 397 break 398 399 def _qt4_tab_window_changed(self, control): 400 """ Handle a tab drag to a different WorkbenchWindow. """ 401 402 editor = self._qt4_remove_editor_with_control(control) 403 kind = self.window.editor_manager.get_editor_kind(editor) 404 405 while not control.isWindow(): 406 control = control.parent() 407 for window in self.window.workbench.windows: 408 if window.control == control: 409 window.editors.append(editor) 410 window.editor_manager.add_editor(editor, kind) 411 window.layout._qt4_get_editor_control(editor) 412 window.activate_editor(editor) 413 editor.window = window 414 break 415 416 def _qt4_remove_editor_with_control(self, control): 417 """ Finds the editor associated with 'control' and removes it. Returns 418 the editor, or None if no editor was found. 419 """ 420 for editor in self.window.editors: 421 if editor.control == control: 422 self.editor_closing = editor 423 control.removeEventFilter(self._qt4_mon) 424 self.editor_closed = editor 425 426 # Make sure that focus events get fired if this editor is 427 # subsequently added to another window. 428 editor.has_focus = False 429 430 return editor 431 432 def _qt4_get_editor_control(self, editor): 433 """ Create the editor control if it hasn't already been done. """ 434 435 if editor.control is None: 436 self.editor_opening = editor 437 438 # We must provide a parent (because TraitsUI checks for it when 439 # deciding what sort of panel to create) but it can't be the editor 440 # area (because it will be automatically added to the base 441 # QSplitter). 442 editor.control = editor.create_control(self.window.control) 443 editor.control.setObjectName(editor.id) 444 445 editor.on_trait_change(self._qt4_editor_tab_spinner, "_loading") 446 447 self.editor_opened = editor 448 449 def on_name_changed(editor, trait_name, old, new): 450 self._qt4_editor_area.setWidgetTitle(editor.control, editor.name) 451 452 editor.on_trait_change(on_name_changed, "name") 453 454 self._qt4_monitor(editor.control) 455 456 return editor.control 457 458 def _qt4_add_view(self, view, position, relative_to, size): 459 """ Add a view. """ 460 461 # If no specific position is specified then use the view's default 462 # position. 463 if position is None: 464 position = view.position 465 466 dw = self._qt4_create_view_dock_widget(view, size) 467 mw = self.window.control 468 469 try: 470 rel_dw = relative_to._qt4_dock 471 except AttributeError: 472 rel_dw = None 473 474 if rel_dw is None: 475 # If we are trying to add a view with a non-existent item, then 476 # just default to the left of the editor area. 477 if position == "with": 478 position = "left" 479 480 # Position the view relative to the editor area. 481 try: 482 dwa = _EDIT_AREA_MAP[position] 483 except KeyError: 484 raise ValueError("unknown view position: %s" % position) 485 486 mw.addDockWidget(dwa, dw) 487 elif position == "with": 488 # FIXME v3: The Qt documentation says that the second should be 489 # placed above the first, but it always seems to be underneath (ie. 490 # hidden) which is not what the user is expecting. 491 mw.tabifyDockWidget(rel_dw, dw) 492 else: 493 try: 494 orient, swap = _VIEW_AREA_MAP[position] 495 except KeyError: 496 raise ValueError("unknown view position: %s" % position) 497 498 mw.splitDockWidget(rel_dw, dw, orient) 499 500 # The Qt documentation implies that the layout direction can be 501 # used to position the new dock widget relative to the existing one 502 # but I could only get the button positions to change. Instead we 503 # move things around afterwards if required. 504 if swap: 505 mw.removeDockWidget(rel_dw) 506 mw.splitDockWidget(dw, rel_dw, orient) 507 rel_dw.show() 508 509 def _qt4_create_view_dock_widget(self, view, size=(-1, -1)): 510 """ Create a dock widget that wraps a view. """ 511 512 # See if it has already been created. 513 try: 514 dw = view._qt4_dock 515 except AttributeError: 516 dw = QtGui.QDockWidget(view.name, self.window.control) 517 dw.setWidget(_ViewContainer(size, self.window.control)) 518 dw.setObjectName(view.id) 519 dw.toggleViewAction().toggled.connect( 520 self._qt4_handle_dock_visibility 521 ) 522 dw.visibilityChanged.connect(self._qt4_handle_dock_visibility) 523 524 # Save the dock window. 525 view._qt4_dock = dw 526 527 def on_name_changed(): 528 view._qt4_dock.setWindowTitle(view.name) 529 530 view.on_trait_change(on_name_changed, "name") 531 532 # Make sure the view control exists. 533 if view.control is None: 534 # Make sure that the view knows which window it is in. 535 view.window = self.window 536 537 try: 538 view.control = view.create_control(dw.widget()) 539 except: 540 # Tidy up if the view couldn't be created. 541 delattr(view, "_qt4_dock") 542 self.window.control.removeDockWidget(dw) 543 dw.deleteLater() 544 del dw 545 raise 546 547 dw.widget().setCentralWidget(view.control) 548 549 return dw 550 551 def _qt4_delete_view_dock_widget(self, view): 552 """ Delete a view's dock widget. """ 553 554 dw = view._qt4_dock 555 556 # Disassociate the view from the dock. 557 if view.control is not None: 558 view.control.setParent(None) 559 560 delattr(view, "_qt4_dock") 561 562 # Delete the dock (and the view container). 563 self.window.control.removeDockWidget(dw) 564 dw.deleteLater() 565 566 def _qt4_handle_dock_visibility(self, checked): 567 """ Handle the visibility of a dock window changing. """ 568 569 # Find the dock window by its toggle action. 570 for v in self.window.views: 571 try: 572 dw = v._qt4_dock 573 except AttributeError: 574 continue 575 576 sender = dw.sender() 577 if sender is dw.toggleViewAction() or sender in dw.children(): 578 # Toggling the action or pressing the close button on 579 # the view 580 v.visible = checked 581 582 def _qt4_monitor(self, control): 583 """ Install an event filter for a view or editor control to keep an eye 584 on certain events. 585 """ 586 587 # Create the monitoring object if needed. 588 try: 589 mon = self._qt4_mon 590 except AttributeError: 591 mon = self._qt4_mon = _Monitor(self) 592 593 control.installEventFilter(mon) 594 595 596class _Monitor(QtCore.QObject): 597 """ This class monitors a view or editor control. """ 598 599 def __init__(self, layout): 600 QtCore.QObject.__init__(self, layout.window.control) 601 602 self._layout = layout 603 604 def eventFilter(self, obj, e): 605 if isinstance(e, QtGui.QCloseEvent): 606 for editor in self._layout.window.editors: 607 if editor.control is obj: 608 self._layout.editor_closing = editor 609 editor.destroy_control() 610 self._layout.editor_closed = editor 611 612 break 613 614 return False 615 616 617class _ViewContainer(QtGui.QMainWindow): 618 """ This class is a container for a view that allows an initial size 619 (specified as a tuple) to be set. 620 """ 621 622 def __init__(self, size, main_window): 623 """ Initialise the object. """ 624 625 QtGui.QMainWindow.__init__(self) 626 627 # Save the size and main window. 628 self._width, self._height = size 629 self._main_window = main_window 630 631 def sizeHint(self): 632 """ Reimplemented to return the initial size or the view's current 633 size. 634 """ 635 636 sh = self.centralWidget().sizeHint() 637 638 if self._width > 0: 639 if self._width > 1: 640 w = self._width 641 else: 642 w = self._main_window.width() * self._width 643 644 sh.setWidth(int(w)) 645 646 if self._height > 0: 647 if self._height > 1: 648 h = self._height 649 else: 650 h = self._main_window.height() * self._height 651 652 sh.setHeight(int(h)) 653 654 return sh 655 656 def showEvent(self, e): 657 """ Reimplemented to use the view's current size once shown. """ 658 659 self._width = self._height = -1 660 661 QtGui.QMainWindow.showEvent(self, e) 662