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:   07/01/2005
15#
16# ------------------------------------------------------------------------------
17
18""" Defines the table column descriptor used by the editor and editor factory
19    classes for numeric and table editors.
20"""
21
22import os
23
24from traits.api import (
25    Any,
26    Bool,
27    Callable,
28    Constant,
29    Enum,
30    Expression,
31    Float,
32    HasPrivateTraits,
33    Instance,
34    Int,
35    Property,
36    Str,
37    Either,
38)
39
40from traits.trait_base import user_name_for, xgetattr
41
42from .editor_factory import EditorFactory
43from .menu import Menu
44from .ui_traits import Image, AView, EditorStyle
45from .toolkit_traits import Color, Font
46from .view import View
47
48# Set up a logger:
49import logging
50
51
52logger = logging.getLogger(__name__)
53
54
55# Flag used to indicate user has not specified a column label
56UndefinedLabel = "???"
57
58
59class TableColumn(HasPrivateTraits):
60    """ Represents a column in a table editor.
61    """
62
63    # -------------------------------------------------------------------------
64    #  Trait definitions:
65    # -------------------------------------------------------------------------
66
67    #: Column label to use for this column:
68    label = Str(UndefinedLabel)
69
70    #: Type of data contained by the column:
71    # XXX currently no other types supported, but potentially there could be...
72    type = Enum("text", "bool")
73
74    #: Text color for this column:
75    text_color = Color("black")
76
77    #: Text font for this column:
78    text_font = Either(None, Font)
79
80    #: Cell background color for this column:
81    cell_color = Color("white", allow_none=True)
82
83    #: Cell background color for non-editable columns:
84    read_only_cell_color = Color(0xF4F3EE, allow_none=True)
85
86    #: Cell graph color:
87    graph_color = Color(0xDDD9CC)
88
89    #: Horizontal alignment of text in the column:
90    horizontal_alignment = Enum("left", ["left", "center", "right"])
91
92    #: Vertical alignment of text in the column:
93    vertical_alignment = Enum("center", ["top", "center", "bottom"])
94
95    #: Horizontal cell margin
96    horizontal_margin = Int(4)
97
98    #: Vertical cell margin
99    vertical_margin = Int(3)
100
101    #: The image to display in the cell:
102    image = Image
103
104    #: Renderer used to render the contents of this column:
105    renderer = Any  # A toolkit specific renderer
106
107    #: Is the table column visible (i.e., viewable)?
108    visible = Bool(True)
109
110    #: Is this column editable?
111    editable = Bool(True)
112
113    #: Is the column automatically edited/viewed (i.e. should the column editor
114    #: or popup be activated automatically on mouse over)?
115    auto_editable = Bool(False)
116
117    #: Should a checkbox be displayed instead of True/False?
118    show_checkbox = Bool(True)
119
120    #: Can external objects be dropped on the column?
121    droppable = Bool(False)
122
123    #: Context menu to display when this column is right-clicked:
124    menu = Instance(Menu)
125
126    #: The tooltip to display when the mouse is over the column:
127    tooltip = Str()
128
129    #: The width of the column (< 0.0: Default, 0.0..1.0: fraction of total
130    #: table width, > 1.0: absolute width in pixels):
131    width = Float(-1.0)
132
133    #: The width of the column while it is being edited (< 0.0: Default,
134    #: 0.0..1.0: fraction of total table width, > 1.0: absolute width in
135    #: pixels):
136    edit_width = Float(-1.0)
137
138    #: The height of the column cell's row while it is being edited
139    #: (< 0.0: Default, 0.0..1.0: fraction of total table height,
140    #: > 1.0: absolute height in pixels):
141    edit_height = Float(-1.0)
142
143    #: The resize mode for this column.  This takes precedence over other
144    #: settings (like **width**, above).
145    #: - "interactive": column can be resized by users or programmatically
146    #: - "fixed": users cannot resize the column, but it can be set programmatically
147    #: - "stretch": the column will be resized to fill the available space
148    #: - "resize_to_contents": column will be sized to fit the contents, but then cannot be resized
149    resize_mode = Enum("interactive", "fixed", "stretch", "resize_to_contents")
150
151    #: The view (if any) to display when clicking a non-editable cell:
152    view = AView
153
154    #: Optional maximum value a numeric cell value can have:
155    maximum = Float(trait_value=True)
156
157    # -------------------------------------------------------------------------
158    #:  Returns the actual object being edited:
159    # -------------------------------------------------------------------------
160
161    def get_object(self, object):
162        """ Returns the actual object being edited.
163        """
164        return object
165
166    def get_label(self):
167        """ Gets the label of the column.
168        """
169        return self.label
170
171    def get_width(self):
172        """ Returns the width of the column.
173        """
174        return self.width
175
176    def get_edit_width(self, object):
177        """ Returns the edit width of the column.
178        """
179        return self.edit_width
180
181    def get_edit_height(self, object):
182        """ Returns the height of the column cell's row while it is being
183            edited.
184        """
185        return self.edit_height
186
187    def get_type(self, object):
188        """ Gets the type of data for the column for a specified object.
189        """
190        return self.type
191
192    def get_text_color(self, object):
193        """ Returns the text color for the column for a specified object.
194        """
195        return self.text_color_
196
197    def get_text_font(self, object):
198        """ Returns the text font for the column for a specified object.
199        """
200        return self.text_font
201
202    def get_cell_color(self, object):
203        """ Returns the cell background color for the column for a specified
204            object.
205        """
206        if self.is_editable(object):
207            return self.cell_color_
208        return self.read_only_cell_color_
209
210    def get_graph_color(self, object):
211        """ Returns the cell background graph color for the column for a
212            specified object.
213        """
214        return self.graph_color_
215
216    def get_horizontal_alignment(self, object):
217        """ Returns the horizontal alignment for the column for a specified
218            object.
219        """
220        return self.horizontal_alignment
221
222    def get_vertical_alignment(self, object):
223        """ Returns the vertical alignment for the column for a specified
224            object.
225        """
226        return self.vertical_alignment
227
228    def get_image(self, object):
229        """ Returns the image to display for the column for a specified object.
230        """
231        return self.image
232
233    def get_renderer(self, object):
234        """ Returns the renderer for the column of a specified object.
235        """
236        return self.renderer
237
238    def is_editable(self, object):
239        """ Returns whether the column is editable for a specified object.
240        """
241        return self.editable
242
243    def is_auto_editable(self, object):
244        """ Returns whether the column is automatically edited/viewed for a
245            specified object.
246        """
247        return self.auto_editable
248
249    def is_droppable(self, object, value):
250        """ Returns whether a specified value is valid for dropping on the
251            column for a specified object.
252        """
253        return self.droppable
254
255    def get_menu(self, object):
256        """ Returns the context menu to display when the user right-clicks on
257            the column for a specified object.
258        """
259        return self.menu
260
261    def get_tooltip(self, object):
262        """ Returns the tooltip to display when the user mouses over the column
263            for a specified object.
264        """
265        return self.tooltip
266
267    def get_view(self, object):
268        """ Returns the view to display when clicking a non-editable cell.
269        """
270        return self.view
271
272    def get_maximum(self, object):
273        """ Returns the maximum value a numeric column can have.
274        """
275        return self.maximum
276
277    def on_click(self, object):
278        """ Called when the user clicks on the column.
279        """
280        pass
281
282    def on_dclick(self, object):
283        """ Called when the user clicks on the column.
284        """
285        pass
286
287    def cmp(self, object1, object2):
288        """ Returns the result of comparing the column of two different objects.
289
290        This is deprecated.
291        """
292        return (self.key(object1) > self.key(object2)) - (
293            self.key(object1) < self.key(object2)
294        )
295
296    def __str__(self):
297        """ Returns the string representation of the table column.
298        """
299        return self.get_label()
300
301
302class ObjectColumn(TableColumn):
303    """ A column for editing objects.
304    """
305
306    # -------------------------------------------------------------------------
307    #  Trait definitions:
308    # -------------------------------------------------------------------------
309
310    #: Name of the object trait associated with this column:
311    name = Str()
312
313    #: Column label to use for this column:
314    label = Property()
315
316    #: Trait editor used to edit the contents of this column:
317    editor = Instance(EditorFactory)
318
319    #: The editor style to use to edit the contents of this column:
320    style = EditorStyle
321
322    #: Format string to apply to column values:
323    format = Str("%s")
324
325    #: Format function to apply to column values:
326    format_func = Callable()
327
328    # -------------------------------------------------------------------------
329    #  Trait view definitions:
330    # -------------------------------------------------------------------------
331
332    traits_view = View(
333        [
334            ["name", "label", "type", "|[Column Information]"],
335            [
336                "horizontal_alignment{Horizontal}@",
337                "vertical_alignment{Vertical}@",
338                "|[Alignment]",
339            ],
340            ["editable", "9", "droppable", "9", "visible", "-[Options]>"],
341            "|{Column}",
342        ],
343        [
344            [
345                "text_color@",
346                "cell_color@",
347                "read_only_cell_color@",
348                "|[UI Colors]",
349            ],
350            "|{Colors}",
351        ],
352        [["text_font@", "|[Font]<>"], "|{Font}"],
353        ["menu@", "|{Menu}"],
354        ["editor@", "|{Editor}"],
355    )
356
357    def _get_label(self):
358        """ Gets the label of the column.
359        """
360        if self._label is not None:
361            return self._label
362        return user_name_for(self.name)
363
364    def _set_label(self, label):
365        old, self._label = self._label, label
366        if old != label:
367            self.trait_property_changed("label", old, label)
368
369    def get_raw_value(self, object):
370        """ Gets the unformatted value of the column for a specified object.
371        """
372        try:
373            return xgetattr(self.get_object(object), self.name)
374        except Exception as e:
375            from traitsui.api import raise_to_debug
376
377            raise_to_debug()
378            return None
379
380    def get_value(self, object):
381        """ Gets the formatted value of the column for a specified object.
382        """
383        try:
384            if self.format_func is not None:
385                return self.format_func(self.get_raw_value(object))
386
387            return self.format % (self.get_raw_value(object),)
388        except:
389            logger.exception(
390                "Error occurred trying to format a %s value"
391                % self.__class__.__name__
392            )
393            return "Format!"
394
395    def get_drag_value(self, object):
396        """Returns the drag value for the column.
397        """
398        return self.get_raw_value(object)
399
400    def set_value(self, object, value):
401        """ Sets the value of the column for a specified object.
402        """
403        target, name = self.target_name(object)
404        setattr(target, name, value)
405
406    def get_editor(self, object):
407        """ Gets the editor for the column of a specified object.
408        """
409        if self.editor is not None:
410            return self.editor
411
412        target, name = self.target_name(object)
413
414        return target.base_trait(name).get_editor()
415
416    def get_style(self, object):
417        """ Gets the editor style for the column of a specified object.
418        """
419        return self.style
420
421    def key(self, object):
422        """ Returns the value to use for sorting.
423        """
424        return self.get_raw_value(object)
425
426    def is_droppable(self, object, value):
427        """ Returns whether a specified value is valid for dropping on the
428            column for a specified object.
429        """
430        if self.droppable:
431            try:
432                target, name = self.target_name(object)
433                target.base_trait(name).validate(target, name, value)
434                return True
435            except:
436                pass
437
438        return False
439
440    def target_name(self, object):
441        """ Returns the target object and name for the column.
442        """
443        object = self.get_object(object)
444        name = self.name
445        col = name.rfind(".")
446        if col < 0:
447            return (object, name)
448
449        return (xgetattr(object, name[:col]), name[col + 1 :])
450
451
452class ExpressionColumn(ObjectColumn):
453    """ A column for displaying computed values.
454    """
455
456    # -------------------------------------------------------------------------
457    #  Trait definitions:
458    # -------------------------------------------------------------------------
459
460    #: The Python expression used to return the value of the column:
461    expression = Expression
462
463    #: Is this column editable?
464    editable = Constant(False)
465
466    #: The globals dictionary that should be passed to the expression
467    #: evaluation:
468    globals = Any({})
469
470    def get_raw_value(self, object):
471        """ Gets the unformatted value of the column for a specified object.
472        """
473        try:
474            return eval(self.expression_, self.globals, {"object": object})
475        except Exception:
476            logger.exception(
477                "Error evaluating table column expression: %s"
478                % self.expression
479            )
480            return None
481
482
483class NumericColumn(ObjectColumn):
484    """ A column for editing Numeric arrays.
485    """
486
487    # -------------------------------------------------------------------------
488    #  Trait definitions:
489    # -------------------------------------------------------------------------
490
491    #: Column label to use for this column
492    label = Property()
493
494    #: Text color this column when selected
495    selected_text_color = Color("black")
496
497    #: Text font for this column when selected
498    selected_text_font = Font
499
500    #: Cell background color for this column when selected
501    selected_cell_color = Color(0xD8FFD8)
502
503    #: Formatting string for the cell value
504    format = Str("%s")
505
506    #: Horizontal alignment of text in the column; this value overrides the
507    #: default.
508    horizontal_alignment = "center"
509
510    def _get_label(self):
511        """ Gets the label of the column.
512        """
513        if self._label is not None:
514            return self._label
515        return self.name
516
517    def _set_label(self, label):
518        old, self._label = self._label, label
519        if old != label:
520            self.trait_property_changed("label", old, label)
521
522    def get_type(self, object):
523        """ Gets the type of data for the column for a specified object row.
524        """
525        return self.type
526
527    def get_text_color(self, object):
528        """ Returns the text color for the column for a specified object row.
529        """
530        if self._is_selected(object):
531            return self.selected_text_color_
532        return self.text_color_
533
534    def get_text_font(self, object):
535        """ Returns the text font for the column for a specified object row.
536        """
537        if self._is_selected(object):
538            return self.selected_text_font
539        return self.text_font
540
541    def get_cell_color(self, object):
542        """ Returns the cell background color for the column for a specified
543            object row.
544        """
545        if self.is_editable(object):
546            if self._is_selected(object):
547                return self.selected_cell_color_
548            return self.cell_color_
549        return self.read_only_cell_color_
550
551    def get_horizontal_alignment(self, object):
552        """ Returns the horizontal alignment for the column for a specified
553            object row.
554        """
555        return self.horizontal_alignment
556
557    def get_vertical_alignment(self, object):
558        """ Returns the vertical alignment for the column for a specified
559            object row.
560        """
561        return self.vertical_alignment
562
563    def is_editable(self, object):
564        """ Returns whether the column is editable for a specified object row.
565        """
566        return self.editable
567
568    def is_droppable(self, object, row, value):
569        """ Returns whether a specified value is valid for dropping on the
570            column for a specified object row.
571        """
572        return self.droppable
573
574    def get_menu(self, object, row):
575        """ Returns the context menu to display when the user right-clicks on
576            the column for a specified object row.
577        """
578        return self.menu
579
580    def get_value(self, object):
581        """ Gets the value of the column for a specified object row.
582        """
583        try:
584            value = getattr(object, self.name)
585            try:
586                return self.format % (value,)
587            except:
588                return "Format!"
589        except:
590            return "Undefined!"
591
592    def set_value(self, object, row, value):
593        """ Sets the value of the column for a specified object row.
594        """
595        column = self.get_data_column(object)
596        column[row] = type(column[row])(value)
597
598    def get_editor(self, object):
599        """ Gets the editor for the column of a specified object row.
600        """
601        return super(NumericColumn, self).get_editor(object)
602
603    def get_data_column(self, object):
604        """ Gets the entire contents of the specified object column.
605        """
606        return getattr(object, self.name)
607
608    def _is_selected(self, object):
609        """ Returns whether a specified object row is selected.
610        """
611        if (
612            hasattr(object, "model_selection")
613            and object.model_selection is not None
614        ):
615            return True
616        return False
617
618
619class ListColumn(TableColumn):
620    """ A column for editing lists.
621    """
622
623    # -------------------------------------------------------------------------
624    #  Trait definitions:
625    # -------------------------------------------------------------------------
626
627    # Label to use for this column
628    label = Property()
629
630    #: Index of the list element associated with this column
631    index = Int()
632
633    # Is this column editable? This value overrides the base class default.
634    editable = False
635
636    # -------------------------------------------------------------------------
637    #  Trait view definitions:
638    # -------------------------------------------------------------------------
639
640    traits_view = View(
641        [
642            ["index", "label", "type", "|[Column Information]"],
643            ["text_color@", "cell_color@", "|[UI Colors]"],
644        ]
645    )
646
647    def _get_label(self):
648        """ Gets the label of the column.
649        """
650        if self._label is not None:
651            return self._label
652        return "Column %d" % (self.index + 1)
653
654    def _set_label(self, label):
655        old, self._label = self._label, label
656        if old != label:
657            self.trait_property_changed("label", old, label)
658
659    def get_value(self, object):
660        """ Gets the value of the column for a specified object.
661        """
662        return str(object[self.index])
663
664    def set_value(self, object, value):
665        """ Sets the value of the column for a specified object.
666        """
667        object[self.index] = value
668
669    def get_editor(self, object):
670        """ Gets the editor for the column of a specified object.
671        """
672        return None
673
674    def key(self, object):
675        """ Returns the value to use for sorting.
676        """
677        return object[self.index]
678