# (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:
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# pyface.action 'controller' interface implementation:
# -------------------------------------------------------------------------
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