Skip to content

Commit

Permalink
Merge pull request #151 from nucleic/feature-focus-events
Browse files Browse the repository at this point in the history
Add focus api
  • Loading branch information
sccolbert committed May 7, 2014
2 parents fa1de79 + fea285c commit 1090b3f
Show file tree
Hide file tree
Showing 10 changed files with 511 additions and 32 deletions.
31 changes: 6 additions & 25 deletions enaml/core/declarative_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,13 @@ def __get__(self, im_self, ignored):
self.im_func, self.im_class, self.im_key, im_self)


def _make_super(im_class, im_self):
""" Create a super() function bound to a class and instance.
This is an internal function is used to create a super object
for use within a declarative function. It should not be consumed
by user code.
Parameters
----------
im_class : type
The class object for the super call.
im_self : object
The instance object for the super call.
Returns
-------
result : FunctionType
A closure which takes no arguments and returns a *real*
bound super object when invoked.
def super_disallowed(*args, **kwargs):
""" A function which disallows super() in a declarative function.
"""
def super():
global super
return super(im_class, im_self)
return super
msg = ('super() is not allowed in a declarative function, '
'use SomeClass.some_method(self, ...) instead.')
raise TypeError(msg)


class BoundDeclarativeMethod(object):
Expand Down Expand Up @@ -122,5 +103,5 @@ def __call__(self, *args, **kwargs):
im_self = self.im_self
f_locals = im_self._d_storage.get(self.im_key) or {}
scope = DynamicScope(im_self, f_locals, f_globals, f_builtins)
scope['super'] = _make_super(self.im_class, im_self)
scope['super'] = super_disallowed
return call_func(im_func, args, kwargs, scope)
6 changes: 6 additions & 0 deletions enaml/qt/qt_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ def flow_item_factory():
return QtFlowItem


def focus_tracker_factory():
from .qt_focus_tracker import QtFocusTracker
return QtFocusTracker


def group_box_factory():
from .qt_group_box import QtGroupBox
return QtGroupBox
Expand Down Expand Up @@ -310,6 +315,7 @@ def window_factory():
'FileDialogEx': file_dialog_ex_factory,
'FlowArea': flow_area_factory,
'FlowItem': flow_item_factory,
'FocusTracker': focus_tracker_factory,
'GroupBox': group_box_factory,
'Html': html_factory,
'Image': image_factory,
Expand Down
54 changes: 54 additions & 0 deletions enaml/qt/qt_focus_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#------------------------------------------------------------------------------
# Copyright (c) 2014, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from enaml.widgets.focus_tracker import ProxyFocusTracker

from .QtGui import QApplication

from .qt_toolkit_object import QtToolkitObject


class QtFocusTracker(QtToolkitObject, ProxyFocusTracker):
""" A Qt implementation of an Enaml ProxyFocusTracker.
"""
def create_widget(self):
""" Create the underlying widget.
"""
# A focus tracker does not have a widget representation.
self.widget = None

def init_widget(self):
""" Initialize the underlying widget.
"""
super(QtFocusTracker, self).init_widget()
QApplication.instance().focusChanged.connect(self._on_focus_changed)
self._update_focus_widget()

def destroy(self):
""" A reimplemented destructor.
"""
QApplication.instance().focusChanged.disconnect(self._on_focus_changed)
super(QtFocusTracker, self).destroy()

def _on_focus_changed(self, old, new):
""" Handle the application 'focusChanged' signal.
"""
self._update_focus_widget()

def _update_focus_widget(self):
""" Update the tracker with currently focused widget.
"""
fw = QApplication.focusWidget()
fp = fw and getattr(fw, '_d_proxy', None)
fd = fp and fp.declaration
self.declaration.focused_widget = fd
8 changes: 6 additions & 2 deletions enaml/qt/qt_toolkit_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def init_widget(self):
"""
widget = self.widget
if widget is not None:
# Tag the widget with a back-reference to the proxy.
widget._d_proxy = self
# Each Qt object gets a name. If one is not provided by the
# widget author, one is generated. This is required so that
# Qt stylesheet cascading can be prevented (Enaml's styling
Expand Down Expand Up @@ -88,8 +90,10 @@ def destroy(self):
and set its parent to None.
"""
if self.widget is not None:
self.widget.setParent(None)
widget = self.widget
if widget is not None:
widget.setParent(None)
widget._d_proxy = None
del self.widget
super(QtToolkitObject, self).destroy()

Expand Down
143 changes: 141 additions & 2 deletions enaml/qt/qt_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
#------------------------------------------------------------------------------
import sys

from atom.api import Typed
from atom.api import Typed, Coerced, Value

from enaml.styling import StyleCache
from enaml.widgets.widget import ProxyWidget
from enaml.widgets.widget import Feature, ProxyWidget

from .QtCore import Qt, QSize
from .QtGui import QFont, QWidget, QApplication
Expand All @@ -20,13 +20,31 @@
from .styleutil import translate_style


#: A mapping of Enaml focus policies -> Qt focus policies.
FOCUS_POLICIES = {
'tab_focus': Qt.TabFocus,
'click_focus': Qt.ClickFocus,
'strong_focus': Qt.StrongFocus,
'wheel_focus': Qt.WheelFocus,
'no_focus': Qt.NoFocus,
}


class QtWidget(QtToolkitObject, ProxyWidget):
""" A Qt implementation of an Enaml ProxyWidget.
"""
#: A reference to the toolkit widget created by the proxy.
widget = Typed(QWidget)

#: A private copy of the declaration features. This ensures that
#: feature cleanup will proceed correctly in the event that user
#: code modifies the declaration features value at runtime.
_features = Coerced(Feature.Flags)

#: An internal cache of the widget's default focus policy.
_default_focus_policy = Value()

#--------------------------------------------------------------------------
# Initialization API
#--------------------------------------------------------------------------
Expand All @@ -41,6 +59,7 @@ def init_widget(self):
"""
super(QtWidget, self).init_widget()
self._install_features()
d = self.declaration
if d.background:
self.set_background(d.background)
Expand All @@ -50,6 +69,8 @@ def init_widget(self):
self.set_font(d.font)
if d.show_focus_rect is not None:
self.set_show_focus_rect(d.show_focus_rect)
if d.focus_policy != 'default':
self.set_focus_policy(d.focus_policy)
if -1 not in d.minimum_size:
self.set_minimum_size(d.minimum_size)
if -1 not in d.maximum_size:
Expand All @@ -69,6 +90,81 @@ def init_widget(self):
if self.widget.parent() or not d.visible:
self.set_visible(d.visible)

def destroy(self):
""" Destroy the underlying QWidget object.
"""
self._remove_features()
super(QtWidget, self).destroy()

#--------------------------------------------------------------------------
# Private API
#--------------------------------------------------------------------------
def _install_features(self):
""" Install the advanced widget feature handlers.
"""
features = self._features = self.declaration.features
if not features:
return
widget = self.widget
if features & Feature.FocusTraversal:
widget.focusNextPrevChild = self._focusNextPrevChild
if features & Feature.FocusEvents:
widget.focusInEvent = self._focusInEvent
widget.focusOutEvent = self._focusOutEvent

def _remove_features(self):
""" Remove the advanced widget feature handlers.
"""
features = self._features
if not features:
return
widget = self.widget
if features & Feature.FocusTraversal:
del widget.focusNextPrevChild
if features & Feature.FocusEvents:
del widget.focusInEvent
del widget.focusOutEvent

def _focusNextPrevChild(self, next_child):
""" The duck-punched 'focusNextPrevChild' implementation.
"""
fw = QApplication.focusWidget()
fp = fw and getattr(fw, '_d_proxy', None)
fd = fp and fp.declaration
if next_child:
child = self.declaration.next_focus_child(fd)
reason = Qt.TabFocusReason
else:
child = self.declaration.previous_focus_child(fd)
reason = Qt.BacktabFocusReason
if child is not None and child.proxy_is_active:
cw = child.proxy.widget
if cw.focusPolicy() & Qt.TabFocus:
cw.setFocus(reason)
return True
widget = self.widget
return type(widget).focusNextPrevChild(widget, next_child)

def _focusInEvent(self, event):
""" The duck-punched 'focusInEvent' implementation.
"""
widget = self.widget
type(widget).focusInEvent(widget, event)
self.declaration.focus_gained()

def _focusOutEvent(self, event):
""" The duck-punched 'focusOutEvent' implementation.
"""
widget = self.widget
type(widget).focusOutEvent(widget, event)
self.declaration.focus_lost()

#--------------------------------------------------------------------------
# Protected API
#--------------------------------------------------------------------------
Expand Down Expand Up @@ -171,6 +267,19 @@ def set_show_focus_rect(self, show):
if sys.platform == 'darwin':
self.widget.setAttribute(Qt.WA_MacShowFocusRect, bool(show))

def set_focus_policy(self, policy):
""" Set the focus policy of the widget.
"""
widget = self.widget
if self._default_focus_policy is None:
self._default_focus_policy = widget.focusPolicy()
if policy == 'default':
q_policy = self._default_focus_policy
else:
q_policy = FOCUS_POLICIES[policy]
widget.setFocusPolicy(q_policy)

def set_tool_tip(self, tool_tip):
""" Set the tool tip for the widget.
Expand Down Expand Up @@ -200,3 +309,33 @@ def restyle(self):
"""
self.refresh_style_sheet()

def set_focus(self):
""" Set the keyboard input focus to this widget.
"""
self.widget.setFocus(Qt.OtherFocusReason)

def clear_focus(self):
""" Clear the keyboard input focus from this widget.
"""
self.widget.clearFocus()

def has_focus(self):
""" Test whether this widget has input focus.
"""
return self.widget.hasFocus()

def focus_next_child(self):
""" Give focus to the next widget in the focus chain.
"""
self.widget.focusNextChild()

def focus_previous_child(self):
""" Give focus to the previous widget in the focus chain.
"""
self.widget.focusPreviousChild()
2 changes: 2 additions & 0 deletions enaml/widgets/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .file_dialog_ex import FileDialogEx
from .flow_area import FlowArea
from .flow_item import FlowItem
from .focus_tracker import FocusTracker
from .form import Form
from .frame import Border
from .group_box import GroupBox
Expand Down Expand Up @@ -61,4 +62,5 @@
from .v_group import VGroup
from .vtk_canvas import VTKCanvas
from .web_view import WebView
from .widget import Feature
from .window import Window
34 changes: 34 additions & 0 deletions enaml/widgets/focus_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#------------------------------------------------------------------------------
# Copyright (c) 2014, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from atom.api import ForwardTyped, Typed

from enaml.core.declarative import d_

from .toolkit_object import ToolkitObject, ProxyToolkitObject
from .widget import Widget


class ProxyFocusTracker(ProxyToolkitObject):
""" The abstract definition of a proxy FocusTracker object.
"""
#: A reference to the FocusTracker declaration.
declaration = ForwardTyped(lambda: FocusTracker)


class FocusTracker(ToolkitObject):
""" An object which tracks the global application focus widget.
"""
#: The application widget with the current input focus. This will
#: be None if no widget in the application has focus, or if the
#: focused widget does not directly correspond to an Enaml widget.
focused_widget = d_(Typed(Widget), writable=False)

#: A reference to the ProxyFocusTracker object.
proxy = Typed(ProxyFocusTracker)
Loading

0 comments on commit 1090b3f

Please sign in to comment.