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 Group class used to represent a group of items used in a
19    Traits-based user interface.
20"""
21
22
23
24from traits.api import (
25    Bool,
26    Delegate,
27    Float,
28    Instance,
29    List,
30    Property,
31    Range,
32    ReadOnly,
33    Str,
34    TraitError,
35    cached_property,
36)
37
38from .view_element import ViewSubElement
39
40from .item import Item
41
42from .include import Include
43
44from .ui_traits import SequenceTypes, ContainerDelegate, Orientation, Layout
45
46from .dock_window_theme import dock_window_theme, DockWindowTheme
47
48
49# -------------------------------------------------------------------------
50#  Trait definitions:
51# -------------------------------------------------------------------------
52
53# Delegate trait to the object being "shadowed"
54ShadowDelegate = Delegate("shadow")
55
56# Amount of padding to add around item
57Padding = Range(0, 15, desc="amount of padding to add around each item")
58
59
60class Group(ViewSubElement):
61    """ Represents a grouping of items in a user interface view.
62    """
63
64    # -------------------------------------------------------------------------
65    # Trait definitions:
66    # -------------------------------------------------------------------------
67
68    #: A list of Group, Item, and Include objects in this group.
69    content = List(ViewSubElement)
70
71    #: A unique identifier for the group.
72    id = Str()
73
74    #: User interface label for the group. How the label is displayed depends
75    #: on the **show_border** attribute, and on the **layout** attribute of
76    #: the group's parent group or view.
77    label = Str()
78
79    style_sheet = Str()
80
81    #: Default context object for group items.
82    object = ContainerDelegate
83
84    #: Default editor style of items in the group.
85    style = ContainerDelegate
86
87    #: Default docking style of items in group.
88    dock = ContainerDelegate
89
90    #: Default image to display on notebook tabs.
91    image = ContainerDelegate
92
93    #: The theme to use for a DockWindow:
94    dock_theme = Instance(DockWindowTheme, allow_none=False)
95
96    #: Category of elements dragged from view.
97    export = ContainerDelegate
98
99    #: Spatial orientation of the group's elements. Can be 'vertical' (default)
100    #: or 'horizontal'.
101    orientation = Orientation
102
103    #: Layout style of the group, which can be one of the following:
104    #:
105    #: * 'normal' (default): Sub-groups are displayed sequentially in a single
106    #:   panel.
107    #: * 'flow': Sub-groups are displayed sequentially, and then "wrap" when
108    #:   they exceed the available space in the **orientation** direction.
109    #: * 'split': Sub-groups are displayed in a single panel, separated by
110    #:   "splitter bars", which the user can drag to adjust the amount of space
111    #:   for each sub-group.
112    #: * 'tabbed': Each sub-group appears on a separate tab, labeled with the
113    #:   sub-group's *label* text, if any.
114    #:
115    #: This attribute is ignored for groups that contain only items, or contain
116    #: only one sub-group.
117    layout = Layout
118
119    #: Should the group be scrollable along the direction of orientation?
120    scrollable = Bool(False)
121
122    #: The number of columns in the group
123    columns = Range(1, 50)
124
125    #: Should a border be drawn around group? If set to True, the **label** text
126    #: is embedded in the border. If set to False, the label appears as a banner
127    #: above the elements of the group.
128    show_border = Bool(False)
129
130    #: Should labels be added to items in group? Only items that are directly
131    #: contained in the group are affected. That is, if the group contains
132    #: a sub-group, the display of labels in the sub-group is not affected by
133    #: the attribute on this group.
134    show_labels = Bool(True)
135
136    #: Should labels be shown to the left of items (True) or the right (False)?
137    #: Only items that are directly contained in the group are affected. That is,
138    #: if the group contains a sub-group, the display of labels in the sub-group
139    #: is not affected by the attribute in this group. If **show_labels** is
140    #: False, this attribute is irrelevant.
141    show_left = Bool(True)
142
143    #: Is this group the tab that is initially selected? If True, the group's
144    #: tab is displayed when the view is opened. If the **layout** of the group's
145    #: parent is not 'tabbed', this attribute is ignored.
146    selected = Bool(False)
147
148    #: Should the group use extra space along its parent group's layout
149    #: orientation?
150    springy = Bool(False)
151
152    #: Optional help text (for top-level group). This help text appears in the
153    #: View-level help window (created by the default help handler), for any
154    #: View that contains *only* this group. Group-level help is ignored for
155    #: nested groups and multiple top-level groups
156    help = Str()
157
158    #: Pre-condition for including the group in the display. If the expression
159    #: evaluates to False, the group is not defined in the display. Conditions
160    #: for **defined_when** are evaluated only once, when the display is first
161    #: constructed. Use this attribute for conditions based on attributes that
162    #: vary from object to object, but that do not change over time.
163    defined_when = Str()
164
165    #: Pre-condition for showing the group. If the expression evaluates to False,
166    #: the group and its items are not visible (and they disappear if they were
167    #: previously visible). If the value evaluates to True, the group and items
168    #: become visible. All **visible_when** conditions are checked each time
169    #: that any trait value is edited in the display. Therefore, you can use
170    #: **visible_when** conditions to hide or show groups in response to user
171    #: input.
172    visible_when = Str()
173
174    #: Pre-condition for enabling the group. If the expression evaluates to False,
175    #: the group is disabled, that is, none of the widgets accept input. All
176    #: **enabled_when** conditions are checked each time that any trait value
177    #: is edited in the display. Therefore, you can use **enabled_when**
178    #: conditions to enable or disable groups in response to user input.
179    enabled_when = Str()
180
181    #: Amount of padding (in pixels) to add around each item in the group. The
182    #: value must be an integer between 0 and 15. (Unlike the Item class, the
183    #: Group class does not support negative padding.) The padding for any
184    #: individual widget is the sum of the padding for its Group, the padding
185    #: for its Item, and the default spacing determined by the toolkit.
186    padding = Padding
187
188    #: Requested width of the group (calculated from widths of contents)
189    width = Property(Float, depends_on="content")
190
191    #: Requested height of the group (calculated from heights of contents)
192    height = Property(Float, depends_on="content")
193
194    def __init__(self, *values, **traits):
195        """ Initializes the group object.
196        """
197        super(ViewSubElement, self).__init__(**traits)
198
199        content = self.content
200
201        # Process any embedded Group options first:
202        for value in values:
203            if (isinstance(value, str)) and (value[0:1] in "-|"):
204                # Parse Group trait options if specified as a string:
205                self._parse(value)
206
207        # Process all of the data passed to the constructor:
208        for value in values:
209            if isinstance(value, ViewSubElement):
210                content.append(value)
211            elif type(value) in SequenceTypes:
212                # Map (...) or [...] to a Group():
213                content.append(Group(*value))
214            elif isinstance(value, str):
215                if value[0:1] in "-|":
216                    # We've already parsed Group trait options above:
217                    pass
218                elif (value[:1] == "<") and (value[-1:] == ">"):
219                    # Convert string to an Include value:
220                    content.append(Include(value[1:-1].strip()))
221                else:
222                    # Else let the Item class try to make sense of it:
223                    content.append(Item(value))
224            else:
225                raise TypeError("Unrecognized argument type: %s" % value)
226
227        # Make sure this Group is the container for all its children:
228        self.set_container()
229
230    # -- Default Trait Values -------------------------------------------------
231
232    def _dock_theme_default(self):
233        return dock_window_theme()
234
235    def get_label(self, ui):
236        """ Gets the label to use this group.
237        """
238        if self.label != "":
239            return self.label
240
241        return "Group"
242
243    def is_includable(self):
244        """ Returns a Boolean value indicating whether the object is replacable
245        by an Include object.
246        """
247        return self.id != ""
248
249    def replace_include(self, view_elements):
250        """ Replaces any items that have an **id** attribute with an Include
251        object with the same ID value, and puts the object with the ID
252        into the specified ViewElements object.
253
254        Parameters
255        ----------
256        view_elements : ViewElements object
257            A set of Group, Item, and Include objects
258        """
259        for i, item in enumerate(self.content):
260            if item.is_includable():
261                id = item.id
262                if id in view_elements.content:
263                    raise TraitError(
264                        "Duplicate definition for view element '%s'" % id
265                    )
266                self.content[i] = Include(id)
267                view_elements.content[id] = item
268            item.replace_include(view_elements)
269
270    def get_shadow(self, ui):
271        """ Returns a ShadowGroup object for the current Group object, which
272        recursively resolves all embedded Include objects and which replaces
273        each embedded Group object with a corresponding ShadowGroup.
274        """
275        content = []
276        groups = 0
277        level = ui.push_level()
278        for value in self.content:
279            # Recursively replace Include objects:
280            while isinstance(value, Include):
281                value = ui.find(value)
282
283            # Convert Group objects to ShadowGroup objects, but include Item
284            # objects as is (ignore any 'None' values caused by a failed
285            # Include):
286            if isinstance(value, Group):
287                if self._defined_when(ui, value):
288                    content.append(value.get_shadow(ui))
289                    groups += 1
290            elif isinstance(value, Item):
291                if self._defined_when(ui, value):
292                    content.append(value)
293
294            ui.pop_level(level)
295
296        # Return the ShadowGroup:
297        return ShadowGroup(shadow=self, content=content, groups=groups)
298
299    def set_container(self):
300        """ Sets the correct container for the content.
301        """
302        for item in self.content:
303            item.container = self
304
305    def _defined_when(self, ui, value):
306        """ Should the object be defined in the user interface?
307        """
308        if value.defined_when == "":
309            return True
310        return ui.eval_when(value.defined_when)
311
312    def _parse(self, value):
313        """ Parses Group options specified as a string.
314        """
315        # Override the defaults, since we only allow 'True' values to be
316        # specified:
317        self.show_border = self.show_labels = self.show_left = False
318
319        # Parse all of the single or multi-character options:
320        value, empty = self._parse_label(value)
321        value = self._parse_style(value)
322        value = self._option(value, "-", "orientation", "horizontal")
323        value = self._option(value, "|", "orientation", "vertical")
324        value = self._option(value, "=", "layout", "split")
325        value = self._option(value, "^", "layout", "tabbed")
326        value = self._option(value, ">", "show_labels", True)
327        value = self._option(value, "<", "show_left", True)
328        value = self._option(value, "!", "selected", True)
329
330        show_labels = not (self.show_labels and self.show_left)
331        self.show_left = not self.show_labels
332        self.show_labels = show_labels
333
334        # Parse all of the punctuation based sub-string options:
335        value = self._split("id", value, ":", str.find, 0, 1)
336        if value != "":
337            self.object = value
338
339    def _parsed_label(self):
340        """ Handles a label being found in the string definition.
341        """
342        self.show_border = True
343
344    def __repr__(self):
345        """ Returns a "pretty print" version of the Group.
346        """
347        result = []
348        items = ",\n".join([item.__repr__() for item in self.content])
349        if len(items) > 0:
350            result.append(items)
351
352        options = self._repr_options(
353            "orientation",
354            "show_border",
355            "show_labels",
356            "show_left",
357            "selected",
358            "id",
359            "object",
360            "label",
361            "style",
362            "layout",
363            "style_sheet",
364        )
365        if options is not None:
366            result.append(options)
367
368        content = ",\n".join(result)
369        if len(content) == 0:
370            return self.__class__.__name__ + "()"
371
372        return "%s(\n%s\n)" % (self.__class__.__name__, self._indent(content))
373
374    # -------------------------------------------------------------------------
375    #  Property getters/setters for width/height attributes
376    # -------------------------------------------------------------------------
377
378    @cached_property
379    def _get_width(self):
380        """ Returns the requested width of the Group.
381        """
382        width = 0.0
383        for item in self.content:
384            if item.width >= 1:
385                if self.orientation == "horizontal":
386                    width += item.width
387                elif self.orientation == "vertical":
388                    width = max(width, item.width)
389
390        if width == 0:
391            width = -1.0
392
393        return width
394
395    @cached_property
396    def _get_height(self):
397        """ Returns the requested height of the Group.
398        """
399        height = 0.0
400        for item in self.content:
401            if item.height >= 1:
402                if self.orientation == "horizontal":
403                    height = max(height, item.height)
404                elif self.orientation == "vertical":
405                    height += item.height
406
407        if height == 0:
408            height = -1.0
409
410        return height
411
412
413class HGroup(Group):
414    """ A group whose items are laid out horizontally.
415    """
416
417    # -------------------------------------------------------------------------
418    #  Trait definitions:
419    # -------------------------------------------------------------------------
420
421    #: Override standard Group trait defaults to give it horizontal group
422    #: behavior:
423    orientation = "horizontal"
424
425
426class VGroup(Group):
427    """ A group whose items are laid out vertically.
428    """
429
430    # -------------------------------------------------------------------------
431    #  Trait definitions:
432    # -------------------------------------------------------------------------
433
434    #: Override standard Group trait defaults to give it vertical group
435    #: behavior:
436    orientation = "vertical"
437
438
439class VGrid(VGroup):
440    """ A group whose items are laid out in 2 columns.
441    """
442
443    # -------------------------------------------------------------------------
444    #  Trait definitions:
445    # -------------------------------------------------------------------------
446
447    #: Override standard Group trait defaults to give it grid behavior:
448    columns = 2
449
450
451class HFlow(HGroup):
452    """ A group in which items are laid out horizontally, and "wrap" when
453    they exceed the available horizontal space..
454    """
455
456    # -------------------------------------------------------------------------
457    #  Trait definitions:
458    # -------------------------------------------------------------------------
459
460    #: Override standard Group trait defaults to give it horizontal flow
461    #: behavior:
462    layout = "flow"
463    show_labels = False
464
465
466class VFlow(VGroup):
467    """ A group in which items are laid out vertically, and "wrap" when they
468    exceed the available vertical space.
469    """
470
471    # -------------------------------------------------------------------------
472    #  Trait definitions:
473    # -------------------------------------------------------------------------
474
475    #: Override standard Group trait defaults to give it vertical flow behavior:
476    layout = "flow"
477    show_labels = False
478
479
480class VFold(VGroup):
481    """ A group in which items are laid out vertically and can be collapsed
482        (i.e. 'folded') by clicking their title.
483    """
484
485    # -------------------------------------------------------------------------
486    #  Trait definitions:
487    # -------------------------------------------------------------------------
488
489    #: Override standard Group trait defaults to give it vertical folding group
490    #: behavior:
491    layout = "fold"
492    show_labels = False
493
494
495class HSplit(Group):
496    """ A horizontal group with splitter bars to separate it from other groups.
497    """
498
499    # -------------------------------------------------------------------------
500    #  Trait definitions:
501    # -------------------------------------------------------------------------
502
503    #: Override standard Group trait defaults to give it horizontal splitter
504    #: behavior:
505    layout = "split"
506    orientation = "horizontal"
507
508
509class VSplit(Group):
510    """ A vertical group with splitter bars to separate it from other groups.
511    """
512
513    # -------------------------------------------------------------------------
514    #  Trait definitions:
515    # -------------------------------------------------------------------------
516
517    #: Override standard Group trait defaults to give it vertical splitter
518    #: behavior:
519    layout = "split"
520    orientation = "vertical"
521
522
523class Tabbed(Group):
524    """ A group that is shown as a tabbed notebook.
525    """
526
527    # -------------------------------------------------------------------------
528    #  Trait definitions:
529    # -------------------------------------------------------------------------
530
531    #: Override standard Group trait defaults to give it tabbed notebook
532    #: behavior:
533    layout = "tabbed"
534    springy = True
535
536
537class ShadowGroup(Group):
538    """ Corresponds to a Group object, but with all embedded Include
539        objects resolved, and with all embedded Group objects replaced by
540        corresponding ShadowGroup objects.
541    """
542
543    def __init__(self, shadow, **traits):
544        # Set the 'shadow' trait before all others, to avoid exceptions
545        # when setting those other traits.
546        self.shadow = shadow
547        super(ShadowGroup, self).__init__(**traits)
548
549    # -------------------------------------------------------------------------
550    # Trait definitions:
551    # -------------------------------------------------------------------------
552
553    #: Group object this is a "shadow" for
554    shadow = ReadOnly()
555
556    #: Number of ShadowGroups in **content**
557    groups = ReadOnly()
558
559    #: Name of the group
560    id = ShadowDelegate
561
562    #: User interface label for the group
563    label = ShadowDelegate
564
565    #: Default context object for group items
566    object = ShadowDelegate
567
568    #: Default style of items in the group
569    style = ShadowDelegate
570
571    #: Default docking style of items in the group
572    dock = ShadowDelegate
573
574    #: Default image to display on notebook tabs
575    image = ShadowDelegate
576
577    #: The theme to use for a DockWindow:
578    dock_theme = ShadowDelegate
579
580    #: Category of elements dragged from the view
581    export = ShadowDelegate
582
583    #: Spatial orientation of the group
584    orientation = ShadowDelegate
585
586    #: Layout style of the group
587    layout = ShadowDelegate
588
589    #: Should the group be scrollable along the direction of orientation?
590    scrollable = ShadowDelegate
591
592    #: The number of columns in the group
593    columns = ShadowDelegate
594
595    #: Should a border be drawn around group?
596    show_border = ShadowDelegate
597
598    #: Should labels be added to items in group?
599    show_labels = ShadowDelegate
600
601    #: Should labels be shown to the left of items (vs. the right)?
602    show_left = ShadowDelegate
603
604    #: Is group the initially selected page?
605    selected = ShadowDelegate
606
607    #: Should the group use extra space along its parent group's layout
608    #: orientation?
609    springy = ShadowDelegate
610
611    #: Optional help text (for top-level group)
612    help = ShadowDelegate
613
614    #: Pre-condition for defining the group
615    defined_when = ShadowDelegate
616
617    #: Pre-condition for showing the group
618    visible_when = ShadowDelegate
619
620    #: Pre-condition for enabling the group
621    enabled_when = ShadowDelegate
622
623    #: Amount of padding to add around each item
624    padding = ShadowDelegate
625
626    #: Style sheet for the panel
627    style_sheet = ShadowDelegate
628
629    def get_content(self, allow_groups=True):
630        """ Returns the contents of the Group within a specified context for
631        building a user interface.
632
633        This method makes sure that all Group types are of the same type (i.e.,
634        Group or Item) and that all Include objects have been replaced by their
635        substituted values.
636        """
637        # Make a copy of the content:
638        result = self.content[:]
639
640        # If result includes any ShadowGroups and they are not allowed,
641        # replace them:
642        if self.groups != 0:
643            if not allow_groups:
644                i = 0
645                while i < len(result):
646                    value = result[i]
647                    if isinstance(value, ShadowGroup):
648                        items = value.get_content(False)
649                        result[i : i + 1] = items
650                        i += len(items)
651                    else:
652                        i += 1
653            elif (self.groups != len(result)) and (self.layout == "normal"):
654                items = []
655                content = []
656                for item in result:
657                    if isinstance(item, ShadowGroup):
658                        self._flush_items(content, items)
659                        content.append(item)
660                    else:
661                        items.append(item)
662                self._flush_items(content, items)
663                result = content
664
665        # Return the resulting list of objects:
666        return result
667
668    def get_id(self):
669        """ Returns an ID for the group.
670        """
671        if self.id != "":
672            return self.id
673
674        return ":".join([item.get_id() for item in self.get_content()])
675
676    def set_container(self):
677        """ Sets the correct container for the content.
678        """
679        pass
680
681    def _flush_items(self, content, items):
682        """ Creates a sub-group for any items contained in a specified list.
683        """
684        if len(items) > 0:
685            content.append(
686                # Set shadow before hand to prevent delegation errors
687                ShadowGroup(shadow=self.shadow).trait_set(
688                    groups=0,
689                    label="",
690                    show_border=False,
691                    content=items,
692                    show_labels=self.show_labels,
693                    show_left=self.show_left,
694                    springy=self.springy,
695                    orientation=self.orientation,
696                )
697            )
698            del items[:]
699
700    def __repr__(self):
701        """ Returns a "pretty print" version of the Group.
702        """
703        return repr(self.shadow)
704