Source code for traitsui.editors.table_editor

# (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 table editor factory for all traits user interface toolkits.
"""

from traits.api import (
    Int,
    Float,
    List,
    Instance,
    Str,
    Any,
    Tuple,
    Dict,
    Enum,
    Bool,
    Callable,
    Range,
    Trait,
    observe,
)

from traitsui.editor_factory import EditorFactory
from traitsui.editors.enum_editor import EnumEditor
from traitsui.handler import Handler
from traitsui.helper import Orientation
from traitsui.item import Item
from traitsui.table_filter import TableFilter
from traitsui.toolkit_traits import Color, Font
from traitsui.ui_traits import AView
from traitsui.view import View

# The filter used to indicate that the user wants to customize the current
# filter
customize_filter = TableFilter(name="Customize...")

# -------------------------------------------------------------------------
#  Trait definitions:
# -------------------------------------------------------------------------

# A trait whose value can be True, False, or a callable function
BoolOrCallable = Trait(False, Bool, Callable)


[docs]class TableEditor(EditorFactory): """Editor factory for table editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: List of initial table column descriptors columns = List(Instance("traitsui.table_column.TableColumn")) #: List of other table column descriptors (not initially displayed) other_columns = List(Instance("traitsui.table_column.TableColumn")) #: The object trait containing the list of column descriptors columns_name = Str() #: The desired number of visible rows in the table rows = Int() #: The optional extended name of the trait used to specify an external #: filter for the table data. The value of the trait must either be an #: instance of TableEditor, a callable that accepts one argument #: (a table row) and returns True or False to indicate whether the #: specified object passes the filter or not, or **None** to indicate that #: no filter is to be applied: filter_name = Str() #: Initial filter that should be applied to the table filter = Instance("traitsui.table_filter.TableFilter") #: List of available filters that can be applied to the table filters = List(Instance("traitsui.table_filter.TableFilter")) #: The optional extended trait name of the trait used to notify that the #: filter has changed and the displayed objects should be updated. #: It should be an Event. update_filter_name = Str() #: Filter object used to allow a user to search the table. #: NOTE: If left as None, the table will not be searchable. search = Instance("traitsui.table_filter.TableFilter") #: Default context menu to display when any cell is right-clicked menu = Instance("traitsui.menu.Menu") #: Default trait name containg menu menu_name = Str() #: Are objects deletable from the table? deletable = BoolOrCallable(False) #: Is the table editable? editable = Bool(True) #: Should the editor become active after the first click edit_on_first_click = Bool(True) #: Can the user reorder the items in the table? reorderable = Bool(False) #: Can the user configure the table columns? configurable = Bool(True) #: Should the cells of the table automatically size to the optimal size? auto_size = Bool(True) #: Mirrors the Qt QSizePolicy.Policy attribute, for horizontal and vertical #: dimensions. For these to be useful, set auto_size to False. If these #: are None, then the table size policy will not be set in that dimension #: (for backwards compatibility). h_size_policy = Enum( None, "preferred", "fixed", "minimum", "maximum", "expanding", "minimum_expanding", "ignored", ) v_size_policy = Enum( None, "preferred", "fixed", "minimum", "maximum", "expanding", "minimum_expanding", "ignored", ) #: Should a new row automatically be added to the end of the table to allow #: the user to create new entries? If True, **row_factory** must be set. auto_add = Bool(False) #: Should the table items be presented in reverse order? reverse = Bool(False) #: The DockWindow graphical theme: dock_theme = Any() #: View to use when editing table items. #: NOTE: If not specified, the table items are not editable in a separate #: pane of the editor. edit_view = AView(" ") #: The handler to apply to **edit_view** edit_view_handler = Instance(Handler) #: Width to use for the edit view edit_view_width = Float(-1.0) #: Height to use for the edit view edit_view_height = Float(-1.0) #: Layout orientation of the table and its associated editor pane. This #: attribute applies only if **edit_view** is not ' '. orientation = Orientation #: Is the table sortable by clicking on the column headers? sortable = Bool(True) #: Does sorting affect the model (vs. just the view)? sort_model = Bool(False) #: Should grid lines be shown on the table? show_lines = Bool(True) #: Should the toolbar be displayed? (Note that False will override settings #: such as 'configurable', etc., and is a quick way to prevent the toolbar #: from being displayed; but True will not cause a toolbar to appear if one #: would not otherwise have been displayed) show_toolbar = Bool(False) #: The vertical scroll increment for the table: scroll_dy = Range(1, 32) #: Grid line color. Does not work on Qt. line_color = Color(0xC4C0A9) #: Show column labels? show_column_labels = Bool(True) #: Show row labels? show_row_labels = Bool(False) #: Font to use for text in cells cell_font = Font #: Color to use for text in cells cell_color = Color(default=None, allow_none=True) #: Color to use for cell backgrounds cell_bg_color = Color(default=None, allow_none=True) #: Color to use for read-only cell backgrounds cell_read_only_bg_color = Color(default=None, allow_none=True) #: Whether to even-odd alternate the background color. Qt only. alternate_bg_color = Bool(False) #: Font to use for text in labels label_font = Font #: Color to use for text in labels label_color = Color(default=None, allow_none=True) #: Color to use for label backgrounds. Some Qt styles (eg. MacOS) ignore #: this. label_bg_color = Color(default=None, allow_none=True) #: Background color of selected item. selection_bg_color = Color(default=None, allow_none=True) #: Color of selected text. selection_color = Color(default=None, allow_none=True) #: Height (in pixels) of column labels column_label_height = Int(25) #: Width (in pixels) of row labels row_label_width = Int(82) #: The initial height of each row (<= 0 means use default value): row_height = Int(0) #: The optional extended name of the trait that the indices of the items #: currently passing the table filter are synced with: filtered_indices = Str() #: The selection mode of the table. The meaning of the various values are #: as follows: #: #: row #: Entire rows are selected. At most one row can be selected at once. #: This is the default. #: rows #: Entire rows are selected. More than one row can be selected at once. #: column #: Entire columns are selected. At most one column can be selected at #: once. #: columns #: Entire columns are selected. More than one column can be selected at #: once. #: cell #: Single cells are selected. Only one cell can be selected at once. #: cells #: Single cells are selected. More than one cell can be selected at once. selection_mode = Enum("row", "rows", "column", "columns", "cell", "cells") #: The optional extended name of the trait that the current selection is #: synced with: selected = Str() #: The optional extended trait name of the trait that the indices of the #: current selection are synced with: selected_indices = Str() #: The optional extended trait name of the trait that should be assigned #: an ( object, column ) tuple when a table cell is clicked on (Note: If #: you want to receive repeated clicks on the same cell, make sure the #: trait is defined as an Event): click = Str() #: The optional extended trait name of the trait that should be assigned #: an ( object, column ) tuple when a table cell is double-clicked on #: (Note: if you want to receive repeated double-clicks on the same cell, #: make sure the trait is defined as an Event): dclick = Str() #: Called when a table item is selected on_select = Any() #: Called when a table item is double clicked on_dclick = Any() #: A factory to generate new rows. #: NOTE: If None, then the user will not be able to add new rows to the #: table. If not None, then it must be a callable that accepts #: **row_factory_args** and **row_factory_kw** and returns a new object #: that can be added to the table. row_factory = Any() #: Arguments to pass to the **row_factory** callable when a new row is #: created row_factory_args = Tuple() #: Keyword arguments to pass to the **row_factory** callable when a new row #: is created row_factory_kw = Dict() #: Hooks for replacing parts of the implementation. table_view_factory = Callable() source_model_factory = Callable() model_factory = Callable() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( [ "{Initial columns}@", Item("columns", resizable=True), "{Other columns}@", Item("other_columns", resizable=True), "|{Columns}<>", ], [ [ "deletable{Are items deletable?}", "9", "editable{Are items editable?}", "9", "-[Item Options]>", ], [ "show_column_labels{Show column labels?}", "9", "configurable{Are columns user configurable?}", "9", "auto_size{Should columns auto size?}", "-[Column Options]>", ], [ "sortable{Are columns sortable?}", Item( "sort_model{Does sorting affect the model?}", enabled_when="sortable", ), "-[Sorting Options]>", ], [ ["show_lines{Show grid lines?}", "|>"], ["_", "line_color{Grid line color}@", "|<>"], "|[Grid Line Options]", ], "|{Options}", ], [ [ "cell_color{Text color}@", "cell_bg_color{Background color}@", "cell_read_only_bg_color{Read only color}@", "|[Cell Colors]", ], ["cell_font", "|[Cell Font]<>"], "|{Cell}", ], [ [ "label_color{Text color}@", "label_bg_color{Background color}@", "|[Label Colors]", ], ["label_font@", "|[Label Font]<>"], "|{Label}", ], [ [ "selection_color{Text color}@", "selection_bg_color{Background color}@", "|[Selection Colors]", ], "|{Selection}", ], height=0.5, ) # ------------------------------------------------------------------------- # 'Editor' factory methods: # -------------------------------------------------------------------------
[docs] def readonly_editor(self, ui, object, name, description, parent): """Generates an "editor" that is read-only. Overridden to set the value of the editable trait to False before generating the editor. """ self.editable = False return super().readonly_editor(ui, object, name, description, parent)
# ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- @observe("filters.items") def _update_filter_editor(self, event): """Handles the set of filters associated with the editor's factory being changed. """ values = {None: "000:No filter"} i = 0 for filter in self.filters: if not filter.template: i += 1 values[filter] = "%03d:%s" % (i, filter.name) values[customize_filter] = "%03d:%s" % ((i + 1), customize_filter.name) if self._filter_editor is None: self._filter_editor = EnumEditor(values=values) else: self._filter_editor.values = values
# This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = TableEditor # ------------------------------------------------------------------------- # Base class for toolkit-specific editors # -------------------------------------------------------------------------
[docs]class BaseTableEditor(object): """Base class for toolkit-specific editors.""" # ------------------------------------------------------------------------- # Interface for toolkit-specific editors: # -------------------------------------------------------------------------
[docs] def set_menu_context(self, selection, object, column): """Call before creating a context menu for a cell, then set self as the controller for the menu. """ self._menu_context = { "selection": selection, "object": object, "column": column, "editor": self, "info": self.ui.info, "handler": self.ui.handler, }
# ------------------------------------------------------------------------- # pyface.action 'controller' interface implementation: # -------------------------------------------------------------------------
[docs] def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed.""" action = menu_item.item.action self.eval_when(action.enabled_when, menu_item, "enabled") self.eval_when(action.checked_when, menu_item, "checked")
[docs] def add_to_toolbar(self, toolbar_item): """Adds a toolbar item to the toolbar being constructed.""" self.add_to_menu(toolbar_item)
[docs] def can_add_to_menu(self, action): """Returns whether the action should be defined in the user interface.""" if action.defined_when != "": if not eval(action.defined_when, globals(), self._menu_context): return False if action.visible_when != "": if not eval(action.visible_when, globals(), self._menu_context): return False return True
[docs] def can_add_to_toolbar(self, action): """Returns whether the toolbar action should be defined in the user interface. """ return self.can_add_to_menu(action)
[docs] def perform(self, action, action_event=None): """Performs the action described by a specified Action object.""" self.ui.do_undoable(self._perform, action)
def _perform(self, action): method_name = action.action info = self.ui.info handler = self.ui.handler context = self._menu_context self._menu_context = None selection = context["selection"] if method_name.find(".") >= 0: if method_name.find("(") < 0: method_name += "()" try: eval(method_name, globals(), context) except: # fixme: Should the exception be logged somewhere? pass return method = getattr(handler, method_name, None) if method is not None: method(info, selection) return if action.on_perform is not None: action.on_perform(selection) return action.perform(selection) # ------------------------------------------------------------------------- # Menu support methods: # -------------------------------------------------------------------------
[docs] def eval_when(self, condition, object, trait): """Evaluates a condition within a defined context and sets a specified object trait based on the result, which is assumed to be a Boolean. """ if condition != "": value = bool(eval(condition, globals(), self._menu_context)) setattr(object, trait, value)
# ------------------------------------------------------------------------- # Helper class for toolkit-specific editors to implement 'reversed' option: # -------------------------------------------------------------------------
[docs]class ReversedList(object): """A list whose order is the reverse of its input.""" def __init__(self, list): self.list = list
[docs] def insert(self, index, value): """Inserts a value at a specified index in the list.""" return self.list.insert(self._index(index - 1), value)
[docs] def index(self, value): """Returns the index of the first occurrence of the specified value in the list. """ list = self.list[:] list.reverse() return list.index(value)
def __len__(self): """Returns the length of the list.""" return len(self.list) def __getitem__(self, index): """Returns the value at a specified index in the list.""" return self.list[self._index(index)] def __setslice__(self, i, j, values): """Sets a slice of a list to the contents of a specified sequence.""" return self.list.__setslice__(self._index(i), self._index(j), values) def __delitem__(self, index): """Deletes the item at a specified index.""" return self.list.__delitem__(self._index(index)) def _index(self, index): """Returns the "reversed" value for a specified index.""" if index < 0: return -1 - index result = len(self.list) - index - 1 if result >= 0: return result return index