"""
The classes in this module provide a property-like interface
to widget instance variables in a class. These properties translate
essential pieces of widget state into more convenient python objects
(for example, the check state of a button to a bool).
Example Use::
class Foo(object):
bar = ButtonProperty('_button')
def __init__(self):
self._button = QtGui.QCheckBox()
f = Foo()
f.bar = True # equivalent to f._button.setChecked(True)
assert f.bar == True
"""
from __future__ import absolute_import, division, print_function
import math
from functools import partial
from glue.logger import logger
from glue.external.six.moves import reduce
from glue.external.qt import QtGui
from glue.external.echo import add_callback
from glue.utils.array import pretty_number
__all__ = ['WidgetProperty', 'CurrentComboDataProperty',
'CurrentComboTextProperty', 'CurrentTabProperty', 'TextProperty',
'ButtonProperty', 'FloatLineProperty', 'ValueProperty',
'connect_bool_button', 'connect_current_combo',
'connect_float_edit', 'connect_int_spin']
class WidgetProperty(object):
"""
Base class for widget properties
Subclasses implement, at a minimum, the "get" and "set" methods,
which translate between widget states and python variables
Parameters
----------
att : str
The location, within a class instance, of the widget to wrap around.
If the widget is nested inside another variable, normal '.' syntax
can be used (e.g. 'sub_window.button')
docstring : str, optional
Optional short summary for the property. Used by sphinx. Should be 1
sentence or less.
"""
def __init__(self, att, docstring=''):
self.__doc__ = docstring
self._att = att.split('.')
def __get__(self, instance, type=None):
# Under certain circumstances, PyQt will try and access these properties
# while loading the ui file, so we have to be robust to failures.
# However, we print out a warning if things fail.
try:
widget = reduce(getattr, [instance] + self._att)
return self.getter(widget)
except Exception:
logger.info("An error occured when accessing attribute {0} of {1}. Returning None.".format('.'.join(self._att), instance))
return None
def __set__(self, instance, value):
widget = reduce(getattr, [instance] + self._att)
self.setter(widget, value)
def getter(self, widget):
""" Return the state of a widget. Depends on type of widget,
and must be overridden"""
raise NotImplementedError() # pragma: no cover
def setter(self, widget, value):
""" Set the state of a widget to a certain value"""
raise NotImplementedError() # pragma: no cover
class CurrentComboDataProperty(WidgetProperty):
"""
Wrapper around the data in QComboBox.
"""
def getter(self, widget):
"""
Return the itemData stored in the currently-selected item
"""
if widget.currentIndex() == -1:
return None
else:
return widget.itemData(widget.currentIndex())
def setter(self, widget, value):
"""
Update the currently selected item to the one which stores value in
its itemData
"""
# Note, we don't use findData here because it doesn't work
# well with non-str data
try:
idx = _find_combo_data(widget, value)
except ValueError:
if value is None:
idx = -1
else:
raise ValueError("Cannot find data '{0}' in combo box".format(value))
widget.setCurrentIndex(idx)
CurrentComboProperty = CurrentComboDataProperty
class CurrentComboTextProperty(WidgetProperty):
"""
Wrapper around the text in QComboBox.
"""
def getter(self, widget):
"""
Return the itemData stored in the currently-selected item
"""
if widget.currentIndex() == -1:
return None
else:
return widget.itemText(widget.currentIndex())
def setter(self, widget, value):
"""
Update the currently selected item to the one which stores value in
its itemData
"""
idx = widget.findText(value)
if idx == -1:
if value is not None:
raise ValueError("Cannot find text '{0}' in combo box".format(value))
widget.setCurrentIndex(idx)
class CurrentTabProperty(WidgetProperty):
"""
Wrapper around QTabWidget.
"""
def getter(self, widget):
"""
Return the itemData stored in the currently-selected item
"""
return widget.tabText(widget.currentIndex())
def setter(self, widget, value):
"""
Update the currently selected item to the one which stores value in
its itemData
"""
for idx in range(widget.count()):
if widget.tabText(idx) == value:
break
else:
raise ValueError("Cannot find value '{0}' in tabs".format(value))
widget.setCurrentIndex(idx)
class TextProperty(WidgetProperty):
"""
Wrapper around the text() and setText() methods for QLabel etc
"""
def getter(self, widget):
return widget.text()
def setter(self, widget, value):
widget.setText(value)
class ButtonProperty(WidgetProperty):
"""
Wrapper around the check state for QAbstractButton widgets
"""
def getter(self, widget):
return widget.isChecked()
def setter(self, widget, value):
widget.setChecked(value)
class FloatLineProperty(WidgetProperty):
"""
Wrapper around the text state for QLineEdit widgets.
Assumes that the text is a floating-point number
"""
def getter(self, widget):
try:
return float(widget.text())
except ValueError:
return 0
def setter(self, widget, value):
widget.setText(pretty_number(value))
widget.editingFinished.emit()
class ValueProperty(WidgetProperty):
"""
Wrapper around widgets with value() and setValue()
Parameters
----------
att : str
The location, within a class instance, of the widget to wrap around.
If the widget is nested inside another variable, normal '.' syntax
can be used (e.g. 'sub_window.button')
docstring : str, optional
Optional short summary for the property. Used by sphinx. Should be 1
sentence or less.
mapping : tuple, optional
If specified, should be a tuple of two functions - the first to map
from Qt widget values to Python values, and the second to map from
Python values to Qt widget values. This can be used for to specify a
non-linear mapping for sliders.
"""
def __init__(self, att, docstring='', mapping=None, value_range=None):
super(ValueProperty, self).__init__(att, docstring=docstring)
self.mapping = mapping
self.value_range = value_range
def getter(self, widget):
if self.mapping is not None:
return self.mapping[0](widget.value())
elif self.value_range is not None:
imin, imax = widget.minimum(), widget.maximum()
vmin, vmax = self.value_range
return (widget.value() - imin) / (imax - imin) * (vmax - vmin) + vmin
else:
return widget.value()
def setter(self, widget, value):
if self.mapping is not None:
widget.setValue(self.mapping[1](value))
elif self.value_range is not None:
imin, imax = widget.minimum(), widget.maximum()
vmin, vmax = self.value_range
widget.setValue((value - vmin) / (vmax - vmin) * (imax - imin) + imin)
else:
widget.setValue(value)
def connect_bool_button(client, prop, widget):
""" Connect widget.setChecked and client.prop
client.prop should be a callback property
"""
add_callback(client, prop, widget.setChecked)
widget.toggled.connect(partial(setattr, client, prop))
def connect_current_combo(client, prop, widget):
"""
Connect widget.currentIndexChanged and client.prop
client.prop should be a callback property
"""
def update_widget(value):
try:
idx = _find_combo_data(widget, value)
except ValueError:
if value is None:
idx = -1
else:
raise
widget.setCurrentIndex(idx)
def update_prop(idx):
if idx == -1:
setattr(client, prop, None)
else:
setattr(client, prop, widget.itemData(idx))
add_callback(client, prop, update_widget)
widget.currentIndexChanged.connect(update_prop)
update_widget(getattr(client, prop))
def connect_float_edit(client, prop, widget):
"""
Connect widget.setText and client.prop
Also pretty-print the number
client.prop should be a callback property
"""
v = QtGui.QDoubleValidator(None)
v.setDecimals(4)
widget.setValidator(v)
def update_prop():
val = widget.text()
try:
setattr(client, prop, float(val))
except ValueError:
setattr(client, prop, 0)
def update_widget(val):
if val is None:
val = 0.
widget.setText(pretty_number(val))
add_callback(client, prop, update_widget)
widget.editingFinished.connect(update_prop)
update_widget(getattr(client, prop))
def connect_value(client, prop, widget, value_range=None, log=False):
"""
Connect client.prop to widget.valueChanged
client.prop should be a callback property
If ``value_range`` is set, the slider values are mapped to that range. If
``log`` is set, the mapping is assumed to be logarithmic instead of linear.
"""
if log:
if value_range is None:
raise ValueError("log option can only be set if value_range is given")
else:
value_range = math.log10(value_range[0]), math.log10(value_range[1])
def update_prop():
val = widget.value()
if value_range is not None:
imin, imax = widget.minimum(), widget.maximum()
val = (val - imin) / (imax - imin) * (value_range[1] - value_range[0]) + value_range[0]
if log:
val = 10 ** val
setattr(client, prop, val)
def update_widget(val):
if val is None:
widget.setValue(0)
return
if log:
val = math.log10(val)
if value_range is not None:
imin, imax = widget.minimum(), widget.maximum()
val = (val - value_range[0]) / (value_range[1] - value_range[0]) * (imax - imin) + imin
widget.setValue(val)
add_callback(client, prop, update_widget)
widget.valueChanged.connect(update_prop)
update_widget(getattr(client, prop))
connect_int_spin = connect_value
def _find_combo_data(widget, value):
"""
Returns the index in a combo box where itemData == value
Raises a ValueError if data is not found
"""
i = widget.findData(value)
if i == -1:
raise ValueError("%s not found in combo box" % value)
else:
return i