Skip to content

Commit 0328089

Browse files
Refactor fields (#1234)
This is a refactor of the Field class to split it into display and editable variants: base `Fields` may not be editable, `EditaleFields` add editability (ie. the value can be changed by the user). Includes tests and documentation, including fixes to make the documentation actually compile. Adds a LabelField and ImageField to demonstrate the non-editable Fields. --------- Co-authored-by: Mark Dickinson <mdickinson@enthought.com>
1 parent 987a061 commit 0328089

40 files changed

+1004
-159
lines changed

docs/source/conf.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
# All configuration values have a default value; values that are commented out
1212
# serve to show the default value.
1313

14-
import os
15-
import runpy
16-
import sys
14+
import importlib.metadata
1715

1816
# General configuration
1917
# ---------------------
@@ -45,10 +43,7 @@
4543

4644
# The default replacements for |version| and |release|, also used in various
4745
# other places throughout the built documents.
48-
version_py = os.path.join('..', '..', 'pyface', '_version.py')
49-
version_content = runpy.run_path(version_py)
50-
version = ".".join(version_content["version"].split(".", 2)[:2])
51-
release = version
46+
version = release = importlib.metadata.version("pyface")
5247

5348
# There are two options for replacing |today|: either, you set today to some
5449
# non-false value, then it is used:

docs/source/fields.rst

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,81 @@ to a Traits interface, so that you can use them in a cross-toolkit manner.
99

1010
Where possible, these classes share a common API for the same functionality.
1111
In particular, all classes have a
12-
:py:attr:`~pyface.fields.i_field.IField.value` trait which holds the (usually
13-
user-editable) value displayed in the field. Code using the field can listen
14-
to changes in this trait to react to the user entering a new value for the
15-
field without needing to know anything about the underlying toolkit event
16-
signalling mechanisms.
12+
:py:attr:`~pyface.fields.i_field.IField.value` trait which holds the value
13+
displayed in the field.
1714

18-
All fields also provide a trait for setting the
19-
:py:attr:`~pyface.fields.i_field.IField.context_menu` of the field. Context
20-
menus should be :py:class:`~pyface.action.menu_manager.MenuManager`
21-
instances.
15+
Fields where the value is user-editable rather than simply displayed implement
16+
the :py:attr:`~pyface.fields.i_editable_field.IEditableField` interface. Code
17+
using the field can listen to changes in the value trait to react to the user
18+
entering a new value for the field without needing to know anything about the
19+
underlying toolkit event signalling mechanisms.
20+
21+
Fields inherit from :py:class:`~pyface.i_widget.IWidget` and
22+
:py:class:`~pyface.i_layout_item.ILayoutItem` which have a number of
23+
additional traits with useful features:
24+
25+
:py:attr:`~pyface.i_widget.IWidget.tooltip`
26+
A tooltip for the widget, which should hold string.
27+
28+
:py:attr:`~pyface.i_widget.IWidget.context_menu`
29+
A context menu for the widget, which should hold an
30+
:py:class:`~pyface.action.i_menu_manager.IMenuManager` instance.
31+
32+
:py:attr:`~pyface.i_layout_item.ILayoutItem.minimum_size`
33+
A tuple holding the minimum size of a layout widget.
34+
35+
:py:attr:`~pyface.i_layout_item.ILayoutItem.maximum_size`
36+
A tuple holding the minimum size of a layout widget.
37+
38+
:py:attr:`~pyface.i_layout_item.ILayoutItem.stretch`
39+
A tuple holding information about the distribution of addtional space into
40+
the widget when growing in a layout. Higher numbers mean proportinally
41+
more space.
42+
43+
:py:attr:`~pyface.i_layout_item.ILayoutItem.size_policy`
44+
A tuple holding information about how the widget can grow and shrink.
45+
46+
:py:attr:`~pyface.fields.i_field.IField.alignment`
47+
A value holding the horizontal alignment of the contents of the field.
48+
49+
ComboField
50+
==========
51+
52+
The :py:class:`~pyface.fields.i_combo_field.IComboField` interface has an arbitrary
53+
:py:attr:`~pyface.fields.i_combo_field.IComboField.value` that must come from a list
54+
of valid :py:attr:`~pyface.fields.i_combo_field.IComboField.values`. For non-text
55+
values, a :py:attr:`~pyface.fields.i_combo_field.IComboField.formatter` function
56+
should be provided, defaulting to :py:func:`str`.
57+
58+
LabelField
59+
==========
60+
61+
The :py:class:`~pyface.fields.i_label_field.ILabelField` interface has a string
62+
for the :py:attr:`~pyface.fields.i_label_field.ILabelField.value` which is not
63+
user-editable.
64+
65+
In the Qt backend they can have an image for an
66+
:py:attr:`~pyface.fields.i_label_field.ILabelField.icon`.
67+
68+
ImageField
69+
==========
70+
71+
The :py:class:`~pyface.fields.i_image_field.IImageField` interface has an
72+
:py:class:`~pyface.i_image.IImage` for its
73+
:py:attr:`~pyface.fields.i_image_field.IImageField.value` which is not
74+
user-editable.
75+
76+
SpinField
77+
=========
78+
79+
The :py:class:`~pyface.fields.i_spin_field.ISpinField` interface has an integer
80+
for the :py:attr:`~pyface.fields.i_spin_field.ISpinField.value`, and also
81+
requires a range to be set, either via setting the min/max values as a tuple to
82+
the :py:attr:`~pyface.fields.i_spin_field.ISpinField.bounds` trait, or by setting
83+
values individually to :py:attr:`~pyface.fields.i_spin_field.ISpinField.minimum`
84+
and :py:attr:`~pyface.fields.i_spin_field.ISpinField.maximum`. The
85+
:py:attr:`~pyface.fields.i_spin_field.ISpinField.wrap` trait determines whether
86+
the spinner wraps around at the extreme values.
2287

2388
TextField
2489
=========
@@ -41,26 +106,6 @@ the Qt backend has a number of other options as well). The text field can be
41106
set to read-only mode via the
42107
:py:attr:`~pyface.fields.i_text_field.ITextField.read_only` trait.
43108

44-
SpinField
45-
=========
46-
47-
The :py:class:`~pyface.fields.i_spin_field.ISpinField` interface has an integer
48-
for the :py:attr:`~pyface.fields.i_spin_field.ISpinField.value`, and also
49-
requires a range to be set, either via setting the min/max values as a tuple to
50-
the :py:attr:`~pyface.fields.i_spin_field.ISpinField.range` trait, or by setting
51-
values individually to :py:attr:`~pyface.fields.i_spin_field.ISpinField.minimum`
52-
and :py:attr:`~pyface.fields.i_spin_field.ISpinField.maximum`.
53-
54-
ComboField
55-
==========
56-
57-
The :py:class:`~pyface.fields.i_combo_field.IComboField` interface has an arbitrary
58-
:py:attr:`~pyface.fields.i_combo_field.IComboField.value` that must come from a list
59-
of valid :py:attr:`~pyface.fields.i_combo_field.IComboField.values`. For non-text
60-
values, a :py:attr:`~pyface.fields.i_combo_field.IComboField.formatter` function
61-
should be provided - this defaults to either :py:func:`str` (Python 3+) or
62-
:py:func:`unicode` (Python 2).
63-
64109
TimeField
65110
==========
66111

pyface/fields/api.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
1515
- :class:`~.CheckBoxField`
1616
- :class:`~.ComboField`
17+
- :class:`~.EditableField`
18+
- :class:`~.Field`
19+
- :class:`~.ImageField`
20+
- :class:`~.LabelField`
1721
- :class:`~.RadioButtonField`
1822
- :class:`~.SpinField`
1923
- :class:`~.TextField`
@@ -23,7 +27,10 @@
2327
Interfaces
2428
----------
2529
- :class:`~.IComboField`
30+
- :class:`~.IEditableField`
2631
- :class:`~.IField`
32+
- :class:`~.IImageField`
33+
- :class:`~.ILabelField`
2734
- :class:`~.ISpinField`
2835
- :class:`~.ITextField`
2936
- :class:`~.ITimeField`
@@ -32,7 +39,10 @@
3239
"""
3340

3441
from .i_combo_field import IComboField
42+
from .i_editable_field import IEditableField
3543
from .i_field import IField
44+
from .i_image_field import IImageField
45+
from .i_label_field import ILabelField
3646
from .i_spin_field import ISpinField
3747
from .i_text_field import ITextField
3848
from .i_time_field import ITimeField
@@ -48,6 +58,10 @@
4858
_toolkit_imports = {
4959
'CheckBoxField': 'toggle_field',
5060
'ComboField': 'combo_field',
61+
'EditableField': 'editable_field',
62+
'Field': 'field',
63+
'ImageField': 'image_field',
64+
'LabelField': 'label_field',
5165
'RadioButtonField': 'toggle_field',
5266
'SpinField': 'spin_field',
5367
'TextField': 'text_field',

pyface/fields/i_combo_field.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
from traits.api import Callable, HasTraits, Enum, List
1515

16-
from pyface.fields.i_field import IField
16+
from pyface.fields.i_editable_field import IEditableField
1717

1818

19-
class IComboField(IField):
19+
class IComboField(IEditableField):
2020
""" The combo field interface.
2121
2222
This is for comboboxes holding a list of values.
@@ -64,21 +64,16 @@ def __init__(self, values, **traits):
6464
def _initialize_control(self):
6565
super()._initialize_control()
6666
self._set_control_values(self.values)
67-
self._set_control_value(self.value)
6867

6968
def _add_event_listeners(self):
7069
""" Set up toolkit-specific bindings for events """
7170
super()._add_event_listeners()
7271
self.observe(
7372
self._values_updated, "values.items,formatter", dispatch="ui"
7473
)
75-
if self.control is not None:
76-
self._observe_control_value()
7774

7875
def _remove_event_listeners(self):
7976
""" Remove toolkit-specific bindings for events """
80-
if self.control is not None:
81-
self._observe_control_value(remove=True)
8277
self.observe(
8378
self._values_updated,
8479
"values.items,formatter",

pyface/fields/i_editable_field.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
2+
# All rights reserved.
3+
#
4+
# This software is provided without warranty under the terms of the BSD
5+
# license included in LICENSE.txt and may be redistributed only under
6+
# the conditions described in the aforementioned license. The license
7+
# is also available online at http://www.enthought.com/licenses/BSD.txt
8+
#
9+
# Thanks for using Enthought open source!
10+
11+
""" The editable field interface. """
12+
13+
14+
from traits.api import HasTraits
15+
16+
from .i_field import IField
17+
18+
19+
class IEditableField(IField):
20+
""" The editable field interface.
21+
22+
A editable field is a widget that displays a user-editable value.
23+
"""
24+
25+
26+
class MEditableField(HasTraits):
27+
"""The editable field mix-in.
28+
29+
Classes which use this mixin should implement _observe_control_value to
30+
connect a toolkit handler that calls _update_value.
31+
"""
32+
33+
# ------------------------------------------------------------------------
34+
# IWidget interface
35+
# ------------------------------------------------------------------------
36+
37+
def _add_event_listeners(self):
38+
""" Set up toolkit-specific bindings for events """
39+
super()._add_event_listeners()
40+
self._observe_control_value()
41+
42+
def _remove_event_listeners(self):
43+
""" Remove toolkit-specific bindings for events """
44+
self._observe_control_value(remove=True)
45+
super()._remove_event_listeners()
46+
47+
# ------------------------------------------------------------------------
48+
# Private interface
49+
# ------------------------------------------------------------------------
50+
51+
def _update_value(self, value):
52+
""" Handle a change to the value from user interaction
53+
54+
This is a method suitable for calling from a toolkit event handler.
55+
"""
56+
self.value = self._get_control_value()
57+
58+
# Toolkit control interface ---------------------------------------------
59+
60+
def _observe_control_value(self, remove=False):
61+
""" Toolkit specific method to change the control value observer. """
62+
raise NotImplementedError()

pyface/fields/i_field.py

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from traits.api import Any, HasTraits
1515

1616
from pyface.i_layout_widget import ILayoutWidget
17+
from pyface.ui_traits import Alignment
1718

1819

1920
class IField(ILayoutWidget):
@@ -26,51 +27,62 @@ class IField(ILayoutWidget):
2627
#: The value held by the field.
2728
value = Any()
2829

30+
#: The alignment of the field's content.
31+
alignment = Alignment()
32+
2933

3034
class MField(HasTraits):
3135
""" The field mix-in. """
3236

3337
#: The value held by the field.
3438
value = Any()
3539

40+
#: The alignment of the text in the field.
41+
alignment = Alignment()
42+
3643
# ------------------------------------------------------------------------
3744
# IWidget interface
3845
# ------------------------------------------------------------------------
3946

47+
def create(self, parent=None):
48+
""" Creates the toolkit specific control.
49+
50+
This method should create the control and assign it to the
51+
:py:attr:``control`` trait.
52+
"""
53+
super().create(parent=parent)
54+
55+
self.show(self.visible)
56+
self.enable(self.enabled)
57+
58+
def _initialize_control(self):
59+
""" Perform any post-creation initialization for the control.
60+
"""
61+
super()._initialize_control()
62+
self._set_control_value(self.value)
63+
if self.alignment != 'default':
64+
self._set_control_alignment(self.alignment)
65+
4066
def _add_event_listeners(self):
4167
""" Set up toolkit-specific bindings for events """
4268
super()._add_event_listeners()
4369
self.observe(self._value_updated, "value", dispatch="ui")
70+
self.observe(self._alignment_updated, "alignment", dispatch="ui")
4471

4572
def _remove_event_listeners(self):
4673
""" Remove toolkit-specific bindings for events """
4774
self.observe(
4875
self._value_updated, "value", dispatch="ui", remove=True
4976
)
77+
self.observe(
78+
self._alignment_updated, "alignment", dispatch="ui", remove=True
79+
)
5080
super()._remove_event_listeners()
5181

5282
# ------------------------------------------------------------------------
5383
# Private interface
5484
# ------------------------------------------------------------------------
5585

56-
def create(self, parent=None):
57-
""" Creates the toolkit specific control.
58-
59-
This method should create the control and assign it to the
60-
:py:attr:``control`` trait.
61-
"""
62-
super().create(parent=parent)
63-
64-
self.show(self.visible)
65-
self.enable(self.enabled)
66-
67-
def _update_value(self, value):
68-
""" Handle a change to the value from user interaction
69-
70-
This is a method suitable for calling from a toolkit event handler.
71-
"""
72-
self.value = self._get_control_value()
73-
7486
def _get_control(self):
7587
""" If control is not passed directly, get it from the trait. """
7688
control = self.control
@@ -88,8 +100,12 @@ def _set_control_value(self, value):
88100
""" Toolkit specific method to set the control's value. """
89101
raise NotImplementedError()
90102

91-
def _observe_control_value(self, remove=False):
92-
""" Toolkit specific method to change the control value observer. """
103+
def _get_control_alignment(self):
104+
""" Toolkit specific method to get the control's read_only state. """
105+
raise NotImplementedError()
106+
107+
def _set_control_alignment(self, alignment):
108+
""" Toolkit specific method to set the control's alignment. """
93109
raise NotImplementedError()
94110

95111
# Trait change handlers -------------------------------------------------
@@ -98,3 +114,8 @@ def _value_updated(self, event):
98114
value = event.new
99115
if self.control is not None:
100116
self._set_control_value(value)
117+
118+
def _alignment_updated(self, event):
119+
alignment = event.new
120+
if self.control is not None:
121+
self._set_control_alignment(alignment)

0 commit comments

Comments
 (0)