# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines the Item class, which is used to represent a single item within
a Traits-based user interface.
"""
import re
from traits.api import (
Bool,
Callable,
Constant,
Delegate,
Float,
Instance,
Range,
Str,
Undefined,
Dict,
)
from traits.trait_base import user_name_for
from .view_element import ViewSubElement
from .ui_traits import ContainerDelegate, EditorStyle
from .editor_factory import EditorFactory
# Pattern of all digits:
all_digits = re.compile(r"\d+")
# Pattern for finding size infomation embedded in an item description:
size_pat = re.compile(r"^(.*)<(.*)>(.*)$", re.MULTILINE | re.DOTALL)
# Pattern for finding tooltip infomation embedded in an item description:
tooltip_pat = re.compile(r"^(.*)`(.*)`(.*)$", re.MULTILINE | re.DOTALL)
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
# Reference to an EditorFactory:
ItemEditor = Instance(EditorFactory, allow_none=True)
# Amount of padding to add around an item:
Padding = Range(-15, 15, 0, desc="amount of padding to add around item")
# -------------------------------------------------------------------------
# 'Item' class:
# -------------------------------------------------------------------------
[docs]class Item(ViewSubElement):
"""An element in a Traits-based user interface.
Magic:
- Items are rendered as layout elements if :attr:`name` is set to
special values:
* ``name=''``, the item is rendered as a static label
* ``name='_'``, the item is rendered as a separator
* ``name=' '``, the item is rendered as a 5 pixel spacer
* ``name='23'`` (any number), the item is rendered as a spacer of
the size specified (number of pixels)
"""
# FIXME: all the logic for the name = '', '_', ' ', '23' magic is in
# _GroupPanel._add_items in qt/ui_panel.py, which is a very unlikely place
# to look for it. Ideally, that logic should be in this class.
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
#: A unique identifier for the item. If not set, it defaults to the value
#: of **name**.
id = Str()
#: User interface label for the item in the GUI. If this attribute is not
#: set, the label is the value of **name** with slight modifications:
#: underscores are replaced by spaces, and the first letter is capitalized.
#: If an item's **name** is not specified, its label is displayed as
#: static text, without any editor widget.
label = Str()
#: Name of the trait the item is editing:
name = Str()
#: Style-sheet to apply to item / group (Qt only)
style_sheet = Str()
#: Help text describing the purpose of the item. The built-in help handler
#: displays this text in a pop-up window if the user clicks the widget's
#: label. View-level help displays the help text for all items in a view.
#: If this attribute is not set, the built-in help handler generates a
#: description based on the trait definition.
help = Str()
#: The HasTraits object whose trait attribute the item is editing:
object = ContainerDelegate
#: Presentation style for the item:
style = ContainerDelegate
#: Docking style for the item:
dock = ContainerDelegate
#: Image to display on notebook tabs:
image = ContainerDelegate
#: Category of elements dragged from view:
export = ContainerDelegate
#: Should a label be displayed for the item?
show_label = Delegate("container", "show_labels")
#: Editor to use for the item:
editor = ItemEditor
#: Additional editor traits to be set if default traits editor to be used:
editor_args = Dict()
#: Should the item use extra space along its Group's non-layout axis? If set to
#: True, the widget expands to fill any extra space that is available in the
#: display. If set to True for more than one item in the same View, any extra
#: space is divided between them. If set to False, the widget uses only
#: whatever space it is explicitly (or implicitly) assigned. The default
#: value of Undefined means that the use (or non-use) of extra space will be
#: determined by the editor associated with the item.
resizable = Bool(Undefined)
#: Should the item use extra space along its Group's layout axis? For
#: example, it a vertical group, should an item expand vertically to use
#: any extra space available in the group?
springy = Bool(False)
#: Should the item's label use emphasized text? If the label is not shown,
#: this attribute is ignored.
emphasized = Bool(False)
#: Should the item receive focus initially?
has_focus = Bool(False)
#: Pre-condition for including the item in the display. If the expression
#: evaluates to False, the item is not defined in the display. Conditions
#: for **defined_when** are evaluated only once, when the display is first
#: constructed. Use this attribute for conditions based on attributes that
#: vary from object to object, but that do not change over time. For example,
#: displaying a 'maiden_name' item only for female employees in a company
#: database.
defined_when = Str()
#: Pre-condition for showing the item. If the expression evaluates to False,
#: the widget is not visible (and disappears if it was previously visible).
#: If the value evaluates to True, the widget becomes visible. All
#: **visible_when** conditions are checked each time that any trait value
#: on an object in the UI's context is changed. Therefore, you can use
#: **visible_when** conditions to hide or show widgets in response to user
#: input. Be aware that this only applies to traits in the UI's context. As
#: a result, changes to nested traits that don't also change a trait on
#: some object in the context may not trigger the expression to be checked.
#: Additionally, the expression needs to be a valid python expression given
#: the context. i.e. eval(visible_when, globals=globals(), locals=context)
#: should succeed.
visible_when = Str()
#: Pre-condition for enabling the item. If the expression evaluates to False,
#: the widget is disabled, that is, it does not accept input. All
#: **enabled_when** conditions are checked each time that any trait value
#: on an object in the UI's context is changed. Therefore, you can use
#: **enabled_when** conditions to enable or disable widgets in response to
#: user input. Be aware that this only applies to traits in the UI's
#: context. As a result, changes to nested traits that don't also change a
#: trait on some object in the context may not trigger the expression to be
#: checked. Additionally, the expression needs to be a valid python
#: expression given the context. i.e.
#: eval(enabled_when, globals=globals(), locals=context) should succeed.
enabled_when = Str()
#: Amount of extra space, in pixels, to add around the item. Values must be
#: integers between -15 and 15. Use negative values to subtract from the
#: default spacing.
padding = Padding
#: Tooltip to display over the item, when the mouse pointer is left idle
#: over the widget. Make this text as concise as possible; use the **help**
#: attribute to provide more detailed information.
tooltip = Str()
#: A Callable to use for formatting the contents of the item. This function
#: or method is called to create the string representation of the trait value
#: to be edited. If the widget does not use a string representation, this
#: attribute is ignored.
format_func = Callable()
#: Python format string to use for formatting the contents of the item.
#: The format string is applied to the string representation of the trait
#: value before it is displayed in the widget. This attribute is ignored if
#: the widget does not use a string representation, or if the
#: **format_func** is set.
format_str = Str()
#: Requested width of the editor (in pixels or fraction of available width).
#: For pixel values (i.e. values not in the range from 0.0 to 1.0), the
#: actual displayed width is at least the maximum of **width** and the
#: optimal width of the widget as calculated by the GUI toolkit. Specify a
#: negative value to ignore the toolkit's optimal width. For example, use
#: -50 to force a width of 50 pixels. The default value of -1 ensures that
#: the toolkit's optimal width is used.
#:
#: A value in the range from 0.0 to 1.0 specifies the fraction of the
#: available width to assign to the editor. Note that the value is not an
#: absolute value, but is relative to other item's whose **width** is also
#: in the 0.0 to 1.0 range. For example, if you have two item's with a width
#: of 0.1, and one item with a width of 0.2, the first two items will each
#: receive 25% of the available width, while the third item will receive
#: 50% of the available width. The available width is the total width of the
#: view minus the width of any item's with fixed pixel sizes (i.e. width
#: values not in the 0.0 to 1.0 range).
width = Float(-1.0)
#: Requested height of the editor (in pixels or fraction of available
#: height). For pixel values (i.e. values not in the range from 0.0 to 1.0),
#: the actual displayed height is at least the maximum of **height** and the
#: optimal height of the widget as calculated by the GUI toolkit. Specify a
#: negative value to ignore the toolkit's optimal height. For example, use
#: -50 to force a height of 50 pixels. The default value of -1 ensures that
#: the toolkit's optimal height is used.
#:
#: A value in the range from 0.0 to 1.0 specifies the fraction of the
#: available height to assign to the editor. Note that the value is not an
#: absolute value, but is relative to other item's whose **height** is also
#: in the 0.0 to 1.0 range. For example, if you have two item's with a height
#: of 0.1, and one item with a height of 0.2, the first two items will each
#: receive 25% of the available height, while the third item will receive
#: 50% of the available height. The available height is the total height of
#: the view minus the height of any item's with fixed pixel sizes (i.e.
#: height values not in the 0.0 to 1.0 range).
height = Float(-1.0)
#: The extended trait name of the trait containing the item's invalid state
#: status (passed through to the item's editor):
invalid = Str()
def __init__(self, value=None, **traits):
"""Initializes the item object."""
super().__init__(**traits)
if value is None:
return
if not isinstance(value, str):
raise TypeError(
"The argument to Item must be a string of the "
"form: [id:][object.[object.]*][name]['['label']']`tooltip`"
"[<width[,height]>][#^][$|@|*|~|;style]"
)
value, empty = self._parse_label(value)
if empty:
self.show_label = False
value = self._parse_style(value)
value = self._parse_size(value)
value = self._parse_tooltip(value)
value = self._option(value, "#", "resizable", True)
value = self._option(value, "^", "emphasized", True)
value = self._split("id", value, ":", str.find, 0, 1)
value = self._split("object", value, ".", str.rfind, 0, 1)
if value != "":
self.name = value
[docs] def is_includable(self):
"""Returns a Boolean indicating whether the object is replaceable by an
Include object.
"""
return self.id != ""
[docs] def is_spacer(self):
"""Returns True if the item represents a spacer or separator."""
name = self.name.strip()
return (
(name == "")
or (name == "_")
or (all_digits.match(name) is not None)
)
[docs] def get_help(self, ui):
"""Gets the help text associated with the Item in a specified UI."""
# Return 'None' if the Item is a separator or spacer:
if self.is_spacer():
return None
# Otherwise, it must be a trait Item:
if self.help != "":
return self.help
object = eval(self.object_, globals(), ui.context)
return object.base_trait(self.name).get_help()
[docs] def get_label(self, ui):
"""Gets the label to use for a specified Item.
If not specified, the label is set as the name of the
corresponding trait, replacing '_' with ' ', and capitalizing
the first letter (see :func:`user_name_for`). This is called
the *user name*.
Magic:
- if attr:`item.label` is specified, and it begins with '...',
the final label is the user name followed by the item label
- if attr:`item.label` is specified, and it ends with '...',
the final label is the item label followed by the user name
"""
# Return 'None' if the Item is a separator or spacer:
if self.is_spacer():
return None
label = self.label
if label != "":
return label
name = self.name
object = eval(self.object_, globals(), ui.context)
trait = object.base_trait(name)
label = user_name_for(name)
tlabel = trait.label
if tlabel is None:
return label
if isinstance(tlabel, str):
if tlabel[0:3] == "...":
return label + tlabel[3:]
if tlabel[-3:] == "...":
return tlabel[:-3] + label
if self.label != "":
return self.label
return tlabel
return tlabel(object, name, label)
[docs] def get_id(self):
"""Returns an ID used to identify the item."""
if self.id != "":
return self.id
return self.name
def _parse_size(self, value):
"""Parses a '<width,height>' value from the string definition."""
match = size_pat.match(value)
if match is not None:
data = match.group(2)
value = match.group(1) + match.group(3)
col = data.find(",")
if col < 0:
self._set_float("width", data)
else:
self._set_float("width", data[:col])
self._set_float("height", data[col + 1 :])
return value
def _parse_tooltip(self, value):
"""Parses a *tooltip* value from the string definition."""
match = tooltip_pat.match(value)
if match is not None:
self.tooltip = match.group(2)
value = match.group(1) + match.group(3)
return value
def _set_float(self, name, value):
"""Sets a specified trait to a specified string converted to a float."""
value = value.strip()
if value != "":
setattr(self, name, float(value))
def __repr__(self):
"""Returns a "pretty print" version of the Item."""
options = self._repr_options(
"id", "object", "label", "style", "show_label", "width", "height"
)
if options is None:
return "Item( '%s' )" % self.name
return "Item( '%s'\n%s\n)" % (
self.name,
self._indent(options, " "),
)
# -------------------------------------------------------------------------
# 'UItem' class:
# -------------------------------------------------------------------------
[docs]class UItem(Item):
"""An Item that has no label."""
show_label = Bool(False)
# -------------------------------------------------------------------------
# 'Custom' class:
# -------------------------------------------------------------------------
[docs]class Custom(Item):
"""An Item using a 'custom' style."""
style = EditorStyle("custom")
# -------------------------------------------------------------------------
# 'UCustom' class:
# -------------------------------------------------------------------------
[docs]class UCustom(Custom):
"""An Item using a 'custom' style with no label."""
show_label = Bool(False)
# -------------------------------------------------------------------------
# 'Readonly' class:
# -------------------------------------------------------------------------
[docs]class Readonly(Item):
"""An Item using a 'readonly' style."""
style = EditorStyle("readonly")
# -------------------------------------------------------------------------
# 'UReadonly' class:
# -------------------------------------------------------------------------
[docs]class UReadonly(Readonly):
"""An Item using a 'readonly' style with no label."""
show_label = Bool(False)
# -------------------------------------------------------------------------
# 'Label' class:
# -------------------------------------------------------------------------
[docs]class Label(Item):
"""An item that is a label."""
def __init__(self, label, **traits):
super().__init__(label=label, **traits)
# -------------------------------------------------------------------------
# 'Heading' class:
# -------------------------------------------------------------------------
[docs]class Heading(Label):
"""An item that is a fancy label."""
#: Override the 'style' trait to default to the fancy 'custom' style:
style = Constant("custom")
# -------------------------------------------------------------------------
# 'Spring' class:
# -------------------------------------------------------------------------
[docs]class Spring(Item):
"""An item that is a layout "spring"."""
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
#: Name of the trait the item is editing
#: Just a dummy trait that exists on all HasTraits objects. It's an Event,
#: so it won't cause Traits UI to add any synchronization, and because it
#: already exists, it won't force the addition of a new trait with a bogus
#: name.
name = "trait_modified"
#: Should a label be displayed?
show_label = Bool(False)
#: Editor to use for the item
editor = Instance("traitsui.api.NullEditor", ())
#: Should the item use extra space along its Group's layout orientation?
springy = True
# A pre-defined spring for convenience
spring = Spring()