Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fix a bug with setting the active tabwidget when an editor contains t…
…abs.

If an editor contains tabs, after certain operations (e.g., navigating
the editor tabs using keyboard shortcuts) a change in focus
sets the editor area pane `active_tabwidget` to one of those internal tabs,
rather than to the tab of the editor in the editor area.
  • Loading branch information
Pietro Berkes committed Jun 20, 2013
1 parent 1719195 commit da80101
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 6 deletions.
20 changes: 15 additions & 5 deletions pyface/ui/qt4/tasks/split_editor_area_pane.py
Expand Up @@ -317,30 +317,40 @@ def _update_tooltip(self, editor, name, new):

#### Signal handlers ######################################################

def _find_ancestor_draggable_tab_widget(self, control):
""" Find the draggable tab widget to which a widget belongs. """

while not isinstance(control, DraggableTabWidget):
control = control.parent()

return control

def _focus_changed(self, old, new):
""" Handle an application-level focus change to set the active_tabwidget
"""Set the active tabwidget after an application-level change in focus.
"""

if new:
if isinstance(new, DraggableTabWidget):
if new.editor_area == self:
self.active_tabwidget = new
elif isinstance(new, QtGui.QTabBar):
if self.control.isAncestorOf(new):
self.active_tabwidget = new.parent()
self.active_tabwidget = \
self._find_ancestor_draggable_tab_widget(new)
else:
# Check if any of the editor widgets have focus.
# If so, make it active.
for editor in self.editors:
control = editor.control
if control is not None and control.isAncestorOf(new):
self.active_tabwidget = editor.control.parent().parent()
self.active_tabwidget = \
self._find_ancestor_draggable_tab_widget(new)
self.active_tabwidget.setCurrentWidget(editor.control)
# Set active_editor at the end so that the notification
# occurs when everything is ready.
self.active_editor = editor
break


###############################################################################
# Auxiliary classes.
###############################################################################
Expand Down Expand Up @@ -457,7 +467,7 @@ def tabwidget(self):

def tabwidgets(self):
""" Return a list of tabwidgets associated with current splitter or
any of its descendents.
any of its descendants.
"""
tabwidgets = []
if self.is_leaf():
Expand Down
85 changes: 84 additions & 1 deletion pyface/ui/qt4/tasks/tests/test_split_editor_area_pane.py
Expand Up @@ -4,10 +4,59 @@
import tempfile
import unittest

from traits.api import HasTraits, Instance
from traitsui.api import Group, UItem, View
from traitsui.api import Tabbed as TabbedGroup

from pyface.qt import QtGui, QtCore
from pyface.tasks.split_editor_area_pane import EditorAreaWidget, \
SplitEditorAreaPane
from pyface.tasks.api import Editor, PaneItem, Splitter, Tabbed
from pyface.tasks.api import Editor, PaneItem, Splitter, Tabbed, Task, \
TaskWindow
from pyface.util.guisupport import get_app_qt4
from pyface.util.testing import event_loop


class ViewWithTabs(HasTraits):
""" A view with tabs used to confuse the SplitEditorAreaPane. """
traits_view = View(
TabbedGroup(
Group(UItem(label='tab 1')),
Group(UItem(label='tab 2')),
)
)


class ViewWithTabsEditor(Editor):
""" Test editor, displaying a TraitsUI view with tabs. """

name = 'Test Editor'

def create(self, parent):
""" Create and set the toolkit-specific contents of the editor.
"""
view = ViewWithTabs()
self.ui = view.edit_traits(kind='subpanel', parent=parent)
self.control = self.ui.control

def destroy(self):
""" Destroy the toolkit-specific control that represents the editor.
"""
self.control = None
self.ui.dispose()
self.ui = None


class SplitEditorAreaPaneTestTask(Task):
""" A test task containing a SplitEditorAreaPane. """

id = 'test_task'
name = 'Test Task'

editor_area = Instance(SplitEditorAreaPane, ())

def create_central_pane(self):
return self.editor_area


class TestEditorAreaWidget(unittest.TestCase):
Expand Down Expand Up @@ -262,5 +311,39 @@ def test_persistence(self):
self.assertEquals(right_bottom.items[0].id, 1)
self.assertEquals(right_bottom.items[1].id, 2)

def test_active_tabwidget_after_editor_containing_tabs_gets_focus(self):
# Regression test: if an editor contains tabs, a change in focus
# sets the editor area pane `active_tabwidget` to one of those tabs,
# rather than the editor's tab, after certain operations (e.g.,
# navigating the editor tabs using keyboard shortcuts).

window = TaskWindow()

task = SplitEditorAreaPaneTestTask()
editor_area = task.editor_area
window.add_task(task)

# Show the window.
with event_loop():
window.open()

with event_loop():
app = get_app_qt4()
app.setActiveWindow(window.control)

# Add and activate an editor which contains tabs.
editor = ViewWithTabsEditor()
with event_loop():
editor_area.add_editor(editor)
with event_loop():
editor_area.activate_editor(editor)

# Check that the active tabwidget is the right one.
self.assertIs(editor_area.active_tabwidget,
editor_area.control.tabwidget())

with event_loop():
window.close()

if __name__=="__main__":
unittest.main()
47 changes: 47 additions & 0 deletions pyface/util/testing.py
@@ -0,0 +1,47 @@
# Copyright (c) 2013 by Enthought, Inc., Austin, TX
# All rights reserved.
# This file is confidential and NOT open source. Do not distribute.
""" Tools for testing. """


from contextlib import contextmanager

from pyface.qt.QtCore import QTimer
from pyface.util.guisupport import get_app_qt4


@contextmanager
def event_loop():
""" Post and process the Qt events at the exit of the code block. """

app = get_app_qt4()

yield

app.sendPostedEvents()
app.processEvents()


@contextmanager
def delete_widget(widget, timeout=1.0):
""" Context manager that executes the event loop so that we can safely
delete a Qt widget.
"""

app = get_app_qt4()

timer = QTimer()
timer.setSingleShot(True)
timer.setInterval(timeout*1000)
timer.timeout.connect(app.quit)
widget.destroyed.connect(app.quit)

yield

timer.start()
app.exec_()

if not timer.isActive():
# We exited the event loop on timeout.
msg = 'Could not destroy widget before timeout: {!r}'
raise AssertionError(msg.format(widget))

0 comments on commit da80101

Please sign in to comment.