Permalink
Browse files

Merge pull request #89 from enthought/fix-split-editor-area-pane-focus

Fix split editor area pane focus
  • Loading branch information...
2 parents 30d12d5 + 87d2c21 commit 8d896fff1334a1ece30e1fcd997f998f4084b623 @agrawalprash agrawalprash committed Jun 21, 2013
View
22 pyface/ui/qt4/tasks/split_editor_area_pane.py
@@ -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.setCurrentWidget(editor.control)
+ self.active_tabwidget = \
+ self._find_ancestor_draggable_tab_widget(control)
+ self.active_tabwidget.setCurrentWidget(control)
# Set active_editor at the end so that the notification
# occurs when everything is ready.
self.active_editor = editor
break
-
###############################################################################
# Auxiliary classes.
###############################################################################
@@ -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():
View
160 pyface/ui/qt4/tasks/tests/test_split_editor_area_pane.py
@@ -1,20 +1,74 @@
-# Tests basic layout operations of the splitter used in split_editor_area_pane.py
+""" Tests for the SplitEditorAreaPane class. """
-import unittest, os, tempfile
+import os
+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.tasks.split_editor_area_pane import SplitEditorAreaPane, EditorAreaWidget
from pyface.qt import QtGui, QtCore
-from pyface.tasks.task_layout import PaneItem, Tabbed, Splitter
-from pyface.tasks.api import Editor
+from pyface.tasks.split_editor_area_pane import EditorAreaWidget, \
+ SplitEditorAreaPane
+from pyface.tasks.api import Editor, PaneItem, Splitter, Tabbed, Task, \
+ TaskWindow
+from pyface.util.guisupport import get_app_qt4
+from pyface.ui.qt4.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):
+ """ Tests for the SplitEditorAreaPane class. """
def _setUp_split(self, parent=None):
""" Sets up the root splitter for splitting. Returns this root.
parent : parent of the returned root
"""
- root = EditorAreaWidget(editor_area=SplitEditorAreaPane(), parent=parent)
+ root = EditorAreaWidget(editor_area=SplitEditorAreaPane(),
+ parent=parent)
btn0 = QtGui.QPushButton('0')
btn1 = QtGui.QPushButton('1')
tabwidget = root.tabwidget()
@@ -58,18 +112,18 @@ def test_split(self):
# does the right tabwidget contain nothing but the empty widget?
self.assertEquals(root.rightchild.tabwidget().count(), 1)
self.assertEquals(root.rightchild.tabwidget().widget(0),
- root.rightchild.tabwidget().empty_widget)
+ root.rightchild.tabwidget().empty_widget)
# do we have an equally sized split?
self.assertEquals(root.leftchild.width(), root.rightchild.width())
# is the rightchild active?
self.assertEquals(root.editor_area.active_tabwidget,
- root.rightchild.tabwidget())
+ root.rightchild.tabwidget())
def _setUp_collapse(self, parent=None):
- """ Creates a root, its leftchild and rightchild, so that collapse can be tested on
- one of the children.
+ """ Creates a root, its leftchild and rightchild, so that collapse can
+ be tested on one of the children.
Returns the root, leftchild and rightchild of such layout.
@@ -105,9 +159,9 @@ def _setUp_collapse(self, parent=None):
return root, left, right
def test_collapse_nonempty(self):
- """ Test for collapse function when the source of collapse is not an empty
- tabwidget. This would result in a new tabwidget which merges the tabs of the
- collapsing tabwidgets.
+ """ Test for collapse function when the source of collapse is not an
+ empty tabwidget. This would result in a new tabwidget which merges
+ the tabs of the collapsing tabwidgets.
"""
# setup root
root, left, right = self._setUp_collapse()
@@ -126,13 +180,12 @@ def test_collapse_nonempty(self):
# how does the combined list look?
self.assertEquals(root.tabwidget().count(), 4)
- self.assertEquals(root.tabwidget().currentWidget(),
- btn2)
+ self.assertEquals(root.tabwidget().currentWidget(), btn2)
def test_collapse_empty(self):
""" Test for collapse function when the collapse origin is an empty
- tabwidget. It's sibling can have an arbitrary layout and the result would
- be such that this layout is transferred to the parent.
+ tabwidget. It's sibling can have an arbitrary layout and the result
+ would be such that this layout is transferred to the parent.
"""
# setup
root = EditorAreaWidget(editor_area=SplitEditorAreaPane(), parent=None)
@@ -159,26 +212,29 @@ def test_collapse_empty(self):
self.assertEquals(root.rightchild.tabwidget().currentIndex(), 0)
# what is the current active_tabwidget?
- self.assertEquals(root.editor_area.active_tabwidget, root.leftchild.tabwidget())
+ self.assertEquals(root.editor_area.active_tabwidget,
+ root.leftchild.tabwidget())
def test_persistence(self):
- """ Tests whether get_layout/set_layout work correctly by setting a given layout
- and getting back the obtained layout.
+ """ Tests whether get_layout/set_layout work correctly by setting a
+ given layout and getting back the obtained layout.
"""
- # setup the test layout - one horizontal split and one vertical split on the
- # rightchild of horizontal split, where the top tabwidget of the vertical split
- # is empty.
- layout = Splitter(Tabbed(PaneItem(id=0, width=600, height=600),
- active_tab=0),
- Splitter(Tabbed(PaneItem(id=-1, width=600, height=300),
- active_tab=0),
- Tabbed(PaneItem(id=1, width=600, height=300),
- PaneItem(id=2, width=600, height=300),
- active_tab=0), orientation='vertical'),
- orientation='horizontal')
- # a total of 3 files are needed to give this layout - one on the leftchild of
- # horizontal split, and the other two on the bottom tabwidget of the
- # rightchild's vertical split
+ # setup the test layout - one horizontal split and one vertical split
+ # on the rightchild of horizontal split, where the top tabwidget of
+ # the vertical split is empty.
+ layout = Splitter(
+ Tabbed(PaneItem(id=0, width=600, height=600),
+ active_tab=0),
+ Splitter(Tabbed(PaneItem(id=-1, width=600, height=300),
+ active_tab=0),
+ Tabbed(PaneItem(id=1, width=600, height=300),
+ PaneItem(id=2, width=600, height=300),
+ active_tab=0),
+ orientation='vertical'),
+ orientation='horizontal')
+ # a total of 3 files are needed to give this layout - one on the
+ # leftchild of horizontal split, and the other two on the bottom
+ # tabwidget of the rightchild's vertical split
file0 = open(os.path.join(tempfile.gettempdir(), 'file0'), 'w+b')
file1 = open(os.path.join(tempfile.gettempdir(), 'file1'), 'w+b')
file2 = open(os.path.join(tempfile.gettempdir(), 'file2'), 'w+b')
@@ -256,5 +312,39 @@ def test_persistence(self):
self.assertEquals(right_bottom.items[0].id, 1)
self.assertEquals(right_bottom.items[1].id, 2)
-if __name__=="__main__":
+ 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()
View
0 pyface/ui/qt4/util/__init__.py
No changes.
View
47 pyface/ui/qt4/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 8d896ff

Please sign in to comment.