Skip to content

Commit

Permalink
Add the ObjectCombo control which is a better ComboBox widget.
Browse files Browse the repository at this point in the history
  • Loading branch information
sccolbert committed Apr 2, 2013
1 parent b264b3b commit 51f3a3c
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 1 deletion.
7 changes: 6 additions & 1 deletion enaml/qt/qt_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ def notebook_factory():
return QtNotebook


def object_combo_factory():
from .qt_object_combo import QtObjectCombo
return QtObjectCombo


def page_factory():
from .qt_page import QtPage
return QtPage
Expand Down Expand Up @@ -263,7 +268,6 @@ def window_factory():
'CheckBox': check_box_factory,
'ComboBox': combo_box_factory,
'Container': container_factory,
'Control': control_factory,
'DateSelector': date_selector_factory,
'DatetimeSelector': datetime_selector_factory,
'DockPane': dock_pane_factory,
Expand All @@ -286,6 +290,7 @@ def window_factory():
'MPLCanvas': mpl_canvas_factory,
'MultilineField': multiline_field_factory,
'Notebook': notebook_factory,
'ObjectCombo': object_combo_factory,
'Page': page_factory,
'PushButton': push_button_factory,
'ProgressBar': progress_bar_factory,
Expand Down
178 changes: 178 additions & 0 deletions enaml/qt/qt_object_combo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, 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 PyQt4.QtCore import QTimer
from PyQt4.QtGui import QComboBox

from atom.api import Int, Typed

from enaml.widgets.object_combo import ProxyObjectCombo

from .q_resource_helpers import get_cached_qicon
from .qt_control import QtControl


# cyclic notification guard flags
SELECTED_GUARD = 0x1


class ComboRefreshTimer(QTimer):
""" A QTimer used for collapsing items refresh requests.
This is a single shot timer which automatically cleans itself up
when its timer event is triggered.
"""
def __init__(self, owner):
""" Initialize a ComboRefreshTimer.
Parameters
----------
owner : QtObjectCombo
The object combo which owns the timer.
"""
super(ComboRefreshTimer, self).__init__()
self.setSingleShot(True)
self.owner = owner

def timerEvent(self, event):
""" Handle the timer event for the timer.
This handler will call the 'refresh_items' method on the owner
and then release all references to itself and the owner.
"""
super(ComboRefreshTimer, self).timerEvent(event)
owner = self.owner
if owner is not None:
del owner.refresh_timer
self.owner = None
owner.refresh_items()


class QtObjectCombo(QtControl, ProxyObjectCombo):
""" A Qt implementation of an Enaml ProxyObjectCombo.
"""
#: A reference to the widget created by the proxy.
widget = Typed(QComboBox)

#: A single shot refresh timer for queing combo refreshes.
refresh_timer = Typed(ComboRefreshTimer)

#: Cyclic notification guard. This a bitfield of multiple guards.
_guard = Int(0)

#--------------------------------------------------------------------------
# Default Value Handlers
#--------------------------------------------------------------------------
def _default_refresh_timer(self):
""" Get a refresh timer for the object combo box.
"""
return ComboRefreshTimer(self)

#--------------------------------------------------------------------------
# Initialization API
#--------------------------------------------------------------------------
def create_widget(self):
""" Create the QComboBox widget.
"""
self.widget = QComboBox(self.parent_widget())
self.widget.setInsertPolicy(QComboBox.NoInsert)

def init_widget(self):
""" Create and initialize the underlying widget.
"""
super(QtObjectCombo, self).init_widget()
self.refresh_items()
self.widget.currentIndexChanged.connect(self.on_index_changed)

#--------------------------------------------------------------------------
# Signal Handlers
#--------------------------------------------------------------------------
def on_index_changed(self, index):
""" The signal handler for the index changed signal.
"""
if not self._guard & SELECTED_GUARD:
self._guard |= SELECTED_GUARD
try:
item = self.declaration.items[index]
self.declaration.selected = item
finally:
self._guard &= ~SELECTED_GUARD

#--------------------------------------------------------------------------
# Public API
#--------------------------------------------------------------------------
def refresh_items(self):
""" Refresh the items in the combo box.
"""
d = self.declaration
selected = d.selected
to_string = d.to_string
to_icon = d.to_icon
widget = self.widget
self._guard |= SELECTED_GUARD
try:
widget.clear()
target_index = -1
for index, item in enumerate(d.items):
text = to_string(item)
icon = to_icon(item)
if icon is None:
qicon = None
else:
qicon = get_cached_qicon(icon)
if qicon is None:
widget.addItem(text)
else:
widget.addItem(qicon, text)
if item is selected:
target_index = index
widget.setCurrentIndex(target_index)
finally:
self._guard &= ~SELECTED_GUARD

#--------------------------------------------------------------------------
# ProxyObjectCombo API
#--------------------------------------------------------------------------
def set_selected(self, selected):
""" Set the selected object in the combo box.
"""
if not self._guard & SELECTED_GUARD:
self._guard |= SELECTED_GUARD
try:
d = self.declaration
try:
index = d.items.index(selected)
except ValueError:
index = -1
self.widget.setCurrentIndex(index)
finally:
self._guard &= ~SELECTED_GUARD

def set_editable(self, editable):
""" Set whether the combo box is editable.
"""
# The update is needed to avoid artificats (at least on Windows)
widget = self.widget
widget.setEditable(editable)
widget.update()

def request_items_refresh(self):
""" Request a refresh of the combo box items.
"""
self.refresh_timer.start()
1 change: 1 addition & 0 deletions enaml/widgets/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .mpl_canvas import MPLCanvas
from .multiline_field import MultilineField
from .notebook import Notebook
from .object_combo import ObjectCombo
from .page import Page
from .progress_bar import ProgressBar
from .push_button import PushButton
Expand Down
92 changes: 92 additions & 0 deletions enaml/widgets/object_combo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, 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 (
Bool, Callable, List, Value, Typed, ForwardTyped, set_default, observe
)

from enaml.core.declarative import d_

from .control import Control, ProxyControl


class ProxyObjectCombo(ProxyControl):
""" The abstract defintion of a proxy ObjectCombo object.
"""
#: A reference to the ObjectCombo declaration.
declaration = ForwardTyped(lambda: ObjectCombo)

def set_selected(self, selected):
raise NotImplementedError

def set_editable(self, editable):
raise NotImplementedError

def request_items_refresh(self):
raise NotImplementedError


class ObjectCombo(Control):
""" A drop-down list from which one item can be selected at a time.
Use a combo box to select a single item from a collection of items.
"""
#: The list of items to display in the combo box.
items = d_(List())

#: The selected item from the list of items. The default will be
#: the first item in the list of items, or None.
selected = d_(Value())

#: The callable to use to convert the items into unicode strings
#: for display. The default is the builtin 'unicode'.
to_string = d_(Callable(unicode))

#: The callable to use to convert the items into icons for
#: display. The default is a lambda which returns None.
to_icon = d_(Callable(lambda item: None))

#: Whether the text in the combo box can be edited by the user.
editable = d_(Bool(False))

#: A combo box hugs its width weakly by default.
hug_width = set_default('weak')

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

#--------------------------------------------------------------------------
# Default Value Handlers
#--------------------------------------------------------------------------
def _default_selected(self):
""" The default value handler for the 'selected' member.
"""
items = self.items
if len(items) > 0:
return items[0]

#--------------------------------------------------------------------------
# Observers
#--------------------------------------------------------------------------
@observe(('items', 'to_string', 'to_icon'))
def _refresh_proxy(self, change):
""" An observer which requests an items refresh from the proxy.
"""
if self.proxy_is_active:
self.proxy.request_items_refresh()

@observe(('selected', 'editable'))
def _update_proxy(self, change):
""" An observer which sends state change to the proxy.
"""
# The superclass handler implementation is sufficient.
super(ObjectCombo, self)._update_proxy(change)

0 comments on commit 51f3a3c

Please sign in to comment.