1# ------------------------------------------------------------------------------
2#
3#  Copyright (c) 2005, Enthought, Inc.
4#  All rights reserved.
5#
6#  This software is provided without warranty under the terms of the BSD
7#  license included in LICENSE.txt and may be redistributed only
8#  under the conditions described in the aforementioned license.  The license
9#  is also available online at http://www.enthought.com/licenses/BSD.txt
10#
11#  Thanks for using Enthought open source!
12#
13#  Author: David C. Morrill
14#  Date:   10/07/2004
15#
16# ------------------------------------------------------------------------------
17
18""" Defines the Item class, which is used to represent a single item within
19    a Traits-based user interface.
20"""
21
22
23
24import re
25
26from traits.api import (
27    Bool,
28    Callable,
29    Constant,
30    Delegate,
31    Float,
32    Instance,
33    Range,
34    Str,
35    Undefined,
36    Dict,
37)
38
39from traits.trait_base import user_name_for
40
41from .view_element import ViewSubElement
42
43from .ui_traits import ContainerDelegate, EditorStyle
44
45from .editor_factory import EditorFactory
46
47
48
49# Pattern of all digits:
50all_digits = re.compile(r"\d+")
51
52# Pattern for finding size infomation embedded in an item description:
53size_pat = re.compile(r"^(.*)<(.*)>(.*)$", re.MULTILINE | re.DOTALL)
54
55# Pattern for finding tooltip infomation embedded in an item description:
56tooltip_pat = re.compile(r"^(.*)`(.*)`(.*)$", re.MULTILINE | re.DOTALL)
57
58# -------------------------------------------------------------------------
59#  Trait definitions:
60# -------------------------------------------------------------------------
61
62# Reference to an EditorFactory:
63ItemEditor = Instance(EditorFactory, allow_none=True)
64
65# Amount of padding to add around an item:
66Padding = Range(-15, 15, 0, desc="amount of padding to add around item")
67
68# -------------------------------------------------------------------------
69#  'Item' class:
70# -------------------------------------------------------------------------
71
72
73class Item(ViewSubElement):
74    """ An element in a Traits-based user interface.
75
76    Magic:
77
78    - Items are rendered as layout elements if :attr:`name` is set to
79      special values:
80
81      * ``name=''``, the item is rendered as a static label
82
83      * ``name='_'``, the item is rendered as a separator
84
85      * ``name=' '``, the item is rendered as a 5 pixel spacer
86
87      * ``name='23'`` (any number), the item is rendered as a spacer of
88        the size specified (number of pixels)
89    """
90
91    # FIXME: all the logic for the name = '', '_', ' ', '23' magic is in
92    # _GroupPanel._add_items in qt/ui_panel.py, which is a very unlikely place
93    # to look for it. Ideally, that logic should be in this class.
94
95    # -------------------------------------------------------------------------
96    #  Trait definitions:
97    # -------------------------------------------------------------------------
98
99    #: A unique identifier for the item. If not set, it defaults to the value
100    #: of **name**.
101    id = Str()
102
103    #: User interface label for the item in the GUI. If this attribute is not
104    #: set, the label is the value of **name** with slight modifications:
105    #: underscores are replaced by spaces, and the first letter is capitalized.
106    #: If an item's **name** is not specified, its label is displayed as
107    #: static text, without any editor widget.
108    label = Str()
109
110    #: Name of the trait the item is editing:
111    name = Str()
112
113    #: Style-sheet to apply to item / group (Qt only)
114    style_sheet = Str()
115
116    #: Help text describing the purpose of the item. The built-in help handler
117    #: displays this text in a pop-up window if the user clicks the widget's
118    #: label. View-level help displays the help text for all items in a view.
119    #: If this attribute is not set, the built-in help handler generates a
120    #: description based on the trait definition.
121    help = Str()
122
123    #: The HasTraits object whose trait attribute the item is editing:
124    object = ContainerDelegate
125
126    #: Presentation style for the item:
127    style = ContainerDelegate
128
129    #: Docking style for the item:
130    dock = ContainerDelegate
131
132    #: Image to display on notebook tabs:
133    image = ContainerDelegate
134
135    #: Category of elements dragged from view:
136    export = ContainerDelegate
137
138    #: Should a label be displayed for the item?
139    show_label = Delegate("container", "show_labels")
140
141    #: Editor to use for the item:
142    editor = ItemEditor
143
144    #: Additional editor traits to be set if default traits editor to be used:
145    editor_args = Dict()
146
147    #: Should the item use extra space along its Group's non-layout axis? If set to
148    #: True, the widget expands to fill any extra space that is available in the
149    #: display. If set to True for more than one item in the same View, any extra
150    #: space is divided between them. If set to False, the widget uses only
151    #: whatever space it is explicitly (or implicitly) assigned. The default
152    #: value of Undefined means that the use (or non-use) of extra space will be
153    #: determined by the editor associated with the item.
154    resizable = Bool(Undefined)
155
156    #: Should the item use extra space along its Group's layout axis? For
157    #: example, it a vertical group, should an item expand vertically to use
158    #: any extra space available in the group?
159    springy = Bool(False)
160
161    #: Should the item use any extra space along its Group's non-layout
162    #: orientation? For example, in a vertical group, should an item expand
163    #: horizontally to the full width of the group? If left to the default value
164    #: of Undefined, the decision will be left up to the associated item editor.
165    full_size = Bool(Undefined)
166
167    #: Should the item's label use emphasized text? If the label is not shown,
168    #: this attribute is ignored.
169    emphasized = Bool(False)
170
171    #: Should the item receive focus initially?
172    has_focus = Bool(False)
173
174    #: Pre-condition for including the item in the display. If the expression
175    #: evaluates to False, the item is not defined in the display. Conditions
176    #: for **defined_when** are evaluated only once, when the display is first
177    #: constructed. Use this attribute for conditions based on attributes that
178    #: vary from object to object, but that do not change over time. For example,
179    #: displaying a 'maiden_name' item only for female employees in a company
180    #: database.
181    defined_when = Str()
182
183    #: Pre-condition for showing the item. If the expression evaluates to False,
184    #: the widget is not visible (and disappears if it was previously visible).
185    #: If the value evaluates to True, the widget becomes visible. All
186    #: **visible_when** conditions are checked each time that any trait value
187    #: is edited in the display. Therefore, you can use **visible_when**
188    #: conditions to hide or show widgets in response to user input.
189    visible_when = Str()
190
191    #: Pre-condition for enabling the item. If the expression evaluates to False,
192    #: the widget is disabled, that is, it does not accept input. All
193    #: **enabled_when** conditions are checked each time that any trait value
194    #: is edited in the display. Therefore, you can use **enabled_when**
195    #: conditions to enable or disable widgets in response to user input.
196    enabled_when = Str()
197
198    #: Amount of extra space, in pixels, to add around the item. Values must be
199    #: integers between -15 and 15. Use negative values to subtract from the
200    #: default spacing.
201    padding = Padding
202
203    #: Tooltip to display over the item, when the mouse pointer is left idle
204    #: over the widget. Make this text as concise as possible; use the **help**
205    #: attribute to provide more detailed information.
206    tooltip = Str()
207
208    #: A Callable to use for formatting the contents of the item. This function
209    #: or method is called to create the string representation of the trait value
210    #: to be edited. If the widget does not use a string representation, this
211    #: attribute is ignored.
212    format_func = Callable()
213
214    #: Python format string to use for formatting the contents of the item.
215    #: The format string is applied to the string representation of the trait
216    #: value before it is displayed in the widget. This attribute is ignored if
217    #: the widget does not use a string representation, or if the
218    #: **format_func** is set.
219    format_str = Str()
220
221    #: Requested width of the editor (in pixels or fraction of available width).
222    #: For pixel values (i.e. values not in the range from 0.0 to 1.0), the
223    #: actual displayed width is at least the maximum of **width** and the
224    #: optimal width of the widget as calculated by the GUI toolkit. Specify a
225    #: negative value to ignore the toolkit's optimal width. For example, use
226    #: -50 to force a width of 50 pixels. The default value of -1 ensures that
227    #: the toolkit's optimal width is used.
228    #:
229    #: A value in the range from 0.0 to 1.0 specifies the fraction of the
230    #: available width to assign to the editor. Note that the value is not an
231    #: absolute value, but is relative to other item's whose **width** is also
232    #: in the 0.0 to 1.0 range. For example, if you have two item's with a width
233    #: of 0.1, and one item with a width of 0.2, the first two items will each
234    #: receive 25% of the available width, while the third item will receive
235    #: 50% of the available width. The available width is the total width of the
236    #: view minus the width of any item's with fixed pixel sizes (i.e. width
237    #: values not in the 0.0 to 1.0 range).
238    width = Float(-1.0)
239
240    #: Requested height of the editor (in pixels or fraction of available
241    #: height). For pixel values (i.e. values not in the range from 0.0 to 1.0),
242    #: the actual displayed height is at least the maximum of **height** and the
243    #: optimal height of the widget as calculated by the GUI toolkit. Specify a
244    #: negative value to ignore the toolkit's optimal height. For example, use
245    #: -50 to force a height of 50 pixels. The default value of -1 ensures that
246    #: the toolkit's optimal height is used.
247    #:
248    #: A value in the range from 0.0 to 1.0 specifies the fraction of the
249    #: available height to assign to the editor. Note that the value is not an
250    #: absolute value, but is relative to other item's whose **height** is also
251    #: in the 0.0 to 1.0 range. For example, if you have two item's with a height
252    #: of 0.1, and one item with a height of 0.2, the first two items will each
253    #: receive 25% of the available height, while the third item will receive
254    #: 50% of the available height. The available height is the total height of
255    #: the view minus the height of any item's with fixed pixel sizes (i.e.
256    #: height values not in the 0.0 to 1.0 range).
257    height = Float(-1.0)
258
259    #: The extended trait name of the trait containing the item's invalid state
260    #: status (passed through to the item's editor):
261    invalid = Str()
262
263    def __init__(self, value=None, **traits):
264        """ Initializes the item object.
265        """
266        super(Item, self).__init__(**traits)
267
268        if value is None:
269            return
270
271        if not isinstance(value, str):
272            raise TypeError(
273                "The argument to Item must be a string of the "
274                "form: [id:][object.[object.]*][name]['['label']']`tooltip`"
275                "[<width[,height]>][#^][$|@|*|~|;style]"
276            )
277
278        value, empty = self._parse_label(value)
279        if empty:
280            self.show_label = False
281
282        value = self._parse_style(value)
283        value = self._parse_size(value)
284        value = self._parse_tooltip(value)
285        value = self._option(value, "#", "resizable", True)
286        value = self._option(value, "^", "emphasized", True)
287        value = self._split("id", value, ":", str.find, 0, 1)
288        value = self._split("object", value, ".", str.rfind, 0, 1)
289
290        if value != "":
291            self.name = value
292
293    def is_includable(self):
294        """ Returns a Boolean indicating whether the object is replaceable by an
295            Include object.
296        """
297        return self.id != ""
298
299    def is_spacer(self):
300        """ Returns True if the item represents a spacer or separator.
301        """
302        name = self.name.strip()
303
304        return (
305            (name == "")
306            or (name == "_")
307            or (all_digits.match(name) is not None)
308        )
309
310    def get_help(self, ui):
311        """ Gets the help text associated with the Item in a specified UI.
312        """
313        # Return 'None' if the Item is a separator or spacer:
314        if self.is_spacer():
315            return None
316
317        # Otherwise, it must be a trait Item:
318        if self.help != "":
319            return self.help
320
321        object = eval(self.object_, globals(), ui.context)
322
323        return object.base_trait(self.name).get_help()
324
325    def get_label(self, ui):
326        """ Gets the label to use for a specified Item.
327
328        If not specified, the label is set as the name of the
329        corresponding trait, replacing '_' with ' ', and capitalizing
330        the first letter (see :func:`user_name_for`). This is called
331        the *user name*.
332
333        Magic:
334
335        - if attr:`item.label` is specified, and it begins with '...',
336          the final label is the user name followed by the item label
337        - if attr:`item.label` is specified, and it ends with '...',
338          the final label is the item label followed by the user name
339        """
340        # Return 'None' if the Item is a separator or spacer:
341        if self.is_spacer():
342            return None
343
344        label = self.label
345        if label != "":
346            return label
347
348        name = self.name
349        object = eval(self.object_, globals(), ui.context)
350        trait = object.base_trait(name)
351        label = user_name_for(name)
352        tlabel = trait.label
353        if tlabel is None:
354            return label
355
356        if isinstance(tlabel, str):
357            if tlabel[0:3] == "...":
358                return label + tlabel[3:]
359            if tlabel[-3:] == "...":
360                return tlabel[:-3] + label
361            if self.label != "":
362                return self.label
363            return tlabel
364
365        return tlabel(object, name, label)
366
367    def get_id(self):
368        """ Returns an ID used to identify the item.
369        """
370        if self.id != "":
371            return self.id
372
373        return self.name
374
375    def _parse_size(self, value):
376        """ Parses a '<width,height>' value from the string definition.
377        """
378        match = size_pat.match(value)
379        if match is not None:
380            data = match.group(2)
381            value = match.group(1) + match.group(3)
382            col = data.find(",")
383            if col < 0:
384                self._set_float("width", data)
385            else:
386                self._set_float("width", data[:col])
387                self._set_float("height", data[col + 1 :])
388
389        return value
390
391    def _parse_tooltip(self, value):
392        """ Parses a *tooltip* value from the string definition.
393        """
394        match = tooltip_pat.match(value)
395        if match is not None:
396            self.tooltip = match.group(2)
397            value = match.group(1) + match.group(3)
398
399        return value
400
401    def _set_float(self, name, value):
402        """ Sets a specified trait to a specified string converted to a float.
403        """
404        value = value.strip()
405        if value != "":
406            setattr(self, name, float(value))
407
408    def __repr__(self):
409        """ Returns a "pretty print" version of the Item.
410        """
411
412        options = self._repr_options(
413            "id", "object", "label", "style", "show_label", "width", "height"
414        )
415        if options is None:
416            return "Item( '%s' )" % self.name
417
418        return "Item( '%s'\n%s\n)" % (
419            self.name,
420            self._indent(options, "      "),
421        )
422
423
424# -------------------------------------------------------------------------
425#  'UItem' class:
426# -------------------------------------------------------------------------
427
428
429class UItem(Item):
430    """ An Item that has no label.
431    """
432
433    show_label = Bool(False)
434
435
436# -------------------------------------------------------------------------
437#  'Custom' class:
438# -------------------------------------------------------------------------
439
440
441class Custom(Item):
442    """ An Item using a 'custom' style.
443    """
444
445    style = EditorStyle("custom")
446
447
448# -------------------------------------------------------------------------
449#  'UCustom' class:
450# -------------------------------------------------------------------------
451
452
453class UCustom(Custom):
454    """ An Item using a 'custom' style with no label.
455    """
456
457    show_label = Bool(False)
458
459
460# -------------------------------------------------------------------------
461#  'Readonly' class:
462# -------------------------------------------------------------------------
463
464
465class Readonly(Item):
466    """ An Item using a 'readonly' style.
467    """
468
469    style = EditorStyle("readonly")
470
471
472# -------------------------------------------------------------------------
473#  'UReadonly' class:
474# -------------------------------------------------------------------------
475
476
477class UReadonly(Readonly):
478    """ An Item using a 'readonly' style with no label.
479    """
480
481    show_label = Bool(False)
482
483
484# -------------------------------------------------------------------------
485#  'Label' class:
486# -------------------------------------------------------------------------
487
488
489class Label(Item):
490    """ An item that is a label.
491    """
492
493    def __init__(self, label, **traits):
494        super(Label, self).__init__(label=label, **traits)
495
496
497# -------------------------------------------------------------------------
498#  'Heading' class:
499# -------------------------------------------------------------------------
500
501
502class Heading(Label):
503    """ An item that is a fancy label.
504    """
505
506    #: Override the 'style' trait to default to the fancy 'custom' style:
507    style = Constant("custom")
508
509
510# -------------------------------------------------------------------------
511#  'Spring' class:
512# -------------------------------------------------------------------------
513
514
515class Spring(Item):
516    """ An item that is a layout "spring".
517    """
518
519    # -------------------------------------------------------------------------
520    #  Trait definitions:
521    # -------------------------------------------------------------------------
522
523    #: Name of the trait the item is editing
524    #: Just a dummy trait that exists on all HasTraits objects. It's an Event,
525    #: so it won't cause Traits UI to add any synchronization, and because it
526    #: already exists, it won't force the addition of a new trait with a bogus
527    #: name.
528    name = "trait_modified"
529
530    #: Should a label be displayed?
531    show_label = Bool(False)
532
533    #: Editor to use for the item
534    editor = Instance("traitsui.api.NullEditor", ())
535
536    #: Should the item use extra space along its Group's layout orientation?
537    springy = True
538
539
540# A pre-defined spring for convenience
541spring = Spring()
542