Source code for traitsui.item

# (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()